@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,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
|
+
}
|