@cryptiklemur/lattice 1.3.0 → 1.5.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 (109) hide show
  1. package/bun.lock +776 -2
  2. package/client/index.html +1 -13
  3. package/client/package.json +7 -1
  4. package/client/src/App.tsx +2 -0
  5. package/client/src/commands.ts +36 -0
  6. package/client/src/components/analytics/AnalyticsView.tsx +61 -0
  7. package/client/src/components/analytics/ChartCard.tsx +22 -0
  8. package/client/src/components/analytics/PeriodSelector.tsx +42 -0
  9. package/client/src/components/analytics/QuickStats.tsx +99 -0
  10. package/client/src/components/analytics/charts/CostAreaChart.tsx +83 -0
  11. package/client/src/components/analytics/charts/CostDistributionChart.tsx +62 -0
  12. package/client/src/components/analytics/charts/CostDonutChart.tsx +93 -0
  13. package/client/src/components/analytics/charts/CumulativeCostChart.tsx +62 -0
  14. package/client/src/components/analytics/charts/SessionBubbleChart.tsx +122 -0
  15. package/client/src/components/chat/AttachmentChips.tsx +116 -0
  16. package/client/src/components/chat/ChatInput.tsx +250 -73
  17. package/client/src/components/chat/ChatView.tsx +242 -10
  18. package/client/src/components/chat/CommandPalette.tsx +162 -0
  19. package/client/src/components/chat/Message.tsx +23 -2
  20. package/client/src/components/chat/PromptQuestion.tsx +271 -0
  21. package/client/src/components/chat/TodoCard.tsx +57 -0
  22. package/client/src/components/chat/ToolResultRenderer.tsx +2 -1
  23. package/client/src/components/chat/VoiceRecorder.tsx +85 -0
  24. package/client/src/components/dashboard/DashboardView.tsx +5 -0
  25. package/client/src/components/project-settings/ProjectMemory.tsx +12 -2
  26. package/client/src/components/project-settings/ProjectNotifications.tsx +48 -0
  27. package/client/src/components/project-settings/ProjectRules.tsx +10 -1
  28. package/client/src/components/project-settings/ProjectSettingsView.tsx +6 -0
  29. package/client/src/components/settings/Appearance.tsx +1 -0
  30. package/client/src/components/settings/ClaudeSettings.tsx +10 -0
  31. package/client/src/components/settings/Editor.tsx +123 -0
  32. package/client/src/components/settings/GlobalMcp.tsx +10 -1
  33. package/client/src/components/settings/GlobalMemory.tsx +19 -0
  34. package/client/src/components/settings/GlobalRules.tsx +149 -0
  35. package/client/src/components/settings/GlobalSkills.tsx +10 -0
  36. package/client/src/components/settings/Notifications.tsx +88 -0
  37. package/client/src/components/settings/SettingsView.tsx +12 -0
  38. package/client/src/components/settings/skill-shared.tsx +2 -1
  39. package/client/src/components/setup/SetupWizard.tsx +1 -1
  40. package/client/src/components/sidebar/NodeSettingsModal.tsx +23 -1
  41. package/client/src/components/sidebar/ProjectDropdown.tsx +176 -27
  42. package/client/src/components/sidebar/SettingsSidebar.tsx +11 -1
  43. package/client/src/components/sidebar/Sidebar.tsx +43 -2
  44. package/client/src/components/sidebar/UserIsland.tsx +18 -7
  45. package/client/src/components/ui/UpdatePrompt.tsx +47 -0
  46. package/client/src/components/workspace/FileBrowser.tsx +174 -0
  47. package/client/src/components/workspace/FileTree.tsx +129 -0
  48. package/client/src/components/workspace/FileViewer.tsx +211 -0
  49. package/client/src/components/workspace/NoteCard.tsx +119 -0
  50. package/client/src/components/workspace/NotesView.tsx +102 -0
  51. package/client/src/components/workspace/ScheduledTasksView.tsx +117 -0
  52. package/client/src/components/workspace/SplitPane.tsx +81 -0
  53. package/client/src/components/workspace/TabBar.tsx +185 -0
  54. package/client/src/components/workspace/TaskCard.tsx +158 -0
  55. package/client/src/components/workspace/TaskEditModal.tsx +114 -0
  56. package/client/src/components/{panels/Terminal.tsx → workspace/TerminalInstance.tsx} +50 -7
  57. package/client/src/components/workspace/TerminalView.tsx +110 -0
  58. package/client/src/components/workspace/WorkspaceView.tsx +116 -0
  59. package/client/src/hooks/useAnalytics.ts +75 -0
  60. package/client/src/hooks/useAttachments.ts +280 -0
  61. package/client/src/hooks/useEditorConfig.ts +28 -0
  62. package/client/src/hooks/useIdleDetection.ts +44 -0
  63. package/client/src/hooks/useInstallPrompt.ts +53 -0
  64. package/client/src/hooks/useNotifications.ts +54 -0
  65. package/client/src/hooks/useOnline.ts +6 -0
  66. package/client/src/hooks/useSession.ts +110 -4
  67. package/client/src/hooks/useSpinnerVerb.ts +36 -0
  68. package/client/src/hooks/useSwipeDrawer.ts +275 -0
  69. package/client/src/hooks/useVoiceRecorder.ts +123 -0
  70. package/client/src/hooks/useWorkspace.ts +48 -0
  71. package/client/src/providers/WebSocketProvider.tsx +18 -0
  72. package/client/src/router.tsx +52 -20
  73. package/client/src/stores/analytics.ts +54 -0
  74. package/client/src/stores/session.ts +136 -0
  75. package/client/src/stores/sidebar.ts +11 -2
  76. package/client/src/stores/workspace.ts +254 -0
  77. package/client/src/styles/global.css +123 -0
  78. package/client/src/utils/editorUrl.ts +62 -0
  79. package/client/vite.config.ts +54 -1
  80. package/package.json +1 -1
  81. package/server/src/analytics/engine.ts +491 -0
  82. package/server/src/daemon.ts +12 -1
  83. package/server/src/features/scheduler.ts +23 -0
  84. package/server/src/features/sticky-notes.ts +5 -3
  85. package/server/src/handlers/analytics.ts +34 -0
  86. package/server/src/handlers/attachment.ts +172 -0
  87. package/server/src/handlers/chat.ts +43 -2
  88. package/server/src/handlers/editor.ts +40 -0
  89. package/server/src/handlers/fs.ts +10 -2
  90. package/server/src/handlers/memory.ts +3 -0
  91. package/server/src/handlers/notes.ts +4 -2
  92. package/server/src/handlers/scheduler.ts +18 -1
  93. package/server/src/handlers/session.ts +14 -8
  94. package/server/src/handlers/settings.ts +37 -2
  95. package/server/src/handlers/terminal.ts +13 -6
  96. package/server/src/project/pty-worker.cjs +83 -0
  97. package/server/src/project/sdk-bridge.ts +266 -11
  98. package/server/src/project/session.ts +4 -4
  99. package/server/src/project/terminal.ts +78 -34
  100. package/shared/src/analytics.ts +24 -0
  101. package/shared/src/index.ts +1 -0
  102. package/shared/src/messages.ts +173 -4
  103. package/shared/src/models.ts +27 -1
  104. package/shared/src/project-settings.ts +1 -1
  105. package/tp.js +19 -0
  106. package/client/public/manifest.json +0 -24
  107. package/client/public/sw.js +0 -61
  108. package/client/src/components/panels/FileBrowser.tsx +0 -241
  109. package/client/src/components/panels/StickyNotes.tsx +0 -187
