@cryptiklemur/lattice 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bun.lock +705 -2
- package/client/index.html +1 -13
- package/client/package.json +6 -1
- package/client/src/App.tsx +2 -0
- package/client/src/commands.ts +36 -0
- package/client/src/components/chat/AttachmentChips.tsx +116 -0
- package/client/src/components/chat/ChatInput.tsx +250 -73
- package/client/src/components/chat/ChatView.tsx +242 -10
- package/client/src/components/chat/CommandPalette.tsx +162 -0
- package/client/src/components/chat/Message.tsx +23 -2
- package/client/src/components/chat/PromptQuestion.tsx +271 -0
- package/client/src/components/chat/TodoCard.tsx +57 -0
- package/client/src/components/chat/ToolResultRenderer.tsx +2 -1
- package/client/src/components/chat/VoiceRecorder.tsx +85 -0
- package/client/src/components/project-settings/ProjectMemory.tsx +12 -2
- package/client/src/components/project-settings/ProjectNotifications.tsx +48 -0
- package/client/src/components/project-settings/ProjectRules.tsx +10 -1
- package/client/src/components/project-settings/ProjectSettingsView.tsx +6 -0
- package/client/src/components/settings/Appearance.tsx +1 -0
- package/client/src/components/settings/ClaudeSettings.tsx +10 -0
- package/client/src/components/settings/Editor.tsx +123 -0
- package/client/src/components/settings/GlobalMcp.tsx +10 -1
- package/client/src/components/settings/GlobalMemory.tsx +19 -0
- package/client/src/components/settings/GlobalRules.tsx +149 -0
- package/client/src/components/settings/GlobalSkills.tsx +10 -0
- package/client/src/components/settings/Notifications.tsx +88 -0
- package/client/src/components/settings/SettingsView.tsx +12 -0
- package/client/src/components/settings/skill-shared.tsx +2 -1
- package/client/src/components/setup/SetupWizard.tsx +1 -1
- package/client/src/components/sidebar/NodeSettingsModal.tsx +23 -1
- package/client/src/components/sidebar/ProjectDropdown.tsx +176 -27
- package/client/src/components/sidebar/SettingsSidebar.tsx +11 -1
- package/client/src/components/sidebar/Sidebar.tsx +35 -2
- package/client/src/components/sidebar/UserIsland.tsx +18 -7
- package/client/src/components/ui/UpdatePrompt.tsx +47 -0
- package/client/src/components/workspace/FileBrowser.tsx +174 -0
- package/client/src/components/workspace/FileTree.tsx +129 -0
- package/client/src/components/workspace/FileViewer.tsx +211 -0
- package/client/src/components/workspace/NoteCard.tsx +119 -0
- package/client/src/components/workspace/NotesView.tsx +102 -0
- package/client/src/components/workspace/ScheduledTasksView.tsx +117 -0
- package/client/src/components/workspace/SplitPane.tsx +81 -0
- package/client/src/components/workspace/TabBar.tsx +185 -0
- package/client/src/components/workspace/TaskCard.tsx +158 -0
- package/client/src/components/workspace/TaskEditModal.tsx +114 -0
- package/client/src/components/{panels/Terminal.tsx → workspace/TerminalInstance.tsx} +50 -7
- package/client/src/components/workspace/TerminalView.tsx +110 -0
- package/client/src/components/workspace/WorkspaceView.tsx +116 -0
- package/client/src/hooks/useAttachments.ts +280 -0
- package/client/src/hooks/useEditorConfig.ts +28 -0
- package/client/src/hooks/useIdleDetection.ts +44 -0
- package/client/src/hooks/useInstallPrompt.ts +53 -0
- package/client/src/hooks/useNotifications.ts +54 -0
- package/client/src/hooks/useOnline.ts +6 -0
- package/client/src/hooks/useSession.ts +110 -4
- package/client/src/hooks/useSpinnerVerb.ts +36 -0
- package/client/src/hooks/useSwipeDrawer.ts +275 -0
- package/client/src/hooks/useVoiceRecorder.ts +123 -0
- package/client/src/hooks/useWorkspace.ts +48 -0
- package/client/src/providers/WebSocketProvider.tsx +18 -0
- package/client/src/router.tsx +48 -20
- package/client/src/stores/session.ts +136 -0
- package/client/src/stores/sidebar.ts +3 -2
- package/client/src/stores/workspace.ts +254 -0
- package/client/src/styles/global.css +123 -0
- package/client/src/utils/editorUrl.ts +62 -0
- package/client/vite.config.ts +53 -1
- package/package.json +1 -1
- package/server/src/daemon.ts +11 -1
- package/server/src/features/scheduler.ts +23 -0
- package/server/src/features/sticky-notes.ts +5 -3
- package/server/src/handlers/attachment.ts +172 -0
- package/server/src/handlers/chat.ts +43 -2
- package/server/src/handlers/editor.ts +40 -0
- package/server/src/handlers/fs.ts +10 -2
- package/server/src/handlers/memory.ts +3 -0
- package/server/src/handlers/notes.ts +4 -2
- package/server/src/handlers/scheduler.ts +18 -1
- package/server/src/handlers/session.ts +14 -8
- package/server/src/handlers/settings.ts +37 -2
- package/server/src/handlers/terminal.ts +13 -6
- package/server/src/project/pty-worker.cjs +83 -0
- package/server/src/project/sdk-bridge.ts +266 -11
- package/server/src/project/terminal.ts +78 -34
- package/shared/src/messages.ts +145 -4
- package/shared/src/models.ts +27 -1
- package/shared/src/project-settings.ts +1 -1
- package/tp.js +19 -0
- package/client/public/manifest.json +0 -24
- package/client/public/sw.js +0 -61
- package/client/src/components/panels/FileBrowser.tsx +0 -241
- package/client/src/components/panels/StickyNotes.tsx +0 -187
|
@@ -1,7 +1,16 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import React, { useEffect, useRef } from "react";
|
|
2
|
+
import { createPortal } from "react-dom";
|
|
3
|
+
import {
|
|
4
|
+
Settings, FileText, Terminal, ScrollText, Shield, Brain,
|
|
5
|
+
Plug, Puzzle, ExternalLink, Copy, Check, FolderOpen,
|
|
6
|
+
} from "lucide-react";
|
|
7
|
+
import { useProjects } from "../../hooks/useProjects";
|
|
4
8
|
import { useSidebar } from "../../hooks/useSidebar";
|
|
9
|
+
import { useEditorConfig } from "../../hooks/useEditorConfig";
|
|
10
|
+
import { getEditorUrl } from "../../utils/editorUrl";
|
|
11
|
+
import { openTab } from "../../stores/workspace";
|
|
12
|
+
import { getSidebarStore } from "../../stores/sidebar";
|
|
13
|
+
import { useState } from "react";
|
|
5
14
|
|
|
6
15
|
interface ProjectDropdownProps {
|
|
7
16
|
anchorRef: React.RefObject<HTMLElement | null>;
|
|
@@ -9,35 +18,175 @@ interface ProjectDropdownProps {
|
|
|
9
18
|
}
|
|
10
19
|
|
|
11
20
|
export function ProjectDropdown(props: ProjectDropdownProps) {
|
|
21
|
+
var menuRef = useRef<HTMLDivElement>(null);
|
|
22
|
+
var { activeProject } = useProjects();
|
|
12
23
|
var sidebar = useSidebar();
|
|
24
|
+
var { editorType, wslDistro } = useEditorConfig();
|
|
25
|
+
var [copied, setCopied] = useState(false);
|
|
13
26
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
} else if (id === "mcp") {
|
|
25
|
-
sidebar.openProjectSettings("mcp");
|
|
26
|
-
} else if (id === "skills") {
|
|
27
|
-
sidebar.openProjectSettings("skills");
|
|
28
|
-
} else if (id === "environment") {
|
|
29
|
-
sidebar.openProjectSettings("environment");
|
|
27
|
+
useEffect(function () {
|
|
28
|
+
function handleClickOutside(e: MouseEvent) {
|
|
29
|
+
if (
|
|
30
|
+
menuRef.current &&
|
|
31
|
+
!menuRef.current.contains(e.target as Node) &&
|
|
32
|
+
props.anchorRef.current &&
|
|
33
|
+
!props.anchorRef.current.contains(e.target as Node)
|
|
34
|
+
) {
|
|
35
|
+
props.onClose();
|
|
36
|
+
}
|
|
30
37
|
}
|
|
38
|
+
function handleEscape(e: KeyboardEvent) {
|
|
39
|
+
if (e.key === "Escape") {
|
|
40
|
+
props.onClose();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
44
|
+
document.addEventListener("keydown", handleEscape);
|
|
45
|
+
return function () {
|
|
46
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
47
|
+
document.removeEventListener("keydown", handleEscape);
|
|
48
|
+
};
|
|
49
|
+
}, [props.onClose, props.anchorRef]);
|
|
50
|
+
|
|
51
|
+
var style: React.CSSProperties = {};
|
|
52
|
+
if (props.anchorRef.current) {
|
|
53
|
+
var rect = props.anchorRef.current.getBoundingClientRect();
|
|
54
|
+
style.top = rect.bottom + 4 + "px";
|
|
55
|
+
style.left = rect.left + "px";
|
|
56
|
+
style.width = Math.max(rect.width, 240) + "px";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function goToSettings(section: string) {
|
|
60
|
+
sidebar.openProjectSettings(section as any);
|
|
31
61
|
props.onClose();
|
|
32
62
|
}
|
|
33
63
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
64
|
+
function handleCopyPath() {
|
|
65
|
+
if (activeProject) {
|
|
66
|
+
navigator.clipboard.writeText(activeProject.path || "");
|
|
67
|
+
setCopied(true);
|
|
68
|
+
setTimeout(function () { setCopied(false); }, 1500);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
var ideUrl = activeProject ? getEditorUrl(editorType, activeProject.path, ".", undefined, wslDistro, activeProject.ideProjectName) : null;
|
|
73
|
+
|
|
74
|
+
function handleOpenTerminal() {
|
|
75
|
+
openTab("terminal");
|
|
76
|
+
var state = getSidebarStore().state;
|
|
77
|
+
if (state.activeView.type !== "chat") {
|
|
78
|
+
getSidebarStore().setState(function (s) {
|
|
79
|
+
return { ...s, activeView: { type: "chat" } };
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
props.onClose();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function handleViewClaudeMd() {
|
|
86
|
+
openTab("files");
|
|
87
|
+
var state = getSidebarStore().state;
|
|
88
|
+
if (state.activeView.type !== "chat") {
|
|
89
|
+
getSidebarStore().setState(function (s) {
|
|
90
|
+
return { ...s, activeView: { type: "chat" } };
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
props.onClose();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!activeProject) return null;
|
|
97
|
+
|
|
98
|
+
return createPortal(
|
|
99
|
+
<div
|
|
100
|
+
ref={menuRef}
|
|
101
|
+
role="menu"
|
|
102
|
+
aria-label="Project actions"
|
|
103
|
+
className="fixed z-[9999] bg-base-300 border border-base-content/15 rounded-xl shadow-2xl overflow-hidden"
|
|
104
|
+
style={style}
|
|
105
|
+
>
|
|
106
|
+
<div className="px-3 py-2.5 border-b border-base-content/10">
|
|
107
|
+
<div className="text-[13px] font-mono font-bold text-base-content truncate">{activeProject.title}</div>
|
|
108
|
+
<div className="text-[10px] text-base-content/30 truncate mt-0.5">{activeProject.path}</div>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<div className="px-1.5 py-1.5">
|
|
112
|
+
<div className="px-2 pt-1.5 pb-1 text-[10px] font-semibold uppercase tracking-wider text-base-content/25">
|
|
113
|
+
Actions
|
|
114
|
+
</div>
|
|
115
|
+
{ideUrl && (
|
|
116
|
+
<a
|
|
117
|
+
role="menuitem"
|
|
118
|
+
href={ideUrl}
|
|
119
|
+
onClick={function () { props.onClose(); }}
|
|
120
|
+
className="w-full flex items-center gap-2.5 px-2.5 py-1.5 rounded-lg text-[12px] text-base-content/60 hover:text-base-content hover:bg-base-content/5 transition-colors"
|
|
121
|
+
>
|
|
122
|
+
<ExternalLink size={13} className="flex-shrink-0 text-base-content/30" />
|
|
123
|
+
Open in IDE
|
|
124
|
+
</a>
|
|
125
|
+
)}
|
|
126
|
+
<button
|
|
127
|
+
role="menuitem"
|
|
128
|
+
onClick={handleOpenTerminal}
|
|
129
|
+
className="w-full flex items-center gap-2.5 px-2.5 py-1.5 rounded-lg text-[12px] text-base-content/60 hover:text-base-content hover:bg-base-content/5 transition-colors"
|
|
130
|
+
>
|
|
131
|
+
<Terminal size={13} className="flex-shrink-0 text-base-content/30" />
|
|
132
|
+
Open terminal
|
|
133
|
+
</button>
|
|
134
|
+
<button
|
|
135
|
+
role="menuitem"
|
|
136
|
+
onClick={handleViewClaudeMd}
|
|
137
|
+
className="w-full flex items-center gap-2.5 px-2.5 py-1.5 rounded-lg text-[12px] text-base-content/60 hover:text-base-content hover:bg-base-content/5 transition-colors"
|
|
138
|
+
>
|
|
139
|
+
<FolderOpen size={13} className="flex-shrink-0 text-base-content/30" />
|
|
140
|
+
Browse files
|
|
141
|
+
</button>
|
|
142
|
+
<button
|
|
143
|
+
role="menuitem"
|
|
144
|
+
onClick={handleCopyPath}
|
|
145
|
+
className="w-full flex items-center gap-2.5 px-2.5 py-1.5 rounded-lg text-[12px] text-base-content/60 hover:text-base-content hover:bg-base-content/5 transition-colors"
|
|
146
|
+
>
|
|
147
|
+
{copied ? (
|
|
148
|
+
<Check size={13} className="flex-shrink-0 text-success" />
|
|
149
|
+
) : (
|
|
150
|
+
<Copy size={13} className="flex-shrink-0 text-base-content/30" />
|
|
151
|
+
)}
|
|
152
|
+
{copied ? "Copied!" : "Copy path"}
|
|
153
|
+
</button>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<div className="px-1.5 py-1.5 border-t border-base-content/10">
|
|
157
|
+
<div className="px-2 pt-1.5 pb-1 text-[10px] font-semibold uppercase tracking-wider text-base-content/25">
|
|
158
|
+
Settings
|
|
159
|
+
</div>
|
|
160
|
+
<button role="menuitem" onClick={function () { goToSettings("general"); }} className="w-full flex items-center gap-2.5 px-2.5 py-1.5 rounded-lg text-[12px] text-base-content/60 hover:text-base-content hover:bg-base-content/5 transition-colors">
|
|
161
|
+
<Settings size={13} className="flex-shrink-0 text-base-content/30" />
|
|
162
|
+
General
|
|
163
|
+
</button>
|
|
164
|
+
<button role="menuitem" onClick={function () { goToSettings("claude"); }} className="w-full flex items-center gap-2.5 px-2.5 py-1.5 rounded-lg text-[12px] text-base-content/60 hover:text-base-content hover:bg-base-content/5 transition-colors">
|
|
165
|
+
<FileText size={13} className="flex-shrink-0 text-base-content/30" />
|
|
166
|
+
Claude
|
|
167
|
+
</button>
|
|
168
|
+
<button role="menuitem" onClick={function () { goToSettings("mcp"); }} className="w-full flex items-center gap-2.5 px-2.5 py-1.5 rounded-lg text-[12px] text-base-content/60 hover:text-base-content hover:bg-base-content/5 transition-colors">
|
|
169
|
+
<Plug size={13} className="flex-shrink-0 text-base-content/30" />
|
|
170
|
+
MCP Servers
|
|
171
|
+
</button>
|
|
172
|
+
<button role="menuitem" onClick={function () { goToSettings("skills"); }} className="w-full flex items-center gap-2.5 px-2.5 py-1.5 rounded-lg text-[12px] text-base-content/60 hover:text-base-content hover:bg-base-content/5 transition-colors">
|
|
173
|
+
<Puzzle size={13} className="flex-shrink-0 text-base-content/30" />
|
|
174
|
+
Skills
|
|
175
|
+
</button>
|
|
176
|
+
<button role="menuitem" onClick={function () { goToSettings("rules"); }} className="w-full flex items-center gap-2.5 px-2.5 py-1.5 rounded-lg text-[12px] text-base-content/60 hover:text-base-content hover:bg-base-content/5 transition-colors">
|
|
177
|
+
<ScrollText size={13} className="flex-shrink-0 text-base-content/30" />
|
|
178
|
+
Rules
|
|
179
|
+
</button>
|
|
180
|
+
<button role="menuitem" onClick={function () { goToSettings("permissions"); }} className="w-full flex items-center gap-2.5 px-2.5 py-1.5 rounded-lg text-[12px] text-base-content/60 hover:text-base-content hover:bg-base-content/5 transition-colors">
|
|
181
|
+
<Shield size={13} className="flex-shrink-0 text-base-content/30" />
|
|
182
|
+
Permissions
|
|
183
|
+
</button>
|
|
184
|
+
<button role="menuitem" onClick={function () { goToSettings("memory"); }} className="w-full flex items-center gap-2.5 px-2.5 py-1.5 rounded-lg text-[12px] text-base-content/60 hover:text-base-content hover:bg-base-content/5 transition-colors">
|
|
185
|
+
<Brain size={13} className="flex-shrink-0 text-base-content/30" />
|
|
186
|
+
Memory
|
|
187
|
+
</button>
|
|
188
|
+
</div>
|
|
189
|
+
</div>,
|
|
190
|
+
document.body
|
|
42
191
|
);
|
|
43
192
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ArrowLeft, Palette, FileText, Terminal, Plug, Puzzle, Network, Settings, ScrollText, Shield, Brain } from "lucide-react";
|
|
1
|
+
import { ArrowLeft, Palette, FileText, Terminal, Plug, Puzzle, Network, Settings, ScrollText, Shield, Brain, MonitorCog, Bell } from "lucide-react";
|
|
2
2
|
import { useSidebar } from "../../hooks/useSidebar";
|
|
3
3
|
import type { SettingsSection, ProjectSettingsSection } from "../../stores/sidebar";
|
|
4
4
|
|
|
@@ -12,8 +12,17 @@ var SETTINGS_NAV = [
|
|
|
12
12
|
group: "GENERAL",
|
|
13
13
|
items: [
|
|
14
14
|
{ id: "appearance" as SettingsSection, label: "Appearance", icon: <Palette size={14} /> },
|
|
15
|
+
{ id: "notifications" as SettingsSection, label: "Notifications", icon: <Bell size={14} /> },
|
|
15
16
|
{ id: "claude" as SettingsSection, label: "Claude Settings", icon: <FileText size={14} /> },
|
|
16
17
|
{ id: "environment" as SettingsSection, label: "Environment", icon: <Terminal size={14} /> },
|
|
18
|
+
{ id: "editor" as SettingsSection, label: "Editor", icon: <MonitorCog size={14} /> },
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
group: "CONFIGURATION",
|
|
23
|
+
items: [
|
|
24
|
+
{ id: "rules" as SettingsSection, label: "Rules", icon: <ScrollText size={14} /> },
|
|
25
|
+
{ id: "memory" as SettingsSection, label: "Memory", icon: <Brain size={14} /> },
|
|
17
26
|
],
|
|
18
27
|
},
|
|
19
28
|
{
|
|
@@ -36,6 +45,7 @@ var PROJECT_SETTINGS_NAV = [
|
|
|
36
45
|
group: "GENERAL",
|
|
37
46
|
items: [
|
|
38
47
|
{ id: "general" as ProjectSettingsSection, label: "General", icon: <Settings size={14} /> },
|
|
48
|
+
{ id: "notifications" as ProjectSettingsSection, label: "Notifications", icon: <Bell size={14} /> },
|
|
39
49
|
{ id: "claude" as ProjectSettingsSection, label: "Claude", icon: <FileText size={14} /> },
|
|
40
50
|
{ id: "environment" as ProjectSettingsSection, label: "Environment", icon: <Terminal size={14} /> },
|
|
41
51
|
],
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useEffect, useRef } from "react";
|
|
2
|
-
import { Plus, ChevronDown, Search, LayoutDashboard } from "lucide-react";
|
|
2
|
+
import { Plus, ChevronDown, Search, LayoutDashboard, FolderOpen, TerminalSquare, StickyNote, Calendar } from "lucide-react";
|
|
3
3
|
import { LatticeLogomark } from "../ui/LatticeLogomark";
|
|
4
4
|
import type { SessionSummary, ServerMessage, SettingsDataMessage } from "@lattice/shared";
|
|
5
5
|
import { useProjects } from "../../hooks/useProjects";
|
|
@@ -8,6 +8,9 @@ import { useWebSocket } from "../../hooks/useWebSocket";
|
|
|
8
8
|
import { useSidebar } from "../../hooks/useSidebar";
|
|
9
9
|
import { useSession } from "../../hooks/useSession";
|
|
10
10
|
import { clearSession } from "../../stores/session";
|
|
11
|
+
import { useOnline } from "../../hooks/useOnline";
|
|
12
|
+
import { openTab } from "../../stores/workspace";
|
|
13
|
+
import { getSidebarStore } from "../../stores/sidebar";
|
|
11
14
|
import { ProjectRail } from "./ProjectRail";
|
|
12
15
|
import { SessionList } from "./SessionList";
|
|
13
16
|
import { UserIsland } from "./UserIsland";
|
|
@@ -35,6 +38,7 @@ export function Sidebar({ onSessionSelect }: { onSessionSelect?: () => void }) {
|
|
|
35
38
|
var { projects, activeProject } = useProjects();
|
|
36
39
|
var { nodes } = useMesh();
|
|
37
40
|
var ws = useWebSocket();
|
|
41
|
+
var online = useOnline();
|
|
38
42
|
var sidebar = useSidebar();
|
|
39
43
|
var session = useSession();
|
|
40
44
|
var [sessionSearch, setSessionSearch] = useState<string>("");
|
|
@@ -144,6 +148,35 @@ export function Sidebar({ onSessionSelect }: { onSessionSelect?: () => void }) {
|
|
|
144
148
|
<span className="font-mono tracking-wide">Dashboard</span>
|
|
145
149
|
</button>
|
|
146
150
|
|
|
151
|
+
<div className="flex flex-col gap-0.5 mx-3 mt-1">
|
|
152
|
+
{[
|
|
153
|
+
{ type: "files" as const, icon: FolderOpen, label: "Files" },
|
|
154
|
+
{ type: "terminal" as const, icon: TerminalSquare, label: "Terminal" },
|
|
155
|
+
{ type: "notes" as const, icon: StickyNote, label: "Notes" },
|
|
156
|
+
{ type: "tasks" as const, icon: Calendar, label: "Tasks" },
|
|
157
|
+
].map(function (item) {
|
|
158
|
+
return (
|
|
159
|
+
<button
|
|
160
|
+
key={item.type}
|
|
161
|
+
type="button"
|
|
162
|
+
onClick={function () {
|
|
163
|
+
openTab(item.type);
|
|
164
|
+
var state = getSidebarStore().state;
|
|
165
|
+
if (state.activeView.type !== "chat") {
|
|
166
|
+
getSidebarStore().setState(function (s) {
|
|
167
|
+
return { ...s, activeView: { type: "chat" } };
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}}
|
|
171
|
+
className="flex items-center gap-2 px-2 py-1.5 rounded-lg text-[11px] text-base-content/40 hover:text-base-content/70 hover:bg-base-300/30 transition-colors"
|
|
172
|
+
>
|
|
173
|
+
<item.icon size={12} />
|
|
174
|
+
<span className="font-mono tracking-wide">{item.label}</span>
|
|
175
|
+
</button>
|
|
176
|
+
);
|
|
177
|
+
})}
|
|
178
|
+
</div>
|
|
179
|
+
|
|
147
180
|
<SectionLabel
|
|
148
181
|
label="Sessions"
|
|
149
182
|
actions={
|
|
@@ -151,7 +184,7 @@ export function Sidebar({ onSessionSelect }: { onSessionSelect?: () => void }) {
|
|
|
151
184
|
<button onClick={function () { setSessionSearchOpen(function (v) { return !v; }); }} className="btn btn-ghost btn-xs btn-square text-base-content/40 hover:text-base-content" aria-label="Search sessions">
|
|
152
185
|
<Search size={13} />
|
|
153
186
|
</button>
|
|
154
|
-
<button onClick={handleNewSession} className="btn btn-ghost btn-xs btn-square text-base-content/40 hover:text-base-content" aria-label="New session">
|
|
187
|
+
<button onClick={handleNewSession} disabled={!online} className="btn btn-ghost btn-xs btn-square text-base-content/40 hover:text-base-content" aria-label="New session">
|
|
155
188
|
<Plus size={13} />
|
|
156
189
|
</button>
|
|
157
190
|
</>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { Sun, Moon, Settings } from "lucide-react";
|
|
1
|
+
import { Sun, Moon, Settings, Download } from "lucide-react";
|
|
2
2
|
import { useTheme } from "../../hooks/useTheme";
|
|
3
3
|
import { useSidebar } from "../../hooks/useSidebar";
|
|
4
|
+
import { useInstallPrompt } from "../../hooks/useInstallPrompt";
|
|
4
5
|
import pkg from "../../../package.json";
|
|
5
6
|
|
|
6
7
|
interface UserIslandProps {
|
|
@@ -11,6 +12,7 @@ interface UserIslandProps {
|
|
|
11
12
|
export function UserIsland(props: UserIslandProps) {
|
|
12
13
|
var { mode, toggleMode } = useTheme();
|
|
13
14
|
var sidebar = useSidebar();
|
|
15
|
+
var { canInstall, install } = useInstallPrompt();
|
|
14
16
|
|
|
15
17
|
var initial = props.nodeName.charAt(0).toUpperCase();
|
|
16
18
|
|
|
@@ -39,19 +41,28 @@ export function UserIsland(props: UserIslandProps) {
|
|
|
39
41
|
</button>
|
|
40
42
|
|
|
41
43
|
<div className="flex items-center gap-0.5 flex-shrink-0">
|
|
44
|
+
{canInstall && (
|
|
45
|
+
<button
|
|
46
|
+
aria-label="Install Lattice"
|
|
47
|
+
onClick={install}
|
|
48
|
+
className="btn btn-ghost btn-xs btn-square text-primary/60 hover:text-primary transition-colors"
|
|
49
|
+
>
|
|
50
|
+
<Download size={14} />
|
|
51
|
+
</button>
|
|
52
|
+
)}
|
|
42
53
|
<button
|
|
43
|
-
aria-label="
|
|
44
|
-
onClick={function () {
|
|
54
|
+
aria-label={mode === "dark" ? "Switch to light mode" : "Switch to dark mode"}
|
|
55
|
+
onClick={function (e) { e.stopPropagation(); toggleMode(); }}
|
|
45
56
|
className="btn btn-ghost btn-xs btn-square text-base-content/30 hover:text-base-content transition-colors"
|
|
46
57
|
>
|
|
47
|
-
<
|
|
58
|
+
{mode === "dark" ? <Sun size={14} /> : <Moon size={14} />}
|
|
48
59
|
</button>
|
|
49
60
|
<button
|
|
50
|
-
aria-label=
|
|
51
|
-
onClick={function (
|
|
61
|
+
aria-label="Global settings"
|
|
62
|
+
onClick={function () { sidebar.openSettings("appearance"); }}
|
|
52
63
|
className="btn btn-ghost btn-xs btn-square text-base-content/30 hover:text-base-content transition-colors"
|
|
53
64
|
>
|
|
54
|
-
|
|
65
|
+
<Settings size={14} />
|
|
55
66
|
</button>
|
|
56
67
|
</div>
|
|
57
68
|
</div>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { RefreshCw } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
export function UpdatePrompt() {
|
|
5
|
+
var [showUpdate, setShowUpdate] = useState(false);
|
|
6
|
+
var [registration, setRegistration] = useState<ServiceWorkerRegistration | null>(null);
|
|
7
|
+
|
|
8
|
+
useEffect(function () {
|
|
9
|
+
if (!("serviceWorker" in navigator)) return;
|
|
10
|
+
|
|
11
|
+
navigator.serviceWorker.ready.then(function (reg) {
|
|
12
|
+
reg.addEventListener("updatefound", function () {
|
|
13
|
+
var newWorker = reg.installing;
|
|
14
|
+
if (!newWorker) return;
|
|
15
|
+
var worker: ServiceWorker = newWorker;
|
|
16
|
+
worker.addEventListener("statechange", function () {
|
|
17
|
+
if (worker.state === "installed" && navigator.serviceWorker.controller) {
|
|
18
|
+
setRegistration(reg);
|
|
19
|
+
setShowUpdate(true);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
}, []);
|
|
25
|
+
|
|
26
|
+
function handleReload() {
|
|
27
|
+
if (registration && registration.waiting) {
|
|
28
|
+
registration.waiting.postMessage({ type: "SKIP_WAITING" });
|
|
29
|
+
}
|
|
30
|
+
window.location.reload();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!showUpdate) return null;
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="fixed bottom-4 right-4 z-[9998] bg-base-300 border border-base-content/15 rounded-xl shadow-2xl px-4 py-3 flex items-center gap-3 max-w-sm">
|
|
37
|
+
<RefreshCw size={16} className="text-primary flex-shrink-0" />
|
|
38
|
+
<div className="flex-1 min-w-0">
|
|
39
|
+
<div className="text-[13px] text-base-content font-semibold">Update available</div>
|
|
40
|
+
<div className="text-[11px] text-base-content/40">A new version of Lattice is ready.</div>
|
|
41
|
+
</div>
|
|
42
|
+
<button onClick={handleReload} className="btn btn-primary btn-xs">
|
|
43
|
+
Reload
|
|
44
|
+
</button>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { ArrowLeft, FileCode, FileX } from "lucide-react";
|
|
3
|
+
import type { FsListResultMessage, FsReadResultMessage, ServerMessage } from "@lattice/shared";
|
|
4
|
+
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
5
|
+
import { useSidebar } from "../../hooks/useSidebar";
|
|
6
|
+
import { useProjects } from "../../hooks/useProjects";
|
|
7
|
+
import { useEditorConfig } from "../../hooks/useEditorConfig";
|
|
8
|
+
import { getEditorUrl } from "../../utils/editorUrl";
|
|
9
|
+
import { FileTree, buildNodes } from "./FileTree";
|
|
10
|
+
import { FileViewer } from "./FileViewer";
|
|
11
|
+
import type { TreeNode } from "./FileTree";
|
|
12
|
+
|
|
13
|
+
export function FileBrowser() {
|
|
14
|
+
var { send, subscribe, unsubscribe } = useWebSocket();
|
|
15
|
+
var { activeProjectSlug } = useSidebar();
|
|
16
|
+
var { activeProject } = useProjects();
|
|
17
|
+
var { editorType, wslDistro } = useEditorConfig();
|
|
18
|
+
var projectSlugRef = useRef<string | null>(null);
|
|
19
|
+
projectSlugRef.current = activeProjectSlug;
|
|
20
|
+
var [rootNodes, setRootNodes] = useState<TreeNode[]>([]);
|
|
21
|
+
var [selectedPath, setSelectedPath] = useState<string | null>(null);
|
|
22
|
+
var [fileContent, setFileContent] = useState<string | null>(null);
|
|
23
|
+
var [loadingContent, setLoadingContent] = useState(false);
|
|
24
|
+
var [mobileShowViewer, setMobileShowViewer] = useState(false);
|
|
25
|
+
var nodesRef = useRef<TreeNode[]>([]);
|
|
26
|
+
|
|
27
|
+
nodesRef.current = rootNodes;
|
|
28
|
+
|
|
29
|
+
var handleListResult = useCallback(function (msg: ServerMessage) {
|
|
30
|
+
var listMsg = msg as FsListResultMessage;
|
|
31
|
+
var newNodes = buildNodes(listMsg.entries);
|
|
32
|
+
|
|
33
|
+
if (listMsg.path === "" || listMsg.path === ".") {
|
|
34
|
+
setRootNodes(newNodes);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function updateNodes(nodes: TreeNode[]): TreeNode[] {
|
|
39
|
+
return nodes.map(function (node) {
|
|
40
|
+
if (node.entry.path === listMsg.path) {
|
|
41
|
+
return Object.assign({}, node, { children: newNodes, expanded: true });
|
|
42
|
+
}
|
|
43
|
+
if (node.children) {
|
|
44
|
+
return Object.assign({}, node, { children: updateNodes(node.children) });
|
|
45
|
+
}
|
|
46
|
+
return node;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
setRootNodes(function (prev) {
|
|
51
|
+
return updateNodes(prev);
|
|
52
|
+
});
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
var handleReadResult = useCallback(function (msg: ServerMessage) {
|
|
56
|
+
var readMsg = msg as FsReadResultMessage;
|
|
57
|
+
setFileContent(readMsg.content);
|
|
58
|
+
setLoadingContent(false);
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
var selectedPathRef = useRef<string | null>(null);
|
|
62
|
+
selectedPathRef.current = selectedPath;
|
|
63
|
+
|
|
64
|
+
var handleFsChanged = useCallback(function (msg: ServerMessage) {
|
|
65
|
+
var changedPath = (msg as { path: string }).path;
|
|
66
|
+
if (changedPath === selectedPathRef.current) {
|
|
67
|
+
send({ type: "fs:read", path: changedPath, projectSlug: projectSlugRef.current || undefined });
|
|
68
|
+
}
|
|
69
|
+
}, [send]);
|
|
70
|
+
|
|
71
|
+
useEffect(function () {
|
|
72
|
+
subscribe("fs:list_result", handleListResult);
|
|
73
|
+
subscribe("fs:read_result", handleReadResult);
|
|
74
|
+
subscribe("fs:changed", handleFsChanged);
|
|
75
|
+
|
|
76
|
+
send({ type: "fs:list", path: ".", projectSlug: projectSlugRef.current || undefined });
|
|
77
|
+
|
|
78
|
+
return function () {
|
|
79
|
+
unsubscribe("fs:list_result", handleListResult);
|
|
80
|
+
unsubscribe("fs:read_result", handleReadResult);
|
|
81
|
+
unsubscribe("fs:changed", handleFsChanged);
|
|
82
|
+
};
|
|
83
|
+
}, [handleListResult, handleReadResult, handleFsChanged, send, subscribe, unsubscribe]);
|
|
84
|
+
|
|
85
|
+
function handleToggle(path: string) {
|
|
86
|
+
function findAndToggle(nodes: TreeNode[]): TreeNode[] {
|
|
87
|
+
return nodes.map(function (node) {
|
|
88
|
+
if (node.entry.path === path) {
|
|
89
|
+
if (!node.expanded && !node.children) {
|
|
90
|
+
send({ type: "fs:list", path: path, projectSlug: projectSlugRef.current || undefined });
|
|
91
|
+
return Object.assign({}, node, { expanded: true });
|
|
92
|
+
}
|
|
93
|
+
return Object.assign({}, node, { expanded: !node.expanded });
|
|
94
|
+
}
|
|
95
|
+
if (node.children) {
|
|
96
|
+
return Object.assign({}, node, { children: findAndToggle(node.children) });
|
|
97
|
+
}
|
|
98
|
+
return node;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
setRootNodes(function (prev) {
|
|
102
|
+
return findAndToggle(prev);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function handleSelect(path: string) {
|
|
107
|
+
setSelectedPath(path);
|
|
108
|
+
setFileContent(null);
|
|
109
|
+
setLoadingContent(true);
|
|
110
|
+
setMobileShowViewer(true);
|
|
111
|
+
send({ type: "fs:read", path: path, projectSlug: activeProjectSlug || undefined });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
var editorUrlForSelected = selectedPath && activeProject
|
|
115
|
+
? getEditorUrl(editorType, activeProject.path, selectedPath, undefined, wslDistro, activeProject.ideProjectName)
|
|
116
|
+
: null;
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div className="flex h-full w-full overflow-hidden bg-base-100">
|
|
120
|
+
<div className={"w-full sm:w-[220px] sm:flex-shrink-0 sm:border-r sm:border-base-content/15 overflow-y-auto p-2" + (mobileShowViewer ? " hidden sm:block" : " block")}>
|
|
121
|
+
<div className="text-[11px] font-semibold tracking-[0.06em] uppercase text-base-content/40 px-2 pb-2 pt-1">
|
|
122
|
+
Files
|
|
123
|
+
</div>
|
|
124
|
+
<FileTree
|
|
125
|
+
nodes={rootNodes}
|
|
126
|
+
selectedPath={selectedPath}
|
|
127
|
+
onToggle={handleToggle}
|
|
128
|
+
onSelect={handleSelect}
|
|
129
|
+
/>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<div className={"flex-1 flex-col overflow-hidden" + (mobileShowViewer ? " flex" : " hidden sm:flex")}>
|
|
133
|
+
{selectedPath && (
|
|
134
|
+
<button
|
|
135
|
+
className="sm:hidden flex items-center gap-1 px-2 py-1.5 text-[12px] text-base-content/60 hover:text-base-content border-b border-base-content/15"
|
|
136
|
+
onClick={function () { setMobileShowViewer(false); }}
|
|
137
|
+
>
|
|
138
|
+
<ArrowLeft size={14} />
|
|
139
|
+
<span>Back to files</span>
|
|
140
|
+
</button>
|
|
141
|
+
)}
|
|
142
|
+
|
|
143
|
+
{!selectedPath && (
|
|
144
|
+
<div className="h-full flex flex-col items-center justify-center gap-3">
|
|
145
|
+
<FileCode size={28} className="text-base-content/15" />
|
|
146
|
+
<div className="text-base-content/40 text-[13px]">Select a file from the tree to view its contents</div>
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
|
|
150
|
+
{selectedPath && loadingContent && (
|
|
151
|
+
<div className="h-full flex items-center justify-center text-base-content/40 text-[13px]">
|
|
152
|
+
Loading...
|
|
153
|
+
</div>
|
|
154
|
+
)}
|
|
155
|
+
|
|
156
|
+
{selectedPath && !loadingContent && fileContent !== null && (
|
|
157
|
+
<FileViewer
|
|
158
|
+
path={selectedPath}
|
|
159
|
+
content={fileContent}
|
|
160
|
+
editorUrl={editorUrlForSelected}
|
|
161
|
+
/>
|
|
162
|
+
)}
|
|
163
|
+
|
|
164
|
+
{selectedPath && !loadingContent && fileContent === null && (
|
|
165
|
+
<div className="h-full flex flex-col items-center justify-center gap-3">
|
|
166
|
+
<FileX size={28} className="text-base-content/15" />
|
|
167
|
+
<div className="text-base-content/40 text-[13px]">Cannot display this file</div>
|
|
168
|
+
<div className="text-base-content/30 text-[11px]">Binary files and files over 512KB are not shown</div>
|
|
169
|
+
</div>
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
}
|