@cryptiklemur/lattice 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bun.lock +705 -2
- package/client/index.html +1 -13
- package/client/package.json +6 -1
- package/client/src/App.tsx +2 -0
- package/client/src/commands.ts +36 -0
- package/client/src/components/chat/AttachmentChips.tsx +116 -0
- package/client/src/components/chat/ChatInput.tsx +250 -73
- package/client/src/components/chat/ChatView.tsx +242 -10
- package/client/src/components/chat/CommandPalette.tsx +162 -0
- package/client/src/components/chat/Message.tsx +23 -2
- package/client/src/components/chat/PromptQuestion.tsx +271 -0
- package/client/src/components/chat/TodoCard.tsx +57 -0
- package/client/src/components/chat/ToolResultRenderer.tsx +2 -1
- package/client/src/components/chat/VoiceRecorder.tsx +85 -0
- package/client/src/components/project-settings/ProjectMemory.tsx +12 -2
- package/client/src/components/project-settings/ProjectNotifications.tsx +48 -0
- package/client/src/components/project-settings/ProjectRules.tsx +10 -1
- package/client/src/components/project-settings/ProjectSettingsView.tsx +6 -0
- package/client/src/components/settings/Appearance.tsx +1 -0
- package/client/src/components/settings/ClaudeSettings.tsx +10 -0
- package/client/src/components/settings/Editor.tsx +123 -0
- package/client/src/components/settings/GlobalMcp.tsx +10 -1
- package/client/src/components/settings/GlobalMemory.tsx +19 -0
- package/client/src/components/settings/GlobalRules.tsx +149 -0
- package/client/src/components/settings/GlobalSkills.tsx +10 -0
- package/client/src/components/settings/Notifications.tsx +88 -0
- package/client/src/components/settings/SettingsView.tsx +12 -0
- package/client/src/components/settings/skill-shared.tsx +2 -1
- package/client/src/components/setup/SetupWizard.tsx +1 -1
- package/client/src/components/sidebar/NodeSettingsModal.tsx +23 -1
- package/client/src/components/sidebar/ProjectDropdown.tsx +176 -27
- package/client/src/components/sidebar/SettingsSidebar.tsx +11 -1
- package/client/src/components/sidebar/Sidebar.tsx +35 -2
- package/client/src/components/sidebar/UserIsland.tsx +18 -7
- package/client/src/components/ui/UpdatePrompt.tsx +47 -0
- package/client/src/components/workspace/FileBrowser.tsx +174 -0
- package/client/src/components/workspace/FileTree.tsx +129 -0
- package/client/src/components/workspace/FileViewer.tsx +211 -0
- package/client/src/components/workspace/NoteCard.tsx +119 -0
- package/client/src/components/workspace/NotesView.tsx +102 -0
- package/client/src/components/workspace/ScheduledTasksView.tsx +117 -0
- package/client/src/components/workspace/SplitPane.tsx +81 -0
- package/client/src/components/workspace/TabBar.tsx +185 -0
- package/client/src/components/workspace/TaskCard.tsx +158 -0
- package/client/src/components/workspace/TaskEditModal.tsx +114 -0
- package/client/src/components/{panels/Terminal.tsx → workspace/TerminalInstance.tsx} +50 -7
- package/client/src/components/workspace/TerminalView.tsx +110 -0
- package/client/src/components/workspace/WorkspaceView.tsx +116 -0
- package/client/src/hooks/useAttachments.ts +280 -0
- package/client/src/hooks/useEditorConfig.ts +28 -0
- package/client/src/hooks/useIdleDetection.ts +44 -0
- package/client/src/hooks/useInstallPrompt.ts +53 -0
- package/client/src/hooks/useNotifications.ts +54 -0
- package/client/src/hooks/useOnline.ts +6 -0
- package/client/src/hooks/useSession.ts +110 -4
- package/client/src/hooks/useSpinnerVerb.ts +36 -0
- package/client/src/hooks/useSwipeDrawer.ts +275 -0
- package/client/src/hooks/useVoiceRecorder.ts +123 -0
- package/client/src/hooks/useWorkspace.ts +48 -0
- package/client/src/providers/WebSocketProvider.tsx +18 -0
- package/client/src/router.tsx +48 -20
- package/client/src/stores/session.ts +136 -0
- package/client/src/stores/sidebar.ts +3 -2
- package/client/src/stores/workspace.ts +254 -0
- package/client/src/styles/global.css +123 -0
- package/client/src/utils/editorUrl.ts +62 -0
- package/client/vite.config.ts +53 -1
- package/package.json +1 -1
- package/server/src/daemon.ts +11 -1
- package/server/src/features/scheduler.ts +23 -0
- package/server/src/features/sticky-notes.ts +5 -3
- package/server/src/handlers/attachment.ts +172 -0
- package/server/src/handlers/chat.ts +43 -2
- package/server/src/handlers/editor.ts +40 -0
- package/server/src/handlers/fs.ts +10 -2
- package/server/src/handlers/memory.ts +3 -0
- package/server/src/handlers/notes.ts +4 -2
- package/server/src/handlers/scheduler.ts +18 -1
- package/server/src/handlers/session.ts +14 -8
- package/server/src/handlers/settings.ts +37 -2
- package/server/src/handlers/terminal.ts +13 -6
- package/server/src/project/pty-worker.cjs +83 -0
- package/server/src/project/sdk-bridge.ts +266 -11
- package/server/src/project/terminal.ts +78 -34
- package/shared/src/messages.ts +145 -4
- package/shared/src/models.ts +27 -1
- package/shared/src/project-settings.ts +1 -1
- package/tp.js +19 -0
- package/client/public/manifest.json +0 -24
- package/client/public/sw.js +0 -61
- package/client/src/components/panels/FileBrowser.tsx +0 -241
- package/client/src/components/panels/StickyNotes.tsx +0 -187
|
@@ -5,6 +5,7 @@ import { WebSocketContext, getWebSocketUrl } from "../hooks/useWebSocket";
|
|
|
5
5
|
import type { WebSocketStatus } from "../hooks/useWebSocket";
|
|
6
6
|
import { showToast } from "../components/ui/Toast";
|
|
7
7
|
import { getSessionStore } from "../stores/session";
|
|
8
|
+
import { sendNotification } from "../hooks/useNotifications";
|
|
8
9
|
|
|
9
10
|
interface WebSocketProviderProps {
|
|
10
11
|
children: ReactNode;
|
|
@@ -40,6 +41,7 @@ export function WebSocketProvider(props: WebSocketProviderProps) {
|
|
|
40
41
|
backoffRef.current = 1000;
|
|
41
42
|
if (hasConnectedRef.current) {
|
|
42
43
|
showToast("Reconnected to daemon", "info");
|
|
44
|
+
sendNotification("Lattice", "Reconnected to daemon", "connection");
|
|
43
45
|
ws.send(JSON.stringify({ type: "settings:get" }));
|
|
44
46
|
|
|
45
47
|
var sessionState = getSessionStore().state;
|
|
@@ -57,6 +59,21 @@ export function WebSocketProvider(props: WebSocketProviderProps) {
|
|
|
57
59
|
ws.onmessage = function (event: MessageEvent) {
|
|
58
60
|
try {
|
|
59
61
|
var msg = JSON.parse(event.data as string) as ServerMessage;
|
|
62
|
+
|
|
63
|
+
if (msg.type === "chat:done" && document.hidden) {
|
|
64
|
+
var sessionState = getSessionStore().state;
|
|
65
|
+
var sessionTitle = sessionState.activeSessionTitle || "Session";
|
|
66
|
+
sendNotification("Claude responded", sessionTitle, "chat-done");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (msg.type === "mesh:node_online") {
|
|
70
|
+
sendNotification("Lattice", (msg as any).nodeId + " came online", "mesh");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (msg.type === "mesh:node_offline") {
|
|
74
|
+
sendNotification("Lattice", (msg as any).nodeId + " went offline", "mesh");
|
|
75
|
+
}
|
|
76
|
+
|
|
60
77
|
var listeners = listenersRef.current.get(msg.type);
|
|
61
78
|
if (listeners) {
|
|
62
79
|
listeners.forEach(function (cb) {
|
|
@@ -75,6 +92,7 @@ export function WebSocketProvider(props: WebSocketProviderProps) {
|
|
|
75
92
|
wsRef.current = null;
|
|
76
93
|
if (hasConnectedRef.current) {
|
|
77
94
|
showToast("Disconnected from daemon. Reconnecting...", "warning");
|
|
95
|
+
sendNotification("Lattice", "Lost connection to daemon", "connection");
|
|
78
96
|
}
|
|
79
97
|
scheduleReconnect();
|
|
80
98
|
};
|
package/client/src/router.tsx
CHANGED
|
@@ -2,7 +2,7 @@ import { createRouter, createRootRoute, createRoute, createMemoryHistory } from
|
|
|
2
2
|
import { Outlet } from "@tanstack/react-router";
|
|
3
3
|
import { useState, useEffect, useRef } from "react";
|
|
4
4
|
import { Sidebar } from "./components/sidebar/Sidebar";
|
|
5
|
-
import {
|
|
5
|
+
import { WorkspaceView } from "./components/workspace/WorkspaceView";
|
|
6
6
|
import { SetupWizard } from "./components/setup/SetupWizard";
|
|
7
7
|
import { SettingsView } from "./components/settings/SettingsView";
|
|
8
8
|
import { ProjectSettingsView } from "./components/project-settings/ProjectSettingsView";
|
|
@@ -12,12 +12,14 @@ import { NodeSettingsModal } from "./components/sidebar/NodeSettingsModal";
|
|
|
12
12
|
import { AddProjectModal } from "./components/sidebar/AddProjectModal";
|
|
13
13
|
import { useSidebar } from "./hooks/useSidebar";
|
|
14
14
|
import { useWebSocket } from "./hooks/useWebSocket";
|
|
15
|
-
import {
|
|
15
|
+
import { useSwipeDrawer } from "./hooks/useSwipeDrawer";
|
|
16
|
+
import { exitSettings, getSidebarStore, handlePopState, closeDrawer, toggleDrawer } from "./stores/sidebar";
|
|
16
17
|
|
|
17
18
|
function LoadingScreen() {
|
|
18
19
|
var ws = useWebSocket();
|
|
19
20
|
var [dataReceived, setDataReceived] = useState(false);
|
|
20
21
|
var [minTimeElapsed, setMinTimeElapsed] = useState(false);
|
|
22
|
+
var [initialLoadDone, setInitialLoadDone] = useState(false);
|
|
21
23
|
var canvasRef = useRef<HTMLCanvasElement>(null);
|
|
22
24
|
var frameRef = useRef<number>(0);
|
|
23
25
|
|
|
@@ -37,16 +39,19 @@ function LoadingScreen() {
|
|
|
37
39
|
return function () { ws.unsubscribe("projects:list", handleProjects); };
|
|
38
40
|
}, [ws]);
|
|
39
41
|
|
|
40
|
-
var
|
|
41
|
-
var
|
|
42
|
+
var initialReady = dataReceived && minTimeElapsed;
|
|
43
|
+
var isDisconnected = initialLoadDone && ws.status !== "connected";
|
|
44
|
+
var ready = initialReady && !isDisconnected;
|
|
45
|
+
var visible = !initialReady || isDisconnected;
|
|
42
46
|
|
|
43
47
|
useEffect(function () {
|
|
44
|
-
if (!
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
if (initialReady && !initialLoadDone) {
|
|
49
|
+
var timer = setTimeout(function () {
|
|
50
|
+
setInitialLoadDone(true);
|
|
51
|
+
}, 300);
|
|
52
|
+
return function () { clearTimeout(timer); };
|
|
53
|
+
}
|
|
54
|
+
}, [initialReady, initialLoadDone]);
|
|
50
55
|
|
|
51
56
|
useEffect(function () {
|
|
52
57
|
var canvas = canvasRef.current;
|
|
@@ -228,17 +233,20 @@ function LoadingScreen() {
|
|
|
228
233
|
};
|
|
229
234
|
}, []);
|
|
230
235
|
|
|
231
|
-
if (!visible) {
|
|
236
|
+
if (!visible && initialReady) {
|
|
232
237
|
return null;
|
|
233
238
|
}
|
|
234
239
|
|
|
235
|
-
var statusText =
|
|
236
|
-
|
|
240
|
+
var statusText = isDisconnected
|
|
241
|
+
? "Reconnecting..."
|
|
242
|
+
: ws.status === "connecting" ? "Connecting..."
|
|
237
243
|
: "Loading projects...";
|
|
238
244
|
|
|
245
|
+
var bgClass = isDisconnected ? "bg-base-100/90" : "bg-base-100";
|
|
246
|
+
|
|
239
247
|
return (
|
|
240
248
|
<div
|
|
241
|
-
className="fixed inset-0 z-[9999] flex flex-col items-center justify-center
|
|
249
|
+
className={"fixed inset-0 z-[9999] flex flex-col items-center justify-center " + bgClass}
|
|
242
250
|
style={{ opacity: ready ? 0 : 1, transition: "opacity 300ms ease-out", pointerEvents: ready ? "none" : "auto" }}
|
|
243
251
|
>
|
|
244
252
|
<div className="flex flex-col items-center gap-7">
|
|
@@ -309,11 +317,27 @@ function RemoveProjectConfirm() {
|
|
|
309
317
|
}
|
|
310
318
|
|
|
311
319
|
function RootLayout() {
|
|
312
|
-
var [setupComplete, setSetupComplete] = useState
|
|
313
|
-
|
|
314
|
-
|
|
320
|
+
var [setupComplete, setSetupComplete] = useState<boolean | null>(null);
|
|
321
|
+
var ws = useWebSocket();
|
|
322
|
+
|
|
323
|
+
useEffect(function () {
|
|
324
|
+
function handleSettingsData(msg: { type: string; config?: { setupComplete?: boolean } }) {
|
|
325
|
+
if (msg.type !== "settings:data") return;
|
|
326
|
+
setSetupComplete(msg.config?.setupComplete === true);
|
|
327
|
+
}
|
|
328
|
+
ws.subscribe("settings:data", handleSettingsData as any);
|
|
329
|
+
if (ws.status === "connected") {
|
|
330
|
+
ws.send({ type: "settings:get" });
|
|
331
|
+
}
|
|
332
|
+
return function () {
|
|
333
|
+
ws.unsubscribe("settings:data", handleSettingsData as any);
|
|
334
|
+
};
|
|
335
|
+
}, [ws.status]);
|
|
315
336
|
|
|
316
337
|
var sidebar = useSidebar();
|
|
338
|
+
var drawerSideRef = useRef<HTMLDivElement>(null);
|
|
339
|
+
|
|
340
|
+
useSwipeDrawer(drawerSideRef, sidebar.drawerOpen, toggleDrawer, closeDrawer);
|
|
317
341
|
|
|
318
342
|
useEffect(function () {
|
|
319
343
|
function handleKeyDown(e: KeyboardEvent) {
|
|
@@ -332,6 +356,10 @@ function RootLayout() {
|
|
|
332
356
|
};
|
|
333
357
|
}, []);
|
|
334
358
|
|
|
359
|
+
if (setupComplete === null) {
|
|
360
|
+
return <LoadingScreen />;
|
|
361
|
+
}
|
|
362
|
+
|
|
335
363
|
if (!setupComplete) {
|
|
336
364
|
return (
|
|
337
365
|
<SetupWizard onComplete={function () { setSetupComplete(true); }} />
|
|
@@ -354,14 +382,14 @@ function RootLayout() {
|
|
|
354
382
|
<Outlet />
|
|
355
383
|
</div>
|
|
356
384
|
|
|
357
|
-
<div className="drawer-side z-50 h-full">
|
|
385
|
+
<div ref={drawerSideRef} className="drawer-side z-50 h-full">
|
|
358
386
|
<label
|
|
359
387
|
htmlFor="sidebar-drawer"
|
|
360
388
|
aria-label="close sidebar"
|
|
361
389
|
className="drawer-overlay"
|
|
362
390
|
onClick={closeDrawer}
|
|
363
391
|
/>
|
|
364
|
-
<div className="h-full w-[284px] flex flex-col overflow-hidden">
|
|
392
|
+
<div className="h-full w-full lg:w-[284px] flex flex-col overflow-hidden">
|
|
365
393
|
<Sidebar onSessionSelect={closeDrawer} />
|
|
366
394
|
</div>
|
|
367
395
|
</div>
|
|
@@ -393,7 +421,7 @@ function IndexPage() {
|
|
|
393
421
|
if (sidebar.activeView.type === "project-dashboard") {
|
|
394
422
|
return <ProjectDashboardView />;
|
|
395
423
|
}
|
|
396
|
-
return <
|
|
424
|
+
return <WorkspaceView />;
|
|
397
425
|
}
|
|
398
426
|
|
|
399
427
|
var rootRoute = createRootRoute({
|
|
@@ -37,6 +37,11 @@ export interface SessionState {
|
|
|
37
37
|
lastReadIndex: number | null;
|
|
38
38
|
historyLoading: boolean;
|
|
39
39
|
wasInterrupted: boolean;
|
|
40
|
+
promptSuggestion: string | null;
|
|
41
|
+
failedInput: string | null;
|
|
42
|
+
messageQueue: string[];
|
|
43
|
+
isBusy: boolean;
|
|
44
|
+
isPlanMode: boolean;
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
var sessionStore = new Store<SessionState>({
|
|
@@ -54,6 +59,11 @@ var sessionStore = new Store<SessionState>({
|
|
|
54
59
|
lastReadIndex: null,
|
|
55
60
|
historyLoading: false,
|
|
56
61
|
wasInterrupted: false,
|
|
62
|
+
promptSuggestion: null,
|
|
63
|
+
failedInput: null,
|
|
64
|
+
messageQueue: [],
|
|
65
|
+
isBusy: false,
|
|
66
|
+
isPlanMode: false,
|
|
57
67
|
});
|
|
58
68
|
|
|
59
69
|
var streamGeneration = 0;
|
|
@@ -208,6 +218,11 @@ export function setActiveSession(projectSlug: string | null, sessionId: string |
|
|
|
208
218
|
lastReadIndex: null,
|
|
209
219
|
historyLoading: true,
|
|
210
220
|
wasInterrupted: false,
|
|
221
|
+
promptSuggestion: null,
|
|
222
|
+
failedInput: null,
|
|
223
|
+
messageQueue: [],
|
|
224
|
+
isBusy: false,
|
|
225
|
+
isPlanMode: false,
|
|
211
226
|
};
|
|
212
227
|
});
|
|
213
228
|
}
|
|
@@ -264,6 +279,11 @@ export function clearSession(): void {
|
|
|
264
279
|
lastReadIndex: null,
|
|
265
280
|
historyLoading: false,
|
|
266
281
|
wasInterrupted: false,
|
|
282
|
+
promptSuggestion: null,
|
|
283
|
+
failedInput: null,
|
|
284
|
+
messageQueue: [],
|
|
285
|
+
isBusy: false,
|
|
286
|
+
isPlanMode: false,
|
|
267
287
|
};
|
|
268
288
|
});
|
|
269
289
|
}
|
|
@@ -280,6 +300,122 @@ export function setWasInterrupted(interrupted: boolean): void {
|
|
|
280
300
|
});
|
|
281
301
|
}
|
|
282
302
|
|
|
303
|
+
export function setPromptSuggestion(suggestion: string | null): void {
|
|
304
|
+
sessionStore.setState(function (state) {
|
|
305
|
+
return { ...state, promptSuggestion: suggestion };
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function setFailedInput(text: string | null): void {
|
|
310
|
+
sessionStore.setState(function (state) {
|
|
311
|
+
return { ...state, failedInput: text };
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function setSessionBusy(busy: boolean): void {
|
|
316
|
+
sessionStore.setState(function (state) {
|
|
317
|
+
return { ...state, isBusy: busy };
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function setIsPlanMode(active: boolean): void {
|
|
322
|
+
sessionStore.setState(function (state) {
|
|
323
|
+
return { ...state, isPlanMode: active };
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function addPromptQuestion(requestId: string, questions: Array<{ question: string; header: string; options: Array<{ label: string; description: string; preview?: string }>; multiSelect: boolean }>): void {
|
|
328
|
+
sessionStore.setState(function (state) {
|
|
329
|
+
return {
|
|
330
|
+
...state,
|
|
331
|
+
messages: [...state.messages, {
|
|
332
|
+
type: "prompt_question",
|
|
333
|
+
toolId: requestId,
|
|
334
|
+
promptQuestions: questions,
|
|
335
|
+
promptStatus: "pending",
|
|
336
|
+
timestamp: Date.now(),
|
|
337
|
+
} as HistoryMessage],
|
|
338
|
+
};
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function resolvePromptQuestion(requestId: string, answers: Record<string, string>): void {
|
|
343
|
+
sessionStore.setState(function (state) {
|
|
344
|
+
return {
|
|
345
|
+
...state,
|
|
346
|
+
messages: state.messages.map(function (msg) {
|
|
347
|
+
if (msg.type === "prompt_question" && msg.toolId === requestId) {
|
|
348
|
+
return { ...msg, promptAnswers: answers, promptStatus: "answered" };
|
|
349
|
+
}
|
|
350
|
+
return msg;
|
|
351
|
+
}),
|
|
352
|
+
};
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function addTodoUpdate(todos: Array<{ id: string; content: string; status: string; priority: string }>): void {
|
|
357
|
+
sessionStore.setState(function (state) {
|
|
358
|
+
var existingIndex = -1;
|
|
359
|
+
for (var i = state.messages.length - 1; i >= 0; i--) {
|
|
360
|
+
if (state.messages[i].type === "todo_update") {
|
|
361
|
+
existingIndex = i;
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (existingIndex >= 0) {
|
|
366
|
+
var updated = state.messages.slice();
|
|
367
|
+
updated[existingIndex] = { ...updated[existingIndex], todos: todos, timestamp: Date.now() } as HistoryMessage;
|
|
368
|
+
return { ...state, messages: updated };
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
...state,
|
|
372
|
+
messages: [...state.messages, {
|
|
373
|
+
type: "todo_update",
|
|
374
|
+
todos: todos,
|
|
375
|
+
timestamp: Date.now(),
|
|
376
|
+
} as HistoryMessage],
|
|
377
|
+
};
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function enqueueMessage(text: string): void {
|
|
382
|
+
sessionStore.setState(function (state) {
|
|
383
|
+
return { ...state, messageQueue: [...state.messageQueue, text] };
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export function dequeueMessage(): string | null {
|
|
388
|
+
var queue = sessionStore.state.messageQueue;
|
|
389
|
+
if (queue.length === 0) return null;
|
|
390
|
+
var first = queue[0];
|
|
391
|
+
sessionStore.setState(function (state) {
|
|
392
|
+
return { ...state, messageQueue: state.messageQueue.slice(1) };
|
|
393
|
+
});
|
|
394
|
+
return first;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export function removeQueuedMessage(index: number): void {
|
|
398
|
+
sessionStore.setState(function (state) {
|
|
399
|
+
var updated = state.messageQueue.slice();
|
|
400
|
+
updated.splice(index, 1);
|
|
401
|
+
return { ...state, messageQueue: updated };
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export function updateQueuedMessage(index: number, text: string): void {
|
|
406
|
+
sessionStore.setState(function (state) {
|
|
407
|
+
var updated = state.messageQueue.slice();
|
|
408
|
+
updated[index] = text;
|
|
409
|
+
return { ...state, messageQueue: updated };
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export function clearMessageQueue(): void {
|
|
414
|
+
sessionStore.setState(function (state) {
|
|
415
|
+
return { ...state, messageQueue: [] };
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
283
419
|
export function setContextBreakdown(breakdown: ContextBreakdown): void {
|
|
284
420
|
sessionStore.setState(function (state) {
|
|
285
421
|
return { ...state, contextBreakdown: breakdown };
|
|
@@ -5,7 +5,8 @@ export type { ProjectSettingsSection };
|
|
|
5
5
|
|
|
6
6
|
export type SettingsSection =
|
|
7
7
|
| "appearance" | "claude" | "environment"
|
|
8
|
-
| "mcp" | "skills" | "nodes"
|
|
8
|
+
| "mcp" | "skills" | "nodes" | "editor"
|
|
9
|
+
| "rules" | "memory" | "notifications";
|
|
9
10
|
|
|
10
11
|
export type SidebarMode = "project" | "settings";
|
|
11
12
|
|
|
@@ -30,7 +31,7 @@ export interface SidebarState {
|
|
|
30
31
|
confirmRemoveSlug: string | null;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
var SETTINGS_SECTIONS: SettingsSection[] = ["appearance", "claude", "environment", "mcp", "skills", "nodes"];
|
|
34
|
+
var SETTINGS_SECTIONS: SettingsSection[] = ["appearance", "claude", "environment", "mcp", "skills", "nodes", "editor", "rules", "memory", "notifications"];
|
|
34
35
|
|
|
35
36
|
function parseInitialUrl(): { projectSlug: string | null; sessionId: string | null; settingsSection: SettingsSection | null; projectSettingsSection: ProjectSettingsSection | null } {
|
|
36
37
|
var path = window.location.pathname;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { Store } from "@tanstack/react-store";
|
|
2
|
+
|
|
3
|
+
export type TabType = "chat" | "files" | "terminal" | "notes" | "tasks";
|
|
4
|
+
|
|
5
|
+
export interface Tab {
|
|
6
|
+
id: string;
|
|
7
|
+
type: TabType;
|
|
8
|
+
label: string;
|
|
9
|
+
closeable: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface Pane {
|
|
13
|
+
id: string;
|
|
14
|
+
tabIds: string[];
|
|
15
|
+
activeTabId: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface WorkspaceState {
|
|
19
|
+
tabs: Tab[];
|
|
20
|
+
panes: Pane[];
|
|
21
|
+
activePaneId: string;
|
|
22
|
+
splitDirection: "horizontal" | "vertical" | null;
|
|
23
|
+
splitRatio: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
var CHAT_TAB: Tab = { id: "chat", type: "chat", label: "Chat", closeable: false };
|
|
27
|
+
|
|
28
|
+
var DEFAULT_PANE: Pane = { id: "pane-1", tabIds: ["chat"], activeTabId: "chat" };
|
|
29
|
+
|
|
30
|
+
var workspaceStore = new Store<WorkspaceState>({
|
|
31
|
+
tabs: [CHAT_TAB],
|
|
32
|
+
panes: [DEFAULT_PANE],
|
|
33
|
+
activePaneId: "pane-1",
|
|
34
|
+
splitDirection: null,
|
|
35
|
+
splitRatio: 0.5,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export function getWorkspaceStore(): Store<WorkspaceState> {
|
|
39
|
+
return workspaceStore;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function openTab(type: TabType): void {
|
|
43
|
+
workspaceStore.setState(function (state) {
|
|
44
|
+
var existing = state.tabs.find(function (t) { return t.type === type; });
|
|
45
|
+
if (existing) {
|
|
46
|
+
var paneWithTab = state.panes.find(function (p) {
|
|
47
|
+
return p.tabIds.indexOf(existing!.id) !== -1;
|
|
48
|
+
});
|
|
49
|
+
if (paneWithTab) {
|
|
50
|
+
return {
|
|
51
|
+
...state,
|
|
52
|
+
activePaneId: paneWithTab.id,
|
|
53
|
+
panes: state.panes.map(function (p) {
|
|
54
|
+
if (p.id === paneWithTab!.id) {
|
|
55
|
+
return { ...p, activeTabId: existing!.id };
|
|
56
|
+
}
|
|
57
|
+
return p;
|
|
58
|
+
}),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return state;
|
|
62
|
+
}
|
|
63
|
+
var labels: Record<TabType, string> = {
|
|
64
|
+
chat: "Chat",
|
|
65
|
+
files: "Files",
|
|
66
|
+
terminal: "Terminal",
|
|
67
|
+
notes: "Notes",
|
|
68
|
+
tasks: "Tasks",
|
|
69
|
+
};
|
|
70
|
+
var tab: Tab = {
|
|
71
|
+
id: type,
|
|
72
|
+
type: type,
|
|
73
|
+
label: labels[type],
|
|
74
|
+
closeable: type !== "chat",
|
|
75
|
+
};
|
|
76
|
+
var newPanes = state.panes.map(function (p) {
|
|
77
|
+
if (p.id === state.activePaneId) {
|
|
78
|
+
return {
|
|
79
|
+
...p,
|
|
80
|
+
tabIds: [...p.tabIds, tab.id],
|
|
81
|
+
activeTabId: tab.id,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return p;
|
|
85
|
+
});
|
|
86
|
+
return {
|
|
87
|
+
...state,
|
|
88
|
+
tabs: [...state.tabs, tab],
|
|
89
|
+
panes: newPanes,
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function closeTab(tabId: string): void {
|
|
95
|
+
workspaceStore.setState(function (state) {
|
|
96
|
+
var tab = state.tabs.find(function (t) { return t.id === tabId; });
|
|
97
|
+
if (!tab || !tab.closeable) return state;
|
|
98
|
+
|
|
99
|
+
var filteredTabs = state.tabs.filter(function (t) { return t.id !== tabId; });
|
|
100
|
+
var newPanes = state.panes.map(function (p) {
|
|
101
|
+
var idx = p.tabIds.indexOf(tabId);
|
|
102
|
+
if (idx === -1) return p;
|
|
103
|
+
var newTabIds = p.tabIds.filter(function (id) { return id !== tabId; });
|
|
104
|
+
var newActiveTabId = p.activeTabId === tabId
|
|
105
|
+
? (newTabIds.length > 0 ? newTabIds[newTabIds.length - 1] : "")
|
|
106
|
+
: p.activeTabId;
|
|
107
|
+
return { ...p, tabIds: newTabIds, activeTabId: newActiveTabId };
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
var emptyPane = newPanes.find(function (p) { return p.tabIds.length === 0; });
|
|
111
|
+
if (emptyPane && newPanes.length > 1) {
|
|
112
|
+
var remainingPanes = newPanes.filter(function (p) { return p.tabIds.length > 0; });
|
|
113
|
+
return {
|
|
114
|
+
tabs: filteredTabs,
|
|
115
|
+
panes: remainingPanes,
|
|
116
|
+
activePaneId: remainingPanes[0].id,
|
|
117
|
+
splitDirection: null,
|
|
118
|
+
splitRatio: 0.5,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
...state,
|
|
124
|
+
tabs: filteredTabs,
|
|
125
|
+
panes: newPanes,
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function setActiveTab(tabId: string): void {
|
|
131
|
+
workspaceStore.setState(function (state) {
|
|
132
|
+
var paneWithTab = state.panes.find(function (p) {
|
|
133
|
+
return p.tabIds.indexOf(tabId) !== -1;
|
|
134
|
+
});
|
|
135
|
+
if (!paneWithTab) return state;
|
|
136
|
+
return {
|
|
137
|
+
...state,
|
|
138
|
+
activePaneId: paneWithTab.id,
|
|
139
|
+
panes: state.panes.map(function (p) {
|
|
140
|
+
if (p.id === paneWithTab!.id) {
|
|
141
|
+
return { ...p, activeTabId: tabId };
|
|
142
|
+
}
|
|
143
|
+
return p;
|
|
144
|
+
}),
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function resetWorkspace(): void {
|
|
150
|
+
workspaceStore.setState(function () {
|
|
151
|
+
return {
|
|
152
|
+
tabs: [CHAT_TAB],
|
|
153
|
+
panes: [{ id: "pane-1", tabIds: ["chat"], activeTabId: "chat" }],
|
|
154
|
+
activePaneId: "pane-1",
|
|
155
|
+
splitDirection: null,
|
|
156
|
+
splitRatio: 0.5,
|
|
157
|
+
};
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function splitPane(tabId: string, direction: "horizontal" | "vertical"): void {
|
|
162
|
+
workspaceStore.setState(function (state) {
|
|
163
|
+
if (state.panes.length >= 2) return state;
|
|
164
|
+
|
|
165
|
+
var sourcePane = state.panes.find(function (p) {
|
|
166
|
+
return p.tabIds.indexOf(tabId) !== -1;
|
|
167
|
+
});
|
|
168
|
+
if (!sourcePane) return state;
|
|
169
|
+
if (sourcePane.tabIds.length < 2) return state;
|
|
170
|
+
|
|
171
|
+
var newPaneId = "pane-" + Date.now();
|
|
172
|
+
var newSourceTabIds = sourcePane.tabIds.filter(function (id) { return id !== tabId; });
|
|
173
|
+
var newSourceActiveTabId = sourcePane.activeTabId === tabId
|
|
174
|
+
? newSourceTabIds[newSourceTabIds.length - 1]
|
|
175
|
+
: sourcePane.activeTabId;
|
|
176
|
+
|
|
177
|
+
var updatedSourcePane: Pane = {
|
|
178
|
+
...sourcePane,
|
|
179
|
+
tabIds: newSourceTabIds,
|
|
180
|
+
activeTabId: newSourceActiveTabId,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
var newPane: Pane = {
|
|
184
|
+
id: newPaneId,
|
|
185
|
+
tabIds: [tabId],
|
|
186
|
+
activeTabId: tabId,
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
var newPanes = state.panes.map(function (p) {
|
|
190
|
+
if (p.id === sourcePane!.id) return updatedSourcePane;
|
|
191
|
+
return p;
|
|
192
|
+
});
|
|
193
|
+
newPanes.push(newPane);
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
...state,
|
|
197
|
+
panes: newPanes,
|
|
198
|
+
activePaneId: newPaneId,
|
|
199
|
+
splitDirection: direction,
|
|
200
|
+
splitRatio: 0.5,
|
|
201
|
+
};
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function closePane(paneId: string): void {
|
|
206
|
+
workspaceStore.setState(function (state) {
|
|
207
|
+
if (state.panes.length <= 1) return state;
|
|
208
|
+
|
|
209
|
+
var closingPane = state.panes.find(function (p) { return p.id === paneId; });
|
|
210
|
+
var remainingPane = state.panes.find(function (p) { return p.id !== paneId; });
|
|
211
|
+
if (!closingPane || !remainingPane) return state;
|
|
212
|
+
|
|
213
|
+
var mergedTabIds = [...remainingPane.tabIds, ...closingPane.tabIds];
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
...state,
|
|
217
|
+
panes: [{
|
|
218
|
+
...remainingPane,
|
|
219
|
+
tabIds: mergedTabIds,
|
|
220
|
+
}],
|
|
221
|
+
activePaneId: remainingPane.id,
|
|
222
|
+
splitDirection: null,
|
|
223
|
+
splitRatio: 0.5,
|
|
224
|
+
};
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function setPaneActiveTab(paneId: string, tabId: string): void {
|
|
229
|
+
workspaceStore.setState(function (state) {
|
|
230
|
+
return {
|
|
231
|
+
...state,
|
|
232
|
+
activePaneId: paneId,
|
|
233
|
+
panes: state.panes.map(function (p) {
|
|
234
|
+
if (p.id === paneId) {
|
|
235
|
+
return { ...p, activeTabId: tabId };
|
|
236
|
+
}
|
|
237
|
+
return p;
|
|
238
|
+
}),
|
|
239
|
+
};
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function setSplitRatio(ratio: number): void {
|
|
244
|
+
var clamped = Math.min(0.8, Math.max(0.2, ratio));
|
|
245
|
+
workspaceStore.setState(function (state) {
|
|
246
|
+
return { ...state, splitRatio: clamped };
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function setActivePaneId(paneId: string): void {
|
|
251
|
+
workspaceStore.setState(function (state) {
|
|
252
|
+
return { ...state, activePaneId: paneId };
|
|
253
|
+
});
|
|
254
|
+
}
|