@@ -0,0 +1,123 @@
1
+ import { useState, useCallback, useRef } from "react";
2
+
3
+ export interface UseVoiceRecorderReturn {
4
+ isRecording: boolean;
5
+ isSupported: boolean;
6
+ isSpeaking: boolean;
7
+ elapsed: number;
8
+ interimTranscript: string;
9
+ start: () => void;
10
+ stop: () => string;
11
+ cancel: () => void;
12
+ }
13
+
14
+ export function useVoiceRecorder(): UseVoiceRecorderReturn {
15
+ var [isRecording, setIsRecording] = useState(false);
16
+ var [isSpeaking, setIsSpeaking] = useState(false);
17
+ var [elapsed, setElapsed] = useState(0);
18
+ var [interimTranscript, setInterimTranscript] = useState("");
19
+
20
+ var recognitionRef = useRef<SpeechRecognition | null>(null);
21
+ var timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
22
+ var finalTranscriptRef = useRef("");
23
+ var startTimeRef = useRef(0);
24
+
25
+ var SpeechRecognitionClass = typeof window !== "undefined"
26
+ ? (window as unknown as { SpeechRecognition?: typeof SpeechRecognition; webkitSpeechRecognition?: typeof SpeechRecognition }).SpeechRecognition
27
+ || (window as unknown as { webkitSpeechRecognition?: typeof SpeechRecognition }).webkitSpeechRecognition
28
+ : undefined;
29
+
30
+ var isSupported = !!SpeechRecognitionClass;
31
+
32
+ var cleanup = useCallback(function () {
33
+ if (timerRef.current) {
34
+ clearInterval(timerRef.current);
35
+ timerRef.current = null;
36
+ }
37
+ setIsRecording(false);
38
+ setIsSpeaking(false);
39
+ setElapsed(0);
40
+ setInterimTranscript("");
41
+ recognitionRef.current = null;
42
+ }, []);
43
+
44
+ var start = useCallback(function () {
45
+ if (!SpeechRecognitionClass || isRecording) return;
46
+
47
+ var recognition = new SpeechRecognitionClass();
48
+ recognition.continuous = true;
49
+ recognition.interimResults = true;
50
+ recognition.lang = navigator.language || "en-US";
51
+
52
+ recognitionRef.current = recognition;
53
+ finalTranscriptRef.current = "";
54
+
55
+ recognition.onresult = function (event: SpeechRecognitionEvent) {
56
+ var interim = "";
57
+ var final = "";
58
+ for (var i = 0; i < event.results.length; i++) {
59
+ var result = event.results[i];
60
+ if (result.isFinal) {
61
+ final += result[0].transcript;
62
+ } else {
63
+ interim += result[0].transcript;
64
+ }
65
+ }
66
+ finalTranscriptRef.current = final;
67
+ setInterimTranscript(final + interim);
68
+ };
69
+
70
+ recognition.onspeechstart = function () {
71
+ setIsSpeaking(true);
72
+ };
73
+
74
+ recognition.onspeechend = function () {
75
+ setIsSpeaking(false);
76
+ };
77
+
78
+ recognition.onerror = function (event: SpeechRecognitionErrorEvent) {
79
+ console.error("[voice] Recognition error:", event.error);
80
+ cleanup();
81
+ };
82
+
83
+ recognition.onend = function () {
84
+ cleanup();
85
+ };
86
+
87
+ recognition.start();
88
+ setIsRecording(true);
89
+ startTimeRef.current = Date.now();
90
+
91
+ timerRef.current = setInterval(function () {
92
+ setElapsed(Math.floor((Date.now() - startTimeRef.current) / 1000));
93
+ }, 1000);
94
+ }, [SpeechRecognitionClass, isRecording, cleanup]);
95
+
96
+ var stop = useCallback(function (): string {
97
+ if (recognitionRef.current) {
98
+ recognitionRef.current.stop();
99
+ }
100
+ var transcript = finalTranscriptRef.current || interimTranscript;
101
+ cleanup();
102
+ return transcript;
103
+ }, [interimTranscript, cleanup]);
104
+
105
+ var cancel = useCallback(function () {
106
+ if (recognitionRef.current) {
107
+ recognitionRef.current.abort();
108
+ }
109
+ finalTranscriptRef.current = "";
110
+ cleanup();
111
+ }, [cleanup]);
112
+
113
+ return {
114
+ isRecording,
115
+ isSupported,
116
+ isSpeaking,
117
+ elapsed,
118
+ interimTranscript,
119
+ start,
120
+ stop,
121
+ cancel,
122
+ };
123
+ }
@@ -0,0 +1,48 @@
1
+ import { useStore } from "@tanstack/react-store";
2
+ import {
3
+ getWorkspaceStore,
4
+ openTab,
5
+ closeTab,
6
+ setActiveTab,
7
+ resetWorkspace,
8
+ splitPane,
9
+ closePane,
10
+ setPaneActiveTab,
11
+ setSplitRatio,
12
+ setActivePaneId,
13
+ } from "../stores/workspace";
14
+ import type { WorkspaceState, TabType } from "../stores/workspace";
15
+
16
+ export interface UseWorkspaceReturn extends WorkspaceState {
17
+ activeTabId: string;
18
+ openTab: (type: TabType) => void;
19
+ closeTab: (tabId: string) => void;
20
+ setActiveTab: (tabId: string) => void;
21
+ resetWorkspace: () => void;
22
+ splitPane: (tabId: string, direction: "horizontal" | "vertical") => void;
23
+ closePane: (paneId: string) => void;
24
+ setPaneActiveTab: (paneId: string, tabId: string) => void;
25
+ setSplitRatio: (ratio: number) => void;
26
+ setActivePaneId: (paneId: string) => void;
27
+ }
28
+
29
+ export function useWorkspace(): UseWorkspaceReturn {
30
+ var state = useStore(getWorkspaceStore(), function (s) { return s; });
31
+ return {
32
+ tabs: state.tabs,
33
+ panes: state.panes,
34
+ activePaneId: state.activePaneId,
35
+ splitDirection: state.splitDirection,
36
+ splitRatio: state.splitRatio,
37
+ activeTabId: state.panes.find(function (p) { return p.id === state.activePaneId; })?.activeTabId ?? state.panes[0]?.activeTabId ?? "chat",
38
+ openTab: openTab,
39
+ closeTab: closeTab,
40
+ setActiveTab: setActiveTab,
41
+ resetWorkspace: resetWorkspace,
42
+ splitPane: splitPane,
43
+ closePane: closePane,
44
+ setPaneActiveTab: setPaneActiveTab,
45
+ setSplitRatio: setSplitRatio,
46
+ setActivePaneId: setActivePaneId,
47
+ };
48
+ }
@@ -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,22 +2,25 @@ 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";
9
9
  import { DashboardView } from "./components/dashboard/DashboardView";
