feat: proje temizleme ve bildirim deneyimini iyilestir
This commit is contained in:
@@ -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) => {
|
||||||
|
|||||||
@@ -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()}` });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
22
web/src/components/ToastStack.jsx
Normal file
22
web/src/components/ToastStack.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user