loom-agent 1.2.33 → 1.2.37
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 +118 -0
- package/README.md +14 -30
- package/bin/loom-bun.js +95 -67
- package/bin/loom-tui.js +21 -1
- package/bin/loom.js +0 -10
- package/package.json +14 -9
- package/scripts/postinstall.js +57 -5
- 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 +56 -7
- package/src/core/permissions.js +12 -1
- package/src/core/plugin-cmd.js +1 -0
- package/src/core/session.js +87 -6
- package/src/mcp/mcp-client.js +42 -2
- package/src/skills/skills-manager.js +25 -2
- package/src/tools/browser.js +204 -0
- package/src/tools/index.js +116 -5
- package/src/tui/App.tsx +19 -4
- 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 +31 -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 +4 -2
- 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 -470
|
@@ -35,12 +35,16 @@ export function ModalFrame(props: { title: string; subtitle?: string; children:
|
|
|
35
35
|
// Floating panel: centered over the terminal but NOT a full-screen takeover —
|
|
36
36
|
// no backdrop fill, so the chat and any running agent stay visible behind it
|
|
37
37
|
// ("same window, hovering"). Keys still belong to the modal while open.
|
|
38
|
+
// Border is subtle (ui.border) — the title carries the accent, no divider
|
|
39
|
+
// rules inside (they read as a grid).
|
|
38
40
|
return (
|
|
39
41
|
<box position="absolute" top={0} left={0} right={0} bottom={0}
|
|
40
42
|
alignItems="center" justifyContent="center" flexDirection="column">
|
|
41
|
-
<box border borderStyle="rounded" borderColor={ui.
|
|
42
|
-
paddingX={3} paddingY={
|
|
43
|
-
<
|
|
43
|
+
<box border borderStyle="rounded" borderColor={ui.border} backgroundColor={ui.bgPanel}
|
|
44
|
+
paddingX={3} paddingY={1} flexDirection="column" minWidth={52} maxWidth={72}>
|
|
45
|
+
<box flexDirection="row" alignItems="center">
|
|
46
|
+
<text fg={ui.primary}>{"◆ " + props.title}</text>
|
|
47
|
+
</box>
|
|
44
48
|
{props.subtitle ? <text fg={ui.fgMuted} marginTop={0}>{props.subtitle}</text> : null}
|
|
45
49
|
<box flexDirection="column" marginTop={1}>{props.children}</box>
|
|
46
50
|
{props.footer ? <text fg={ui.fgMuted} marginTop={1}>{props.footer}</text> : null}
|
|
@@ -298,6 +302,7 @@ export function SelectModal(props: {
|
|
|
298
302
|
return (
|
|
299
303
|
<box
|
|
300
304
|
flexDirection="row" paddingLeft={2}
|
|
305
|
+
backgroundColor={active ? ui.bgPanelAlt : undefined}
|
|
301
306
|
// Hover moves the selection (live theme preview via onPreview) —
|
|
302
307
|
// but only for genuine pointer movement, never while a
|
|
303
308
|
// keyboard/wheel scroll is settling (see hoverLockUntil).
|
|
@@ -306,10 +311,10 @@ export function SelectModal(props: {
|
|
|
306
311
|
onMouseUp={() => clickRow(abs)}
|
|
307
312
|
>
|
|
308
313
|
<text fg={active ? ui.primary : ui.fgDim}>
|
|
309
|
-
{(active ? "
|
|
314
|
+
{(active ? " ▸ " : " ") + fit(opt.label, LABEL_MAX)}
|
|
310
315
|
</text>
|
|
311
|
-
{opt.recent ? <text fg={ui.fgMuted}> {"
|
|
312
|
-
{opt.sub ? <text fg={ui.fgDim}> {"(" + fit(opt.sub, SUB_MAX) + ")"}</text> : null}
|
|
316
|
+
{opt.recent ? <text fg={ui.fgMuted}> {" ✓ recent"}</text> : null}
|
|
317
|
+
{opt.sub ? <text fg={active ? ui.fg : ui.fgDim}> {"(" + fit(opt.sub, SUB_MAX) + ")"}</text> : null}
|
|
313
318
|
</box>
|
|
314
319
|
);
|
|
315
320
|
})}
|
|
@@ -615,25 +620,31 @@ function ServerBrowser(props: { kind: "mcp" | "connector" }) {
|
|
|
615
620
|
// capped so the command column keeps ~28 chars. Without an explicit width the
|
|
616
621
|
// name text yoga-shrinks to zero width when the command is long.
|
|
617
622
|
const nameW = () => Math.min(30, Math.max(18, ...servers().map(s => String(s.name).length + 9)));
|
|
618
|
-
// Command column budget: modal inner ~
|
|
619
|
-
//
|
|
620
|
-
|
|
623
|
+
// Command column budget: modal inner ~62 chars (64 minus the row's left
|
|
624
|
+
// padding) minus name column — any longer and the text pixel-punches past
|
|
625
|
+
// the right border.
|
|
626
|
+
const cmdW = () => Math.max(22, 60 - nameW());
|
|
621
627
|
|
|
622
628
|
return (
|
|
623
|
-
<ModalFrame title={title} subtitle={"Enter toggles a server
|
|
629
|
+
<ModalFrame title={title} subtitle={"Enter toggles a server" + rangeSub()} footer={kbNav().submit + " toggle · A add · " + kbNav().cancel + " close"}>
|
|
624
630
|
<box onMouseScroll={scrollBy} flexDirection="column" flexShrink={0}>
|
|
625
631
|
{win().items.map((s, i) => {
|
|
626
632
|
const abs = win().start + i;
|
|
633
|
+
const active = abs === sel();
|
|
627
634
|
return (
|
|
628
635
|
<box
|
|
629
|
-
|
|
636
|
+
flexDirection="row" paddingY={0} height={1} flexShrink={0}
|
|
637
|
+
paddingLeft={1}
|
|
638
|
+
backgroundColor={active ? ui.bgPanelAlt : undefined}
|
|
630
639
|
onMouseDown={() => setSel(abs)}
|
|
631
640
|
onMouseUp={() => { if (abs === sel()) { const r = toggleServer(s.name); if (!r || !r.error) refresh(); } }}
|
|
632
641
|
>
|
|
633
|
-
<text fg={
|
|
634
|
-
{" " + (s.enabled ? "
|
|
642
|
+
<text fg={active ? ui.primary : ui.fgDim} width={nameW()} height={1} flexShrink={0}>
|
|
643
|
+
{(active ? "▸ " : " ") + "[" + (s.enabled ? "on" : "off") + "] " + s.name}
|
|
644
|
+
</text>
|
|
645
|
+
<text fg={s.enabled ? ui.success : ui.fgMuted} height={1} flexGrow={1}>
|
|
646
|
+
{mcpFit(s.command + " " + (s.args || []).join(" "), cmdW())}
|
|
635
647
|
</text>
|
|
636
|
-
<text fg={ui.fgMuted} height={1} flexGrow={1}>{"\u2192 " + mcpFit(s.command + " " + (s.args || []).join(" "), cmdW())}</text>
|
|
637
648
|
</box>
|
|
638
649
|
);
|
|
639
650
|
})}
|
|
@@ -705,15 +716,18 @@ export function PaletteModal(props: { onPick: (cmd: string) => void }) {
|
|
|
705
716
|
<box onMouseScroll={scrollBy}>
|
|
706
717
|
{win().items.map((it, i) => {
|
|
707
718
|
const abs = win().start + i;
|
|
719
|
+
const active = abs === sel();
|
|
708
720
|
return (
|
|
709
721
|
<box
|
|
710
|
-
|
|
722
|
+
flexDirection="row" paddingY={0}
|
|
723
|
+
backgroundColor={active ? ui.bgPanelAlt : undefined}
|
|
711
724
|
onMouseDown={() => setSel(abs)}
|
|
712
725
|
onMouseUp={() => clickRow(abs)}
|
|
713
726
|
>
|
|
714
|
-
<text fg={
|
|
715
|
-
{
|
|
727
|
+
<text fg={active ? ui.primary : ui.fgDim}>
|
|
728
|
+
{active ? "▸ " : " "}{it.label.split("/")[1] || it.label}
|
|
716
729
|
</text>
|
|
730
|
+
{it.sub ? <text fg={active ? ui.fg : ui.fgMuted}>{" " + String(it.sub).slice(0, 36)}</text> : null}
|
|
717
731
|
</box>
|
|
718
732
|
);
|
|
719
733
|
})}
|
|
@@ -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__" });
|
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: {
|