@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
@@ -1,5 +1,4 @@
1
1
  import type {
2
- Attachment,
3
2
  FileEntry,
4
3
  HistoryMessage,
5
4
  LatticeConfig,
@@ -44,7 +43,7 @@ export interface SessionListRequestMessage {
44
43
  export interface ChatSendMessage {
45
44
  type: "chat:send";
46
45
  text: string;
47
- attachments?: Attachment[];
46
+ attachmentIds?: string[];
48
47
  model?: string;
49
48
  effort?: string;
50
49
  }
@@ -71,14 +70,85 @@ export interface ChatSetPermissionModeMessage {
71
70
  mode: "default" | "acceptEdits" | "plan" | "dontAsk";
72
71
  }
73
72
 
73
+ export interface AttachmentChunkMessage {
74
+ type: "attachment:chunk";
75
+ attachmentId: string;
76
+ chunkIndex: number;
77
+ totalChunks: number;
78
+ data: string;
79
+ }
80
+
81
+ export interface AttachmentCompleteMessage {
82
+ type: "attachment:complete";
83
+ attachmentId: string;
84
+ attachmentType: "file" | "image" | "paste";
85
+ name: string;
86
+ mimeType: string;
87
+ size: number;
88
+ lineCount?: number;
89
+ }
90
+
91
+ export interface AttachmentProgressMessage {
92
+ type: "attachment:progress";
93
+ attachmentId: string;
94
+ received: number;
95
+ total: number;
96
+ }
97
+
98
+ export interface AttachmentErrorMessage {
99
+ type: "attachment:error";
100
+ attachmentId: string;
101
+ error: string;
102
+ }
103
+
104
+ export interface ChatPromptRequestMessage {
105
+ type: "chat:prompt_request";
106
+ requestId: string;
107
+ questions: Array<{
108
+ question: string;
109
+ header: string;
110
+ options: Array<{ label: string; description: string; preview?: string }>;
111
+ multiSelect: boolean;
112
+ }>;
113
+ }
114
+
115
+ export interface ChatPromptResponseMessage {
116
+ type: "chat:prompt_response";
117
+ requestId: string;
118
+ answers: Record<string, string>;
119
+ annotations?: Record<string, { notes?: string; preview?: string }>;
120
+ }
121
+
122
+ export interface ChatPromptResolvedMessage {
123
+ type: "chat:prompt_resolved";
124
+ requestId: string;
125
+ }
126
+
127
+ export interface ChatTodoUpdateMessage {
128
+ type: "chat:todo_update";
129
+ todos: Array<{
130
+ id: string;
131
+ content: string;
132
+ status: "pending" | "in_progress" | "completed";
133
+ priority: "high" | "medium" | "low";
134
+ }>;
135
+ }
136
+
137
+ export interface ChatPlanModeMessage {
138
+ type: "chat:plan_mode";
139
+ active: boolean;
140
+ }
141
+
74
142
  export interface FsListMessage {
75
143
  type: "fs:list";
76
144
  path: string;
145
+ projectSlug?: string;
77
146
  }
78
147
 
79
148
  export interface FsReadMessage {
80
149
  type: "fs:read";
81
150
  path: string;
151
+ projectSlug?: string;
82
152
  }
83
153
 
84
154
  export interface FsWriteMessage {
@@ -89,6 +159,7 @@ export interface FsWriteMessage {
89
159
 
90
160
  export interface TerminalCreateMessage {
91
161
  type: "terminal:create";
162
+ projectSlug?: string;
92
163
  }
93
164
 
94
165
  export interface TerminalInputMessage {
@@ -168,13 +239,23 @@ export interface SchedulerToggleMessage {
168
239
  taskId: string;
169
240
  }
170
241
 
242
+ export interface SchedulerUpdateMessage {
243
+ type: "scheduler:update";
244
+ taskId: string;
245
+ name?: string;
246
+ prompt?: string;
247
+ cron?: string;
248
+ }
249
+
171
250
  export interface NotesListMessage {
172
251
  type: "notes:list";
252
+ projectSlug?: string;
173
253
  }
174
254
 
175
255
  export interface NotesCreateMessage {
176
256
  type: "notes:create";
177
257
  content: string;
258
+ projectSlug?: string;
178
259
  }
179
260
 
180
261
  export interface NotesUpdateMessage {
@@ -256,6 +337,24 @@ export interface BrowseSuggestionsMessage {
256
337
  type: "browse:suggestions";
257
338
  }
258
339
 
340
+ export interface EditorOpenMessage {
341
+ type: "editor:open";
342
+ path: string;
343
+ line?: number;
344
+ projectSlug?: string;
345
+ }
346
+
347
+ export interface EditorDetectMessage {
348
+ type: "editor:detect";
349
+ editorType: string;
350
+ }
351
+
352
+ export interface EditorDetectResultMessage {
353
+ type: "editor:detect_result";
354
+ editorType: string;
355
+ path: string | null;
356
+ }
357
+
259
358
  export interface ProjectSettingsGetMessage {
260
359
  type: "project-settings:get";
261
360
  projectSlug: string;
@@ -280,12 +379,18 @@ export interface ProjectSettingsErrorMessage {
280
379
  message: string;
281
380
  }
282
381
 
382
+ export interface SessionStopExternalMessage {
383
+ type: "session:stop_external";
384
+ sessionId: string;
385
+ }
386
+
283
387
  export type ClientMessage =
284
388
  | SessionCreateMessage
285
389
  | SessionActivateMessage
286
390
  | SessionRenameMessage
287
391
  | SessionDeleteMessage
288
392
  | SessionListRequestMessage
393
+ | SessionStopExternalMessage
289
394
  | ChatSendMessage
290
395
  | ChatPermissionResponseMessage
291
396
  | ChatRewindMessage
@@ -309,12 +414,15 @@ export type ClientMessage =
309
414
  | SchedulerCreateMessage
310
415
  | SchedulerDeleteMessage
311
416
  | SchedulerToggleMessage
417
+ | SchedulerUpdateMessage
312
418
  | NotesListMessage
313
419
  | NotesCreateMessage
314
420
  | NotesUpdateMessage
315
421
  | NotesDeleteMessage
316
422
  | SkillsListRequestMessage
317
423
  | ChatSetPermissionModeMessage
424
+ | AttachmentChunkMessage
425
+ | AttachmentCompleteMessage
318
426
  | ProjectSettingsGetMessage
319
427
  | ProjectSettingsUpdateMessage
320
428
  | SessionListAllRequestMessage
@@ -328,7 +436,10 @@ export type ClientMessage =
328
436
  | MemoryViewMessage
329
437
  | MemorySaveMessage
330
438
  | MemoryDeleteMessage
331
- | BrowseSuggestionsMessage;
439
+ | BrowseSuggestionsMessage
440
+ | EditorOpenMessage
441
+ | EditorDetectMessage
442
+ | ChatPromptResponseMessage;
332
443
 
333
444
  export interface SessionListMessage {
334
445
  type: "session:list";
@@ -348,6 +459,13 @@ export interface SessionHistoryMessage {
348
459
  messages: HistoryMessage[];
349
460
  title?: string;
350
461
  interrupted?: boolean;
462
+ busy?: boolean;
463
+ }
464
+
465
+ export interface SessionBusyMessage {
466
+ type: "session:busy";
467
+ sessionId: string;
468
+ busy: boolean;
351
469
  }
352
470
 
353
471
  export interface ChatUserMessage {
@@ -380,6 +498,11 @@ export interface ChatDoneMessage {
380
498
  duration: number;
381
499
  }
382
500
 
501
+ export interface ChatPromptSuggestionMessage {
502
+ type: "chat:prompt_suggestion";
503
+ suggestion: string;
504
+ }
505
+
383
506
  export interface ChatErrorMessage {
384
507
  type: "chat:error";
385
508
  message: string;
@@ -502,6 +625,9 @@ export interface SettingsDataMessage {
502
625
  config: LatticeConfig;
503
626
  mcpServers?: Record<string, McpServerConfig>;
504
627
  globalSkills?: SkillInfo[];
628
+ globalRules?: Array<{ filename: string; content: string }>;
629
+ spinnerVerbs?: string[];
630
+ wslDistro?: string;
505
631
  }
506
632
 
507
633
  export interface LoopStatusMessage {
@@ -531,6 +657,11 @@ export interface SchedulerTaskCreatedMessage {
531
657
  task: ScheduledTask;
532
658
  }
533
659
 
660
+ export interface SchedulerTaskUpdatedMessage {
661
+ type: "scheduler:task_updated";
662
+ task: ScheduledTask;
663
+ }
664
+
534
665
  export interface NotesListResultMessage {
535
666
  type: "notes:list_result";
536
667
  notes: StickyNote[];
@@ -641,11 +772,13 @@ export type ServerMessage =
641
772
  | SessionListMessage
642
773
  | SessionCreatedMessage
643
774
  | SessionHistoryMessage
775
+ | SessionBusyMessage
644
776
  | ChatUserMessage
645
777
  | ChatDeltaMessage
646
778
  | ChatToolStartMessage
647
779
  | ChatToolResultMessage
648
780
  | ChatDoneMessage
781
+ | ChatPromptSuggestionMessage
649
782
  | ChatErrorMessage
650
783
  | ChatPermissionRequestMessage
651
784
  | ChatStatusMessage
@@ -669,6 +802,7 @@ export type ServerMessage =
669
802
  | LoopDeltaMessage
670
803
  | SchedulerTasksMessage
671
804
  | SchedulerTaskCreatedMessage
805
+ | SchedulerTaskUpdatedMessage
672
806
  | NotesListResultMessage
673
807
  | NoteCreatedMessage
674
808
  | NoteUpdatedMessage
@@ -687,7 +821,14 @@ export type ServerMessage =
687
821
  | MemoryViewResultMessage
688
822
  | MemorySaveResultMessage
689
823
  | MemoryDeleteResultMessage
690
- | BrowseSuggestionsResultMessage;
824
+ | BrowseSuggestionsResultMessage
825
+ | EditorDetectResultMessage
826
+ | AttachmentProgressMessage
827
+ | AttachmentErrorMessage
828
+ | ChatPromptRequestMessage
829
+ | ChatPromptResolvedMessage
830
+ | ChatTodoUpdateMessage
831
+ | ChatPlanModeMessage;
691
832
 
692
833
  export interface MeshHelloMessage {
693
834
  type: "mesh:hello";
@@ -20,6 +20,7 @@ export interface ProjectSummary {
20
20
  export interface ProjectInfo extends ProjectSummary {
21
21
  nodeName: string;
22
22
  isRemote: boolean;
23
+ ideProjectName?: string;
23
24
  }
24
25
 
25
26
  export interface SessionSummary {
@@ -40,9 +41,12 @@ export interface FileEntry {
40
41
  }
41
42
 
42
43
  export interface Attachment {
43
- type: "file" | "image";
44
+ type: "file" | "image" | "paste";
44
45
  name: string;
45
46
  content: string;
47
+ mimeType?: string;
48
+ size: number;
49
+ lineCount?: number;
46
50
  }
47
51
 
48
52
  export interface HistoryMessage {
@@ -63,6 +67,20 @@ export interface HistoryMessage {
63
67
  model?: string;
64
68
  costEstimate?: number;
65
69
  duration?: number;
70
+ promptQuestions?: Array<{
71
+ question: string;
72
+ header: string;
73
+ options: Array<{ label: string; description: string; preview?: string }>;
74
+ multiSelect: boolean;
75
+ }>;
76
+ promptAnswers?: Record<string, string>;
77
+ promptStatus?: "pending" | "answered" | "timed_out";
78
+ todos?: Array<{
79
+ id: string;
80
+ content: string;
81
+ status: "pending" | "in_progress" | "completed";
82
+ priority: "high" | "medium" | "low";
83
+ }>;
66
84
  }
67
85
 
68
86
  export interface PeerInfo {
@@ -87,6 +105,13 @@ export interface LatticeConfig {
87
105
  env: Record<string, string>;
88
106
  icon?: ProjectIcon;
89
107
  }>;
108
+ editor?: {
109
+ type: "vscode" | "vscode-insiders" | "cursor" | "webstorm" | "intellij" | "pycharm" | "goland" | "notepad++" | "sublime" | "custom";
110
+ paths?: Record<string, string>;
111
+ customCommand?: string;
112
+ };
113
+ setupComplete?: boolean;
114
+ wsl?: boolean | "auto";
90
115
  }
91
116
 
92
117
  export interface StickyNote {
@@ -94,6 +119,7 @@ export interface StickyNote {
94
119
  content: string;
95
120
  createdAt: number;
96
121
  updatedAt: number;
122
+ projectSlug?: string;
97
123
  }
98
124
 
99
125
  export interface ScheduledTask {
@@ -15,7 +15,7 @@ export type McpServerConfig =
15
15
  | { type: "sse"; url: string; headers?: Record<string, string> };
16
16
 
17
17
  export type ProjectSettingsSection =
18
- | "general" | "claude" | "environment" | "mcp" | "skills" | "rules" | "permissions" | "memory";
18
+ | "general" | "claude" | "environment" | "mcp" | "skills" | "rules" | "permissions" | "memory" | "notifications";
19
19
 
20
20
  export interface ProjectSettings {
21
21
  title: string;
package/tp.js ADDED
@@ -0,0 +1,19 @@
1
+ var p = Bun.spawn(["bun", "run", "/home/aequasi/projects/cryptiklemur/lattice/server/src/project/pty-worker.js"], {
2
+ stdin: "pipe", stdout: "pipe", stderr: "pipe", cwd: "/tmp"
3
+ });
4
+ async function readOut() {
5
+ var d = new TextDecoder();
6
+ var rd = p.stdout.getReader();
7
+ while (true) { var x = await rd.read(); if (x.done) break; process.stdout.write("[OUT] " + d.decode(x.value)); }
8
+ }
9
+ async function readErr() {
10
+ var d = new TextDecoder();
11
+ var rd = p.stderr.getReader();
12
+ while (true) { var x = await rd.read(); if (x.done) break; process.stderr.write("[ERR] " + d.decode(x.value)); }
13
+ }
14
+ readOut();
15
+ readErr();
16
+ p.stdin.write('{"type":"create","cwd":"/tmp"}\n');
17
+ setTimeout(function() { p.stdin.write('{"type":"input","data":"echo hello\\r"}\n'); }, 1000);
18
+ setTimeout(function() { p.stdin.write('{"type":"kill"}\n'); }, 3000);
19
+ setTimeout(function() { process.exit(); }, 4000);
@@ -1,24 +0,0 @@
1
- {
2
- "name": "Lattice",
3
- "short_name": "Lattice",
4
- "description": "Multi-machine agentic dashboard for Claude Code",
5
- "display": "standalone",
6
- "start_url": "/",
7
- "scope": "/",
8
- "theme_color": "#0d0d0d",
9
- "background_color": "#0d0d0d",
10
- "icons": [
11
- {
12
- "src": "/icons/icon-192.svg",
13
- "sizes": "192x192",
14
- "type": "image/svg+xml",
15
- "purpose": "any maskable"
16
- },
17
- {
18
- "src": "/icons/icon-512.svg",
19
- "sizes": "512x512",
20
- "type": "image/svg+xml",
21
- "purpose": "any maskable"
22
- }
23
- ]
24
- }
@@ -1,61 +0,0 @@
1
- var CACHE_NAME = "lattice-v1";
2
- var STATIC_ASSETS = [
3
- "/",
4
- "/manifest.json",
5
- "/icons/icon-192.svg",
6
- "/icons/icon-512.svg",
7
- ];
8
-
9
- self.addEventListener("install", function (event) {
10
- event.waitUntil(
11
- caches.open(CACHE_NAME).then(function (cache) {
12
- return cache.addAll(STATIC_ASSETS);
13
- }).then(function () {
14
- return self.skipWaiting();
15
- })
16
- );
17
- });
18
-
19
- self.addEventListener("activate", function (event) {
20
- event.waitUntil(
21
- caches.keys().then(function (keys) {
22
- return Promise.all(
23
- keys.filter(function (key) {
24
- return key !== CACHE_NAME;
25
- }).map(function (key) {
26
- return caches.delete(key);
27
- })
28
- );
29
- }).then(function () {
30
- return self.clients.claim();
31
- })
32
- );
33
- });
34
-
35
- self.addEventListener("fetch", function (event) {
36
- var url = new URL(event.request.url);
37
-
38
- if (url.pathname.startsWith("/ws") || url.pathname.startsWith("/auth")) {
39
- return;
40
- }
41
-
42
- if (event.request.method !== "GET") {
43
- return;
44
- }
45
-
46
- event.respondWith(
47
- caches.match(event.request).then(function (cached) {
48
- var networkRequest = fetch(event.request).then(function (response) {
49
- if (response && response.status === 200 && response.type === "basic") {
50
- var clone = response.clone();
51
- caches.open(CACHE_NAME).then(function (cache) {
52
- cache.put(event.request, clone);
53
- });
54
- }
55
- return response;
56
- });
57
-
58
- return cached || networkRequest;
59
- })
60
- );
61
- });
@@ -1,241 +0,0 @@
1
- import { useCallback, useEffect, useRef, useState } from "react";
2
- import { ChevronRight, FileIcon } from "lucide-react";
3
- import type { FileEntry, FsListResultMessage, FsReadResultMessage } from "@lattice/shared";
4
- import { useWebSocket } from "../../hooks/useWebSocket";
5
- import type { ServerMessage } from "@lattice/shared";
6
-
7
- interface TreeNode {
8
- entry: FileEntry;
9
- children: TreeNode[] | null;
10
- expanded: boolean;
11
- }
12
-
13
- function buildNodes(entries: FileEntry[]): TreeNode[] {
14
- return entries.map(function (entry) {
15
- return { entry, children: null, expanded: false };
16
- });
17
- }
18
-
19
- interface FileTreeItemProps {
20
- node: TreeNode;
21
- depth: number;
22
- selectedPath: string | null;
23
- onToggle: (path: string) => void;
24
- onSelect: (path: string) => void;
25
- }
26
-
27
- function FileTreeItem(props: FileTreeItemProps) {
28
- var { node, depth, selectedPath, onToggle, onSelect } = props;
29
- var isSelected = selectedPath === node.entry.path;
30
- var isDir = node.entry.isDirectory;
31
-
32
- return (
33
- <div>
34
- <div
35
- onClick={function () {
36
- if (isDir) {
37
- onToggle(node.entry.path);
38
- } else {
39
- onSelect(node.entry.path);
40
- }
41
- }}
42
- className={
43
- "flex items-center gap-1.5 py-[3px] pr-2 cursor-pointer text-[13px] rounded select-none " +
44
- (isSelected ? "bg-base-300 text-primary" : "text-base-content hover:bg-base-200")
45
- }
46
- style={{ paddingLeft: (8 + depth * 16) + "px" }}
47
- >
48
- {isDir ? (
49
- <ChevronRight
50
- size={12}
51
- className="flex-shrink-0 text-base-content/40 transition-transform duration-[120ms]"
52
- style={{ transform: node.expanded ? "rotate(90deg)" : "none" }}
53
- />
54
- ) : (
55
- <FileIcon
56
- size={12}
57
- className="flex-shrink-0 text-base-content/40"
58
- />
59
- )}
60
- <span
61
- className={
62
- "truncate " +
63
- (isDir ? "text-info" : "text-base-content")
64
- }
65
- >
66
- {node.entry.name}
67
- </span>
68
- </div>
69
- {isDir && node.expanded && node.children && (
70
- <div>
71
- {node.children.map(function (child) {
72
- return (
73
- <FileTreeItem
74
- key={child.entry.path}
75
- node={child}
76
- depth={depth + 1}
77
- selectedPath={selectedPath}
78
- onToggle={onToggle}
79
- onSelect={onSelect}
80
- />
81
- );
82
- })}
83
- </div>
84
- )}
85
- </div>
86
- );
87
- }
88
-
89
- export function FileBrowser() {
90
- var { send, subscribe, unsubscribe } = useWebSocket();
91
- var [rootNodes, setRootNodes] = useState<TreeNode[]>([]);
92
- var [selectedPath, setSelectedPath] = useState<string | null>(null);
93
- var [fileContent, setFileContent] = useState<string | null>(null);
94
- var [loadingContent, setLoadingContent] = useState(false);
95
- var nodesRef = useRef<TreeNode[]>([]);
96
-
97
- nodesRef.current = rootNodes;
98
-
99
- var handleListResult = useCallback(function (msg: ServerMessage) {
100
- var listMsg = msg as FsListResultMessage;
101
- var newNodes = buildNodes(listMsg.entries);
102
-
103
- if (listMsg.path === "" || listMsg.path === ".") {
104
- setRootNodes(newNodes);
105
- return;
106
- }
107
-
108
- function updateNodes(nodes: TreeNode[]): TreeNode[] {
109
- return nodes.map(function (node) {
110
- if (node.entry.path === listMsg.path) {
111
- return Object.assign({}, node, { children: newNodes, expanded: true });
112
- }
113
- if (node.children) {
114
- return Object.assign({}, node, { children: updateNodes(node.children) });
115
- }
116
- return node;
117
- });
118
- }
119
-
120
- setRootNodes(function (prev) {
121
- return updateNodes(prev);
122
- });
123
- }, []);
124
-
125
- var handleReadResult = useCallback(function (msg: ServerMessage) {
126
- var readMsg = msg as FsReadResultMessage;
127
- setFileContent(readMsg.content);
128
- setLoadingContent(false);
129
- }, []);
130
-
131
- var handleFsChanged = useCallback(function (msg: ServerMessage) {
132
- var changedPath = (msg as { path: string }).path;
133
- if (changedPath === selectedPath) {
134
- send({ type: "fs:read", path: changedPath });
135
- }
136
- }, [selectedPath, send]);
137
-
138
- useEffect(function () {
139
- subscribe("fs:list_result", handleListResult);
140
- subscribe("fs:read_result", handleReadResult);
141
- subscribe("fs:changed", handleFsChanged);
142
-
143
- send({ type: "fs:list", path: "." });
144
-
145
- return function () {
146
- unsubscribe("fs:list_result", handleListResult);
147
- unsubscribe("fs:read_result", handleReadResult);
148
- unsubscribe("fs:changed", handleFsChanged);
149
- };
150
- }, [handleListResult, handleReadResult, handleFsChanged, send, subscribe, unsubscribe]);
151
-
152
- function handleToggle(path: string) {
153
- function findAndToggle(nodes: TreeNode[]): TreeNode[] {
154
- return nodes.map(function (node) {
155
- if (node.entry.path === path) {
156
- if (!node.expanded && !node.children) {
157
- send({ type: "fs:list", path });
158
- return Object.assign({}, node, { expanded: true });
159
- }
160
- return Object.assign({}, node, { expanded: !node.expanded });
161
- }
162
- if (node.children) {
163
- return Object.assign({}, node, { children: findAndToggle(node.children) });
164
- }
165
- return node;
166
- });
167
- }
168
- setRootNodes(function (prev) {
169
- return findAndToggle(prev);
170
- });
171
- }
172
-
173
- function handleSelect(path: string) {
174
- setSelectedPath(path);
175
- setFileContent(null);
176
- setLoadingContent(true);
177
- send({ type: "fs:read", path });
178
- }
179
-
180
- return (
181
- <div className="flex h-full w-full overflow-hidden bg-base-100">
182
- <div className="w-[220px] flex-shrink-0 border-r border-base-300 overflow-y-auto p-2">
183
- <div className="text-[11px] font-semibold tracking-[0.06em] uppercase text-base-content/40 px-2 pb-2 pt-1">
184
- Files
185
- </div>
186
- {rootNodes.length === 0 ? (
187
- <div className="px-2 py-3 text-[12px] text-base-content/40">
188
- Loading...
189
- </div>
190
- ) : (
191
- rootNodes.map(function (node) {
192
- return (
193
- <FileTreeItem
194
- key={node.entry.path}
195
- node={node}
196
- depth={0}
197
- selectedPath={selectedPath}
198
- onToggle={handleToggle}
199
- onSelect={handleSelect}
200
- />
201
- );
202
- })
203
- )}
204
- </div>
205
-
206
- <div className="flex-1 flex flex-col overflow-hidden">
207
- {selectedPath && (
208
- <div className="h-9 flex-shrink-0 flex items-center px-4 border-b border-base-300 bg-base-200 text-[12px] text-base-content/60 font-mono overflow-hidden">
209
- <span className="truncate">{selectedPath}</span>
210
- </div>
211
- )}
212
-
213
- <div className="flex-1 overflow-auto">
214
- {!selectedPath && (
215
- <div className="h-full flex items-center justify-center text-base-content/40 text-[13px]">
216
- Select a file to view its contents
217
- </div>
218
- )}
219
-
220
- {selectedPath && loadingContent && (
221
- <div className="h-full flex items-center justify-center text-base-content/40 text-[13px]">
222
- Loading...
223
- </div>
224
- )}
225
-
226
- {selectedPath && !loadingContent && fileContent !== null && (
227
- <pre className="m-0 p-4 text-[13px] font-mono text-base-content leading-relaxed whitespace-pre-wrap break-words">
228
- {fileContent}
229
- </pre>
230
- )}
231
-
232
- {selectedPath && !loadingContent && fileContent === null && (
233
- <div className="h-full flex items-center justify-center text-base-content/40 text-[13px]">
234
- Cannot display this file (binary or too large)
235
- </div>
236
- )}
237
- </div>
238
- </div>
239
- </div>
240
- );
241
- }