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