@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.
- package/bun.lock +776 -2
- package/client/index.html +1 -13
- package/client/package.json +7 -1
- package/client/src/App.tsx +2 -0
- package/client/src/commands.ts +36 -0
- package/client/src/components/analytics/AnalyticsView.tsx +61 -0
- package/client/src/components/analytics/ChartCard.tsx +22 -0
- package/client/src/components/analytics/PeriodSelector.tsx +42 -0
- package/client/src/components/analytics/QuickStats.tsx +99 -0
- package/client/src/components/analytics/charts/CostAreaChart.tsx +83 -0
- package/client/src/components/analytics/charts/CostDistributionChart.tsx +62 -0
- package/client/src/components/analytics/charts/CostDonutChart.tsx +93 -0
- package/client/src/components/analytics/charts/CumulativeCostChart.tsx +62 -0
- package/client/src/components/analytics/charts/SessionBubbleChart.tsx +122 -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/dashboard/DashboardView.tsx +5 -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 +43 -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/useAnalytics.ts +75 -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 +52 -20
- package/client/src/stores/analytics.ts +54 -0
- package/client/src/stores/session.ts +136 -0
- package/client/src/stores/sidebar.ts +11 -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 +54 -1
- package/package.json +1 -1
- package/server/src/analytics/engine.ts +491 -0
- package/server/src/daemon.ts +12 -1
- package/server/src/features/scheduler.ts +23 -0
- package/server/src/features/sticky-notes.ts +5 -3
- package/server/src/handlers/analytics.ts +34 -0
- 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/session.ts +4 -4
- package/server/src/project/terminal.ts +78 -34
- package/shared/src/analytics.ts +24 -0
- package/shared/src/index.ts +1 -0
- package/shared/src/messages.ts +173 -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
|
@@ -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
|
};
|
package/client/src/router.tsx
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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
|
|
41
|
-
var
|
|
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 (!
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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 =
|
|
236
|
-
|
|
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
|
|
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
|
|
313
|
-
|
|
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
|
-
|
|
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) {
|