@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,727 @@
1
+ import { useEffect, useRef, useCallback, useState } from "react";
2
+ import { Terminal, Info, ArrowDown, Pencil, Copy, Check, Menu, AlertTriangle } from "lucide-react";
3
+ import { LatticeLogomark } from "../ui/LatticeLogomark";
4
+ import { useVirtualizer } from "@tanstack/react-virtual";
5
+ import { useSession } from "../../hooks/useSession";
6
+ import { useProjects } from "../../hooks/useProjects";
7
+ import { useWebSocket } from "../../hooks/useWebSocket";
8
+ import { setSessionTitle } from "../../stores/session";
9
+ import { Message } from "./Message";
10
+ import { ToolGroup } from "./ToolGroup";
11
+ import { ChatInput } from "./ChatInput";
12
+ import { ModelSelector } from "./ModelSelector";
13
+ import { PermissionModeSelector } from "./PermissionModeSelector";
14
+ import { StatusBar } from "./StatusBar";
15
+ import { useSidebar } from "../../hooks/useSidebar";
16
+
17
+ export function ChatView() {
18
+ var { messages, isProcessing, sendMessage, activeSessionId, activeSessionTitle, currentStatus, contextUsage, contextBreakdown, lastResponseCost, lastResponseDuration, historyLoading, wasInterrupted } = useSession();
19
+ var { activeProject } = useProjects();
20
+ var { toggleDrawer } = useSidebar();
21
+ var ws = useWebSocket();
22
+ var scrollParentRef = useRef<HTMLDivElement>(null);
23
+ var prevLengthRef = useRef<number>(0);
24
+ var isLiveChatRef = useRef<boolean>(false);
25
+ var [isNearBottom, setIsNearBottom] = useState<boolean>(true);
26
+ var isNearBottomRef = useRef<boolean>(true);
27
+ var [isMobile, setIsMobile] = useState<boolean>(function () { return typeof window !== "undefined" && window.innerWidth < 640; });
28
+ var [selectedModel, setSelectedModel] = useState<string>("default");
29
+ var [selectedEffort, setSelectedEffort] = useState<string>("medium");
30
+ var [showInfo, setShowInfo] = useState<boolean>(false);
31
+ var [copiedField, setCopiedField] = useState<string | null>(null);
32
+ var [isRenaming, setIsRenaming] = useState<boolean>(false);
33
+ var [renameValue, setRenameValue] = useState<string>("");
34
+ var [showContext, setShowContext] = useState<boolean>(false);
35
+ var infoRef = useRef<HTMLButtonElement>(null);
36
+ var infoPanelRef = useRef<HTMLDivElement>(null);
37
+ var contextBarRef = useRef<HTMLButtonElement>(null);
38
+ var contextBarMobileRef = useRef<HTMLButtonElement>(null);
39
+ var contextPanelRef = useRef<HTMLDivElement>(null);
40
+ var renameInputRef = useRef<HTMLInputElement>(null);
41
+
42
+ useEffect(function () {
43
+ var el = scrollParentRef.current;
44
+ if (!el) return;
45
+ var scrollEl = el;
46
+ function handleScroll() {
47
+ var near = (scrollEl.scrollHeight - scrollEl.scrollTop - scrollEl.clientHeight) < 200;
48
+ if (near !== isNearBottomRef.current) {
49
+ isNearBottomRef.current = near;
50
+ setIsNearBottom(near);
51
+ }
52
+ }
53
+ scrollEl.addEventListener("scroll", handleScroll, { passive: true });
54
+ return function () { scrollEl.removeEventListener("scroll", handleScroll); };
55
+ }, []);
56
+
57
+ useEffect(function () {
58
+ function handleResize() {
59
+ setIsMobile(window.innerWidth < 640);
60
+ }
61
+ window.addEventListener("resize", handleResize);
62
+ return function () { window.removeEventListener("resize", handleResize); };
63
+ }, []);
64
+
65
+ var virtualizer = useVirtualizer({
66
+ count: messages.length,
67
+ getScrollElement: function () {
68
+ return scrollParentRef.current;
69
+ },
70
+ estimateSize: function (index) {
71
+ var msg = messages[index];
72
+ if (!msg) return 120;
73
+ if (msg.type === "tool_start") return 52;
74
+ if (msg.type === "user") return 100;
75
+ return 200;
76
+ },
77
+ overscan: 30,
78
+ });
79
+
80
+ var scrollToBottom = useCallback(function () {
81
+ if (messages.length === 0) return;
82
+ if (isMobile) {
83
+ var el = scrollParentRef.current;
84
+ if (el) el.scrollTop = el.scrollHeight;
85
+ } else {
86
+ virtualizer.scrollToIndex(messages.length - 1, { align: "end", behavior: "smooth" });
87
+ }
88
+ }, [messages.length, virtualizer, isMobile]);
89
+
90
+ useEffect(
91
+ function () {
92
+ if (messages.length === 0) {
93
+ prevLengthRef.current = 0;
94
+ isLiveChatRef.current = false;
95
+ return;
96
+ }
97
+ var prevLen = prevLengthRef.current;
98
+ var delta = messages.length - prevLen;
99
+ prevLengthRef.current = messages.length;
100
+
101
+ if (prevLen === 0 && delta > 1) {
102
+ isLiveChatRef.current = false;
103
+ if (isMobile) {
104
+ requestAnimationFrame(function () {
105
+ var el = scrollParentRef.current;
106
+ if (el) el.scrollTop = el.scrollHeight;
107
+ requestAnimationFrame(function () {
108
+ if (el) el.scrollTop = el.scrollHeight;
109
+ });
110
+ });
111
+ } else {
112
+ var count = messages.length;
113
+ var virt = virtualizer;
114
+ requestAnimationFrame(function () {
115
+ virt.scrollToIndex(count - 1, { align: "end" });
116
+ requestAnimationFrame(function () {
117
+ virt.scrollToIndex(count - 1, { align: "end" });
118
+ });
119
+ });
120
+ }
121
+ return;
122
+ }
123
+
124
+ isLiveChatRef.current = true;
125
+ scrollToBottom();
126
+ },
127
+ [messages.length, scrollToBottom]
128
+ );
129
+
130
+ useEffect(
131
+ function () {
132
+ if (isProcessing && isLiveChatRef.current) {
133
+ scrollToBottom();
134
+ }
135
+ },
136
+ [isProcessing, scrollToBottom]
137
+ );
138
+
139
+ useEffect(function () {
140
+ if (!showInfo) return;
141
+ function handleClick(e: MouseEvent) {
142
+ if (
143
+ infoPanelRef.current && !infoPanelRef.current.contains(e.target as Node) &&
144
+ infoRef.current && !infoRef.current.contains(e.target as Node)
145
+ ) {
146
+ setShowInfo(false);
147
+ }
148
+ }
149
+ document.addEventListener("mousedown", handleClick);
150
+ return function () { document.removeEventListener("mousedown", handleClick); };
151
+ }, [showInfo]);
152
+
153
+ useEffect(function () {
154
+ if (!showContext) return;
155
+ function handleClick(e: MouseEvent) {
156
+ var target = e.target as Node;
157
+ if (contextPanelRef.current && contextPanelRef.current.contains(target)) return;
158
+ if (contextBarRef.current && contextBarRef.current.contains(target)) return;
159
+ if (contextBarMobileRef.current && contextBarMobileRef.current.contains(target)) return;
160
+ setShowContext(false);
161
+ }
162
+ document.addEventListener("mousedown", handleClick);
163
+ return function () { document.removeEventListener("mousedown", handleClick); };
164
+ }, [showContext]);
165
+
166
+ useEffect(function () {
167
+ if (isRenaming && renameInputRef.current) {
168
+ renameInputRef.current.focus();
169
+ renameInputRef.current.select();
170
+ }
171
+ }, [isRenaming]);
172
+
173
+ function handleCopy(text: string, field: string) {
174
+ navigator.clipboard.writeText(text);
175
+ setCopiedField(field);
176
+ setTimeout(function () { setCopiedField(null); }, 1500);
177
+ }
178
+
179
+ function handleRenameStart() {
180
+ setRenameValue(activeSessionTitle || "");
181
+ setIsRenaming(true);
182
+ }
183
+
184
+ function handleRenameCommit() {
185
+ if (renameValue.trim() && activeSessionId) {
186
+ if (renameValue.trim() !== activeSessionTitle) {
187
+ ws.send({ type: "session:rename", sessionId: activeSessionId, title: renameValue.trim() });
188
+ setSessionTitle(renameValue.trim());
189
+ }
190
+ }
191
+ setIsRenaming(false);
192
+ setRenameValue("");
193
+ }
194
+
195
+ function handleRenameKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
196
+ if (e.key === "Enter") {
197
+ handleRenameCommit();
198
+ } else if (e.key === "Escape") {
199
+ setIsRenaming(false);
200
+ setRenameValue("");
201
+ }
202
+ }
203
+
204
+ function formatTokens(n: number): string {
205
+ if (n >= 1000000) return (n / 1000000).toFixed(1).replace(/\.0$/, "") + "M";
206
+ if (n >= 1000) return (n / 1000).toFixed(1).replace(/\.0$/, "") + "k";
207
+ return String(n);
208
+ }
209
+
210
+ var SEGMENT_COLORS: Record<string, string> = {
211
+ system: "var(--color-neutral)",
212
+ builtin_tools: "var(--color-info)",
213
+ instructions: "var(--color-success)",
214
+ memory: "var(--color-warning)",
215
+ user: "var(--color-primary)",
216
+ assistant: "var(--color-secondary)",
217
+ tool_results: "var(--color-accent)",
218
+ };
219
+ var MCP_HUES = [180, 160, 140, 200, 220];
220
+ function getSegmentColor(id: string): string {
221
+ if (SEGMENT_COLORS[id]) return SEGMENT_COLORS[id];
222
+ if (id.startsWith("mcp_")) {
223
+ var idx = 0;
224
+ for (var c = 0; c < id.length; c++) idx += id.charCodeAt(c);
225
+ return "oklch(0.65 0.15 " + MCP_HUES[idx % MCP_HUES.length] + ")";
226
+ }
227
+ return "oklch(0.5 0.1 250)";
228
+ }
229
+
230
+ var contextPercent = 0;
231
+ var contextFilled = 0;
232
+ if (contextUsage && contextUsage.contextWindow > 0) {
233
+ contextFilled = contextUsage.inputTokens + contextUsage.cacheReadTokens + contextUsage.cacheCreationTokens;
234
+ contextPercent = Math.min(100, Math.round((contextFilled / contextUsage.contextWindow) * 100));
235
+ }
236
+
237
+ var autocompactPercent = contextBreakdown ? Math.round((contextBreakdown.autocompactAt / contextBreakdown.contextWindow) * 100) : 90;
238
+ var isApproachingCompact = contextPercent >= autocompactPercent - 10;
239
+ var isCritical = contextPercent >= autocompactPercent;
240
+
241
+ var resumeCommand = activeSessionId && activeProject
242
+ ? "cd " + activeProject.path + " && claude --resume " + activeSessionId
243
+ : activeSessionId
244
+ ? "claude --resume " + activeSessionId
245
+ : "";
246
+
247
+ var virtualItems = virtualizer.getVirtualItems();
248
+
249
+ return (
250
+ <div className="flex flex-col h-full w-full bg-base-100 overflow-hidden relative">
251
+ <div className="bg-base-100 border-b border-base-300 flex-shrink-0 px-2 sm:px-4">
252
+ <div className="flex items-center min-h-10 sm:min-h-12 gap-1.5">
253
+ <button
254
+ className="btn btn-ghost btn-sm btn-square lg:hidden"
255
+ aria-label="Toggle sidebar"
256
+ onClick={toggleDrawer}
257
+ >
258
+ <Menu size={18} />
259
+ </button>
260
+ <div className="flex-1 min-w-0 flex items-center gap-1.5">
261
+ {isRenaming ? (
262
+ <input
263
+ ref={renameInputRef}
264
+ value={renameValue}
265
+ onChange={function (e) { setRenameValue(e.target.value); }}
266
+ onBlur={handleRenameCommit}
267
+ onKeyDown={handleRenameKeyDown}
268
+ className="input input-sm input-bordered text-sm font-semibold w-full max-w-[280px] bg-base-300 border-base-content/15"
269
+ />
270
+ ) : (
271
+ <>
272
+ <span className="text-sm font-semibold text-base-content truncate">
273
+ {activeSessionTitle || (activeSessionId ? "Session" : "New Session")}
274
+ </span>
275
+ {activeSessionId && (
276
+ <button
277
+ onClick={handleRenameStart}
278
+ aria-label="Rename session"
279
+ className="btn btn-ghost btn-xs btn-square text-base-content/30 hover:text-base-content/70 transition-colors"
280
+ >
281
+ <Pencil size={12} />
282
+ </button>
283
+ )}
284
+ </>
285
+ )}
286
+ </div>
287
+ <div className="flex gap-1.5 items-center relative">
288
+ {activeSessionId && (
289
+ <button
290
+ ref={contextBarRef}
291
+ onClick={function () { setShowContext(!showContext); }}
292
+ aria-label="Context usage"
293
+ className={"hidden sm:flex items-center gap-1.5 px-1.5 py-1 rounded transition-colors " + (showContext ? "bg-base-300" : "hover:bg-base-300/50")}
294
+ >
295
+ <div className="w-16 h-1.5 rounded-full bg-base-300 overflow-hidden relative">
296
+ <div
297
+ className={"h-full rounded-full transition-all duration-300 " + (isCritical ? "bg-error animate-pulse" : isApproachingCompact ? "bg-warning" : "bg-primary/60")}
298
+ style={{ width: Math.max(contextPercent, 1) + "%" }}
299
+ />
300
+ </div>
301
+ <span className={"text-[10px] font-mono tabular-nums " + (isCritical ? "text-error" : isApproachingCompact ? "text-warning" : "text-base-content/40")}>
302
+ {contextPercent}%
303
+ </span>
304
+ </button>
305
+ )}
306
+ {activeSessionId && (
307
+ <button
308
+ ref={infoRef}
309
+ onClick={function () { setShowInfo(!showInfo); }}
310
+ aria-label="Session info"
311
+ className={"btn btn-ghost btn-sm btn-square transition-colors " + (showInfo ? "text-primary" : "text-base-content/50 hover:text-base-content/70")}
312
+ >
313
+ <Info size={15} />
314
+ </button>
315
+ )}
316
+ {!activeSessionId && (
317
+ <button
318
+ aria-label="Session info"
319
+ disabled
320
+ className="btn btn-ghost btn-sm btn-square text-base-content/30 opacity-40 cursor-not-allowed"
321
+ >
322
+ <Info size={15} />
323
+ </button>
324
+ )}
325
+ <button
326
+ aria-label="Open terminal"
327
+ title="Coming soon"
328
+ disabled
329
+ className="btn btn-ghost btn-sm btn-square text-base-content/30 opacity-40 cursor-not-allowed"
330
+ >
331
+ <Terminal size={15} />
332
+ </button>
333
+
334
+ {showContext && activeSessionId && (
335
+ <div
336
+ ref={contextPanelRef}
337
+ className="absolute top-full right-0 mt-1 z-50 bg-base-300 border border-base-content/15 rounded-lg shadow-xl p-3 min-w-[300px] max-w-[340px]"
338
+ >
339
+ <div className="flex flex-col gap-3">
340
+ <div className="flex items-center justify-between">
341
+ <div className="text-[10px] uppercase tracking-widest text-base-content/40 font-mono font-bold">Context Window</div>
342
+ <span className={"text-xs font-mono font-bold tabular-nums " + (isCritical ? "text-error" : isApproachingCompact ? "text-warning" : "text-primary")}>
343
+ {contextPercent}%
344
+ </span>
345
+ </div>
346
+
347
+ {contextBreakdown ? (
348
+ <>
349
+ <div className="relative">
350
+ <div className="w-full h-3 rounded bg-base-200 overflow-hidden flex">
351
+ {contextBreakdown.segments.filter(function (seg) {
352
+ return seg.tokens > 0;
353
+ }).map(function (seg) {
354
+ var pct = (seg.tokens / contextBreakdown!.contextWindow) * 100;
355
+ if (pct < 0.2) return null;
356
+ return (
357
+ <div
358
+ key={seg.id}
359
+ className="h-full transition-all duration-300"
360
+ style={{ width: pct + "%", backgroundColor: getSegmentColor(seg.id) }}
361
+ />
362
+ );
363
+ })}
364
+ </div>
365
+ <div
366
+ className="absolute top-0 w-px h-full bg-base-content/25"
367
+ style={{ left: autocompactPercent + "%" }}
368
+ title={"Auto-compact at " + autocompactPercent + "%"}
369
+ />
370
+ </div>
371
+
372
+ <div className="flex justify-between text-[11px] font-mono tabular-nums text-base-content/50">
373
+ <span>{formatTokens(contextFilled)} used</span>
374
+ <span>{formatTokens(contextBreakdown.contextWindow)} total</span>
375
+ </div>
376
+
377
+ <div className="border-t border-base-content/10 pt-2.5 flex flex-col gap-0.5">
378
+ {contextBreakdown.segments.filter(function (seg) {
379
+ return seg.tokens > 0;
380
+ }).map(function (seg) {
381
+ var pct = contextBreakdown!.contextWindow > 0 ? ((seg.tokens / contextBreakdown!.contextWindow) * 100) : 0;
382
+ return (
383
+ <div key={seg.id} className="flex items-center justify-between py-0.5">
384
+ <div className="flex items-center gap-2 min-w-0">
385
+ <div className="w-2 h-2 rounded-sm flex-shrink-0" style={{ backgroundColor: getSegmentColor(seg.id) }} />
386
+ <span className="text-[11px] text-base-content/50 truncate">{seg.label}</span>
387
+ </div>
388
+ <div className="flex items-center gap-2 flex-shrink-0 ml-3">
389
+ <span className="text-[11px] font-mono tabular-nums text-base-content/70">
390
+ {seg.estimated ? "~" : ""}{formatTokens(seg.tokens)}
391
+ </span>
392
+ <span className="text-[10px] font-mono tabular-nums text-base-content/30 w-9 text-right">{pct < 1 ? "<1" : Math.round(pct)}%</span>
393
+ </div>
394
+ </div>
395
+ );
396
+ })}
397
+ </div>
398
+
399
+ {(function () {
400
+ var totalUsed = contextBreakdown!.segments.reduce(function (sum, seg) { return sum + seg.tokens; }, 0);
401
+ var available = contextBreakdown!.contextWindow - totalUsed;
402
+ if (available > 0) {
403
+ return (
404
+ <div className="border-t border-base-content/10 pt-2 flex items-center justify-between">
405
+ <span className="text-[11px] text-base-content/30">Available</span>
406
+ <span className="text-[11px] font-mono tabular-nums text-base-content/40">{formatTokens(available)}</span>
407
+ </div>
408
+ );
409
+ }
410
+ return null;
411
+ })()}
412
+ </>
413
+ ) : (
414
+ <>
415
+ <div className="w-full h-3 rounded bg-base-200 overflow-hidden">
416
+ <div
417
+ className={"h-full transition-all duration-300 " + (isCritical ? "bg-error" : isApproachingCompact ? "bg-warning" : "bg-primary/60")}
418
+ style={{ width: contextPercent + "%" }}
419
+ />
420
+ </div>
421
+
422
+ <div className="flex justify-between text-[11px] font-mono tabular-nums text-base-content/50">
423
+ <span>{formatTokens(contextFilled)} used</span>
424
+ <span>{formatTokens(contextUsage?.contextWindow || 0)} total</span>
425
+ </div>
426
+
427
+ <div className="text-[11px] text-base-content/30 text-center py-1">
428
+ Computing breakdown...
429
+ </div>
430
+ </>
431
+ )}
432
+
433
+ {isCritical && (
434
+ <div className="text-[10px] font-mono px-2 py-1.5 rounded bg-error/10 text-error">
435
+ Context nearly full — session will auto-compact soon
436
+ </div>
437
+ )}
438
+ {isApproachingCompact && !isCritical && (
439
+ <div className="text-[10px] font-mono px-2 py-1.5 rounded bg-warning/10 text-warning">
440
+ Approaching auto-compact threshold
441
+ </div>
442
+ )}
443
+ </div>
444
+ </div>
445
+ )}
446
+
447
+ {showInfo && activeSessionId && (
448
+ <div
449
+ ref={infoPanelRef}
450
+ className="absolute top-full right-0 mt-1 z-50 bg-base-300 border border-base-content/15 rounded-lg shadow-xl p-3 min-w-[340px]"
451
+ >
452
+ <div className="flex flex-col gap-2.5">
453
+ <div>
454
+ <div className="text-[10px] uppercase tracking-widest text-base-content/40 mb-1 font-mono font-bold">Session ID</div>
455
+ <div className="flex items-center gap-1.5">
456
+ <code className="text-[12px] font-mono text-base-content/70 bg-base-200 px-2 py-1 rounded flex-1 truncate select-all">
457
+ {activeSessionId}
458
+ </code>
459
+ <button
460
+ onClick={function () { handleCopy(activeSessionId!, "sessionId"); }}
461
+ aria-label="Copy session ID"
462
+ className="btn btn-ghost btn-xs btn-square text-base-content/40 hover:text-base-content/70 flex-shrink-0"
463
+ >
464
+ {copiedField === "sessionId" ? <Check size={13} className="text-success" /> : <Copy size={13} />}
465
+ </button>
466
+ </div>
467
+ </div>
468
+ <div>
469
+ <div className="text-[10px] uppercase tracking-widest text-base-content/40 mb-1 font-mono font-bold">Resume Command</div>
470
+ <div className="flex items-center gap-1.5">
471
+ <code className="text-[12px] font-mono text-base-content/70 bg-base-200 px-2 py-1 rounded flex-1 truncate select-all">
472
+ {resumeCommand}
473
+ </code>
474
+ <button
475
+ onClick={function () { handleCopy(resumeCommand, "resume"); }}
476
+ aria-label="Copy resume command"
477
+ className="btn btn-ghost btn-xs btn-square text-base-content/40 hover:text-base-content/70 flex-shrink-0"
478
+ >
479
+ {copiedField === "resume" ? <Check size={13} className="text-success" /> : <Copy size={13} />}
480
+ </button>
481
+ </div>
482
+ </div>
483
+ </div>
484
+ </div>
485
+ )}
486
+ </div>
487
+ </div>
488
+ {activeSessionId && (
489
+ <button
490
+ ref={contextBarMobileRef}
491
+ onClick={function () { setShowContext(!showContext); }}
492
+ aria-label="Context usage"
493
+ className={"sm:hidden flex items-center gap-1.5 px-1.5 pb-1 rounded transition-colors w-full " + (showContext ? "bg-base-300" : "hover:bg-base-300/50")}
494
+ >
495
+ <div className="flex-1 h-1.5 rounded-full bg-base-300 overflow-hidden relative">
496
+ <div
497
+ className={"h-full rounded-full transition-all duration-300 " + (isCritical ? "bg-error animate-pulse" : isApproachingCompact ? "bg-warning" : "bg-primary/60")}
498
+ style={{ width: Math.max(contextPercent, 1) + "%" }}
499
+ />
500
+ </div>
501
+ <span className={"text-[10px] font-mono tabular-nums " + (isCritical ? "text-error" : isApproachingCompact ? "text-warning" : "text-base-content/40")}>
502
+ {contextPercent}%
503
+ </span>
504
+ </button>
505
+ )}
506
+ </div>
507
+
508
+ <div
509
+ ref={scrollParentRef}
510
+ className="flex-1 overflow-y-auto min-h-0 bg-lattice-grid"
511
+ style={{ WebkitOverflowScrolling: "touch" }}
512
+ >
513
+ {messages.length === 0 && historyLoading ? (
514
+ <div className="flex items-center justify-center h-full">
515
+ <div className="flex flex-col items-center gap-3">
516
+ <div className="flex gap-1.5">
517
+ {[0, 1, 2].map(function (i) {
518
+ return (
519
+ <div
520
+ key={i}
521
+ className="w-2 h-2 rounded-full bg-primary/50"
522
+ style={{ animation: "pulse 1.2s ease-in-out infinite", animationDelay: i * 0.2 + "s" }}
523
+ />
524
+ );
525
+ })}
526
+ </div>
527
+ <span className="text-[12px] text-base-content/30 font-mono">Loading session...</span>
528
+ </div>
529
+ </div>
530
+ ) : messages.length === 0 ? (
531
+ <div className="flex items-center justify-center p-10 h-full">
532
+ <div className="text-center max-w-[360px]">
533
+ <div className="text-primary mb-4 flex justify-center">
534
+ <LatticeLogomark size={48} />
535
+ </div>
536
+ <p className="text-[17px] font-mono font-bold text-base-content mb-2 tracking-tight">
537
+ {activeSessionId
538
+ ? "Start the conversation"
539
+ : activeProject
540
+ ? "Create or select a session"
541
+ : "Select a project"}
542
+ </p>
543
+ <p className="text-[13px] text-base-content/40 leading-relaxed">
544
+ {activeSessionId
545
+ ? "Type a message below to begin chatting with Claude."
546
+ : activeProject
547
+ ? "Click the + button in the sidebar to start a new session."
548
+ : "Choose a project from the rail to get started."}
549
+ </p>
550
+ </div>
551
+ </div>
552
+ ) : isMobile ? (
553
+ <div className="pt-4">
554
+ {messages.map(function (msg, idx) {
555
+ if (msg.type === "tool_start") {
556
+ var groupStart = idx;
557
+ while (groupStart > 0 && messages[groupStart - 1].type === "tool_start") {
558
+ groupStart--;
559
+ }
560
+ var groupEnd = idx;
561
+ while (groupEnd < messages.length - 1 && messages[groupEnd + 1].type === "tool_start") {
562
+ groupEnd++;
563
+ }
564
+ var groupSize = groupEnd - groupStart + 1;
565
+ if (groupSize >= 2) {
566
+ if (idx === groupStart) {
567
+ var groupTools = messages.slice(groupStart, groupEnd + 1);
568
+ return <ToolGroup key={idx} tools={groupTools} />;
569
+ }
570
+ return null;
571
+ }
572
+ }
573
+ var isLastAssistant = false;
574
+ if (msg.type === "assistant" && !isProcessing && lastResponseCost != null) {
575
+ var foundLater = false;
576
+ for (var li = idx + 1; li < messages.length; li++) {
577
+ if (messages[li].type === "assistant") { foundLater = true; break; }
578
+ }
579
+ if (!foundLater) isLastAssistant = true;
580
+ }
581
+ return (
582
+ <Message
583
+ key={idx}
584
+ message={msg}
585
+ responseCost={isLastAssistant ? lastResponseCost : undefined}
586
+ responseDuration={isLastAssistant ? lastResponseDuration : undefined}
587
+ />
588
+ );
589
+ })}
590
+ </div>
591
+ ) : (
592
+ <div
593
+ className="relative w-full"
594
+ style={{ height: virtualizer.getTotalSize() + "px" }}
595
+ >
596
+ <div
597
+ className="absolute top-0 left-0 w-full will-change-transform"
598
+ style={{
599
+ transform: "translateY(" + (virtualItems.length > 0 ? virtualItems[0].start : 0) + "px)",
600
+ }}
601
+ >
602
+ {virtualItems.map(function (virtualItem) {
603
+ var msg = messages[virtualItem.index];
604
+ var idx = virtualItem.index;
605
+
606
+ if (msg.type === "tool_start") {
607
+ var groupStart = idx;
608
+ while (groupStart > 0 && messages[groupStart - 1].type === "tool_start") {
609
+ groupStart--;
610
+ }
611
+ var groupEnd = idx;
612
+ while (groupEnd < messages.length - 1 && messages[groupEnd + 1].type === "tool_start") {
613
+ groupEnd++;
614
+ }
615
+ var groupSize = groupEnd - groupStart + 1;
616
+
617
+ if (groupSize >= 2) {
618
+ if (idx === groupStart) {
619
+ var groupTools = messages.slice(groupStart, groupEnd + 1);
620
+ return (
621
+ <div
622
+ key={virtualItem.key}
623
+ data-index={virtualItem.index}
624
+ ref={virtualizer.measureElement}
625
+ className={virtualItem.index === 0 ? "pt-4" : ""}
626
+ >
627
+ <ToolGroup tools={groupTools} />
628
+ </div>
629
+ );
630
+ }
631
+ return (
632
+ <div
633
+ key={virtualItem.key}
634
+ data-index={virtualItem.index}
635
+ ref={virtualizer.measureElement}
636
+ />
637
+ );
638
+ }
639
+ }
640
+
641
+ var isLastAssistant = false;
642
+ if (msg.type === "assistant" && !isProcessing && lastResponseCost != null) {
643
+ var foundLater = false;
644
+ for (var li = virtualItem.index + 1; li < messages.length; li++) {
645
+ if (messages[li].type === "assistant") { foundLater = true; break; }
646
+ }
647
+ if (!foundLater) isLastAssistant = true;
648
+ }
649
+ return (
650
+ <div
651
+ key={virtualItem.key}
652
+ data-index={virtualItem.index}
653
+ ref={virtualizer.measureElement}
654
+ className={virtualItem.index === 0 ? "pt-4" : ""}
655
+ >
656
+ <Message
657
+ message={msg}
658
+ responseCost={isLastAssistant ? lastResponseCost : undefined}
659
+ responseDuration={isLastAssistant ? lastResponseDuration : undefined}
660
+ />
661
+ </div>
662
+ );
663
+ })}
664
+ </div>
665
+ </div>
666
+ )}
667
+
668
+ {isProcessing && (
669
+ <div className="flex px-5 py-3 gap-3 items-center">
670
+ <div className="w-7 h-7 rounded-full bg-primary/10 border border-primary/20 flex items-center justify-center flex-shrink-0">
671
+ <div className="w-3 h-3 rounded-full bg-primary" />
672
+ </div>
673
+ <div className="flex gap-1.5 items-center">
674
+ {[0, 1, 2].map(function (i) {
675
+ return (
676
+ <div
677
+ key={i}
678
+ className="w-1.5 h-1.5 rounded-full bg-base-content/30"
679
+ style={{
680
+ animation: "pulse 1.2s ease-in-out infinite",
681
+ animationDelay: i * 0.2 + "s",
682
+ }}
683
+ />
684
+ );
685
+ })}
686
+ </div>
687
+ </div>
688
+ )}
689
+ </div>
690
+
691
+ {messages.length > 0 && !isNearBottom && (
692
+ <div className="absolute bottom-32 left-1/2 -translate-x-1/2 z-10">
693
+ <button
694
+ onClick={scrollToBottom}
695
+ className="btn btn-sm btn-circle bg-base-300 border border-base-content/15 shadow-lg hover:bg-base-200 text-base-content/60 hover:text-base-content transition-all duration-150"
696
+ aria-label="Scroll to bottom"
697
+ >
698
+ <ArrowDown size={14} />
699
+ </button>
700
+ </div>
701
+ )}
702
+
703
+ <StatusBar status={currentStatus} />
704
+
705
+ {wasInterrupted && !isProcessing && (
706
+ <div className="flex items-center gap-2 px-3 sm:px-5 py-2 bg-warning/10 border-t border-warning/20">
707
+ <AlertTriangle size={13} className="text-warning flex-shrink-0" />
708
+ <span className="text-[12px] text-warning">Session was interrupted — send a message to continue</span>
709
+ </div>
710
+ )}
711
+
712
+ <div className="flex-shrink-0 border-t border-base-300 bg-base-200 px-2 sm:px-4 pb-3 pt-2">
713
+ <ChatInput
714
+ onSend={function (text) { sendMessage(text, selectedModel, selectedEffort); }}
715
+ disabled={isProcessing || !activeSessionId}
716
+ toolbarContent={
717
+ <>
718
+ <PermissionModeSelector />
719
+ <span className="text-base-content/15 hidden sm:inline">·</span>
720
+ <ModelSelector onChange={function (state) { setSelectedModel(state.model); setSelectedEffort(state.effort); }} />
721
+ </>
722
+ }
723
+ />
724
+ </div>
725
+ </div>
726
+ );
727
+ }