10
10
  import { ProjectDashboardView } from "./components/dashboard/ProjectDashboardView";
11
+ import { AnalyticsView } from "./components/analytics/AnalyticsView";
11
12
  import { NodeSettingsModal } from "./components/sidebar/NodeSettingsModal";
12
13
  import { AddProjectModal } from "./components/sidebar/AddProjectModal";
13
14
  import { useSidebar } from "./hooks/useSidebar";
14
15
  import { useWebSocket } from "./hooks/useWebSocket";
15
- import { exitSettings, getSidebarStore, handlePopState, closeDrawer } from "./stores/sidebar";
16
+ import { useSwipeDrawer } from "./hooks/useSwipeDrawer";
17
+ import { exitSettings, getSidebarStore, handlePopState, closeDrawer, toggleDrawer } from "./stores/sidebar";
16
18
 
17
19
  function LoadingScreen() {
18
20
  var ws = useWebSocket();
19
21
  var [dataReceived, setDataReceived] = useState(false);
20
22
  var [minTimeElapsed, setMinTimeElapsed] = useState(false);
23
+ var [initialLoadDone, setInitialLoadDone] = useState(false);
21
24
  var canvasRef = useRef<HTMLCanvasElement>(null);
22
25
  var frameRef = useRef<number>(0);
23
26
 
@@ -37,16 +40,19 @@ function LoadingScreen() {
37
40
  return function () { ws.unsubscribe("projects:list", handleProjects); };
38
41
  }, [ws]);
