loom-agent 1.2.36 → 1.3.2
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/CHANGELOG.md +128 -0
- package/README.md +38 -56
- package/bin/loom-bun.js +2 -10
- package/bin/loom.js +0 -10
- package/package.json +126 -126
- package/src/connectors/convex.js +356 -0
- package/src/connectors/deploy.js +390 -0
- package/src/connectors/index.js +23 -0
- package/src/connectors/supabase.js +279 -0
- package/src/core/agents.js +32 -2
- package/src/core/cli.js +30 -5
- package/src/core/permissions.js +12 -1
- package/src/core/plugin-cmd.js +1 -0
- package/src/core/sandbox.js +400 -0
- package/src/core/session-store.js +245 -167
- package/src/core/session.js +157 -11
- package/src/providers/anthropic.js +10 -5
- package/src/providers/openai-compat.js +12 -4
- package/src/tools/browser.js +204 -0
- package/src/tools/index.js +35 -1
- package/src/tui/App.tsx +103 -8
- package/src/tui/components/BreadcrumbBar.tsx +10 -9
- package/src/tui/components/ChatArea.tsx +3 -3
- package/src/tui/components/InputBar.tsx +22 -11
- package/src/tui/components/MdText.tsx +2 -1
- package/src/tui/components/Modals.tsx +191 -17
- package/src/tui/components/PermissionPopup.tsx +15 -12
- package/src/tui/components/Sidebar.tsx +6 -2
- package/src/tui/components/SplashScreen.tsx +17 -24
- package/src/tui/components/SubagentPanel.tsx +1 -1
- package/src/tui/components/ToastOverlay.tsx +23 -12
- package/src/tui/store.ts +38 -35
- package/src/tui/theme.ts +45 -30
- package/src/tui/tool-display.ts +13 -2
- package/docs/web.md +0 -205
- package/src/web/attach.js +0 -242
- package/src/web/graph-view.html +0 -262
- package/src/web/index.html +0 -824
- package/src/web/web-server.js +0 -535
|
@@ -200,44 +200,47 @@ function PermissionPrompt() {
|
|
|
200
200
|
<box flexDirection="column" marginTop={1} gap={0}>
|
|
201
201
|
{isQ ? (
|
|
202
202
|
opts.map((o, i) => (
|
|
203
|
-
<box {...row(i)} flexDirection="row" paddingY={0}
|
|
203
|
+
<box {...row(i)} flexDirection="row" paddingY={0}
|
|
204
|
+
backgroundColor={sel() === i ? ui.bgPanelAlt : ui.bgPanel}>
|
|
204
205
|
<text fg={sel() === i ? ui.primary : ui.fgDim} paddingRight={1}>
|
|
205
|
-
{(sel() === i ? "
|
|
206
|
+
{(sel() === i ? "▸ " : " ") + fit(o, 40)}
|
|
206
207
|
</text>
|
|
207
208
|
</box>
|
|
208
209
|
))
|
|
209
210
|
) : (
|
|
210
211
|
<>
|
|
211
|
-
<box {...row(0)} flexDirection="row" paddingY={0}
|
|
212
|
+
<box {...row(0)} flexDirection="row" paddingY={0}
|
|
213
|
+
backgroundColor={sel() === 0 ? ui.bgPanelAlt : ui.bgPanel}>
|
|
212
214
|
<text fg={sel() === 0 ? ui.primary : ui.fgDim} paddingRight={1}>
|
|
213
|
-
{(sel() === 0 ? "
|
|
215
|
+
{(sel() === 0 ? "▸ " : " ") + "Allow"}
|
|
214
216
|
</text>
|
|
215
217
|
<text fg={ui.success}>{"(recommended)"}</text>
|
|
216
218
|
</box>
|
|
217
|
-
<box {...row(1)} flexDirection="row" paddingY={0}
|
|
219
|
+
<box {...row(1)} flexDirection="row" paddingY={0}
|
|
220
|
+
backgroundColor={sel() === 1 ? ui.bgPanelAlt : ui.bgPanel}>
|
|
218
221
|
<text fg={sel() === 1 ? ui.primary : ui.fgDim} paddingRight={1}>
|
|
219
|
-
{(sel() === 1 ? "
|
|
222
|
+
{(sel() === 1 ? "▸ " : " ") + "Always allow"}
|
|
220
223
|
</text>
|
|
221
224
|
<text fg={ui.fgMuted}>{"remember for this command"}</text>
|
|
222
225
|
</box>
|
|
223
|
-
<box {...row(2)}>
|
|
226
|
+
<box {...row(2)} backgroundColor={sel() === 2 ? ui.bgPanelAlt : ui.bgPanel}>
|
|
224
227
|
<text fg={sel() === 2 ? ui.primary : ui.fgDim} paddingY={0}>
|
|
225
|
-
{(sel() === 2 ? "
|
|
228
|
+
{(sel() === 2 ? "▸ " : " ") + "Deny"}
|
|
226
229
|
</text>
|
|
227
230
|
</box>
|
|
228
231
|
{pr.tool === "bash" ? (
|
|
229
|
-
<box {...row(3)}>
|
|
232
|
+
<box {...row(3)} backgroundColor={sel() === 3 ? ui.bgPanelAlt : ui.bgPanel}>
|
|
230
233
|
<text fg={sel() === 3 ? ui.primary : ui.fgDim} paddingY={0}>
|
|
231
|
-
{(sel() === 3 ? "
|
|
234
|
+
{(sel() === 3 ? "▸ " : " ") + "Allow all commands in this session"}
|
|
232
235
|
</text>
|
|
233
236
|
</box>
|
|
234
237
|
) : null}
|
|
235
238
|
</>
|
|
236
239
|
)}
|
|
237
240
|
{isQ && !isStart ? (
|
|
238
|
-
<box {...row(opts.length)}>
|
|
241
|
+
<box {...row(opts.length)} backgroundColor={sel() === opts.length ? ui.bgPanelAlt : ui.bgPanel}>
|
|
239
242
|
<text fg={sel() === opts.length ? ui.primary : ui.fgDim} paddingY={0}>
|
|
240
|
-
{(sel() === opts.length ? "
|
|
243
|
+
{(sel() === opts.length ? "▸ " : " ") + "Type your answer…"}
|
|
241
244
|
</text>
|
|
242
245
|
</box>
|
|
243
246
|
) : null}
|
|
@@ -71,7 +71,7 @@ function speedInfo(sp: any): { label: string; color: string } {
|
|
|
71
71
|
if (tps == null) color = ui.fgMuted;
|
|
72
72
|
else if (tps >= 25 && (latency == null || latency <= 2500)) color = ui.success;
|
|
73
73
|
else if (tps >= 8 || (latency != null && latency <= 6000)) color = ui.warning;
|
|
74
|
-
else color =
|
|
74
|
+
else color = ui.error;
|
|
75
75
|
return { label, color };
|
|
76
76
|
}
|
|
77
77
|
|
|
@@ -86,6 +86,8 @@ export function Sidebar(props: { show: boolean }) {
|
|
|
86
86
|
<text fg={ui.fgMuted} height={1} flexShrink={0}>{"v" + VERSION}</text>
|
|
87
87
|
</box>
|
|
88
88
|
|
|
89
|
+
<text fg={ui.border} height={1} flexShrink={0}>{"─".repeat(36)}</text>
|
|
90
|
+
|
|
89
91
|
<box flexDirection="column" height={skillActive().length > 0 ? 8 : 7} flexShrink={0}>
|
|
90
92
|
<box flexDirection="row" height={1} flexShrink={0}><text fg={ui.fgMuted}>{"Provider: "}</text><text fg={ui.primary}>{providerName()}</text></box>
|
|
91
93
|
<box flexDirection="row" height={1} flexShrink={0}><text fg={ui.fgMuted}>{"Model: "}</text><text fg={ui.fg}>{modelName()}</text></box>
|
|
@@ -106,11 +108,13 @@ export function Sidebar(props: { show: boolean }) {
|
|
|
106
108
|
) : null}
|
|
107
109
|
</box>
|
|
108
110
|
|
|
111
|
+
<text fg={ui.border} height={1} flexShrink={0}>{"─".repeat(36)}</text>
|
|
112
|
+
|
|
109
113
|
<box flexDirection="row" gap={1} flexShrink={0}>
|
|
110
114
|
{TAB_NAMES.map((name, i) => (
|
|
111
115
|
<text fg={sidebarTab() === i ? ui.primary : ui.fgMuted} height={1} flexShrink={0}
|
|
112
116
|
onMouseUp={() => setSidebarTab(i)}>
|
|
113
|
-
{" " + name + " "}
|
|
117
|
+
{(sidebarTab() === i ? "▌" : " ") + name + " "}
|
|
114
118
|
</text>
|
|
115
119
|
))}
|
|
116
120
|
</box>
|
|
@@ -1,21 +1,8 @@
|
|
|
1
1
|
// Splash screen -- logo + embedded InputBar when no messages yet. Signal reads in JSX.
|
|
2
|
-
import
|
|
3
|
-
import path from "path";
|
|
4
|
-
import { palette, LOOM_LOGO } from "../theme.ts";
|
|
2
|
+
import { palette, LOOM_LOGO, VERSION } from "../theme.ts";
|
|
5
3
|
import { providerName, modelName, providerKeyOk } from "../store.ts";
|
|
6
4
|
import { InputBar } from "./InputBar.tsx";
|
|
7
5
|
|
|
8
|
-
// Read the real version from package.json (same source as `loom --version`),
|
|
9
|
-
// never a hardcoded label that drifts out of sync with releases.
|
|
10
|
-
const LOOM_VERSION = (() => {
|
|
11
|
-
try {
|
|
12
|
-
const pkg = fs.readFileSync(path.join(import.meta.dir, "..", "..", "package.json"), "utf8");
|
|
13
|
-
return "v" + pkg.match(/"version":\s*"([^"]+)"/)[1];
|
|
14
|
-
} catch {
|
|
15
|
-
return "";
|
|
16
|
-
}
|
|
17
|
-
})();
|
|
18
|
-
|
|
19
6
|
export function SplashScreen() {
|
|
20
7
|
const ui = palette("loom");
|
|
21
8
|
|
|
@@ -24,23 +11,29 @@ export function SplashScreen() {
|
|
|
24
11
|
<box flexGrow={1} />
|
|
25
12
|
|
|
26
13
|
<box flexDirection="column" alignItems="center" marginBottom={1}>
|
|
27
|
-
{LOOM_LOGO.map((line
|
|
14
|
+
{LOOM_LOGO.map((line) => (
|
|
28
15
|
<text fg={ui.primary}>{line}</text>
|
|
29
16
|
))}
|
|
30
17
|
</box>
|
|
31
18
|
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
19
|
+
<text fg={ui.border}>{"─".repeat(60)}</text>
|
|
20
|
+
|
|
21
|
+
<box marginTop={1} flexDirection="row" alignItems="center">
|
|
22
|
+
{/* OpenTUI text nodes take fg/bg via the style prop — a direct
|
|
23
|
+
backgroundColor prop is silently ignored (the pill vanished). */}
|
|
24
|
+
{/* @ts-ignore styled span for the build chip */}
|
|
25
|
+
<text><span style={{ fg: ui.bgPanel, bg: ui.primary }}>{" build "}</span></text>
|
|
26
|
+
<text fg={ui.fgMuted}>{" " + providerName()}</text>
|
|
27
|
+
<text fg={ui.fgMuted}>{" / "}</text>
|
|
37
28
|
<text fg={ui.fg}>{modelName()}</text>
|
|
38
|
-
<text fg={ui.
|
|
39
|
-
<text fg={providerKeyOk() ?
|
|
29
|
+
<text fg={ui.fgMuted}>{" "}</text>
|
|
30
|
+
<text fg={providerKeyOk() ? ui.success : ui.warning}>
|
|
31
|
+
{"● " + (providerKeyOk() ? "connected" : "no key")}
|
|
32
|
+
</text>
|
|
40
33
|
</box>
|
|
41
34
|
|
|
42
35
|
<box marginTop={0}>
|
|
43
|
-
<text fg={ui.fgMuted}>{
|
|
36
|
+
<text fg={ui.fgMuted}>{"v" + VERSION}</text>
|
|
44
37
|
</box>
|
|
45
38
|
|
|
46
39
|
<box marginY={1} width={74}>
|
|
@@ -58,4 +51,4 @@ export function SplashScreen() {
|
|
|
58
51
|
<box flexGrow={1} />
|
|
59
52
|
</box>
|
|
60
53
|
);
|
|
61
|
-
}
|
|
54
|
+
}
|
|
@@ -25,7 +25,7 @@ function statusColor(s: SubagentEntry["status"]): string {
|
|
|
25
25
|
if (s === "running") return ui.warning || ui.primary;
|
|
26
26
|
if (s === "done") return ui.success || ui.primary;
|
|
27
27
|
if (s === "cancelled") return ui.fgMuted;
|
|
28
|
-
return "#ff5555";
|
|
28
|
+
return ui.error || "#ff5555";
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
function fmtCost(c: number): string {
|
|
@@ -15,18 +15,29 @@ export function ToastOverlay() {
|
|
|
15
15
|
flexDirection="column" paddingX={1} paddingY={0}
|
|
16
16
|
>
|
|
17
17
|
<For each={list()}>
|
|
18
|
-
{(t) =>
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
>
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
18
|
+
{(t) => {
|
|
19
|
+
const c = t.kind === "error" ? ui.error : t.kind === "ok" ? ui.success : ui.primary;
|
|
20
|
+
const dot = t.kind === "error" ? "✖" : t.kind === "ok" ? "●" : "◆";
|
|
21
|
+
// Cap the rendered width: a wide toast overlaps the chat's last
|
|
22
|
+
// rows (the box is opaque) — the store keeps the full text, only
|
|
23
|
+
// the floating chip clips with an ellipsis.
|
|
24
|
+
const body = t.text.length > 40 ? t.text.slice(0, 39) + "\u2026" : t.text;
|
|
25
|
+
return (
|
|
26
|
+
<box
|
|
27
|
+
border borderStyle="rounded"
|
|
28
|
+
borderColor={c}
|
|
29
|
+
paddingX={1} paddingY={0} marginTop={0}
|
|
30
|
+
backgroundColor={ui.bgPanel}
|
|
31
|
+
>
|
|
32
|
+
<text fg={c}>
|
|
33
|
+
{" " + dot + " "}
|
|
34
|
+
</text>
|
|
35
|
+
<text fg={t.kind === "error" ? ui.error : ui.fg}>
|
|
36
|
+
{body}
|
|
37
|
+
</text>
|
|
38
|
+
</box>
|
|
39
|
+
);
|
|
40
|
+
}}
|
|
30
41
|
</For>
|
|
31
42
|
</box>
|
|
32
43
|
</Show>
|
package/src/tui/store.ts
CHANGED
|
@@ -324,7 +324,9 @@ export function allModelOptions() {
|
|
|
324
324
|
if (!m) continue;
|
|
325
325
|
const key = r.provider + "/" + r.model;
|
|
326
326
|
seen.add(key);
|
|
327
|
-
|
|
327
|
+
// No sub: the full id repeated under every name was pure clutter — the
|
|
328
|
+
// id is still searchable (filtered() matches value + tags).
|
|
329
|
+
out.push({ label: m.name, value: m.id, provider: r.provider, tags: m.tags, recent: true });
|
|
328
330
|
}
|
|
329
331
|
}
|
|
330
332
|
for (const p of PROVIDER_ORDER) {
|
|
@@ -337,7 +339,7 @@ export function allModelOptions() {
|
|
|
337
339
|
for (const m of mods) {
|
|
338
340
|
const key = p + "/" + m.id;
|
|
339
341
|
if (seen.has(key)) continue;
|
|
340
|
-
out.push({ label: m.name, value: m.id,
|
|
342
|
+
out.push({ label: m.name, value: m.id, provider: p, tags: m.tags });
|
|
341
343
|
}
|
|
342
344
|
}
|
|
343
345
|
out.push({ label: "(custom model ID)", value: "__custom__" });
|
|
@@ -492,50 +494,51 @@ export function wireTodoEvents() {
|
|
|
492
494
|
}
|
|
493
495
|
|
|
494
496
|
// ─── Slash commands (for autocomplete) ───
|
|
497
|
+
// Terse descs only: this list feeds the popup + command palette, so every
|
|
498
|
+
// extra word adds clutter. Syntax lives in `args` and shows up in /help,
|
|
499
|
+
// NOT in the popup. Aliases keep their handlers but stay out of the list:
|
|
500
|
+
// /model → /models · /thinking + /details → /settings · /rewind → /restore
|
|
495
501
|
export interface SlashCmd { cmd: string; desc: string; args?: string; }
|
|
496
502
|
|
|
497
503
|
export const SLASH_LIST: SlashCmd[] = [
|
|
498
|
-
{ cmd: "help", desc: "Show help
|
|
499
|
-
{ cmd: "agents", desc: "List agents
|
|
500
|
-
{ cmd: "build", desc: "Build mode — full
|
|
501
|
-
{ cmd: "plan", desc: "Plan mode — read
|
|
502
|
-
{ cmd: "chat", desc: "Chat mode —
|
|
503
|
-
{ cmd: "connect", desc: "
|
|
504
|
+
{ cmd: "help", desc: "Show help" },
|
|
505
|
+
{ cmd: "agents", desc: "List agents" },
|
|
506
|
+
{ cmd: "build", desc: "Build mode — full tools" },
|
|
507
|
+
{ cmd: "plan", desc: "Plan mode — read only" },
|
|
508
|
+
{ cmd: "chat", desc: "Chat mode — no tools" },
|
|
509
|
+
{ cmd: "connect", desc: "Connect a provider" },
|
|
504
510
|
{ cmd: "key", desc: "Edit API key" },
|
|
505
|
-
{ cmd: "baseurl", desc: "Set provider base URL"
|
|
506
|
-
{ cmd: "
|
|
507
|
-
{ cmd: "
|
|
508
|
-
{ cmd: "
|
|
509
|
-
{ cmd: "
|
|
510
|
-
{ cmd: "
|
|
511
|
-
{ cmd: "
|
|
512
|
-
{ cmd: "new", desc: "Start a new session" },
|
|
511
|
+
{ cmd: "baseurl", desc: "Set provider base URL" },
|
|
512
|
+
{ cmd: "models", desc: "Model picker" },
|
|
513
|
+
{ cmd: "providers", desc: "List providers" },
|
|
514
|
+
{ cmd: "status", desc: "Connection status" },
|
|
515
|
+
{ cmd: "usage", desc: "Token usage & billing" },
|
|
516
|
+
{ cmd: "budget", desc: "Budget level", args: "free | cheap | best | auto" },
|
|
517
|
+
{ cmd: "new", desc: "New session" },
|
|
513
518
|
{ cmd: "clear", desc: "Clear the chat" },
|
|
514
|
-
{ cmd: "restore", desc: "Restore
|
|
515
|
-
{ cmd: "settings", desc: "
|
|
516
|
-
{ cmd: "sessions", desc: "Browse saved sessions
|
|
517
|
-
{ cmd: "thinking", desc: "Toggle thinking visibility" },
|
|
518
|
-
{ cmd: "details", desc: "Toggle tool detail visibility" },
|
|
519
|
+
{ cmd: "restore", desc: "Restore earlier state" },
|
|
520
|
+
{ cmd: "settings", desc: "Settings — details, thinking, sidebar" },
|
|
521
|
+
{ cmd: "sessions", desc: "Browse saved sessions" },
|
|
519
522
|
{ cmd: "theme", desc: "Switch UI theme" },
|
|
520
|
-
{ cmd: "graph", desc: "
|
|
523
|
+
{ cmd: "graph", desc: "Memory graph" },
|
|
521
524
|
{ cmd: "skills", desc: "Manage skills", args: "install <dir|git> | remove <name>" },
|
|
522
525
|
{ cmd: "mcp", desc: "Manage MCP servers", args: "add <name> <cmd> | remove | toggle" },
|
|
523
|
-
{ cmd: "connectors", desc: "Manage connectors
|
|
524
|
-
{ cmd: "permissions", desc: "
|
|
526
|
+
{ cmd: "connectors", desc: "Manage connectors", args: "add <name> <cmd> | remove | toggle" },
|
|
527
|
+
{ cmd: "permissions", desc: "Permission rules", args: "reset | auto" },
|
|
525
528
|
{ cmd: "exit", desc: "Quit Loom Code" },
|
|
526
529
|
// Newer commands sit at the END so early rows (help/connect/…) keep their
|
|
527
530
|
// popup positions — the interactive suite and muscle memory rely on it.
|
|
528
|
-
{ cmd: "subagents", desc: "
|
|
529
|
-
{ cmd: "context", desc: "
|
|
530
|
-
{ cmd: "think", desc: "Thinking budget", args: "off|low|medium|high" },
|
|
531
|
-
{ cmd: "approve", desc: "Approve
|
|
532
|
-
{ cmd: "tasks", desc: "
|
|
533
|
-
{ cmd: "
|
|
534
|
-
{ cmd: "
|
|
535
|
-
{ cmd: "
|
|
536
|
-
{ cmd: "style", desc: "Output style preset
|
|
537
|
-
{ cmd: "vim", desc: "Toggle vim
|
|
538
|
-
{ cmd: "remember", desc: "Save a fact to
|
|
531
|
+
{ cmd: "subagents", desc: "Subagent runs" },
|
|
532
|
+
{ cmd: "context", desc: "Context window usage" },
|
|
533
|
+
{ cmd: "think", desc: "Thinking budget", args: "off | low | medium | high" },
|
|
534
|
+
{ cmd: "approve", desc: "Approve plan → Build" },
|
|
535
|
+
{ cmd: "tasks", desc: "Background tasks" },
|
|
536
|
+
{ cmd: "share", desc: "Export chat as HTML" },
|
|
537
|
+
{ cmd: "worktree", desc: "New git worktree", args: "<name>" },
|
|
538
|
+
{ cmd: "sandbox", desc: "Docker sandbox", args: "setup | status | on | off | rm" },
|
|
539
|
+
{ cmd: "style", desc: "Output style preset" },
|
|
540
|
+
{ cmd: "vim", desc: "Toggle vim editing" },
|
|
541
|
+
{ cmd: "remember", desc: "Save a fact to LOOM.md", args: "<fact>" },
|
|
539
542
|
];
|
|
540
543
|
|
|
541
544
|
// ─── TUI prefs ───
|
package/src/tui/theme.ts
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// palette() returns a live Proxy: each property is read from the currently
|
|
3
3
|
// selected theme at render time, so switching themes re-renders instantly.
|
|
4
4
|
import { createSignal } from "solid-js";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
5
7
|
import { loadTuiJson, saveTuiJson } from "./tui-config.ts";
|
|
6
8
|
|
|
7
9
|
export type Palette = {
|
|
@@ -22,49 +24,50 @@ export type Palette = {
|
|
|
22
24
|
export type Theme = { id: string; label: string; desc: string; palette: Palette };
|
|
23
25
|
|
|
24
26
|
const LOOM_DARK: Palette = {
|
|
25
|
-
// Deep warm charcoal — terracotta accents
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
27
|
+
// Deep warm charcoal — terracotta accents (ember layers: every surface
|
|
28
|
+
// steps clearly toward bgPanelAlt so panels read without any borders)
|
|
29
|
+
bg: "#171512",
|
|
30
|
+
bgMsg: "#121009",
|
|
31
|
+
bgPanel: "#1f1c18",
|
|
32
|
+
bgPanelAlt: "#282420",
|
|
33
|
+
bgInput: "#201d18",
|
|
34
|
+
bgHover: "#322c24",
|
|
35
|
+
border: "#463f36",
|
|
36
|
+
borderFocus: "#e8845c",
|
|
37
|
+
fg: "#f2ede4",
|
|
38
|
+
fgDim: "#a89d8d",
|
|
39
|
+
fgMuted: "#6b6252",
|
|
40
|
+
primary: "#e8845c",
|
|
41
|
+
primarySoft: "#f5a883",
|
|
42
|
+
secondary: "#a8c496",
|
|
43
|
+
accent: "#edb56f",
|
|
41
44
|
success: "#a3c084",
|
|
42
45
|
warning: "#e0b356",
|
|
43
|
-
error: "#
|
|
46
|
+
error: "#e06858",
|
|
44
47
|
thinking: "#d9a35f",
|
|
45
48
|
syntax: {
|
|
46
|
-
kw: "#
|
|
49
|
+
kw: "#e8845c",
|
|
47
50
|
str: "#a3c084",
|
|
48
|
-
com: "#
|
|
51
|
+
com: "#6b6252",
|
|
49
52
|
num: "#e0b356",
|
|
50
|
-
call: "#
|
|
51
|
-
plain: "#
|
|
53
|
+
call: "#edb56f",
|
|
54
|
+
plain: "#f2ede4",
|
|
52
55
|
},
|
|
53
56
|
};
|
|
54
57
|
|
|
55
58
|
const LIGHT: Palette = {
|
|
56
59
|
// Paper light — warm parchment with terracotta
|
|
57
|
-
bg: "#
|
|
58
|
-
bgMsg: "#
|
|
59
|
-
bgPanel: "#
|
|
60
|
-
bgPanelAlt: "#
|
|
60
|
+
bg: "#faf7f0",
|
|
61
|
+
bgMsg: "#efe8dc",
|
|
62
|
+
bgPanel: "#f2ece1",
|
|
63
|
+
bgPanelAlt: "#e8e0d1",
|
|
61
64
|
bgInput: "#ffffff",
|
|
62
|
-
bgHover: "#
|
|
63
|
-
border: "#
|
|
65
|
+
bgHover: "#ddd3c2",
|
|
66
|
+
border: "#c4b9a5",
|
|
64
67
|
borderFocus: "#c2593a",
|
|
65
68
|
fg: "#2b2620",
|
|
66
69
|
fgDim: "#6f675c",
|
|
67
|
-
fgMuted: "#
|
|
70
|
+
fgMuted: "#968c7c",
|
|
68
71
|
primary: "#c2593a",
|
|
69
72
|
primarySoft: "#a34a2f",
|
|
70
73
|
secondary: "#5c7a4a",
|
|
@@ -76,7 +79,7 @@ const LIGHT: Palette = {
|
|
|
76
79
|
syntax: {
|
|
77
80
|
kw: "#c2593a",
|
|
78
81
|
str: "#5f8a3d",
|
|
79
|
-
com: "#
|
|
82
|
+
com: "#968c7c",
|
|
80
83
|
num: "#a97a1a",
|
|
81
84
|
call: "#b0742f",
|
|
82
85
|
plain: "#2b2620",
|
|
@@ -301,8 +304,20 @@ export function palette(_name?: string): Palette {
|
|
|
301
304
|
}) as Palette;
|
|
302
305
|
}
|
|
303
306
|
|
|
304
|
-
|
|
307
|
+
// Version read from package.json — the single source of truth for `loom
|
|
308
|
+
// --version`, the splash, and the sidebar (a hardcoded const drifted to 1.2.0
|
|
309
|
+
// while the package moved on).
|
|
310
|
+
export const VERSION = (() => {
|
|
311
|
+
try {
|
|
312
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(import.meta.dir, "..", "..", "package.json"), "utf8"));
|
|
313
|
+
return String(pkg.version || "0.0.0");
|
|
314
|
+
} catch {
|
|
315
|
+
return "0.0.0";
|
|
316
|
+
}
|
|
317
|
+
})();
|
|
305
318
|
|
|
319
|
+
// Full "LOOM CODE" block wordmark for the splash, rendered in the theme's
|
|
320
|
+
// primary (terracotta on the default theme).
|
|
306
321
|
export const LOOM_LOGO = [
|
|
307
322
|
" ██╗ ██████╗ ██████╗ ███╗ ███╗ ██████╗ ██████╗ ██████╗ ███████╗",
|
|
308
323
|
" ██║ ██╔═══██╗██╔═══██╗████╗ ████║ ██╔════╝██╔═══██╗██╔══██╗██╔════╝",
|
package/src/tui/tool-display.ts
CHANGED
|
@@ -87,9 +87,20 @@ const TOOL_DISPLAY: Record<string, ToolDisplay> = Object.assign(Object.create(nu
|
|
|
87
87
|
diffs: null,
|
|
88
88
|
},
|
|
89
89
|
websearch: {
|
|
90
|
-
icon: "
|
|
90
|
+
icon: "◈",
|
|
91
91
|
pending: "Searching web...",
|
|
92
|
-
label: (i) => "WebSearch \
|
|
92
|
+
label: (i) => "WebSearch \"" + String(i?.query || "") + "\"",
|
|
93
|
+
diffs: null,
|
|
94
|
+
},
|
|
95
|
+
browser: {
|
|
96
|
+
icon: "◉",
|
|
97
|
+
pending: "Browsing...",
|
|
98
|
+
spinner: true,
|
|
99
|
+
label: (i) => {
|
|
100
|
+
const a = String(i?.action || "");
|
|
101
|
+
const target = String(i?.url || i?.selector || i?.key || "");
|
|
102
|
+
return "Browser " + a + (target ? " " + target.slice(0, 60) : "");
|
|
103
|
+
},
|
|
93
104
|
diffs: null,
|
|
94
105
|
},
|
|
95
106
|
todowrite: {
|