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;