From 2d78b821d0e02330e574f677e43b4ee4a7102c62 Mon Sep 17 00:00:00 2001 From: wisecolt Date: Tue, 17 Mar 2026 01:09:36 +0300 Subject: [PATCH] feat: proje temizleme ve bildirim deneyimini iyilestir --- server/index.js | 15 +++++++ server/sessionManager.js | 9 ++--- web/src/App.jsx | 9 +++-- web/src/components/SessionToolbar.jsx | 8 ++-- web/src/components/ToastStack.jsx | 22 +++++++++++ web/src/hooks/useSession.js | 43 +++++++++++++++++++- web/src/styles/app.css | 57 +++++++++++++++++++++++++++ 7 files changed, 148 insertions(+), 15 deletions(-) create mode 100644 web/src/components/ToastStack.jsx diff --git a/server/index.js b/server/index.js index 36f6695..a30b6dd 100644 --- a/server/index.js +++ b/server/index.js @@ -56,6 +56,21 @@ app.post("/api/project/select", async (req, res) => { } }); +app.post("/api/project/clear", async (req, res) => { + try { + await sessionManager.setProjectPath(null); + res.json({ + ok: true, + projectPath: sessionManager.getState().currentProjectPath + }); + } catch (error) { + res.status(500).json({ + ok: false, + error: error.message + }); + } +}); + if (config.nodeEnv === "production") { app.use(express.static(webDistPath)); app.get("*", (req, res) => { diff --git a/server/sessionManager.js b/server/sessionManager.js index feb1637..09a3642 100644 --- a/server/sessionManager.js +++ b/server/sessionManager.js @@ -2,7 +2,6 @@ import fs from "fs"; import path from "path"; import stripAnsi from "strip-ansi"; import { buildBootstrapPrompt } from "./bootstrapPrompt.js"; -import { buildProjectSelectionPrompt } from "./bootstrapPrompt.js"; import { LogService } from "./logService.js"; import { PtyService } from "./ptyService.js"; import { getClaudeEnv, getPublicRuntimeConfig } from "./config.js"; @@ -125,7 +124,10 @@ export class SessionManager { this.currentProjectPath = resolved; this.lastDirectedMember = null; - this.setState({ currentProjectPath: resolved }); + this.setState({ + currentProjectPath: resolved, + teamActivated: false + }); this.addLog("system", `Current project set to ${resolved ?? "None"}`); if (wasRunning) { @@ -133,9 +135,6 @@ export class SessionManager { if (resolved) { await this.activateTeam(); - } else { - const prompt = buildProjectSelectionPrompt(this.getActiveWorkspaceDir()); - await this.sendRawPrompt(prompt, { label: `[project] Switched active project to ${this.getActiveWorkspaceDir()}` }); } } } diff --git a/web/src/App.jsx b/web/src/App.jsx index fc47dd7..af457fb 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -4,12 +4,13 @@ import SessionToolbar from "./components/SessionToolbar.jsx"; import ChatStream from "./components/ChatStream.jsx"; import PromptComposer from "./components/PromptComposer.jsx"; import TeamBoard from "./components/TeamBoard.jsx"; +import ToastStack from "./components/ToastStack.jsx"; import { useSocket } from "./hooks/useSocket.js"; import { useSession } from "./hooks/useSession.js"; export default function App() { const { socket, connected } = useSocket(); - const { session, chat, error, startSession, stopSession, sendPrompt, selectProject, clearError } = useSession(socket); + const { session, chat, startSession, clearProject, sendPrompt, selectProject, clearError, toasts, dismissToast } = useSession(socket); const [busy, setBusy] = useState(false); const autoStartedRef = useRef(false); @@ -57,8 +58,6 @@ export default function App() { - {error ?
{error}
: null} -
@@ -71,7 +70,7 @@ export default function App() { runAction(stopSession)} + onClearProject={() => runAction(clearProject)} onSelectProject={() => runAction(selectProject)} /> } @@ -82,6 +81,8 @@ export default function App() { />
+ +
); diff --git a/web/src/components/SessionToolbar.jsx b/web/src/components/SessionToolbar.jsx index 507d71f..964f34e 100644 --- a/web/src/components/SessionToolbar.jsx +++ b/web/src/components/SessionToolbar.jsx @@ -1,12 +1,10 @@ import PixelButton from "./PixelButton.jsx"; -export default function SessionToolbar({ session, busy, onStop, onSelectProject }) { - const isRunning = session.status === "running"; - +export default function SessionToolbar({ session, busy, onClearProject, onSelectProject }) { return (
- - Stop Session + + Clean Project Select Project diff --git a/web/src/components/ToastStack.jsx b/web/src/components/ToastStack.jsx new file mode 100644 index 0000000..65772fd --- /dev/null +++ b/web/src/components/ToastStack.jsx @@ -0,0 +1,22 @@ +export default function ToastStack({ toasts = [], onDismiss }) { + if (!toasts.length) { + return null; + } + + return ( +
+ {toasts.map((toast) => ( + + ))} +
+ ); +} diff --git a/web/src/hooks/useSession.js b/web/src/hooks/useSession.js index fde055e..67ba12b 100644 --- a/web/src/hooks/useSession.js +++ b/web/src/hooks/useSession.js @@ -17,13 +17,30 @@ export function useSession(socket) { const [session, setSession] = useState(initialState); const [chat, setChat] = useState(""); const [error, setError] = useState(""); + const [toasts, setToasts] = useState([]); + + function pushToast(message, tone = "error") { + if (!message) { + return; + } + + const id = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + setToasts((current) => [...current, { id, message, tone }].slice(-4)); + + window.setTimeout(() => { + setToasts((current) => current.filter((toast) => toast.id !== id)); + }, 4200); + } useEffect(() => { const handleState = (value) => setSession(value); const handleChunk = ({ chunk }) => setChat((current) => current + chunk); const handleSnapshot = ({ content }) => setChat(content ?? ""); const handleReset = () => setChat(""); - const handleError = ({ message }) => setError(message); + const handleError = ({ message }) => { + setError(message); + pushToast(message, "error"); + }; socket.on("session:state", handleState); socket.on("chat:chunk", handleChunk); @@ -46,6 +63,7 @@ export function useSession(socket) { if (!response?.ok) { const message = response?.error ?? "Unknown socket error"; setError(message); + pushToast(message, "error"); reject(new Error(message)); return; } @@ -60,7 +78,9 @@ export function useSession(socket) { session, chat, error, + toasts, clearError: () => setError(""), + dismissToast: (id) => setToasts((current) => current.filter((toast) => toast.id !== id)), startSession: () => emitWithAck("session:start"), stopSession: () => emitWithAck("session:stop"), activateTeam: () => emitWithAck("team:activate"), @@ -78,6 +98,27 @@ export function useSession(socket) { if (!response.ok || !payload.ok) { const message = payload.error ?? "Project selection failed"; setError(message); + pushToast(message, "error"); + throw new Error(message); + } + + setError(""); + return payload; + }, + clearProject: async () => { + const response = await fetch("/api/project/clear", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({}) + }); + + const payload = await response.json(); + if (!response.ok || !payload.ok) { + const message = payload.error ?? "Project cleanup failed"; + setError(message); + pushToast(message, "error"); throw new Error(message); } diff --git a/web/src/styles/app.css b/web/src/styles/app.css index 61793ca..62eabaf 100644 --- a/web/src/styles/app.css +++ b/web/src/styles/app.css @@ -194,6 +194,47 @@ color: var(--accent-red); } +.toast-stack { + position: absolute; + right: 18px; + bottom: 18px; + z-index: 5; + display: grid; + gap: 10px; + width: min(420px, calc(100vw - 64px)); +} + +.toast { + display: grid; + gap: 8px; + width: 100%; + padding: 12px 14px; + text-align: left; + border: 3px solid var(--border-dark); + box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.05), var(--shadow-panel); + background: linear-gradient(180deg, rgba(60, 22, 22, 0.98), rgba(18, 8, 8, 0.98)); + color: #ffd9d9; + cursor: pointer; +} + +.toast--info { + background: linear-gradient(180deg, rgba(18, 52, 55, 0.98), rgba(8, 17, 20, 0.98)); + color: #d8feff; +} + +.toast__label { + font-family: var(--font-display); + font-size: 0.58rem; + letter-spacing: 0.16em; + color: var(--accent-amber); +} + +.toast__message { + font-size: 0.82rem; + line-height: 1.45; + word-break: break-word; +} + .console-grid { display: grid; grid-template-columns: minmax(0, 1.55fr) minmax(320px, 0.9fr); @@ -233,6 +274,16 @@ flex-shrink: 0; } +.chat-panel { + display: flex; + flex-direction: column; +} + +.chat-panel .panel-frame__body { + height: 62vh; + min-height: 62vh; +} + .chat-stream, .team-card__body { min-height: 420px; @@ -244,6 +295,12 @@ linear-gradient(180deg, rgba(4, 8, 5, 0.88) 0%, rgba(8, 12, 9, 0.92) 100%); } +.chat-panel .chat-stream { + height: 100%; + min-height: 100%; + max-height: none; +} + .chat-stream pre, .team-message pre { margin: 0;