@cryptiklemur/lattice 0.0.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/.editorconfig +12 -0
- package/.github/workflows/release.yml +44 -0
- package/.impeccable.md +66 -0
- package/.releaserc.json +32 -0
- package/.serena/project.yml +138 -0
- package/CLAUDE.md +35 -0
- package/CONTRIBUTING.md +93 -0
- package/LICENSE +21 -0
- package/README.md +83 -0
- package/bun.lock +1459 -0
- package/bunfig.toml +2 -0
- package/client/index.html +32 -0
- package/client/package.json +37 -0
- package/client/public/icons/icon-192.svg +11 -0
- package/client/public/icons/icon-512.svg +11 -0
- package/client/public/manifest.json +24 -0
- package/client/public/sw.js +61 -0
- package/client/src/App.tsx +28 -0
- package/client/src/components/auth/PassphrasePrompt.tsx +70 -0
- package/client/src/components/chat/ChatInput.tsx +241 -0
- package/client/src/components/chat/ChatView.tsx +727 -0
- package/client/src/components/chat/Message.tsx +362 -0
- package/client/src/components/chat/ModelSelector.tsx +87 -0
- package/client/src/components/chat/PermissionModeSelector.tsx +41 -0
- package/client/src/components/chat/StatusBar.tsx +50 -0
- package/client/src/components/chat/ToolGroup.tsx +129 -0
- package/client/src/components/chat/ToolResultRenderer.tsx +343 -0
- package/client/src/components/chat/toolSummary.ts +41 -0
- package/client/src/components/dashboard/DashboardView.tsx +219 -0
- package/client/src/components/dashboard/ProjectDashboardView.tsx +168 -0
- package/client/src/components/mesh/NodeBadge.tsx +24 -0
- package/client/src/components/mesh/PairingDialog.tsx +281 -0
- package/client/src/components/panels/FileBrowser.tsx +241 -0
- package/client/src/components/panels/StickyNotes.tsx +187 -0
- package/client/src/components/panels/Terminal.tsx +128 -0
- package/client/src/components/project-settings/ProjectClaude.tsx +304 -0
- package/client/src/components/project-settings/ProjectEnvironment.tsx +235 -0
- package/client/src/components/project-settings/ProjectGeneral.tsx +76 -0
- package/client/src/components/project-settings/ProjectMcp.tsx +232 -0
- package/client/src/components/project-settings/ProjectPermissions.tsx +209 -0
- package/client/src/components/project-settings/ProjectRules.tsx +277 -0
- package/client/src/components/project-settings/ProjectSettingsView.tsx +99 -0
- package/client/src/components/project-settings/ProjectSkills.tsx +91 -0
- package/client/src/components/settings/Appearance.tsx +151 -0
- package/client/src/components/settings/ClaudeSettings.tsx +151 -0
- package/client/src/components/settings/Environment.tsx +185 -0
- package/client/src/components/settings/GlobalMcp.tsx +207 -0
- package/client/src/components/settings/GlobalSkills.tsx +125 -0
- package/client/src/components/settings/MeshStatus.tsx +145 -0
- package/client/src/components/settings/SettingsView.tsx +57 -0
- package/client/src/components/settings/SkillMarketplace.tsx +175 -0
- package/client/src/components/settings/mcp-shared.tsx +194 -0
- package/client/src/components/settings/skill-shared.tsx +177 -0
- package/client/src/components/setup/SetupWizard.tsx +750 -0
- package/client/src/components/sidebar/NodeSettingsModal.tsx +180 -0
- package/client/src/components/sidebar/ProjectDropdown.tsx +43 -0
- package/client/src/components/sidebar/ProjectRail.tsx +291 -0
- package/client/src/components/sidebar/SearchFilter.tsx +52 -0
- package/client/src/components/sidebar/SessionList.tsx +384 -0
- package/client/src/components/sidebar/SettingsSidebar.tsx +128 -0
- package/client/src/components/sidebar/Sidebar.tsx +209 -0
- package/client/src/components/sidebar/UserIsland.tsx +59 -0
- package/client/src/components/sidebar/UserMenu.tsx +101 -0
- package/client/src/components/ui/CommandPalette.tsx +321 -0
- package/client/src/components/ui/ErrorBoundary.tsx +56 -0
- package/client/src/components/ui/IconPicker.tsx +209 -0
- package/client/src/components/ui/LatticeLogomark.tsx +19 -0
- package/client/src/components/ui/PopupMenu.tsx +98 -0
- package/client/src/components/ui/SaveFooter.tsx +38 -0
- package/client/src/components/ui/Toast.tsx +112 -0
- package/client/src/hooks/useMesh.ts +89 -0
- package/client/src/hooks/useProjectSettings.ts +56 -0
- package/client/src/hooks/useProjects.ts +66 -0
- package/client/src/hooks/useSaveState.ts +59 -0
- package/client/src/hooks/useSession.ts +317 -0
- package/client/src/hooks/useSidebar.ts +74 -0
- package/client/src/hooks/useSkills.ts +30 -0
- package/client/src/hooks/useTheme.ts +114 -0
- package/client/src/hooks/useWebSocket.ts +26 -0
- package/client/src/main.tsx +10 -0
- package/client/src/providers/WebSocketProvider.tsx +146 -0
- package/client/src/router.tsx +391 -0
- package/client/src/stores/mesh.ts +78 -0
- package/client/src/stores/session.ts +322 -0
- package/client/src/stores/sidebar.ts +336 -0
- package/client/src/stores/theme.ts +44 -0
- package/client/src/styles/global.css +167 -0
- package/client/src/styles/theme-vars.css +18 -0
- package/client/src/themes/index.ts +79 -0
- package/client/src/utils/findDuplicateKeys.ts +12 -0
- package/client/tsconfig.json +14 -0
- package/client/vite.config.ts +20 -0
- package/package.json +46 -0
- package/server/package.json +22 -0
- package/server/src/auth/passphrase.ts +48 -0
- package/server/src/config.ts +55 -0
- package/server/src/daemon.ts +338 -0
- package/server/src/features/ralph-loop.ts +173 -0
- package/server/src/features/scheduler.ts +281 -0
- package/server/src/features/sticky-notes.ts +102 -0
- package/server/src/handlers/chat.ts +194 -0
- package/server/src/handlers/fs.ts +84 -0
- package/server/src/handlers/loop.ts +37 -0
- package/server/src/handlers/mesh.ts +125 -0
- package/server/src/handlers/notes.ts +45 -0
- package/server/src/handlers/project-settings.ts +174 -0
- package/server/src/handlers/scheduler.ts +47 -0
- package/server/src/handlers/session.ts +159 -0
- package/server/src/handlers/settings.ts +109 -0
- package/server/src/handlers/skills.ts +380 -0
- package/server/src/handlers/terminal.ts +70 -0
- package/server/src/identity.ts +26 -0
- package/server/src/index.ts +190 -0
- package/server/src/mesh/connector.ts +209 -0
- package/server/src/mesh/discovery.ts +123 -0
- package/server/src/mesh/pairing.ts +94 -0
- package/server/src/mesh/peers.ts +52 -0
- package/server/src/mesh/proxy.ts +103 -0
- package/server/src/mesh/session-sync.ts +107 -0
- package/server/src/project/context-breakdown.ts +289 -0
- package/server/src/project/file-browser.ts +106 -0
- package/server/src/project/project-files.ts +267 -0
- package/server/src/project/registry.ts +57 -0
- package/server/src/project/sdk-bridge.ts +566 -0
- package/server/src/project/session.ts +432 -0
- package/server/src/project/terminal.ts +69 -0
- package/server/src/tls.ts +51 -0
- package/server/src/ws/broadcast.ts +31 -0
- package/server/src/ws/router.ts +104 -0
- package/server/src/ws/server.ts +2 -0
- package/server/tsconfig.json +16 -0
- package/shared/package.json +11 -0
- package/shared/src/constants.ts +7 -0
- package/shared/src/index.ts +4 -0
- package/shared/src/messages.ts +638 -0
- package/shared/src/models.ts +136 -0
- package/shared/src/project-settings.ts +45 -0
- package/shared/tsconfig.json +11 -0
- package/themes/amoled.json +20 -0
- package/themes/ayu-light.json +9 -0
- package/themes/catppuccin-latte.json +9 -0
- package/themes/catppuccin-mocha.json +9 -0
- package/themes/clay-light.json +10 -0
- package/themes/clay.json +10 -0
- package/themes/dracula.json +9 -0
- package/themes/everforest-light.json +9 -0
- package/themes/everforest.json +9 -0
- package/themes/github-light.json +9 -0
- package/themes/gruvbox-dark.json +9 -0
- package/themes/gruvbox-light.json +9 -0
- package/themes/monokai.json +9 -0
- package/themes/nord-light.json +9 -0
- package/themes/nord.json +9 -0
- package/themes/one-dark.json +9 -0
- package/themes/one-light.json +9 -0
- package/themes/rose-pine-dawn.json +9 -0
- package/themes/rose-pine.json +9 -0
- package/themes/solarized-dark.json +9 -0
- package/themes/solarized-light.json +9 -0
- package/themes/tokyo-night-light.json +9 -0
- package/themes/tokyo-night.json +9 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,241 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import type { StickyNote, ServerMessage } from "@lattice/shared";
|
|
3
|
+
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
4
|
+
|
|
5
|
+
interface NoteCardProps {
|
|
6
|
+
note: StickyNote;
|
|
7
|
+
onEdit: (id: string) => void;
|
|
8
|
+
onDelete: (id: string) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function NoteCard(props: NoteCardProps) {
|
|
12
|
+
var { note, onEdit, onDelete } = props;
|
|
13
|
+
return (
|
|
14
|
+
<div className="card bg-base-200 border border-base-300">
|
|
15
|
+
<div className="card-body p-3">
|
|
16
|
+
<div className="text-[13px] text-base-content whitespace-pre-wrap break-words leading-relaxed min-h-12">
|
|
17
|
+
{note.content}
|
|
18
|
+
</div>
|
|
19
|
+
<div className="flex gap-1.5 justify-end mt-2">
|
|
20
|
+
<button
|
|
21
|
+
onClick={function () { onEdit(note.id); }}
|
|
22
|
+
className="btn btn-ghost btn-xs border border-base-300"
|
|
23
|
+
>
|
|
24
|
+
Edit
|
|
25
|
+
</button>
|
|
26
|
+
<button
|
|
27
|
+
onClick={function () { onDelete(note.id); }}
|
|
28
|
+
className="btn btn-ghost btn-xs border border-base-300 text-base-content/60"
|
|
29
|
+
>
|
|
30
|
+
Delete
|
|
31
|
+
</button>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface EditModalProps {
|
|
39
|
+
initial: string;
|
|
40
|
+
onSave: (content: string) => void;
|
|
41
|
+
onCancel: () => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function EditModal(props: EditModalProps) {
|
|
45
|
+
var { initial, onSave, onCancel } = props;
|
|
46
|
+
var [content, setContent] = useState(initial);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className="fixed inset-0 bg-black/50 z-[1000] flex items-center justify-center" role="dialog" aria-modal="true" aria-label="Edit note">
|
|
50
|
+
<div className="card bg-base-200 border border-base-300 w-[400px] max-w-[90vw] shadow-2xl">
|
|
51
|
+
<div className="card-body p-5">
|
|
52
|
+
<div className="text-[13px] font-semibold text-base-content mb-3">Edit Note</div>
|
|
53
|
+
<textarea
|
|
54
|
+
autoFocus
|
|
55
|
+
value={content}
|
|
56
|
+
onChange={function (e) { setContent(e.target.value); }}
|
|
57
|
+
className="textarea textarea-bordered w-full min-h-[120px] bg-base-300 text-base-content text-[13px] resize-y"
|
|
58
|
+
/>
|
|
59
|
+
<div className="flex gap-2 justify-end mt-3">
|
|
60
|
+
<button
|
|
61
|
+
onClick={onCancel}
|
|
62
|
+
className="btn btn-ghost btn-sm"
|
|
63
|
+
>
|
|
64
|
+
Cancel
|
|
65
|
+
</button>
|
|
66
|
+
<button
|
|
67
|
+
onClick={function () { onSave(content); }}
|
|
68
|
+
className="btn btn-primary btn-sm"
|
|
69
|
+
>
|
|
70
|
+
Save
|
|
71
|
+
</button>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function StickyNotes() {
|
|
80
|
+
var { send, subscribe, unsubscribe } = useWebSocket();
|
|
81
|
+
var [notes, setNotes] = useState<StickyNote[]>([]);
|
|
82
|
+
var [editingId, setEditingId] = useState<string | null>(null);
|
|
83
|
+
var [creating, setCreating] = useState(false);
|
|
84
|
+
|
|
85
|
+
var editingNote = editingId ? notes.find(function (n) { return n.id === editingId; }) : null;
|
|
86
|
+
|
|
87
|
+
var handleMessage = useCallback(function (msg: ServerMessage) {
|
|
88
|
+
if (msg.type === "notes:list_result") {
|
|
89
|
+
setNotes(msg.notes);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (msg.type === "notes:created") {
|
|
93
|
+
setNotes(function (prev) { return [...prev, msg.note]; });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (msg.type === "notes:updated") {
|
|
97
|
+
setNotes(function (prev) {
|
|
98
|
+
return prev.map(function (n) { return n.id === msg.note.id ? msg.note : n; });
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (msg.type === "notes:deleted") {
|
|
103
|
+
setNotes(function (prev) { return prev.filter(function (n) { return n.id !== msg.id; }); });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
}, []);
|
|
107
|
+
|
|
108
|
+
useEffect(function () {
|
|
109
|
+
subscribe("notes:list_result", handleMessage);
|
|
110
|
+
subscribe("notes:created", handleMessage);
|
|
111
|
+
subscribe("notes:updated", handleMessage);
|
|
112
|
+
subscribe("notes:deleted", handleMessage);
|
|
113
|
+
send({ type: "notes:list" });
|
|
114
|
+
return function () {
|
|
115
|
+
unsubscribe("notes:list_result", handleMessage);
|
|
116
|
+
unsubscribe("notes:created", handleMessage);
|
|
117
|
+
unsubscribe("notes:updated", handleMessage);
|
|
118
|
+
unsubscribe("notes:deleted", handleMessage);
|
|
119
|
+
};
|
|
120
|
+
}, [send, subscribe, unsubscribe, handleMessage]);
|
|
121
|
+
|
|
122
|
+
function handleCreate(content: string) {
|
|
123
|
+
if (content.trim()) {
|
|
124
|
+
send({ type: "notes:create", content: content.trim() });
|
|
125
|
+
}
|
|
126
|
+
setCreating(false);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function handleEdit(content: string) {
|
|
130
|
+
if (editingId && content.trim()) {
|
|
131
|
+
send({ type: "notes:update", id: editingId, content: content.trim() });
|
|
132
|
+
}
|
|
133
|
+
setEditingId(null);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function handleDelete(id: string) {
|
|
137
|
+
send({ type: "notes:delete", id });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<div className="flex flex-col h-full bg-base-100">
|
|
142
|
+
<div className="flex items-center justify-between px-4 py-3 border-b border-base-300">
|
|
143
|
+
<span className="text-[13px] font-semibold text-base-content">Sticky Notes</span>
|
|
144
|
+
<button
|
|
145
|
+
onClick={function () { setCreating(true); }}
|
|
146
|
+
className="btn btn-primary btn-xs"
|
|
147
|
+
>
|
|
148
|
+
New Note
|
|
149
|
+
</button>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<div className="flex-1 overflow-auto p-3 flex flex-col gap-2.5">
|
|
153
|
+
{notes.length === 0 && (
|
|
154
|
+
<div className="text-base-content/50 text-[13px] text-center mt-10">
|
|
155
|
+
No notes yet. Create one to get started.
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
{notes.map(function (note) {
|
|
159
|
+
return (
|
|
160
|
+
<NoteCard
|
|
161
|
+
key={note.id}
|
|
162
|
+
note={note}
|
|
163
|
+
onEdit={setEditingId}
|
|
164
|
+
onDelete={handleDelete}
|
|
165
|
+
/>
|
|
166
|
+
);
|
|
167
|
+
})}
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
{creating && (
|
|
171
|
+
<EditModal
|
|
172
|
+
initial=""
|
|
173
|
+
onSave={handleCreate}
|
|
174
|
+
onCancel={function () { setCreating(false); }}
|
|
175
|
+
/>
|
|
176
|
+
)}
|
|
177
|
+
|
|
178
|
+
{editingNote && (
|
|
179
|
+
<EditModal
|
|
180
|
+
initial={editingNote.content}
|
|
181
|
+
onSave={handleEdit}
|
|
182
|
+
onCancel={function () { setEditingId(null); }}
|
|
183
|
+
/>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { Terminal as XTerm } from "@xterm/xterm";
|
|
3
|
+
import { FitAddon } from "@xterm/addon-fit";
|
|
4
|
+
import { WebLinksAddon } from "@xterm/addon-web-links";
|
|
5
|
+
import "@xterm/xterm/css/xterm.css";
|
|
6
|
+
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
7
|
+
import type { ServerMessage, TerminalCreatedMessage, TerminalOutputMessage } from "@lattice/shared";
|
|
8
|
+
|
|
9
|
+
function getXtermTheme(): Record<string, string> {
|
|
10
|
+
var root = document.documentElement;
|
|
11
|
+
var cs = getComputedStyle(root);
|
|
12
|
+
|
|
13
|
+
function resolveVar(prop: string, fallback: string): string {
|
|
14
|
+
var val = cs.getPropertyValue(prop).trim();
|
|
15
|
+
return val || fallback;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
background: resolveVar("--base00", "#0d0d0d"),
|
|
20
|
+
foreground: resolveVar("--base05", "#d4d4d4"),
|
|
21
|
+
cursor: resolveVar("--base05", "#d4d4d4"),
|
|
22
|
+
selectionBackground: resolveVar("--base02", "#264f78"),
|
|
23
|
+
black: resolveVar("--base00", "#1e1e1e"),
|
|
24
|
+
red: resolveVar("--base08", "#f44747"),
|
|
25
|
+
green: resolveVar("--base0B", "#4ec9b0"),
|
|
26
|
+
yellow: resolveVar("--base0A", "#dcdcaa"),
|
|
27
|
+
blue: resolveVar("--base0D", "#569cd6"),
|
|
28
|
+
magenta: resolveVar("--base0E", "#c586c0"),
|
|
29
|
+
cyan: resolveVar("--base0C", "#9cdcfe"),
|
|
30
|
+
white: resolveVar("--base05", "#d4d4d4"),
|
|
31
|
+
brightBlack: resolveVar("--base03", "#808080"),
|
|
32
|
+
brightRed: resolveVar("--base08", "#f44747"),
|
|
33
|
+
brightGreen: resolveVar("--base0B", "#4ec9b0"),
|
|
34
|
+
brightYellow: resolveVar("--base0A", "#dcdcaa"),
|
|
35
|
+
brightBlue: resolveVar("--base0D", "#569cd6"),
|
|
36
|
+
brightMagenta: resolveVar("--base0E", "#c586c0"),
|
|
37
|
+
brightCyan: resolveVar("--base0C", "#9cdcfe"),
|
|
38
|
+
brightWhite: resolveVar("--base07", "#ffffff"),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function Terminal() {
|
|
43
|
+
var containerRef = useRef<HTMLDivElement | null>(null);
|
|
44
|
+
var xtermRef = useRef<XTerm | null>(null);
|
|
45
|
+
var fitAddonRef = useRef<FitAddon | null>(null);
|
|
46
|
+
var termIdRef = useRef<string | null>(null);
|
|
47
|
+
var { send, subscribe, unsubscribe } = useWebSocket();
|
|
48
|
+
var [ready, setReady] = useState(false);
|
|
49
|
+
|
|
50
|
+
useEffect(function() {
|
|
51
|
+
if (!containerRef.current) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
var term = new XTerm({
|
|
56
|
+
cursorBlink: true,
|
|
57
|
+
fontFamily: getComputedStyle(document.documentElement).getPropertyValue("--font-mono").trim() || "JetBrains Mono, monospace",
|
|
58
|
+
fontSize: 13,
|
|
59
|
+
theme: getXtermTheme(),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
var fitAddon = new FitAddon();
|
|
63
|
+
var webLinksAddon = new WebLinksAddon();
|
|
64
|
+
term.loadAddon(fitAddon);
|
|
65
|
+
term.loadAddon(webLinksAddon);
|
|
66
|
+
term.open(containerRef.current);
|
|
67
|
+
fitAddon.fit();
|
|
68
|
+
|
|
69
|
+
xtermRef.current = term;
|
|
70
|
+
fitAddonRef.current = fitAddon;
|
|
71
|
+
|
|
72
|
+
send({ type: "terminal:create" });
|
|
73
|
+
|
|
74
|
+
function onCreated(msg: ServerMessage) {
|
|
75
|
+
var created = msg as TerminalCreatedMessage;
|
|
76
|
+
termIdRef.current = created.termId;
|
|
77
|
+
setReady(true);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function onOutput(msg: ServerMessage) {
|
|
81
|
+
var output = msg as TerminalOutputMessage;
|
|
82
|
+
if (xtermRef.current && output.termId === termIdRef.current) {
|
|
83
|
+
xtermRef.current.write(output.data);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
subscribe("terminal:created", onCreated);
|
|
88
|
+
subscribe("terminal:output", onOutput);
|
|
89
|
+
|
|
90
|
+
term.onData(function(data: string) {
|
|
91
|
+
var termId = termIdRef.current;
|
|
92
|
+
if (termId) {
|
|
93
|
+
send({ type: "terminal:input", termId: termId, data: data });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
var resizeObserver = new ResizeObserver(function() {
|
|
98
|
+
if (fitAddonRef.current) {
|
|
99
|
+
fitAddonRef.current.fit();
|
|
100
|
+
var termId = termIdRef.current;
|
|
101
|
+
var dim = fitAddonRef.current.proposeDimensions();
|
|
102
|
+
if (termId && dim) {
|
|
103
|
+
send({ type: "terminal:resize", termId: termId, cols: dim.cols, rows: dim.rows });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (containerRef.current) {
|
|
109
|
+
resizeObserver.observe(containerRef.current);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return function() {
|
|
113
|
+
unsubscribe("terminal:created", onCreated);
|
|
114
|
+
unsubscribe("terminal:output", onOutput);
|
|
115
|
+
resizeObserver.disconnect();
|
|
116
|
+
term.dispose();
|
|
117
|
+
xtermRef.current = null;
|
|
118
|
+
fitAddonRef.current = null;
|
|
119
|
+
};
|
|
120
|
+
}, []);
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<div
|
|
124
|
+
ref={containerRef}
|
|
125
|
+
className="w-full h-full min-h-[200px] overflow-hidden bg-base-100"
|
|
126
|
+
/>
|
|
127
|
+
);
|
|
128
|
+
}
|