feat: proje temizleme ve bildirim deneyimini iyilestir

This commit is contained in:
2026-03-17 01:09:36 +03:00
parent 6312ebc9e7
commit 2d78b821d0
7 changed files with 148 additions and 15 deletions

View File

@@ -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") { if (config.nodeEnv === "production") {
app.use(express.static(webDistPath)); app.use(express.static(webDistPath));
app.get("*", (req, res) => { app.get("*", (req, res) => {

View File

@@ -2,7 +2,6 @@ import fs from "fs";
import path from "path"; import path from "path";
import stripAnsi from "strip-ansi"; import stripAnsi from "strip-ansi";
import { buildBootstrapPrompt } from "./bootstrapPrompt.js"; import { buildBootstrapPrompt } from "./bootstrapPrompt.js";
import { buildProjectSelectionPrompt } from "./bootstrapPrompt.js";
import { LogService } from "./logService.js"; import { LogService } from "./logService.js";
import { PtyService } from "./ptyService.js"; import { PtyService } from "./ptyService.js";
import { getClaudeEnv, getPublicRuntimeConfig } from "./config.js"; import { getClaudeEnv, getPublicRuntimeConfig } from "./config.js";
@@ -125,7 +124,10 @@ export class SessionManager {
this.currentProjectPath = resolved; this.currentProjectPath = resolved;
this.lastDirectedMember = null; this.lastDirectedMember = null;
this.setState({ currentProjectPath: resolved }); this.setState({
currentProjectPath: resolved,
teamActivated: false
});
this.addLog("system", `Current project set to ${resolved ?? "None"}`); this.addLog("system", `Current project set to ${resolved ?? "None"}`);
if (wasRunning) { if (wasRunning) {
@@ -133,9 +135,6 @@ export class SessionManager {
if (resolved) { if (resolved) {
await this.activateTeam(); await this.activateTeam();
} else {
const prompt = buildProjectSelectionPrompt(this.getActiveWorkspaceDir());
await this.sendRawPrompt(prompt, { label: `[project] Switched active project to ${this.getActiveWorkspaceDir()}` });
} }
} }
} }

View File

@@ -4,12 +4,13 @@ import SessionToolbar from "./components/SessionToolbar.jsx";
import ChatStream from "./components/ChatStream.jsx"; import ChatStream from "./components/ChatStream.jsx";
import PromptComposer from "./components/PromptComposer.jsx"; import PromptComposer from "./components/PromptComposer.jsx";
import TeamBoard from "./components/TeamBoard.jsx"; import TeamBoard from "./components/TeamBoard.jsx";
import ToastStack from "./components/ToastStack.jsx";
import { useSocket } from "./hooks/useSocket.js"; import { useSocket } from "./hooks/useSocket.js";
import { useSession } from "./hooks/useSession.js"; import { useSession } from "./hooks/useSession.js";
export default function App() { export default function App() {
const { socket, connected } = useSocket(); 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 [busy, setBusy] = useState(false);
const autoStartedRef = useRef(false); const autoStartedRef = useRef(false);
@@ -57,8 +58,6 @@ export default function App() {
</div> </div>
<ShellFrame> <ShellFrame>
{error ? <div className="error-banner">{error}</div> : null}
<div className="console-grid"> <div className="console-grid">
<div className="console-grid__side"> <div className="console-grid__side">
<TeamBoard chat={chat} /> <TeamBoard chat={chat} />
@@ -71,7 +70,7 @@ export default function App() {
<SessionToolbar <SessionToolbar
session={session} session={session}
busy={busy} busy={busy}
onStop={() => runAction(stopSession)} onClearProject={() => runAction(clearProject)}
onSelectProject={() => runAction(selectProject)} onSelectProject={() => runAction(selectProject)}
/> />
} }
@@ -82,6 +81,8 @@ export default function App() {
/> />
</div> </div>
</div> </div>
<ToastStack toasts={toasts} onDismiss={dismissToast} />
</ShellFrame> </ShellFrame>
</main> </main>
); );

View File

@@ -1,12 +1,10 @@
import PixelButton from "./PixelButton.jsx"; import PixelButton from "./PixelButton.jsx";
export default function SessionToolbar({ session, busy, onStop, onSelectProject }) { export default function SessionToolbar({ session, busy, onClearProject, onSelectProject }) {
const isRunning = session.status === "running";
return ( return (
<div className="session-toolbar session-toolbar--inline"> <div className="session-toolbar session-toolbar--inline">
<PixelButton tone="red" disabled={busy || (!isRunning && session.status !== "starting")} onClick={onStop}> <PixelButton tone="red" disabled={busy || !session.currentProjectPath} onClick={onClearProject}>
Stop Session Clean Project
</PixelButton> </PixelButton>
<PixelButton tone="amber" disabled={busy} onClick={onSelectProject}> <PixelButton tone="amber" disabled={busy} onClick={onSelectProject}>
Select Project Select Project

View File

@@ -0,0 +1,22 @@
export default function ToastStack({ toasts = [], onDismiss }) {
if (!toasts.length) {
return null;
}
return (
<div className="toast-stack" aria-live="polite" aria-atomic="true">
{toasts.map((toast) => (
<button
key={toast.id}
type="button"
className={`toast toast--${toast.tone || "error"}`}
onClick={() => onDismiss?.(toast.id)}
title="Dismiss notification"
>
<span className="toast__label">{toast.tone === "error" ? "ALERT" : "NOTICE"}</span>
<span className="toast__message">{toast.message}</span>
</button>
))}
</div>
);
}

View File

@@ -17,13 +17,30 @@ export function useSession(socket) {
const [session, setSession] = useState(initialState); const [session, setSession] = useState(initialState);
const [chat, setChat] = useState(""); const [chat, setChat] = useState("");
const [error, setError] = 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(() => { useEffect(() => {
const handleState = (value) => setSession(value); const handleState = (value) => setSession(value);
const handleChunk = ({ chunk }) => setChat((current) => current + chunk); const handleChunk = ({ chunk }) => setChat((current) => current + chunk);
const handleSnapshot = ({ content }) => setChat(content ?? ""); const handleSnapshot = ({ content }) => setChat(content ?? "");
const handleReset = () => setChat(""); const handleReset = () => setChat("");
const handleError = ({ message }) => setError(message); const handleError = ({ message }) => {
setError(message);
pushToast(message, "error");
};
socket.on("session:state", handleState); socket.on("session:state", handleState);
socket.on("chat:chunk", handleChunk); socket.on("chat:chunk", handleChunk);
@@ -46,6 +63,7 @@ export function useSession(socket) {
if (!response?.ok) { if (!response?.ok) {
const message = response?.error ?? "Unknown socket error"; const message = response?.error ?? "Unknown socket error";
setError(message); setError(message);
pushToast(message, "error");
reject(new Error(message)); reject(new Error(message));
return; return;
} }
@@ -60,7 +78,9 @@ export function useSession(socket) {
session, session,
chat, chat,
error, error,
toasts,
clearError: () => setError(""), clearError: () => setError(""),
dismissToast: (id) => setToasts((current) => current.filter((toast) => toast.id !== id)),
startSession: () => emitWithAck("session:start"), startSession: () => emitWithAck("session:start"),
stopSession: () => emitWithAck("session:stop"), stopSession: () => emitWithAck("session:stop"),
activateTeam: () => emitWithAck("team:activate"), activateTeam: () => emitWithAck("team:activate"),
@@ -78,6 +98,27 @@ export function useSession(socket) {
if (!response.ok || !payload.ok) { if (!response.ok || !payload.ok) {
const message = payload.error ?? "Project selection failed"; const message = payload.error ?? "Project selection failed";
setError(message); 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); throw new Error(message);
} }

View File

@@ -194,6 +194,47 @@
color: var(--accent-red); 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 { .console-grid {
display: grid; display: grid;
grid-template-columns: minmax(0, 1.55fr) minmax(320px, 0.9fr); grid-template-columns: minmax(0, 1.55fr) minmax(320px, 0.9fr);
@@ -233,6 +274,16 @@
flex-shrink: 0; flex-shrink: 0;
} }
.chat-panel {
display: flex;
flex-direction: column;
}
.chat-panel .panel-frame__body {
height: 62vh;
min-height: 62vh;
}
.chat-stream, .chat-stream,
.team-card__body { .team-card__body {
min-height: 420px; min-height: 420px;
@@ -244,6 +295,12 @@
linear-gradient(180deg, rgba(4, 8, 5, 0.88) 0%, rgba(8, 12, 9, 0.92) 100%); 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, .chat-stream pre,
.team-message pre { .team-message pre {
margin: 0; margin: 0;