@cryptiklemur/lattice 1.2.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.
Files changed (95) hide show
  1. package/.serena/project.yml +138 -0
  2. package/bun.lock +705 -2
  3. package/client/index.html +1 -13
  4. package/client/package.json +6 -1
  5. package/client/src/App.tsx +2 -0
  6. package/client/src/commands.ts +36 -0
  7. package/client/src/components/chat/AttachmentChips.tsx +116 -0
  8. package/client/src/components/chat/ChatInput.tsx +250 -73
  9. package/client/src/components/chat/ChatView.tsx +242 -10
  10. package/client/src/components/chat/CommandPalette.tsx +162 -0
  11. package/client/src/components/chat/Message.tsx +23 -2
  12. package/client/src/components/chat/PromptQuestion.tsx +271 -0
  13. package/client/src/components/chat/TodoCard.tsx +57 -0
  14. package/client/src/components/chat/ToolResultRenderer.tsx +2 -1
  15. package/client/src/components/chat/VoiceRecorder.tsx +85 -0
  16. package/client/src/components/project-settings/ProjectClaude.tsx +14 -0
  17. package/client/src/components/project-settings/ProjectMemory.tsx +12 -2
  18. package/client/src/components/project-settings/ProjectNotifications.tsx +48 -0
  19. package/client/src/components/project-settings/ProjectRules.tsx +10 -1
  20. package/client/src/components/project-settings/ProjectSettingsView.tsx +6 -0
  21. package/client/src/components/settings/Appearance.tsx +1 -0
  22. package/client/src/components/settings/ClaudeSettings.tsx +24 -0
  23. package/client/src/components/settings/Editor.tsx +123 -0
  24. package/client/src/components/settings/GlobalMcp.tsx +10 -1
  25. package/client/src/components/settings/GlobalMemory.tsx +19 -0
  26. package/client/src/components/settings/GlobalRules.tsx +149 -0
  27. package/client/src/components/settings/GlobalSkills.tsx +10 -0
  28. package/client/src/components/settings/Notifications.tsx +88 -0
  29. package/client/src/components/settings/SettingsView.tsx +12 -0
  30. package/client/src/components/settings/skill-shared.tsx +2 -1
  31. package/client/src/components/setup/SetupWizard.tsx +1 -1
  32. package/client/src/components/sidebar/AddProjectModal.tsx +3 -2
  33. package/client/src/components/sidebar/NodeSettingsModal.tsx +23 -1
  34. package/client/src/components/sidebar/ProjectDropdown.tsx +176 -27
  35. package/client/src/components/sidebar/SettingsSidebar.tsx +11 -1
  36. package/client/src/components/sidebar/Sidebar.tsx +35 -2
  37. package/client/src/components/sidebar/UserIsland.tsx +18 -7
  38. package/client/src/components/ui/UpdatePrompt.tsx +47 -0
  39. package/client/src/components/workspace/FileBrowser.tsx +174 -0
  40. package/client/src/components/workspace/FileTree.tsx +129 -0
  41. package/client/src/components/workspace/FileViewer.tsx +211 -0
  42. package/client/src/components/workspace/NoteCard.tsx +119 -0
  43. package/client/src/components/workspace/NotesView.tsx +102 -0
  44. package/client/src/components/workspace/ScheduledTasksView.tsx +117 -0
  45. package/client/src/components/workspace/SplitPane.tsx +81 -0
  46. package/client/src/components/workspace/TabBar.tsx +185 -0
  47. package/client/src/components/workspace/TaskCard.tsx +158 -0
  48. package/client/src/components/workspace/TaskEditModal.tsx +114 -0
  49. package/client/src/components/{panels/Terminal.tsx → workspace/TerminalInstance.tsx} +50 -7
  50. package/client/src/components/workspace/TerminalView.tsx +110 -0
  51. package/client/src/components/workspace/WorkspaceView.tsx +116 -0
  52. package/client/src/hooks/useAttachments.ts +280 -0
  53. package/client/src/hooks/useEditorConfig.ts +28 -0
  54. package/client/src/hooks/useIdleDetection.ts +44 -0
  55. package/client/src/hooks/useInstallPrompt.ts +53 -0
  56. package/client/src/hooks/useNotifications.ts +54 -0
  57. package/client/src/hooks/useOnline.ts +6 -0
  58. package/client/src/hooks/useSession.ts +110 -4
  59. package/client/src/hooks/useSpinnerVerb.ts +36 -0
  60. package/client/src/hooks/useSwipeDrawer.ts +275 -0
  61. package/client/src/hooks/useVoiceRecorder.ts +123 -0
  62. package/client/src/hooks/useWorkspace.ts +48 -0
  63. package/client/src/providers/WebSocketProvider.tsx +18 -0
  64. package/client/src/router.tsx +48 -20
  65. package/client/src/stores/session.ts +136 -0
  66. package/client/src/stores/sidebar.ts +3 -2
  67. package/client/src/stores/workspace.ts +254 -0
  68. package/client/src/styles/global.css +131 -0
  69. package/client/src/utils/editorUrl.ts +62 -0
  70. package/client/vite.config.ts +53 -1
  71. package/package.json +1 -1
  72. package/server/src/daemon.ts +11 -1
  73. package/server/src/features/scheduler.ts +23 -0
  74. package/server/src/features/sticky-notes.ts +5 -3
  75. package/server/src/handlers/attachment.ts +172 -0
  76. package/server/src/handlers/chat.ts +43 -2
  77. package/server/src/handlers/editor.ts +40 -0
  78. package/server/src/handlers/fs.ts +10 -2
  79. package/server/src/handlers/memory.ts +3 -0
  80. package/server/src/handlers/notes.ts +4 -2
  81. package/server/src/handlers/scheduler.ts +18 -1
  82. package/server/src/handlers/session.ts +14 -8
  83. package/server/src/handlers/settings.ts +37 -2
  84. package/server/src/handlers/terminal.ts +13 -6
  85. package/server/src/project/pty-worker.cjs +83 -0
  86. package/server/src/project/sdk-bridge.ts +266 -11
  87. package/server/src/project/terminal.ts +78 -34
  88. package/shared/src/messages.ts +145 -4
  89. package/shared/src/models.ts +27 -1
  90. package/shared/src/project-settings.ts +1 -1
  91. package/tp.js +19 -0
  92. package/client/public/manifest.json +0 -24
  93. package/client/public/sw.js +0 -61
  94. package/client/src/components/panels/FileBrowser.tsx +0 -241
  95. 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
  };
@@ -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 { ChatView } from "./components/chat/ChatView";
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 { exitSettings, getSidebarStore, handlePopState, closeDrawer } from "./stores/sidebar";
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 ready = dataReceived && minTimeElapsed;
41
- var [visible, setVisible] = useState(true);
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 (!ready) return;
45
- var timer = setTimeout(function () {
46
- setVisible(false);
47
- }, 300);
48
- return function () { clearTimeout(timer); };
49
- }, [ready]);
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 = ws.status === "connecting" ? "Connecting..."
236
- : ws.status === "disconnected" ? "Reconnecting..."
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 bg-base-100"
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(function () {
313
- return localStorage.getItem("lattice-setup-complete") === "1";
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 <ChatView />;
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
+ }