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.
Files changed (39) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/README.md +38 -56
  3. package/bin/loom-bun.js +2 -10
  4. package/bin/loom.js +0 -10
  5. package/package.json +126 -126
  6. package/src/connectors/convex.js +356 -0
  7. package/src/connectors/deploy.js +390 -0
  8. package/src/connectors/index.js +23 -0
  9. package/src/connectors/supabase.js +279 -0
  10. package/src/core/agents.js +32 -2
  11. package/src/core/cli.js +30 -5
  12. package/src/core/permissions.js +12 -1
  13. package/src/core/plugin-cmd.js +1 -0
  14. package/src/core/sandbox.js +400 -0
  15. package/src/core/session-store.js +245 -167
  16. package/src/core/session.js +157 -11
  17. package/src/providers/anthropic.js +10 -5
  18. package/src/providers/openai-compat.js +12 -4
  19. package/src/tools/browser.js +204 -0
  20. package/src/tools/index.js +35 -1
  21. package/src/tui/App.tsx +103 -8
  22. package/src/tui/components/BreadcrumbBar.tsx +10 -9
  23. package/src/tui/components/ChatArea.tsx +3 -3
  24. package/src/tui/components/InputBar.tsx +22 -11
  25. package/src/tui/components/MdText.tsx +2 -1
  26. package/src/tui/components/Modals.tsx +191 -17
  27. package/src/tui/components/PermissionPopup.tsx +15 -12
  28. package/src/tui/components/Sidebar.tsx +6 -2
  29. package/src/tui/components/SplashScreen.tsx +17 -24
  30. package/src/tui/components/SubagentPanel.tsx +1 -1
  31. package/src/tui/components/ToastOverlay.tsx +23 -12
  32. package/src/tui/store.ts +38 -35
  33. package/src/tui/theme.ts +45 -30
  34. package/src/tui/tool-display.ts +13 -2
  35. package/docs/web.md +0 -205
  36. package/src/web/attach.js +0 -242
  37. package/src/web/graph-view.html +0 -262
  38. package/src/web/index.html +0 -824
  39. 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 ? "\u25B6 " : " ") + fit(o, 40)}
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 ? "\u25B6 " : " ") + "Allow"}
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 ? "\u25B6 " : " ") + "Always allow"}
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 ? "\u25B6 " : " ") + "Deny"}
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 ? "\u25B6 " : " ") + "Allow all commands in this session"}
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 ? "\u25B6 " : " ") + "Type your answer\u2026"}
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 = "#ff5555";
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 fs from "fs";
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, i) => (
14
+ {LOOM_LOGO.map((line) => (
28
15
  <text fg={ui.primary}>{line}</text>
29
16
  ))}
30
17
  </box>
31
18
 
32
- <box marginTop={2} flexDirection="row">
33
- <text fg={ui.primary}>{"Build"}</text>
34
- <text fg={ui.fgDim}>{" "}</text>
35
- <text fg={ui.fg}>{providerName()}</text>
36
- <text fg={ui.fgDim}>{" "}</text>
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.fgDim}>{" "}</text>
39
- <text fg={providerKeyOk() ? "green" : "yellow"}>{providerKeyOk() ? "connected" : "no key"}</text>
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}>{LOOM_VERSION}</text>
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
- <box
20
- border borderStyle="rounded"
21
- borderColor={t.kind === "error" ? ui.error : t.kind === "ok" ? ui.success : ui.primary}
22
- paddingX={1} paddingY={0} marginTop={0}
23
- backgroundColor={ui.bgPanel}
24
- >
25
- <text fg={t.kind === "error" ? ui.error : t.kind === "ok" ? ui.success : ui.fg}>
26
- {" " + t.text}
27
- </text>
28
- </box>
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
- out.push({ label: m.name, value: m.id, sub: m.id, provider: r.provider, tags: m.tags, recent: true });
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, sub: m.id, provider: p, tags: m.tags });
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 dialog" },
499
- { cmd: "agents", desc: "List agents — primaries + subagents (@mention or task tool)" },
500
- { cmd: "build", desc: "Build mode — full agent tools (Tab cycles)" },
501
- { cmd: "plan", desc: "Plan mode — read-only analysis, no file changes" },
502
- { cmd: "chat", desc: "Chat mode — conversation only, no tools" },
503
- { cmd: "connect", desc: "Add/connect a provider", args: "[provider]" },
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", args: "[provider] [url]" },
506
- { cmd: "model", desc: "Pick the active model", args: "[model-id]" },
507
- { cmd: "models", desc: "Open the model picker (grouped by provider)" },
508
- { cmd: "providers", desc: "List supported providers" },
509
- { cmd: "status", desc: "Show connection status" },
510
- { cmd: "usage", desc: "Show token usage and billing" },
511
- { cmd: "budget", desc: "Budget level: free | cheap | best | auto", args: "[level]" },
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 project to an earlier state" },
515
- { cmd: "settings", desc: "Toggle details/thinking display" },
516
- { cmd: "sessions", desc: "Browse saved sessions (jump to one)" },
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: "View the memory graph (nodes + links)" },
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 (hosting & cloud services)", args: "add <name> <cmd> | remove | toggle" },
524
- { cmd: "permissions", desc: "Show saved permission rules", args: "| reset | auto" },
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: "View subagent runs (live + history, cancel, details)" },
529
- { cmd: "context", desc: "Show ~token breakdown of the current context window" },
530
- { cmd: "think", desc: "Thinking budget", args: "off|low|medium|high" },
531
- { cmd: "approve", desc: "Approve the plan & switch to Build (Plan mode)" },
532
- { cmd: "tasks", desc: "List background tasks (bash background:true)" },
533
- { cmd: "rewind", desc: "Pick a restore point and rewind files" },
534
- { cmd: "share", desc: "Export this chat as a self-contained HTML file" },
535
- { cmd: "worktree", desc: "New git worktree + branch for parallel work", args: "<name>" },
536
- { cmd: "style", desc: "Output style preset or free text; empty clears" },
537
- { cmd: "vim", desc: "Toggle vim modal editing (Esc = NORMAL)" },
538
- { cmd: "remember", desc: "Save a fact to project LOOM.md (or type # fact)", args: "<fact>" },
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
- bg: "#191817",
27
- bgMsg: "#131211",
28
- bgPanel: "#211f1e",
29
- bgPanelAlt: "#2a2725",
30
- bgInput: "#25221f",
31
- bgHover: "#322e2a",
32
- border: "#4a4440",
33
- borderFocus: "#e07856",
34
- fg: "#f0ebe4",
35
- fgDim: "#a89e93",
36
- fgMuted: "#6e655c",
37
- primary: "#e07856",
38
- primarySoft: "#f4a27f",
39
- secondary: "#b8d0a8",
40
- accent: "#e8b06f",
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: "#e06c5a",
46
+ error: "#e06858",
44
47
  thinking: "#d9a35f",
