@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.
Files changed (162) hide show
  1. package/.editorconfig +12 -0
  2. package/.github/workflows/release.yml +44 -0
  3. package/.impeccable.md +66 -0
  4. package/.releaserc.json +32 -0
  5. package/.serena/project.yml +138 -0
  6. package/CLAUDE.md +35 -0
  7. package/CONTRIBUTING.md +93 -0
  8. package/LICENSE +21 -0
  9. package/README.md +83 -0
  10. package/bun.lock +1459 -0
  11. package/bunfig.toml +2 -0
  12. package/client/index.html +32 -0
  13. package/client/package.json +37 -0
  14. package/client/public/icons/icon-192.svg +11 -0
  15. package/client/public/icons/icon-512.svg +11 -0
  16. package/client/public/manifest.json +24 -0
  17. package/client/public/sw.js +61 -0
  18. package/client/src/App.tsx +28 -0
  19. package/client/src/components/auth/PassphrasePrompt.tsx +70 -0
  20. package/client/src/components/chat/ChatInput.tsx +241 -0
  21. package/client/src/components/chat/ChatView.tsx +727 -0
  22. package/client/src/components/chat/Message.tsx +362 -0
  23. package/client/src/components/chat/ModelSelector.tsx +87 -0
  24. package/client/src/components/chat/PermissionModeSelector.tsx +41 -0
  25. package/client/src/components/chat/StatusBar.tsx +50 -0
  26. package/client/src/components/chat/ToolGroup.tsx +129 -0
  27. package/client/src/components/chat/ToolResultRenderer.tsx +343 -0
  28. package/client/src/components/chat/toolSummary.ts +41 -0
  29. package/client/src/components/dashboard/DashboardView.tsx +219 -0
  30. package/client/src/components/dashboard/ProjectDashboardView.tsx +168 -0
  31. package/client/src/components/mesh/NodeBadge.tsx +24 -0
  32. package/client/src/components/mesh/PairingDialog.tsx +281 -0
  33. package/client/src/components/panels/FileBrowser.tsx +241 -0
  34. package/client/src/components/panels/StickyNotes.tsx +187 -0
  35. package/client/src/components/panels/Terminal.tsx +128 -0
  36. package/client/src/components/project-settings/ProjectClaude.tsx +304 -0
  37. package/client/src/components/project-settings/ProjectEnvironment.tsx +235 -0
  38. package/client/src/components/project-settings/ProjectGeneral.tsx +76 -0
  39. package/client/src/components/project-settings/ProjectMcp.tsx +232 -0
  40. package/client/src/components/project-settings/ProjectPermissions.tsx +209 -0
  41. package/client/src/components/project-settings/ProjectRules.tsx +277 -0
  42. package/client/src/components/project-settings/ProjectSettingsView.tsx +99 -0
  43. package/client/src/components/project-settings/ProjectSkills.tsx +91 -0
  44. package/client/src/components/settings/Appearance.tsx +151 -0
  45. package/client/src/components/settings/ClaudeSettings.tsx +151 -0
  46. package/client/src/components/settings/Environment.tsx +185 -0
  47. package/client/src/components/settings/GlobalMcp.tsx +207 -0
  48. package/client/src/components/settings/GlobalSkills.tsx +125 -0
  49. package/client/src/components/settings/MeshStatus.tsx +145 -0
  50. package/client/src/components/settings/SettingsView.tsx +57 -0
  51. package/client/src/components/settings/SkillMarketplace.tsx +175 -0
  52. package/client/src/components/settings/mcp-shared.tsx +194 -0
  53. package/client/src/components/settings/skill-shared.tsx +177 -0
  54. package/client/src/components/setup/SetupWizard.tsx +750 -0
  55. package/client/src/components/sidebar/NodeSettingsModal.tsx +180 -0
  56. package/client/src/components/sidebar/ProjectDropdown.tsx +43 -0
  57. package/client/src/components/sidebar/ProjectRail.tsx +291 -0
  58. package/client/src/components/sidebar/SearchFilter.tsx +52 -0
  59. package/client/src/components/sidebar/SessionList.tsx +384 -0
  60. package/client/src/components/sidebar/SettingsSidebar.tsx +128 -0
  61. package/client/src/components/sidebar/Sidebar.tsx +209 -0
  62. package/client/src/components/sidebar/UserIsland.tsx +59 -0
  63. package/client/src/components/sidebar/UserMenu.tsx +101 -0
  64. package/client/src/components/ui/CommandPalette.tsx +321 -0
  65. package/client/src/components/ui/ErrorBoundary.tsx +56 -0
  66. package/client/src/components/ui/IconPicker.tsx +209 -0
  67. package/client/src/components/ui/LatticeLogomark.tsx +19 -0
  68. package/client/src/components/ui/PopupMenu.tsx +98 -0
  69. package/client/src/components/ui/SaveFooter.tsx +38 -0
  70. package/client/src/components/ui/Toast.tsx +112 -0
  71. package/client/src/hooks/useMesh.ts +89 -0
  72. package/client/src/hooks/useProjectSettings.ts +56 -0
  73. package/client/src/hooks/useProjects.ts +66 -0
  74. package/client/src/hooks/useSaveState.ts +59 -0
  75. package/client/src/hooks/useSession.ts +317 -0
  76. package/client/src/hooks/useSidebar.ts +74 -0
  77. package/client/src/hooks/useSkills.ts +30 -0
  78. package/client/src/hooks/useTheme.ts +114 -0
  79. package/client/src/hooks/useWebSocket.ts +26 -0
  80. package/client/src/main.tsx +10 -0
  81. package/client/src/providers/WebSocketProvider.tsx +146 -0
  82. package/client/src/router.tsx +391 -0
  83. package/client/src/stores/mesh.ts +78 -0
  84. package/client/src/stores/session.ts +322 -0
  85. package/client/src/stores/sidebar.ts +336 -0
  86. package/client/src/stores/theme.ts +44 -0
  87. package/client/src/styles/global.css +167 -0
  88. package/client/src/styles/theme-vars.css +18 -0
  89. package/client/src/themes/index.ts +79 -0
  90. package/client/src/utils/findDuplicateKeys.ts +12 -0
  91. package/client/tsconfig.json +14 -0
  92. package/client/vite.config.ts +20 -0
  93. package/package.json +46 -0
  94. package/server/package.json +22 -0
  95. package/server/src/auth/passphrase.ts +48 -0
  96. package/server/src/config.ts +55 -0
  97. package/server/src/daemon.ts +338 -0
  98. package/server/src/features/ralph-loop.ts +173 -0
  99. package/server/src/features/scheduler.ts +281 -0
  100. package/server/src/features/sticky-notes.ts +102 -0
  101. package/server/src/handlers/chat.ts +194 -0
  102. package/server/src/handlers/fs.ts +84 -0
  103. package/server/src/handlers/loop.ts +37 -0
  104. package/server/src/handlers/mesh.ts +125 -0
  105. package/server/src/handlers/notes.ts +45 -0
  106. package/server/src/handlers/project-settings.ts +174 -0
  107. package/server/src/handlers/scheduler.ts +47 -0
  108. package/server/src/handlers/session.ts +159 -0
  109. package/server/src/handlers/settings.ts +109 -0
  110. package/server/src/handlers/skills.ts +380 -0
  111. package/server/src/handlers/terminal.ts +70 -0
  112. package/server/src/identity.ts +26 -0
  113. package/server/src/index.ts +190 -0
  114. package/server/src/mesh/connector.ts +209 -0
  115. package/server/src/mesh/discovery.ts +123 -0
  116. package/server/src/mesh/pairing.ts +94 -0
  117. package/server/src/mesh/peers.ts +52 -0
  118. package/server/src/mesh/proxy.ts +103 -0
  119. package/server/src/mesh/session-sync.ts +107 -0
  120. package/server/src/project/context-breakdown.ts +289 -0
  121. package/server/src/project/file-browser.ts +106 -0
  122. package/server/src/project/project-files.ts +267 -0
  123. package/server/src/project/registry.ts +57 -0
  124. package/server/src/project/sdk-bridge.ts +566 -0
  125. package/server/src/project/session.ts +432 -0
  126. package/server/src/project/terminal.ts +69 -0
  127. package/server/src/tls.ts +51 -0
  128. package/server/src/ws/broadcast.ts +31 -0
  129. package/server/src/ws/router.ts +104 -0
  130. package/server/src/ws/server.ts +2 -0
  131. package/server/tsconfig.json +16 -0
  132. package/shared/package.json +11 -0
  133. package/shared/src/constants.ts +7 -0
  134. package/shared/src/index.ts +4 -0
  135. package/shared/src/messages.ts +638 -0
  136. package/shared/src/models.ts +136 -0
  137. package/shared/src/project-settings.ts +45 -0
  138. package/shared/tsconfig.json +11 -0
  139. package/themes/amoled.json +20 -0
  140. package/themes/ayu-light.json +9 -0
  141. package/themes/catppuccin-latte.json +9 -0
  142. package/themes/catppuccin-mocha.json +9 -0
  143. package/themes/clay-light.json +10 -0
  144. package/themes/clay.json +10 -0
  145. package/themes/dracula.json +9 -0
  146. package/themes/everforest-light.json +9 -0
  147. package/themes/everforest.json +9 -0
  148. package/themes/github-light.json +9 -0
  149. package/themes/gruvbox-dark.json +9 -0
  150. package/themes/gruvbox-light.json +9 -0
  151. package/themes/monokai.json +9 -0
  152. package/themes/nord-light.json +9 -0
  153. package/themes/nord.json +9 -0
  154. package/themes/one-dark.json +9 -0
  155. package/themes/one-light.json +9 -0
  156. package/themes/rose-pine-dawn.json +9 -0
  157. package/themes/rose-pine.json +9 -0
  158. package/themes/solarized-dark.json +9 -0
  159. package/themes/solarized-light.json +9 -0
  160. package/themes/tokyo-night-light.json +9 -0
  161. package/themes/tokyo-night.json +9 -0
  162. package/tsconfig.json +26 -0