39
42
 
40
- var ready = dataReceived && minTimeElapsed;
41
- var [visible, setVisible] = useState(true);
43
+ var initialReady = dataReceived && minTimeElapsed;
44
+ var isDisconnected = initialLoadDone && ws.status !== "connected";
45
+ var ready = initialReady && !isDisconnected;
46
+ var visible = !initialReady || isDisconnected;
42
47
 
43
48
  useEffect(function () {
44
- if (!ready) return;
45
- var timer = setTimeout(function () {
46
- setVisible(false);
47
- }, 300);
48
- return function () { clearTimeout(timer); };
49
- }, [ready]);
49
+ if (initialReady && !initialLoadDone) {
50
+ var timer = setTimeout(function () {
51
+ setInitialLoadDone(true);
52
+ }, 300);
53
+ return function () { clearTimeout(timer); };
54
+ }
55
+ }, [initialReady, initialLoadDone]);
50
56
 
51
57
  useEffect(function () {
52
58
  var canvas = canvasRef.current;
@@ -228,17 +234,20 @@ function LoadingScreen() {
228
234
  };
229
235
  }, []);
230
236
 
231
- if (!visible) {
237
+ if (!visible && initialReady) {
232
238
  return null;
233
239
  }
234
240
 
235
- var statusText = ws.status === "connecting" ? "Connecting..."
236
- : ws.status === "disconnected" ? "Reconnecting..."
241
+ var statusText = isDisconnected
242
+ ? "Reconnecting..."
243
+ : ws.status === "connecting" ? "Connecting..."
237
244
  : "Loading projects...";
238
245
 
246
+ var bgClass = isDisconnected ? "bg-base-100/90" : "bg-base-100";
247
+
239
248
  return (
240
249
  <div
241
- className="fixed inset-0 z-[9999] flex flex-col items-center justify-center bg-base-100"
250
+ className={"fixed inset-0 z-[9999] flex flex-col items-center justify-center " + bgClass}
242
251
  style={{ opacity: ready ? 0 : 1, transition: "opacity 300ms ease-out", pointerEvents: ready ? "none" : "auto" }}
243
252
  >
244
253
  <div className="flex flex-col items-center gap-7">
@@ -309,11 +318,27 @@ function RemoveProjectConfirm() {
309
318
  }
310
319
 
311
320
  function RootLayout() {
312
- var [setupComplete, setSetupComplete] = useState(function () {
313
- return localStorage.getItem("lattice-setup-complete") === "1";
314
- });
321
+ var [setupComplete, setSetupComplete] = useState<boolean | null>(null);
322
+ var ws = useWebSocket();
323
+
324
+ useEffect(function () {
325
+ function handleSettingsData(msg: { type: string; config?: { setupComplete?: boolean } }) {
326
+ if (msg.type !== "settings:data") return;
327
+ setSetupComplete(msg.config?.setupComplete === true);
328
+ }
329
+ ws.subscribe("settings:data", handleSettingsData as any);
330
+ if (ws.status === "connected") {
331
+ ws.send({ type: "settings:get" });
332
+ }
333
+ return function () {
334
+ ws.unsubscribe("settings:data", handleSettingsData as any);
335
+ };
336
+ }, [ws.status]);
315
337
 
316
338
  var sidebar = useSidebar();
339
+ var drawerSideRef = useRef<HTMLDivElement>(null);
340
+
341
+ useSwipeDrawer(drawerSideRef, sidebar.drawerOpen, toggleDrawer, closeDrawer);
317
342
 
318
343
  useEffect(function () {
319
344
  function handleKeyDown(e: KeyboardEvent) {
@@ -332,6 +357,10 @@ function RootLayout() {
332
357
  };
333
358
  }, []);
334
359
 
360
+ if (setupComplete === null) {
361
+ return <LoadingScreen />;
362
+ }
363
+
335
364
  if (!setupComplete) {
336
365
  return (
337
366
  <SetupWizard onComplete={function () { setSetupComplete(true); }} />
@@ -354,14 +383,14 @@ function RootLayout() {
354
383
  <Outlet />
355
384
  </div>
356
385
 
357
- <div className="drawer-side z-50 h-full">
386
+ <div ref={drawerSideRef} className="drawer-side z-50 h-full">
358
387
  <label
359
388
  htmlFor="sidebar-drawer"
360
389
  aria-label="close sidebar"
361
390
  className="drawer-overlay"
362
391
  onClick={closeDrawer}
363
392
  />
364
- <div className="h-full w-[284px] flex flex-col overflow-hidden">
393
+ <div className="h-full w-full lg:w-[284px] flex flex-col overflow-hidden">
365
394
  <Sidebar onSessionSelect={closeDrawer} />
366
395
  </div>
367
396
  </div>
@@ -393,7 +422,10 @@ function IndexPage() {
393
422
  if (sidebar.activeView.type === "project-dashboard") {
394
423
  return <ProjectDashboardView />;
395
424
  }
396
- return <ChatView />;
425
+ if (sidebar.activeView.type === "analytics") {
426
+ return <AnalyticsView />;
427
+ }
428
+ return <WorkspaceView />;
397
429
  }
398
430
 
399
431
  var rootRoute = createRootRoute({
@@ -0,0 +1,54 @@
1
+ import { Store } from "@tanstack/react-store";
2
+ import type { AnalyticsPayload, AnalyticsPeriod, AnalyticsScope } from "@lattice/shared";
3
+
4
+ export interface AnalyticsState {
5
+ data: AnalyticsPayload | null;
6
+ loading: boolean;
7
+ error: string | null;
8
+ period: AnalyticsPeriod;
9
+ scope: AnalyticsScope;
10
+ projectSlug: string | null;
11
+ }
12
+
13
+ var analyticsStore = new Store<AnalyticsState>({
14
+ data: null,
15
+ loading: false,
16
+ error: null,
17
+ period: "7d",
18
+ scope: "global",
19
+ projectSlug: null,
20
+ });
21
+
22
+ export function getAnalyticsStore(): Store<AnalyticsState> {
23
+ return analyticsStore;
24
+ }
25
+
26
+ export function setAnalyticsData(data: AnalyticsPayload): void {
27
+ analyticsStore.setState(function (state) {
28
+ return { ...state, data: data, loading: false, error: null };
29
+ });
30
+ }
31
+
32
+ export function setAnalyticsLoading(loading: boolean): void {
33
+ analyticsStore.setState(function (state) {
34
+ return { ...state, loading: loading };
35
+ });
36
+ }
37
+
38
+ export function setAnalyticsError(error: string): void {
39
+ analyticsStore.setState(function (state) {
40
+ return { ...state, error: error, loading: false };
41
+ });
42
+ }
43
+
44
+ export function setAnalyticsPeriod(period: AnalyticsPeriod): void {
45
+ analyticsStore.setState(function (state) {
46
+ return { ...state, period: period };
47
+ });
48
+ }
49
+
50
+ export function setAnalyticsScope(scope: AnalyticsScope, projectSlug?: string): void {
51
+ analyticsStore.setState(function (state) {
52
+ return { ...state, scope: scope, projectSlug: projectSlug || null };
53
+ });
54
+ }
@@ -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,13 +5,15 @@ 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
 
12
13
  export type ActiveView =
13
14
  | { type: "dashboard" }
14
15
  | { type: "project-dashboard" }
16
+ | { type: "analytics" }
15
17
  | { type: "chat" }
16
18
  | { type: "settings"; section: SettingsSection }
17
19
  | { type: "project-settings"; section: ProjectSettingsSection };
@@ -30,7 +32,7 @@ export interface SidebarState {
30
32
  confirmRemoveSlug: string | null;
31
33
  }
32
34
 
33
- var SETTINGS_SECTIONS: SettingsSection[] = ["appearance", "claude", "environment", "mcp", "skills", "nodes"];
35
+ var SETTINGS_SECTIONS: SettingsSection[] = ["appearance", "claude", "environment", "mcp", "skills", "nodes", "editor", "rules", "memory", "notifications"];
34
36
 
35
37
  function parseInitialUrl(): { projectSlug: string | null; sessionId: string | null; settingsSection: SettingsSection | null; projectSettingsSection: ProjectSettingsSection | null } {
36
38
  var path = window.location.pathname;
@@ -245,6 +247,13 @@ export function goToDashboard(): void {
245
247
  pushUrl(null, null);
246
248
  }
247
249
 
250
+ export function goToAnalytics(): void {
251
+ sidebarStore.setState(function (state) {
252
+ return { ...state, activeView: { type: "analytics" }, sidebarMode: "project" };
253
+ });
254
+ pushUrl(sidebarStore.state.activeProjectSlug, null);
255
+ }
256
+
248
257
  export function handlePopState(): void {
249
258
  var url = parseInitialUrl();
250
259
  if (url.settingsSection) {