45
48
  syntax: {
46
- kw: "#e07856",
49
+ kw: "#e8845c",
47
50
  str: "#a3c084",
48
- com: "#6e655c",
51
+ com: "#6b6252",
49
52
  num: "#e0b356",
50
- call: "#e8b06f",
51
- plain: "#f0ebe4",
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: "#faf7f2",
58
- bgMsg: "#efe9df",
59
- bgPanel: "#f3eee6",
60
- bgPanelAlt: "#eae3d8",
60
+ bg: "#faf7f0",
61
+ bgMsg: "#efe8dc",
62
+ bgPanel: "#f2ece1",
63
+ bgPanelAlt: "#e8e0d1",
61
64
  bgInput: "#ffffff",
62
- bgHover: "#e0d6c8",
63
- border: "#c9bfae",
65
+ bgHover: "#ddd3c2",
66
+ border: "#c4b9a5",
64
67
  borderFocus: "#c2593a",
65
68
  fg: "#2b2620",
66
69
  fgDim: "#6f675c",
67
- fgMuted: "#9a9082",
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: "#9a9082",
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
- export const VERSION = "1.2.0";
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
  " ██║ ██╔═══██╗██╔═══██╗████╗ ████║ ██╔════╝██╔═══██╗██╔══██╗██╔════╝",
@@ -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: "\u25C8",
90
+ icon: "◈",
91
91
  pending: "Searching web...",
92
- label: (i) => "WebSearch \u0022" + String(i?.query || "") + "\u0022",
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: {