@@ -0,0 +1,362 @@
1
+ import { useState, useRef, useEffect } from "react";
2
+ import Markdown from "react-markdown";
3
+ import { Wrench, TriangleAlert, ChevronDown, Check, X, Shield } from "lucide-react";
4
+ import type { HistoryMessage, ChatPermissionResponseMessage } from "@lattice/shared";
5
+ import { useWebSocket } from "../../hooks/useWebSocket";
6
+ import { ToolResultRenderer } from "./ToolResultRenderer";
7
+ import { formatToolSummary } from "./toolSummary";
8
+
9
+ interface MessageProps {
10
+ message: HistoryMessage;
11
+ responseCost?: number | null;
12
+ responseDuration?: number | null;
13
+ }
14
+
15
+ function formatTime(timestamp: number): string {
16
+ if (!timestamp) return "";
17
+ var d = new Date(timestamp);
18
+ var now = new Date();
19
+ var h = d.getHours().toString().padStart(2, "0");
20
+ var m = d.getMinutes().toString().padStart(2, "0");
21
+ var time = h + ":" + m;
22
+
23
+ var today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
24
+ var yesterday = new Date(today.getTime() - 86400000);
25
+ var msgDay = new Date(d.getFullYear(), d.getMonth(), d.getDate());
26
+
27
+ if (msgDay.getTime() === today.getTime()) {
28
+ return time;
29
+ }
30
+ if (msgDay.getTime() === yesterday.getTime()) {
31
+ return "Yesterday " + time;
32
+ }
33
+ var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
34
+ return months[d.getMonth()] + " " + d.getDate() + ", " + time;
35
+ }
36
+
37
+ function UserMessage(props: { message: HistoryMessage }) {
38
+ var msg = props.message;
39
+ var time = formatTime(msg.timestamp);
40
+ return (
41
+ <div className="chat chat-end px-5 py-1">
42
+ <div className="chat-bubble chat-bubble-primary text-[13px] leading-relaxed break-words max-w-[95%] sm:max-w-[85%] shadow-sm">
43
+ <div className="prose prose-sm max-w-none [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 prose-headings:text-primary-content prose-p:text-primary-content prose-strong:text-primary-content prose-code:text-primary-content/80 prose-pre:bg-primary/20 prose-a:text-primary-content/90 prose-a:underline">
44
+ <Markdown>{msg.text || ""}</Markdown>
45
+ </div>
46
+ </div>
47
+ {time && (
48
+ <div className="chat-footer text-[10px] text-base-content/30 mt-0.5">
49
+ {time}
50
+ </div>
51
+ )}
52
+ </div>
53
+ );
54
+ }
55
+
56
+ function formatDuration(ms: number): string {
57
+ var seconds = Math.round(ms / 1000);
58
+ if (seconds < 60) return seconds + "s";
59
+ var minutes = Math.floor(seconds / 60);
60
+ var remainingSeconds = seconds % 60;
61
+ return minutes + "m " + remainingSeconds + "s";
62
+ }
63
+
64
+ function formatTokenCount(n: number): string {
65
+ if (n >= 1000000) return (n / 1000000).toFixed(1).replace(/\.0$/, "") + "M";
66
+ if (n >= 1000) return (n / 1000).toFixed(1).replace(/\.0$/, "") + "k";
67
+ return String(n);
68
+ }
69
+
70
+ function AssistantMessage(props: { message: HistoryMessage; responseCost?: number | null; responseDuration?: number | null }) {
71
+ var msg = props.message;
72
+ var time = formatTime(msg.timestamp);
73
+ return (
74
+ <div className="chat chat-start px-5 py-1">
75
+ <div className="chat-image">
76
+ <div className="w-6 h-6 rounded-full bg-primary/15 border border-primary/20 flex items-center justify-center">
77
+ <div className="w-2.5 h-2.5 rounded-full bg-primary" />
78
+ </div>
79
+ </div>
80
+ <div className="chat-bubble bg-base-300/70 text-base-content text-[13px] leading-relaxed break-words max-w-[95%] sm:max-w-[85%] shadow-sm border border-base-content/5">
81
+ <div className="prose prose-sm max-w-none [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 prose-headings:text-base-content prose-p:text-base-content prose-strong:text-base-content prose-code:text-base-content/70 prose-code:bg-base-100/50 prose-pre:bg-base-100 prose-pre:text-base-content/70 prose-a:text-primary prose-a:underline prose-li:text-base-content">
82
+ <Markdown>{msg.text || ""}</Markdown>
83
+ </div>
84
+ </div>
85
+ {time && (
86
+ <div className="chat-footer text-[10px] text-base-content/30 mt-0.5 flex items-center gap-2">
87
+ <span>{time}</span>
88
+ {props.responseDuration != null && props.responseDuration > 0 && (
89
+ <span className="text-base-content/20">{formatDuration(props.responseDuration)}</span>
90
+ )}
91
+ {(props.responseCost != null && props.responseCost > 0) ? (
92
+ <span className="text-base-content/20">{"$" + props.responseCost.toFixed(4)}</span>
93
+ ) : (msg.costEstimate != null && msg.costEstimate > 0) ? (
94
+ <span className="text-base-content/20">{"~$" + msg.costEstimate.toFixed(4)}</span>
95
+ ) : null}
96
+ {msg.outputTokens != null && msg.outputTokens > 0 && (
97
+ <span className="text-base-content/15">{formatTokenCount(msg.outputTokens)} out</span>
98
+ )}
99
+ </div>
100
+ )}
101
+ </div>
102
+ );
103
+ }
104
+
105
+ function ToolMessage(props: { message: HistoryMessage }) {
106
+ var msg = props.message;
107
+ var [expanded, setExpanded] = useState<boolean>(false);
108
+ var hasResult = Boolean(msg.content);
109
+
110
+ var parsedArgs: string = msg.args || "";
111
+ try {
112
+ if (msg.args) {
113
+ parsedArgs = JSON.stringify(JSON.parse(msg.args), null, 2);
114
+ }
115
+ } catch {
116
+ parsedArgs = msg.args || "";
117
+ }
118
+
119
+ return (
120
+ <div className="ml-14 mr-5 py-0.5 max-w-[95%] sm:max-w-[75%]">
121
+ <div
122
+ className={
123
+ "rounded-lg overflow-hidden text-[12px] border transition-colors duration-100 " +
124
+ (hasResult
125
+ ? "bg-base-200/50 border-base-content/8"
126
+ : "bg-base-200/70 border-primary/20")
127
+ }
128
+ >
129
+ <button
130
+ type="button"
131
+ onClick={function () { setExpanded(function (v) { return !v; }); }}
132
+ className="flex items-center gap-2 w-full py-1.5 px-2.5 hover:bg-base-content/5 transition-colors duration-100 cursor-pointer outline-none focus-visible:ring-2 focus-visible:ring-primary/30 focus-visible:ring-inset rounded-lg"
133
+ >
134
+ <Wrench size={11} className={hasResult ? "text-base-content/30" : "text-primary/70"} />
135
+ <span className="font-mono font-medium text-[12px] text-base-content/70 flex-shrink-0">
136
+ {msg.name}
137
+ </span>
138
+ <span className="text-[10px] text-base-content/30 truncate min-w-0 flex-1 text-left">
139
+ {formatToolSummary(msg.name || "", msg.args || "")}
140
+ </span>
141
+ {hasResult ? (
142
+ <span className="text-[10px] text-base-content/30 flex-shrink-0">done</span>
143
+ ) : (
144
+ <span className="text-[10px] text-primary/70 flex-shrink-0">running</span>
145
+ )}
146
+ <ChevronDown
147
+ size={11}
148
+ className={"text-base-content/30 transition-transform duration-150 " + (expanded ? "rotate-180" : "")}
149
+ />
150
+ </button>
151
+
152
+ {expanded && (
153
+ <div className="border-t border-base-content/8">
154
+ <ToolResultRenderer toolName={msg.name || ""} args={msg.args || ""} result={msg.content || ""} />
155
+ {!(msg.name === "Edit" || msg.name === "MultiEdit") && parsedArgs && (
156
+ <div className="px-2.5 py-2 border-t border-base-content/8">
157
+ <div className="text-[9px] text-base-content/25 mb-0.5 uppercase tracking-wider font-semibold">Args</div>
158
+ <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-[120px] overflow-y-auto">
159
+ {parsedArgs}
160
+ </pre>
161
+ </div>
162
+ )}
163
+ </div>
164
+ )}
165
+ </div>
166
+ </div>
167
+ );
168
+ }
169
+
170
+ function PermissionMessage(props: { message: HistoryMessage }) {
171
+ var msg = props.message;
172
+ var { send } = useWebSocket();
173
+ var [showScopeMenu, setShowScopeMenu] = useState<boolean>(false);
174
+ var [expanded, setExpanded] = useState<boolean>(false);
175
+ var [dropUp, setDropUp] = useState<boolean>(false);
176
+ var scopeBtnRef = useRef<HTMLButtonElement>(null);
177
+
178
+ useEffect(function () {
179
+ if (showScopeMenu && scopeBtnRef.current) {
180
+ var rect = scopeBtnRef.current.getBoundingClientRect();
181
+ var spaceBelow = window.innerHeight - rect.bottom;
182
+ setDropUp(spaceBelow < 120);
183
+ }
184
+ }, [showScopeMenu]);
185
+
186
+ var isResolved = msg.permissionStatus && msg.permissionStatus !== "pending";
187
+
188
+ var parsedArgs: string = msg.args || "";
189
+ try {
190
+ if (msg.args) {
191
+ parsedArgs = JSON.stringify(JSON.parse(msg.args), null, 2);
192
+ }
193
+ } catch {
194
+ parsedArgs = msg.args || "";
195
+ }
196
+
197
+ function respond(allow: boolean, alwaysAllow?: boolean, alwaysAllowScope?: "session" | "project") {
198
+ if (isResolved || !msg.toolId) {
199
+ return;
200
+ }
201
+ send({
202
+ type: "chat:permission_response",
203
+ requestId: msg.toolId,
204
+ allow: allow,
205
+ alwaysAllow: alwaysAllow,
206
+ alwaysAllowScope: alwaysAllowScope,
207
+ } as ChatPermissionResponseMessage);
208
+ }
209
+
210
+ function handleKeyDown(e: React.KeyboardEvent) {
211
+ if (isResolved) return;
212
+ if (e.key === "Enter") {
213
+ e.preventDefault();
214
+ respond(true);
215
+ } else if (e.key === "Escape") {
216
+ e.preventDefault();
217
+ respond(false);
218
+ }
219
+ }
220
+
221
+ if (isResolved) {
222
+ var statusIcon = msg.permissionStatus === "denied"
223
+ ? <X size={12} className="text-error" />
224
+ : <Check size={12} className="text-success" />;
225
+ var statusText = msg.permissionStatus === "denied"
226
+ ? "Denied"
227
+ : msg.permissionStatus === "always_allowed"
228
+ ? "Always allowed"
229
+ : "Allowed";
230
+ var borderClass = msg.permissionStatus === "denied"
231
+ ? "border-error/15 bg-error/3"
232
+ : "border-success/15 bg-success/3";
233
+
234
+ return (
235
+ <div className="ml-14 mr-5 py-0.5 max-w-[95%] sm:max-w-[75%]">
236
+ <div className={"rounded-lg text-[12px] border px-2.5 py-1.5 flex items-center gap-2 " + borderClass}>
237
+ {statusIcon}
238
+ <span className="text-base-content/35">{statusText}</span>
239
+ <code className="font-mono text-[11px] bg-base-300/40 px-1.5 py-0.5 rounded text-base-content/30">
240
+ {msg.name}
241
+ </code>
242
+ <span className="text-[10px] text-base-content/15 ml-auto">{formatTime(msg.timestamp)}</span>
243
+ </div>
244
+ </div>
245
+ );
246
+ }
247
+
248
+ return (
249
+ <div className="ml-14 mr-5 py-1 max-w-[95%] sm:max-w-[75%]" onKeyDown={handleKeyDown} tabIndex={0} role="group" aria-label={"Permission request: " + (msg.name || "unknown tool")}>
250
+ <div className="border border-warning/30 bg-warning/5 rounded-lg p-3 flex flex-col gap-2 text-[13px]">
251
+ <div className="flex flex-col gap-1">
252
+ <div className="flex items-center gap-2">
253
+ <TriangleAlert size={14} className="text-warning flex-shrink-0" />
254
+ <code className="font-mono text-[11px] bg-base-300/60 px-1.5 py-0.5 rounded text-base-content/60">
255
+ {msg.name}
256
+ </code>
257
+ </div>
258
+ <div className="text-[13px] text-base-content/80">
259
+ {msg.title || "Permission required"}
260
+ </div>
261
+ </div>
262
+
263
+ {parsedArgs && (
264
+ <div>
265
+ <button
266
+ type="button"
267
+ onClick={function () { setExpanded(function (v) { return !v; }); }}
268
+ className="text-[10px] text-base-content/30 hover:text-base-content/50 transition-colors flex items-center gap-1 cursor-pointer"
269
+ >
270
+ <ChevronDown size={10} className={"transition-transform duration-150 " + (expanded ? "rotate-180" : "")} />
271
+ args
272
+ </button>
273
+ {expanded && (
274
+ <pre className="font-mono text-[11px] text-base-content/50 whitespace-pre-wrap break-words m-0 mt-1 leading-relaxed bg-base-100/50 px-2.5 py-2 rounded-md w-full max-h-[160px] overflow-y-auto">
275
+ {parsedArgs}
276
+ </pre>
277
+ )}
278
+ </div>
279
+ )}
280
+
281
+ <div className="flex gap-2 items-center relative">
282
+ <button
283
+ className="btn btn-warning btn-sm btn-outline"
284
+ onClick={function () { respond(true); }}
285
+ >
286
+ Allow
287
+ </button>
288
+ <div className="inline-flex">
289
+ <button
290
+ className="btn btn-ghost btn-sm text-warning/70 border border-warning/25 rounded-r-none border-r-0 text-[11px] px-2"
291
+ onClick={function () { respond(true, true, "session"); }}
292
+ >
293
+ Always Allow
294
+ </button>
295
+ <button
296
+ ref={scopeBtnRef}
297
+ className="btn btn-ghost btn-sm text-warning/70 border border-warning/25 rounded-l-none text-[11px] px-1"
298
+ onClick={function () { setShowScopeMenu(function (v) { return !v; }); }}
299
+ >
300
+ <ChevronDown size={10} />
301
+ </button>
302
+ </div>
303
+ <button
304
+ className="btn btn-ghost btn-sm text-base-content/40"
305
+ onClick={function () { respond(false); }}
306
+ >
307
+ Deny
308
+ </button>
309
+ <span className="text-[10px] text-base-content/20 italic ml-auto">waiting for approval...</span>
310
+
311
+ {showScopeMenu && (
312
+ <div className={"absolute left-[88px] z-50 bg-base-300 border border-warning/20 rounded-lg shadow-xl p-1 text-[12px] font-mono min-w-[220px] " + (dropUp ? "bottom-full mb-1" : "top-full mt-1")}>
313
+ <button
314
+ className="flex flex-col w-full px-2.5 py-1.5 rounded hover:bg-warning/10 text-left text-base-content/70 transition-colors"
315
+ onClick={function () { setShowScopeMenu(false); respond(true, true, "session"); }}
316
+ >
317
+ <div className="flex items-center gap-2">
318
+ <Shield size={11} className="text-warning/60" />
319
+ This session only
320
+ </div>
321
+ </button>
322
+ <button
323
+ className="flex flex-col w-full px-2.5 py-1.5 rounded hover:bg-warning/10 text-left text-base-content/70 transition-colors"
324
+ onClick={function () { setShowScopeMenu(false); respond(true, true, "project"); }}
325
+ >
326
+ <div className="flex items-center gap-2">
327
+ <Shield size={11} className="text-warning/60" />
328
+ This project
329
+ </div>
330
+ {msg.permissionRule && (
331
+ <code className="text-[9px] text-base-content/25 mt-0.5 ml-[19px]">{msg.permissionRule}</code>
332
+ )}
333
+ </button>
334
+ </div>
335
+ )}
336
+ </div>
337
+ </div>
338
+ </div>
339
+ );
340
+ }
341
+
342
+ export function Message(props: MessageProps) {
343
+ var msg = props.message;
344
+
345
+ if (msg.type === "user") {
346
+ return <UserMessage message={msg} />;
347
+ }
348
+
349
+ if (msg.type === "assistant") {
350
+ return <AssistantMessage message={msg} responseCost={props.responseCost} responseDuration={props.responseDuration} />;
351
+ }
352
+
353
+ if (msg.type === "tool_start") {
354
+ return <ToolMessage message={msg} />;
355
+ }
356
+
357
+ if (msg.type === "permission_request") {
358
+ return <PermissionMessage message={msg} />;
359
+ }
360
+
361
+ return null;
362
+ }
@@ -0,0 +1,87 @@
1
+ import { useState } from "react";
2
+
3
+ interface ModelSelectorState {
4
+ model: string;
5
+ effort: string;
6
+ }
7
+
8
+ var MODEL_OPTIONS = [
9
+ { value: "default", label: "Model: Default" },
10
+ { value: "opus", label: "Model: Opus" },
11
+ { value: "sonnet", label: "Model: Sonnet" },
12
+ { value: "haiku", label: "Model: Haiku" },
13
+ ];
14
+
15
+ var EFFORT_OPTIONS = [
16
+ { value: "low", label: "Effort: Low" },
17
+ { value: "medium", label: "Effort: Medium" },
18
+ { value: "high", label: "Effort: High" },
19
+ { value: "max", label: "Effort: Max" },
20
+ ];
21
+
22
+ interface ModelSelectorProps {
23
+ onChange?: (state: ModelSelectorState) => void;
24
+ }
25
+
26
+ export function ModelSelector(props: ModelSelectorProps) {
27
+ var [model, setModel] = useState<string>("default");
28
+ var [effort, setEffort] = useState<string>("medium");
29
+
30
+ function handleModelChange(e: React.ChangeEvent<HTMLSelectElement>) {
31
+ var val = e.currentTarget.value;
32
+ setModel(val);
33
+ if (props.onChange) {
34
+ props.onChange({ model: val, effort });
35
+ }
36
+ }
37
+
38
+ function handleEffortChange(e: React.ChangeEvent<HTMLSelectElement>) {
39
+ var val = e.currentTarget.value;
40
+ setEffort(val);
41
+ if (props.onChange) {
42
+ props.onChange({ model, effort: val });
43
+ }
44
+ }
45
+
46
+ return (
47
+ <div className="flex items-center gap-0.5 font-mono text-[10px]">
48
+ <select
49
+ value={model}
50
+ onChange={handleModelChange}
51
+ title="Select model"
52
+ aria-label="Model"
53
+ className={
54
+ "select select-xs select-ghost font-mono text-[10px] min-h-0 h-6 min-w-0 w-auto " +
55
+ (model === "default" ? "text-base-content/40" : "text-primary")
56
+ }
57
+ >
58
+ {MODEL_OPTIONS.map(function (opt) {
59
+ return (
60
+ <option key={opt.value} value={opt.value}>
61
+ {opt.label}
62
+ </option>
63
+ );
64
+ })}
65
+ </select>
66
+ <span className="text-base-content/20">·</span>
67
+ <select
68
+ value={effort}
69
+ onChange={handleEffortChange}
70
+ title="Select effort"
71
+ aria-label="Effort level"
72
+ className={
73
+ "select select-xs select-ghost font-mono text-[10px] min-h-0 h-6 min-w-0 w-auto " +
74
+ (effort === "medium" ? "text-base-content/40" : "text-primary")
75
+ }
76
+ >
77
+ {EFFORT_OPTIONS.map(function (opt) {
78
+ return (
79
+ <option key={opt.value} value={opt.value}>
80
+ {opt.label}
81
+ </option>
82
+ );
83
+ })}
84
+ </select>
85
+ </div>
86
+ );
87
+ }
@@ -0,0 +1,41 @@
1
+ import { useState } from "react";
2
+ import { useWebSocket } from "../../hooks/useWebSocket";
3
+
4
+ var MODE_OPTIONS = [
5
+ { value: "default", label: "Mode: Default" },
6
+ { value: "acceptEdits", label: "Mode: Accept Edits" },
7
+ { value: "plan", label: "Mode: Plan" },
8
+ { value: "dontAsk", label: "Mode: Don't Ask" },
9
+ ];
10
+
11
+ export function PermissionModeSelector() {
12
+ var [mode, setMode] = useState<string>("default");
13
+ var { send } = useWebSocket();
14
+
15
+ function handleChange(e: React.ChangeEvent<HTMLSelectElement>) {
16
+ var val = e.currentTarget.value;
17
+ setMode(val);
18
+ send({ type: "chat:set_permission_mode", mode: val as "default" | "acceptEdits" | "plan" | "dontAsk" });
19
+ }
20
+
21
+ return (
22
+ <select
23
+ value={mode}
24
+ onChange={handleChange}
25
+ title="Permission mode"
26
+ aria-label="Permission mode"
27
+ className={
28
+ "select select-xs select-ghost font-mono text-[10px] min-h-0 h-6 min-w-0 w-auto " +
29
+ (mode === "default" ? "text-base-content/40" : "text-primary")
30
+ }
31
+ >
32
+ {MODE_OPTIONS.map(function (opt) {
33
+ return (
34
+ <option key={opt.value} value={opt.value}>
35
+ {opt.label}
36
+ </option>
37
+ );
38
+ })}
39
+ </select>
40
+ );
41
+ }
@@ -0,0 +1,50 @@
1
+ import { Brain, Wrench } from "lucide-react";
2
+
3
+ interface StatusBarProps {
4
+ status: {
5
+ phase: string;
6
+ toolName?: string;
7
+ elapsed?: number;
8
+ summary?: string;
9
+ } | null;
10
+ }
11
+
12
+ export function StatusBar(props: StatusBarProps) {
13
+ var active = props.status !== null;
14
+
15
+ return (
16
+ <div
17
+ className="grid transition-all duration-200 ease-out"
18
+ style={{ gridTemplateRows: active ? "1fr" : "0fr" }}
19
+ >
20
+ <div className="overflow-hidden">
21
+ <div className="flex items-center gap-2 px-5 h-7 text-[12px] font-mono text-base-content/50 border-t border-base-300 bg-base-200">
22
+ {props.status && (
23
+ <>
24
+ {props.status.phase === "thinking" ? (
25
+ <Brain size={12} className="text-primary animate-pulse" />
26
+ ) : (
27
+ <Wrench size={12} className="text-primary" />
28
+ )}
29
+ <span className="truncate">
30
+ {props.status.phase === "thinking"
31
+ ? "Thinking..."
32
+ : props.status.toolName || "Processing..."}
33
+ </span>
34
+ {props.status.summary && (
35
+ <span className="text-base-content/30 truncate">
36
+ {props.status.summary}
37
+ </span>
38
+ )}
39
+ {props.status.elapsed != null && (
40
+ <span className="text-base-content/30 ml-auto flex-shrink-0">
41
+ {(props.status.elapsed / 1000).toFixed(1)}s
42
+ </span>
43
+ )}
44
+ </>
45
+ )}
46
+ </div>
47
+ </div>
48
+ </div>
49
+ );
50
+ }
@@ -0,0 +1,129 @@
1
+ import { useState } from "react";
2
+ import { Wrench, ChevronDown, Check, FileText, Search, Terminal, Pencil, FolderOpen } from "lucide-react";
3
+ import type { HistoryMessage } from "@lattice/shared";
4
+ import { ToolResultRenderer } from "./ToolResultRenderer";
5
+ import { formatToolSummary } from "./toolSummary";
6
+
7
+ var TOOL_ICONS: Record<string, typeof Wrench> = {
8
+ Read: FileText,
9
+ Grep: Search,
10
+ Glob: Search,
11
+ Bash: Terminal,
12
+ Write: Pencil,
13
+ Edit: Pencil,
14
+ MultiEdit: Pencil,
15
+ LS: FolderOpen,
16
+ };
17
+
18
+ function getToolIcon(name: string) {
19
+ return TOOL_ICONS[name] || Wrench;
20
+ }
21
+
22
+
23
+
24
+ function ToolDetail(props: { tool: HistoryMessage }) {
25
+ var tool = props.tool;
26
+ var [detailOpen, setDetailOpen] = useState(false);
27
+ var hasResult = Boolean(tool.content);
28
+
29
+ var parsedArgs = tool.args || "";
30
+ try {
31
+ if (tool.args) {
32
+ parsedArgs = JSON.stringify(JSON.parse(tool.args), null, 2);
33
+ }
34
+ } catch {
35
+ parsedArgs = tool.args || "";
36
+ }
37
+
38
+ var Icon = getToolIcon(tool.name || "");
39
+ var summary = formatToolSummary(tool.name || "", tool.args || "");
40
+
41
+ return (
42
+ <div className="border-t border-base-content/6 first:border-t-0">
43
+ <button
44
+ type="button"
45
+ onClick={function () { setDetailOpen(function (v) { return !v; }); }}
46
+ className="flex items-center gap-2 w-full px-2.5 py-1.5 hover:bg-base-content/5 transition-colors cursor-pointer text-left"
47
+ >
48
+ <Icon size={11} className={hasResult ? "text-base-content/25 flex-shrink-0" : "text-primary/60 flex-shrink-0"} />
49
+ <span className="font-mono text-[11px] text-base-content/60 flex-shrink-0">{tool.name}</span>
50
+ {summary && (
51
+ <span className="text-[10px] text-base-content/30 truncate min-w-0 flex-1">{summary}</span>
52
+ )}
53
+ {!summary && <span className="flex-1" />}
54
+ {hasResult ? (
55
+ <Check size={10} className="text-success/40 flex-shrink-0" />
56
+ ) : (
57
+ <span className="text-[10px] text-primary/70 flex-shrink-0">running</span>
58
+ )}
59
+ <ChevronDown
60
+ size={10}
61
+ className={"text-base-content/25 transition-transform duration-150 flex-shrink-0 " + (detailOpen ? "rotate-180" : "")}
62
+ />
63
+ </button>
64
+
65
+ {detailOpen && (
66
+ <div className="pb-1">
67
+ <ToolResultRenderer toolName={tool.name || ""} args={tool.args || ""} result={tool.content || ""} />
68
+ {!(tool.name === "Edit" || tool.name === "MultiEdit") && parsedArgs && (
69
+ <div className="px-2.5 py-1.5 border-t border-base-content/6">
70
+ <div className="text-[9px] text-base-content/25 uppercase tracking-wider font-semibold mb-0.5">Args</div>
71
+ <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-[120px] overflow-y-auto">
72
+ {parsedArgs}
73
+ </pre>
74
+ </div>
75
+ )}
76
+ </div>
77
+ )}
78
+ </div>
79
+ );
80
+ }
81
+
82
+ interface ToolGroupProps {
83
+ tools: HistoryMessage[];
84
+ }
85
+
86
+ export function ToolGroup(props: ToolGroupProps) {
87
+ var [expanded, setExpanded] = useState(false);
88
+ var tools = props.tools;
89
+ var allDone = tools.every(function (t) { return Boolean(t.content); });
90
+ var uniqueNames = Array.from(new Set(tools.map(function (t) { return t.name || "unknown"; })));
91
+ var summary = uniqueNames.length <= 3
92
+ ? uniqueNames.join(", ")
93
+ : uniqueNames.slice(0, 2).join(", ") + " + " + (uniqueNames.length - 2) + " more";
94
+
95
+ return (
96
+ <div className="ml-14 mr-5 py-0.5 max-w-[95%] sm:max-w-[85%]">
97
+ <div className={"rounded-lg border text-[12px] overflow-hidden " + (allDone ? "bg-base-200/50 border-base-content/8" : "bg-base-200/70 border-primary/20")}>
98
+ <button
99
+ type="button"
100
+ onClick={function () { setExpanded(function (v) { return !v; }); }}
101
+ className="flex items-center gap-2 w-full py-1.5 px-2.5 hover:bg-base-content/5 transition-colors duration-100 cursor-pointer outline-none focus-visible:ring-2 focus-visible:ring-primary/30 focus-visible:ring-inset"
102
+ >
103
+ <Wrench size={11} className={allDone ? "text-base-content/30" : "text-primary/70"} />
104
+ <span className="font-mono font-medium text-base-content/70 flex-1 text-left">
105
+ Ran {tools.length} commands
106
+ </span>
107
+ <span className="text-[10px] text-base-content/40 truncate max-w-[200px]">{summary}</span>
108
+ {allDone ? (
109
+ <Check size={11} className="text-success/50 flex-shrink-0" />
110
+ ) : (
111
+ <span className="text-[10px] text-primary/70 flex-shrink-0">running</span>
112
+ )}
113
+ <ChevronDown
114
+ size={11}
115
+ className={"text-base-content/30 transition-transform duration-150 flex-shrink-0 " + (expanded ? "rotate-180" : "")}
116
+ />
117
+ </button>
118
+
119
+ {expanded && (
120
+ <div className="border-t border-base-content/8">
121
+ {tools.map(function (tool, i) {
122
+ return <ToolDetail key={tool.toolId || i} tool={tool} />;
123
+ })}
124
+ </div>
125
+ )}
126
+ </div>
127
+ </div>
128
+ );
129
+ }