@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,343 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
import { createPortal } from "react-dom";
|
|
3
|
+
import Markdown from "react-markdown";
|
|
4
|
+
import { Columns2, AlignLeft, FileText, Image } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
interface ToolResultRendererProps {
|
|
7
|
+
toolName: string;
|
|
8
|
+
args: string;
|
|
9
|
+
result: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function parseArgs(argsStr: string): Record<string, unknown> {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(argsStr);
|
|
15
|
+
} catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isImagePath(path: string): boolean {
|
|
21
|
+
var ext = path.split(".").pop()?.toLowerCase() || "";
|
|
22
|
+
return ["png", "jpg", "jpeg", "gif", "svg", "webp", "bmp", "ico"].includes(ext);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function hasMarkdownTable(text: string): boolean {
|
|
26
|
+
return /\|.+\|[\r\n]+\|[\s-:]+\|/.test(text);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function computeLineDiff(oldText: string, newText: string): Array<{ type: "same" | "add" | "remove"; text: string }> {
|
|
30
|
+
var oldLines = oldText.split("\n");
|
|
31
|
+
var newLines = newText.split("\n");
|
|
32
|
+
var result: Array<{ type: "same" | "add" | "remove"; text: string }> = [];
|
|
33
|
+
|
|
34
|
+
var prefixLen = 0;
|
|
35
|
+
while (prefixLen < oldLines.length && prefixLen < newLines.length && oldLines[prefixLen] === newLines[prefixLen]) {
|
|
36
|
+
prefixLen++;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
var suffixLen = 0;
|
|
40
|
+
while (
|
|
41
|
+
suffixLen < oldLines.length - prefixLen &&
|
|
42
|
+
suffixLen < newLines.length - prefixLen &&
|
|
43
|
+
oldLines[oldLines.length - 1 - suffixLen] === newLines[newLines.length - 1 - suffixLen]
|
|
44
|
+
) {
|
|
45
|
+
suffixLen++;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
var contextBefore = Math.max(0, prefixLen - 3);
|
|
49
|
+
for (var i = contextBefore; i < prefixLen; i++) {
|
|
50
|
+
result.push({ type: "same", text: oldLines[i] });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (var j = prefixLen; j < oldLines.length - suffixLen; j++) {
|
|
54
|
+
result.push({ type: "remove", text: oldLines[j] });
|
|
55
|
+
}
|
|
56
|
+
for (var k = prefixLen; k < newLines.length - suffixLen; k++) {
|
|
57
|
+
result.push({ type: "add", text: newLines[k] });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
var suffixStart = Math.max(oldLines.length - suffixLen, prefixLen);
|
|
61
|
+
var contextAfter = Math.min(suffixStart + 3, oldLines.length);
|
|
62
|
+
for (var l = suffixStart; l < contextAfter; l++) {
|
|
63
|
+
result.push({ type: "same", text: oldLines[l] });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function DiffUnified(props: { oldText: string; newText: string }) {
|
|
70
|
+
var lines = computeLineDiff(props.oldText, props.newText);
|
|
71
|
+
return (
|
|
72
|
+
<div className="font-mono text-[11px] leading-relaxed overflow-x-auto">
|
|
73
|
+
{lines.map(function (line, i) {
|
|
74
|
+
var bg = line.type === "add" ? "bg-success/10" : line.type === "remove" ? "bg-error/10" : "";
|
|
75
|
+
var prefix = line.type === "add" ? "+" : line.type === "remove" ? "-" : " ";
|
|
76
|
+
var color = line.type === "add" ? "text-success/70" : line.type === "remove" ? "text-error/70" : "text-base-content/40";
|
|
77
|
+
return (
|
|
78
|
+
<div key={i} className={bg + " px-2 whitespace-pre-wrap break-words"}>
|
|
79
|
+
<span className={color + " select-none inline-block w-4"}>{prefix}</span>
|
|
80
|
+
<span className={color}>{line.text}</span>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
})}
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function DiffSideBySide(props: { oldText: string; newText: string }) {
|
|
89
|
+
var lines = computeLineDiff(props.oldText, props.newText);
|
|
90
|
+
var leftLines: Array<{ text: string; type: string }> = [];
|
|
91
|
+
var rightLines: Array<{ text: string; type: string }> = [];
|
|
92
|
+
|
|
93
|
+
for (var i = 0; i < lines.length; i++) {
|
|
94
|
+
if (lines[i].type === "same") {
|
|
95
|
+
leftLines.push({ text: lines[i].text, type: "same" });
|
|
96
|
+
rightLines.push({ text: lines[i].text, type: "same" });
|
|
97
|
+
} else if (lines[i].type === "remove") {
|
|
98
|
+
leftLines.push({ text: lines[i].text, type: "remove" });
|
|
99
|
+
} else if (lines[i].type === "add") {
|
|
100
|
+
rightLines.push({ text: lines[i].text, type: "add" });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
var maxLen = Math.max(leftLines.length, rightLines.length);
|
|
105
|
+
while (leftLines.length < maxLen) leftLines.push({ text: "", type: "pad" });
|
|
106
|
+
while (rightLines.length < maxLen) rightLines.push({ text: "", type: "pad" });
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div className="font-mono text-[11px] leading-relaxed overflow-x-auto grid grid-cols-2 gap-0">
|
|
110
|
+
<div className="border-r border-base-content/8">
|
|
111
|
+
{leftLines.map(function (line, i) {
|
|
112
|
+
var bg = line.type === "remove" ? "bg-error/10" : "";
|
|
113
|
+
var color = line.type === "remove" ? "text-error/70" : line.type === "same" ? "text-base-content/40" : "text-transparent";
|
|
114
|
+
return (
|
|
115
|
+
<div key={i} className={bg + " px-2 whitespace-pre-wrap break-words min-h-[1.4em]"}>
|
|
116
|
+
<span className={color}>{line.text}</span>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
})}
|
|
120
|
+
</div>
|
|
121
|
+
<div>
|
|
122
|
+
{rightLines.map(function (line, i) {
|
|
123
|
+
var bg = line.type === "add" ? "bg-success/10" : "";
|
|
124
|
+
var color = line.type === "add" ? "text-success/70" : line.type === "same" ? "text-base-content/40" : "text-transparent";
|
|
125
|
+
return (
|
|
126
|
+
<div key={i} className={bg + " px-2 whitespace-pre-wrap break-words min-h-[1.4em]"}>
|
|
127
|
+
<span className={color}>{line.text}</span>
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
})}
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function DiffRenderer(props: { oldText: string; newText: string }) {
|
|
137
|
+
var [mode, setMode] = useState<"unified" | "split">("unified");
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<div>
|
|
141
|
+
<div className="flex items-center justify-between mb-1">
|
|
142
|
+
<div className="text-[9px] text-base-content/25 uppercase tracking-wider font-semibold">Diff</div>
|
|
143
|
+
<div className="flex items-center gap-0.5">
|
|
144
|
+
<button
|
|
145
|
+
type="button"
|
|
146
|
+
onClick={function () { setMode("unified"); }}
|
|
147
|
+
className={"p-0.5 rounded transition-colors " + (mode === "unified" ? "text-primary/70 bg-primary/10" : "text-base-content/25 hover:text-base-content/40")}
|
|
148
|
+
title="Unified view"
|
|
149
|
+
>
|
|
150
|
+
<AlignLeft size={11} />
|
|
151
|
+
</button>
|
|
152
|
+
<button
|
|
153
|
+
type="button"
|
|
154
|
+
onClick={function () { setMode("split"); }}
|
|
155
|
+
className={"p-0.5 rounded transition-colors " + (mode === "split" ? "text-primary/70 bg-primary/10" : "text-base-content/25 hover:text-base-content/40")}
|
|
156
|
+
title="Side-by-side view"
|
|
157
|
+
>
|
|
158
|
+
<Columns2 size={11} />
|
|
159
|
+
</button>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
<div className="rounded-md bg-base-100/50 overflow-hidden">
|
|
163
|
+
{mode === "unified" ? (
|
|
164
|
+
<DiffUnified oldText={props.oldText} newText={props.newText} />
|
|
165
|
+
) : (
|
|
166
|
+
<DiffSideBySide oldText={props.oldText} newText={props.newText} />
|
|
167
|
+
)}
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function ImageRenderer(props: { path: string }) {
|
|
174
|
+
var [error, setError] = useState(false);
|
|
175
|
+
var [modalOpen, setModalOpen] = useState(false);
|
|
176
|
+
var imgSrc = "/api/file?path=" + encodeURIComponent(props.path);
|
|
177
|
+
|
|
178
|
+
useEffect(function () {
|
|
179
|
+
if (!modalOpen) return;
|
|
180
|
+
var root = document.getElementById("root");
|
|
181
|
+
if (root) root.style.overflow = "hidden";
|
|
182
|
+
document.body.style.overflow = "hidden";
|
|
183
|
+
document.documentElement.style.overflow = "hidden";
|
|
184
|
+
return function () {
|
|
185
|
+
if (root) root.style.overflow = "";
|
|
186
|
+
document.body.style.overflow = "";
|
|
187
|
+
document.documentElement.style.overflow = "";
|
|
188
|
+
};
|
|
189
|
+
}, [modalOpen]);
|
|
190
|
+
|
|
191
|
+
if (error) {
|
|
192
|
+
return (
|
|
193
|
+
<div className="flex items-center gap-1.5 text-[11px] text-base-content/40">
|
|
194
|
+
<Image size={12} />
|
|
195
|
+
<span className="font-mono">{props.path}</span>
|
|
196
|
+
</div>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
return (
|
|
200
|
+
<>
|
|
201
|
+
<div>
|
|
202
|
+
<div className="text-[9px] text-base-content/25 uppercase tracking-wider font-semibold mb-1">Screenshot</div>
|
|
203
|
+
<img
|
|
204
|
+
src={imgSrc}
|
|
205
|
+
alt={props.path}
|
|
206
|
+
className="max-w-full max-h-[240px] rounded-md border border-base-content/10 cursor-pointer hover:border-primary/30 hover:shadow-lg transition-all object-contain"
|
|
207
|
+
onError={function () { setError(true); }}
|
|
208
|
+
onClick={function () { setModalOpen(true); }}
|
|
209
|
+
loading="lazy"
|
|
210
|
+
/>
|
|
211
|
+
<div className="text-[10px] text-base-content/25 font-mono mt-1 truncate">{props.path}</div>
|
|
212
|
+
</div>
|
|
213
|
+
{modalOpen && createPortal(
|
|
214
|
+
<div
|
|
215
|
+
className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/80 backdrop-blur-sm p-4 sm:p-8 cursor-pointer overscroll-contain"
|
|
216
|
+
onClick={function () { setModalOpen(false); }}
|
|
217
|
+
onKeyDown={function (e) { if (e.key === "Escape") setModalOpen(false); }}
|
|
218
|
+
onWheel={function (e) { e.stopPropagation(); }}
|
|
219
|
+
onTouchMove={function (e) { e.stopPropagation(); }}
|
|
220
|
+
role="dialog"
|
|
221
|
+
aria-label="Image preview"
|
|
222
|
+
tabIndex={0}
|
|
223
|
+
>
|
|
224
|
+
<div className="relative max-w-[95vw] max-h-[90vh] sm:max-w-[85vw] sm:max-h-[85vh]" onClick={function (e) { e.stopPropagation(); }}>
|
|
225
|
+
<img
|
|
226
|
+
src={imgSrc}
|
|
227
|
+
alt={props.path}
|
|
228
|
+
className="max-w-full max-h-[90vh] sm:max-h-[85vh] rounded-lg shadow-2xl object-contain"
|
|
229
|
+
/>
|
|
230
|
+
<div className="absolute -top-10 right-0 flex items-center gap-3">
|
|
231
|
+
<a
|
|
232
|
+
href={imgSrc}
|
|
233
|
+
target="_blank"
|
|
234
|
+
rel="noopener noreferrer"
|
|
235
|
+
className="text-[11px] font-mono text-white/50 hover:text-white/80 transition-colors"
|
|
236
|
+
onClick={function (e) { e.stopPropagation(); }}
|
|
237
|
+
>
|
|
238
|
+
Open in tab
|
|
239
|
+
</a>
|
|
240
|
+
<button
|
|
241
|
+
onClick={function () { setModalOpen(false); }}
|
|
242
|
+
className="text-white/50 hover:text-white transition-colors text-lg leading-none"
|
|
243
|
+
aria-label="Close preview"
|
|
244
|
+
>
|
|
245
|
+
×
|
|
246
|
+
</button>
|
|
247
|
+
</div>
|
|
248
|
+
<div className="text-[11px] text-white/30 font-mono mt-2 truncate text-center">{props.path}</div>
|
|
249
|
+
</div>
|
|
250
|
+
</div>,
|
|
251
|
+
document.body
|
|
252
|
+
)}
|
|
253
|
+
</>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function FileHeader(props: { path: string }) {
|
|
258
|
+
var parts = props.path.split("/");
|
|
259
|
+
var filename = parts[parts.length - 1] || props.path;
|
|
260
|
+
var ext = filename.split(".").pop() || "";
|
|
261
|
+
return (
|
|
262
|
+
<div className="flex items-center gap-1.5 mb-1">
|
|
263
|
+
<FileText size={10} className="text-base-content/25" />
|
|
264
|
+
<span className="text-[10px] font-mono text-base-content/40 truncate">{filename}</span>
|
|
265
|
+
{ext && <span className="text-[9px] font-mono text-base-content/20 uppercase">{ext}</span>}
|
|
266
|
+
</div>
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function ToolResultRenderer(props: ToolResultRendererProps) {
|
|
271
|
+
var args = parseArgs(props.args);
|
|
272
|
+
var result = props.result;
|
|
273
|
+
|
|
274
|
+
if ((props.toolName === "Edit" || props.toolName === "MultiEdit") && args.old_string && args.new_string) {
|
|
275
|
+
return (
|
|
276
|
+
<div className="px-2.5 py-2">
|
|
277
|
+
<DiffRenderer oldText={String(args.old_string)} newText={String(args.new_string)} />
|
|
278
|
+
{result && !result.includes("has been updated successfully") && (
|
|
279
|
+
<div className="mt-2">
|
|
280
|
+
<div className="text-[9px] text-base-content/25 uppercase tracking-wider font-semibold mb-0.5">Result</div>
|
|
281
|
+
<pre className="font-mono text-[11px] text-base-content/45 whitespace-pre-wrap break-words m-0 leading-relaxed bg-base-100/50 rounded-md p-2 max-h-[160px] overflow-y-auto">
|
|
282
|
+
{result}
|
|
283
|
+
</pre>
|
|
284
|
+
</div>
|
|
285
|
+
)}
|
|
286
|
+
</div>
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (props.toolName === "mcp__playwright__browser_take_screenshot" || props.toolName === "mcp__playwright__browser_snapshot") {
|
|
291
|
+
var screenshotMatch = result.match(/\[Screenshot[^\]]*\]\(([^)]+)\)/);
|
|
292
|
+
var pathMatch = result.match(/path:\s*['"]?([^\s'"]+\.(?:png|jpg|jpeg))/i);
|
|
293
|
+
var imagePath = screenshotMatch ? screenshotMatch[1] : pathMatch ? pathMatch[1] : null;
|
|
294
|
+
if (imagePath) {
|
|
295
|
+
return (
|
|
296
|
+
<div className="px-2.5 py-2">
|
|
297
|
+
<ImageRenderer path={imagePath} />
|
|
298
|
+
</div>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (props.toolName === "Read" && args.file_path && isImagePath(String(args.file_path))) {
|
|
304
|
+
return (
|
|
305
|
+
<div className="px-2.5 py-2">
|
|
306
|
+
<ImageRenderer path={String(args.file_path)} />
|
|
307
|
+
</div>
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (props.toolName === "Read" && args.file_path && result) {
|
|
312
|
+
return (
|
|
313
|
+
<div className="px-2.5 py-2">
|
|
314
|
+
<FileHeader path={String(args.file_path)} />
|
|
315
|
+
<pre className="font-mono text-[11px] text-base-content/45 whitespace-pre-wrap break-words m-0 leading-relaxed bg-base-100/50 rounded-md p-2 max-h-[240px] overflow-y-auto">
|
|
316
|
+
{result}
|
|
317
|
+
</pre>
|
|
318
|
+
</div>
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (result && hasMarkdownTable(result)) {
|
|
323
|
+
return (
|
|
324
|
+
<div className="px-2.5 py-2">
|
|
325
|
+
<div className="text-[9px] text-base-content/25 uppercase tracking-wider font-semibold mb-0.5">Result</div>
|
|
326
|
+
<div className="prose prose-sm max-w-none text-[11px] [&_table]:text-[11px] [&_th]:px-2 [&_th]:py-1 [&_td]:px-2 [&_td]:py-1 [&_th]:text-base-content/60 [&_td]:text-base-content/45 [&_table]:border-base-content/10 [&_th]:border-base-content/10 [&_td]:border-base-content/10 [&_th]:bg-base-100/50">
|
|
327
|
+
<Markdown>{result}</Markdown>
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (!result) return null;
|
|
334
|
+
|
|
335
|
+
return (
|
|
336
|
+
<div className="px-2.5 py-2">
|
|
337
|
+
<div className="text-[9px] text-base-content/25 uppercase tracking-wider font-semibold mb-0.5">Result</div>
|
|
338
|
+
<pre className="font-mono text-[11px] text-base-content/45 whitespace-pre-wrap break-words m-0 leading-relaxed bg-base-100/50 rounded-md p-2 max-h-[240px] overflow-y-auto">
|
|
339
|
+
{result}
|
|
340
|
+
</pre>
|
|
341
|
+
</div>
|
|
342
|
+
);
|
|
343
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export function formatToolSummary(name: string, argsStr: string): string {
|
|
2
|
+
try {
|
|
3
|
+
var args = JSON.parse(argsStr);
|
|
4
|
+
if (name === "Read" && args.file_path) return args.file_path;
|
|
5
|
+
if (name === "Write" && args.file_path) return args.file_path;
|
|
6
|
+
if (name === "Edit" && args.file_path) return args.file_path;
|
|
7
|
+
if (name === "MultiEdit" && args.file_path) return args.file_path;
|
|
8
|
+
if (name === "Grep" && args.pattern) return args.pattern + (args.path ? " in " + args.path : "");
|
|
9
|
+
if (name === "Glob" && args.pattern) return args.pattern + (args.path ? " in " + args.path : "");
|
|
10
|
+
if (name === "Bash" && (args.command || args.description)) {
|
|
11
|
+
var cmd = args.description || args.command;
|
|
12
|
+
return cmd.length > 60 ? cmd.slice(0, 57) + "..." : cmd;
|
|
13
|
+
}
|
|
14
|
+
if (name === "LS" && args.path) return args.path;
|
|
15
|
+
if (name === "Agent" && args.description) return args.description;
|
|
16
|
+
if (name === "Skill" && args.skill) return args.skill;
|
|
17
|
+
if (name === "NotebookEdit" && args.file_path) return args.file_path;
|
|
18
|
+
if (name === "WebSearch" && args.query) return args.query;
|
|
19
|
+
if (name === "WebFetch" && args.url) return args.url.length > 60 ? args.url.slice(0, 57) + "..." : args.url;
|
|
20
|
+
if (name === "TodoWrite" || name === "TaskCreate" || name === "TaskUpdate") {
|
|
21
|
+
if (args.description) return args.description.length > 50 ? args.description.slice(0, 47) + "..." : args.description;
|
|
22
|
+
}
|
|
23
|
+
if (name.startsWith("mcp__playwright__")) {
|
|
24
|
+
var short = name.replace("mcp__playwright__browser_", "");
|
|
25
|
+
if (args.url) return short + " " + args.url;
|
|
26
|
+
if (args.element) return short + " " + args.element;
|
|
27
|
+
if (args.filename) return short + " → " + args.filename;
|
|
28
|
+
return short;
|
|
29
|
+
}
|
|
30
|
+
if (name.startsWith("mcp__")) {
|
|
31
|
+
var parts = name.split("__");
|
|
32
|
+
return parts.length >= 3 ? parts.slice(2).join(".") : name;
|
|
33
|
+
}
|
|
34
|
+
if (args.file_path) return args.file_path;
|
|
35
|
+
if (args.path) return args.path;
|
|
36
|
+
if (args.query) return args.query;
|
|
37
|
+
return "";
|
|
38
|
+
} catch {
|
|
39
|
+
return "";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
import { useMesh } from "../../hooks/useMesh";
|
|
3
|
+
import { useProjects } from "../../hooks/useProjects";
|
|
4
|
+
import { useSidebar } from "../../hooks/useSidebar";
|
|
5
|
+
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
6
|
+
import { LatticeLogomark } from "../ui/LatticeLogomark";
|
|
7
|
+
import {
|
|
8
|
+
Network, FolderOpen, Activity, MessageSquare, Menu,
|
|
9
|
+
ChevronRight, Lock, Bug,
|
|
10
|
+
} from "lucide-react";
|
|
11
|
+
import type { ServerMessage, SessionSummary, LatticeConfig } from "@lattice/shared";
|
|
12
|
+
|
|
13
|
+
function relativeTime(ts: number): string {
|
|
14
|
+
var diff = Date.now() - ts;
|
|
15
|
+
var seconds = Math.floor(diff / 1000);
|
|
16
|
+
if (seconds < 60) return seconds + "s ago";
|
|
17
|
+
var minutes = Math.floor(seconds / 60);
|
|
18
|
+
if (minutes < 60) return minutes + "m ago";
|
|
19
|
+
var hours = Math.floor(minutes / 60);
|
|
20
|
+
if (hours < 24) return hours + "h ago";
|
|
21
|
+
var days = Math.floor(hours / 24);
|
|
22
|
+
return days + "d ago";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function DashboardView() {
|
|
26
|
+
var { nodes } = useMesh();
|
|
27
|
+
var { projects } = useProjects();
|
|
28
|
+
var sidebar = useSidebar();
|
|
29
|
+
var { send, subscribe, unsubscribe } = useWebSocket();
|
|
30
|
+
var [sessions, setSessions] = useState<SessionSummary[]>([]);
|
|
31
|
+
var [localConfig, setLocalConfig] = useState<LatticeConfig | null>(null);
|
|
32
|
+
|
|
33
|
+
var onlineNodes = nodes.filter(function (n) { return n.online; });
|
|
34
|
+
|
|
35
|
+
useEffect(function () {
|
|
36
|
+
function handleSessions(msg: ServerMessage) {
|
|
37
|
+
if (msg.type !== "session:list_all") return;
|
|
38
|
+
var data = msg as { type: "session:list_all"; sessions: SessionSummary[] };
|
|
39
|
+
setSessions(data.sessions);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function handleSettings(msg: ServerMessage) {
|
|
43
|
+
if (msg.type !== "settings:data") return;
|
|
44
|
+
var data = msg as { type: "settings:data"; config: LatticeConfig };
|
|
45
|
+
setLocalConfig(data.config);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
subscribe("session:list_all", handleSessions);
|
|
49
|
+
subscribe("settings:data", handleSettings);
|
|
50
|
+
send({ type: "session:list_all_request" });
|
|
51
|
+
send({ type: "settings:get" });
|
|
52
|
+
|
|
53
|
+
return function () {
|
|
54
|
+
unsubscribe("session:list_all", handleSessions);
|
|
55
|
+
unsubscribe("settings:data", handleSettings);
|
|
56
|
+
};
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
59
|
+
function getProjectTitle(slug: string): string {
|
|
60
|
+
var project = projects.find(function (p) { return p.slug === slug; });
|
|
61
|
+
return project ? project.title : slug;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
var totalSessions = sessions.length;
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div className="flex-1 overflow-auto">
|
|
68
|
+
<div className="max-w-2xl mx-auto px-4 sm:px-8 py-8 sm:py-12">
|
|
69
|
+
<div className="flex items-center gap-3 mb-8">
|
|
70
|
+
<button
|
|
71
|
+
className="btn btn-ghost btn-sm btn-square lg:hidden"
|
|
72
|
+
aria-label="Toggle sidebar"
|
|
73
|
+
onClick={sidebar.toggleDrawer}
|
|
74
|
+
>
|
|
75
|
+
<Menu size={18} />
|
|
76
|
+
</button>
|
|
77
|
+
<LatticeLogomark size={32} />
|
|
78
|
+
<div>
|
|
79
|
+
<h1 className="text-xl font-mono font-bold text-base-content">Lattice</h1>
|
|
80
|
+
<p className="text-[13px] text-base-content/40">Multi-machine agentic dashboard</p>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-8">
|
|
85
|
+
<div className="bg-base-200 rounded-xl p-3 px-4 border border-base-content/15">
|
|
86
|
+
<div className="flex items-center gap-2 mb-1.5">
|
|
87
|
+
<Network size={14} className="text-primary" />
|
|
88
|
+
<span className="text-[11px] font-semibold tracking-wider uppercase text-base-content/40">Nodes</span>
|
|
89
|
+
</div>
|
|
90
|
+
<div className="text-xl font-mono font-bold text-base-content">
|
|
91
|
+
{onlineNodes.length}
|
|
92
|
+
<span className="text-base-content/30 text-sm font-normal">/{nodes.length}</span>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div className="bg-base-200 rounded-xl p-3 px-4 border border-base-content/15">
|
|
97
|
+
<div className="flex items-center gap-2 mb-1.5">
|
|
98
|
+
<FolderOpen size={14} className="text-accent" />
|
|
99
|
+
<span className="text-[11px] font-semibold tracking-wider uppercase text-base-content/40">Projects</span>
|
|
100
|
+
</div>
|
|
101
|
+
<div className="text-xl font-mono font-bold text-base-content">{projects.length}</div>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<div className="bg-base-200 rounded-xl p-3 px-4 border border-base-content/15">
|
|
105
|
+
<div className="flex items-center gap-2 mb-1.5">
|
|
106
|
+
<MessageSquare size={14} className="text-info" />
|
|
107
|
+
<span className="text-[11px] font-semibold tracking-wider uppercase text-base-content/40">Sessions</span>
|
|
108
|
+
</div>
|
|
109
|
+
<div className="text-xl font-mono font-bold text-base-content">{totalSessions}</div>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<div className="bg-base-200 rounded-xl p-3 px-4 border border-base-content/15">
|
|
113
|
+
<div className="flex items-center gap-2 mb-1.5">
|
|
114
|
+
<Activity size={14} className="text-success" />
|
|
115
|
+
<span className="text-[11px] font-semibold tracking-wider uppercase text-base-content/40">Status</span>
|
|
116
|
+
</div>
|
|
117
|
+
<div className="text-xl font-mono font-bold text-success">OK</div>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{sessions.length > 0 && (
|
|
122
|
+
<div className="mb-8">
|
|
123
|
+
<h2 className="text-[11px] font-semibold tracking-wider uppercase text-base-content/40 mb-3">Recent Sessions</h2>
|
|
124
|
+
<div className="flex flex-col gap-1.5">
|
|
125
|
+
{sessions.slice(0, 8).map(function (s) {
|
|
126
|
+
return (
|
|
127
|
+
<button
|
|
128
|
+
key={s.id}
|
|
129
|
+
onClick={function () { sidebar.navigateToSession(s.projectSlug, s.id); }}
|
|
130
|
+
className="flex items-center gap-3 px-3 py-2 rounded-xl border border-base-content/15 bg-base-200 hover:border-base-content/30 transition-colors duration-[120ms] cursor-pointer text-left focus-visible:ring-2 focus-visible:ring-primary"
|
|
131
|
+
>
|
|
132
|
+
<MessageSquare size={12} className="text-base-content/30 flex-shrink-0" />
|
|
133
|
+
<span className="flex-1 text-[12px] text-base-content truncate">{s.title || "Untitled"}</span>
|
|
134
|
+
<span className="px-1.5 py-0.5 rounded-md text-[10px] font-mono bg-base-content/8 text-base-content/40 flex-shrink-0">
|
|
135
|
+
{getProjectTitle(s.projectSlug)}
|
|
136
|
+
</span>
|
|
137
|
+
<span className="text-[10px] text-base-content/30 font-mono flex-shrink-0">
|
|
138
|
+
{relativeTime(s.updatedAt)}
|
|
139
|
+
</span>
|
|
140
|
+
<ChevronRight size={12} className="text-base-content/20 flex-shrink-0" />
|
|
141
|
+
</button>
|
|
142
|
+
);
|
|
143
|
+
})}
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
147
|
+
|
|
148
|
+
{projects.length > 0 && (
|
|
149
|
+
<div className="mb-8">
|
|
150
|
+
<h2 className="text-[11px] font-semibold tracking-wider uppercase text-base-content/40 mb-3">Projects</h2>
|
|
151
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
152
|
+
{projects.map(function (project) {
|
|
153
|
+
var projectSessions = sessions.filter(function (s) { return s.projectSlug === project.slug; });
|
|
154
|
+
return (
|
|
155
|
+
<button
|
|
156
|
+
key={project.slug}
|
|
157
|
+
onClick={function () { sidebar.setActiveProjectSlug(project.slug); }}
|
|
158
|
+
className="flex items-start gap-3 px-4 py-3 rounded-xl bg-base-200 border border-base-content/15 hover:border-base-content/30 transition-colors duration-[120ms] cursor-pointer text-left focus-visible:ring-2 focus-visible:ring-primary group"
|
|
159
|
+
>
|
|
160
|
+
<FolderOpen size={16} className="text-base-content/30 mt-0.5 flex-shrink-0" />
|
|
161
|
+
<div className="flex-1 min-w-0">
|
|
162
|
+
<div className="text-[13px] font-semibold text-base-content truncate">{project.title}</div>
|
|
163
|
+
<div className="text-[11px] text-base-content/30 font-mono truncate">{project.path}</div>
|
|
164
|
+
<div className="text-[10px] text-base-content/30 mt-1">
|
|
165
|
+
{projectSessions.length} session{projectSessions.length !== 1 ? "s" : ""}
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
<ChevronRight size={14} className="text-base-content/20 mt-0.5 flex-shrink-0 group-hover:text-base-content/40" />
|
|
169
|
+
</button>
|
|
170
|
+
);
|
|
171
|
+
})}
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
)}
|
|
175
|
+
|
|
176
|
+
{nodes.length > 0 && (
|
|
177
|
+
<div>
|
|
178
|
+
<h2 className="text-[11px] font-semibold tracking-wider uppercase text-base-content/40 mb-3">Mesh Nodes</h2>
|
|
179
|
+
<div className="flex flex-col gap-2">
|
|
180
|
+
{nodes.map(function (node) {
|
|
181
|
+
return (
|
|
182
|
+
<div key={node.id} className="flex items-center gap-3 bg-base-200 rounded-xl px-4 py-3 border border-base-content/15">
|
|
183
|
+
<div className={"w-2.5 h-2.5 rounded-full flex-shrink-0 " + (node.online ? "bg-success" : "bg-error")} />
|
|
184
|
+
<div className="flex-1 min-w-0">
|
|
185
|
+
<div className="text-[13px] font-semibold text-base-content truncate">
|
|
186
|
+
{node.name}
|
|
187
|
+
{node.isLocal && <span className="text-base-content/30 font-normal ml-2">(this machine)</span>}
|
|
188
|
+
</div>
|
|
189
|
+
<div className="text-[11px] text-base-content/40">{node.address}:{node.port}</div>
|
|
190
|
+
</div>
|
|
191
|
+
{node.isLocal && localConfig && (
|
|
192
|
+
<div className="flex gap-1.5 flex-shrink-0">
|
|
193
|
+
{localConfig.tls && (
|
|
194
|
+
<span className="flex items-center gap-1 px-1.5 py-0.5 rounded-md text-[10px] font-mono bg-base-content/8 text-base-content/40">
|
|
195
|
+
<Lock size={9} />
|
|
196
|
+
TLS
|
|
197
|
+
</span>
|
|
198
|
+
)}
|
|
199
|
+
{localConfig.debug && (
|
|
200
|
+
<span className="flex items-center gap-1 px-1.5 py-0.5 rounded-md text-[10px] font-mono bg-warning/20 text-warning">
|
|
201
|
+
<Bug size={9} />
|
|
202
|
+
Debug
|
|
203
|
+
</span>
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
)}
|
|
207
|
+
<div className="text-[11px] text-base-content/40 flex-shrink-0">
|
|
208
|
+
{node.projects.length} project{node.projects.length !== 1 ? "s" : ""}
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
})}
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
)}
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
);
|
|
219
|
+
}
|