loom-agent 1.2.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.
Files changed (79) hide show
  1. package/.env.example +25 -0
  2. package/CHANGELOG.md +402 -0
  3. package/LICENSE +21 -0
  4. package/LOOM.md +235 -0
  5. package/README.md +433 -0
  6. package/bin/loom-tui.js +43 -0
  7. package/bin/loom.js +44 -0
  8. package/docs/acp.md +151 -0
  9. package/docs/web.md +205 -0
  10. package/package.json +97 -0
  11. package/scripts/acp-smoke.js +146 -0
  12. package/src/acp/acp-server.js +287 -0
  13. package/src/config/provider-cmd.js +37 -0
  14. package/src/config/settings.js +164 -0
  15. package/src/core/agents.js +361 -0
  16. package/src/core/background-tasks.js +103 -0
  17. package/src/core/cli.js +579 -0
  18. package/src/core/custom-commands.js +70 -0
  19. package/src/core/errors.js +29 -0
  20. package/src/core/events.js +24 -0
  21. package/src/core/file-diffs.js +282 -0
  22. package/src/core/format.js +206 -0
  23. package/src/core/graph.js +257 -0
  24. package/src/core/hooks.js +82 -0
  25. package/src/core/lsp.js +385 -0
  26. package/src/core/memory.js +87 -0
  27. package/src/core/model-router.js +87 -0
  28. package/src/core/permissions.js +327 -0
  29. package/src/core/platform.js +33 -0
  30. package/src/core/plugin-cmd.js +380 -0
  31. package/src/core/restore.js +207 -0
  32. package/src/core/session-store.js +167 -0
  33. package/src/core/session.js +910 -0
  34. package/src/core/subagent-log.js +134 -0
  35. package/src/core/tokens.js +31 -0
  36. package/src/core/update.js +6 -0
  37. package/src/core/usage.js +166 -0
  38. package/src/index.js +41 -0
  39. package/src/mcp/mcp-client.js +201 -0
  40. package/src/mcp/mcp-manager.js +193 -0
  41. package/src/providers/anthropic.js +243 -0
  42. package/src/providers/google.js +29 -0
  43. package/src/providers/index.js +175 -0
  44. package/src/providers/local.js +27 -0
  45. package/src/providers/nvidia.js +85 -0
  46. package/src/providers/openai-compat.js +269 -0
  47. package/src/providers/openai.js +35 -0
  48. package/src/providers/openrouter.js +43 -0
  49. package/src/providers/registry.js +196 -0
  50. package/src/providers/tokenrouter.js +19 -0
  51. package/src/skills/skill-matcher.js +133 -0
  52. package/src/skills/skills-manager.js +213 -0
  53. package/src/tools/index.js +543 -0
  54. package/src/tui/App.tsx +1578 -0
  55. package/src/tui/components/BreadcrumbBar.tsx +34 -0
  56. package/src/tui/components/ChatArea.tsx +518 -0
  57. package/src/tui/components/InputBar.tsx +354 -0
  58. package/src/tui/components/MdText.tsx +105 -0
  59. package/src/tui/components/Modals.tsx +851 -0
  60. package/src/tui/components/PermissionPopup.tsx +264 -0
  61. package/src/tui/components/Sidebar.tsx +182 -0
  62. package/src/tui/components/SplashScreen.tsx +51 -0
  63. package/src/tui/components/SubagentPanel.tsx +217 -0
  64. package/src/tui/components/ToastOverlay.tsx +34 -0
  65. package/src/tui/keybinds.ts +318 -0
  66. package/src/tui/mcp-presets.ts +189 -0
  67. package/src/tui/md-render.ts +228 -0
  68. package/src/tui/store.ts +714 -0
  69. package/src/tui/suite-home.ts +20 -0
  70. package/src/tui/theme.ts +313 -0
  71. package/src/tui/themes.generated.ts +968 -0
  72. package/src/tui/tool-display.ts +176 -0
  73. package/src/tui/toolname.ts +60 -0
  74. package/src/tui/tui-config.ts +28 -0
  75. package/src/tui-open.tsx +51 -0
  76. package/src/web/attach.js +242 -0
  77. package/src/web/graph-view.html +262 -0
  78. package/src/web/index.html +824 -0
  79. package/src/web/web-server.js +470 -0
@@ -0,0 +1,20 @@
1
+ // Isolate the entire interactive suite from the developer's real home
2
+ // configuration: create a throwaway HOME/USERPROFILE BEFORE the App and its
3
+ // theme/store/keybind modules initialize, so tui.json, keybinds.json, and
4
+ // session prefs are read and written inside the temp dir only.
5
+ import fs from "fs";
6
+ import path from "path";
7
+ import os from "os";
8
+
9
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "loom-suite-home-"));
10
+ process.env.HOME = dir;
11
+ process.env.USERPROFILE = dir;
12
+ process.env.LOOM_CONFIG_DIR = path.join(dir, ".loom");
13
+ fs.mkdirSync(process.env.LOOM_CONFIG_DIR, { recursive: true });
14
+ // Seed one harmless MCP server: the /mcp popup test toggles servers[0] and
15
+ // the /sessions picker renders the MCP status column — both need a server.
16
+ fs.writeFileSync(path.join(process.env.LOOM_CONFIG_DIR, "mcp.json"), JSON.stringify({
17
+ servers: { "suite-local": { command: "echo", args: ["suite-mcp-ok"], enabled: true } },
18
+ }, null, 2));
19
+
20
+ (globalThis as any).__LOOM_SUITE_HOME = dir;
@@ -0,0 +1,313 @@
1
+ // Loom Code TUI — color themes + palette access.
2
+ // palette() returns a live Proxy: each property is read from the currently
3
+ // selected theme at render time, so switching themes re-renders instantly.
4
+ import { createSignal } from "solid-js";
5
+ import { loadTuiJson, saveTuiJson } from "./tui-config.ts";
6
+
7
+ export type Palette = {
8
+ bg: string; bgMsg: string; bgPanel: string; bgPanelAlt: string; bgInput: string; bgHover: string;
9
+ border: string; borderFocus: string;
10
+ fg: string; fgDim: string; fgMuted: string;
11
+ primary: string; // brand accent
12
+ primarySoft: string;
13
+ secondary: string; // user accent
14
+ accent: string; // tool activity
15
+ success: string; warning: string; error: string;
16
+ thinking: string;
17
+ // Syntax colors for markdown code blocks — each theme ships real VS Code
18
+ // TextMate colors (keyword/string/comment/number/call/base) via fetch-themes.
19
+ syntax: { kw: string; str: string; com: string; num: string; call: string; plain: string };
20
+ };
21
+
22
+ export type Theme = { id: string; label: string; desc: string; palette: Palette };
23
+
24
+ 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",
41
+ success: "#a3c084",
42
+ warning: "#e0b356",
43
+ error: "#e06c5a",
44
+ thinking: "#d9a35f",
45
+ syntax: {
46
+ kw: "#e07856",
47
+ str: "#a3c084",
48
+ com: "#6e655c",
49
+ num: "#e0b356",
50
+ call: "#e8b06f",
51
+ plain: "#f0ebe4",
52
+ },
53
+ };
54
+
55
+ const LIGHT: Palette = {
56
+ // Paper light — warm parchment with terracotta
57
+ bg: "#faf7f2",
58
+ bgMsg: "#efe9df",
59
+ bgPanel: "#f3eee6",
60
+ bgPanelAlt: "#eae3d8",
61
+ bgInput: "#ffffff",
62
+ bgHover: "#e0d6c8",
63
+ border: "#c9bfae",
64
+ borderFocus: "#c2593a",
65
+ fg: "#2b2620",
66
+ fgDim: "#6f675c",
67
+ fgMuted: "#9a9082",
68
+ primary: "#c2593a",
69
+ primarySoft: "#a34a2f",
70
+ secondary: "#5c7a4a",
71
+ accent: "#b0742f",
72
+ success: "#5f8a3d",
73
+ warning: "#a97a1a",
74
+ error: "#b33c2e",
75
+ thinking: "#a5682a",
76
+ syntax: {
77
+ kw: "#c2593a",
78
+ str: "#5f8a3d",
79
+ com: "#9a9082",
80
+ num: "#a97a1a",
81
+ call: "#b0742f",
82
+ plain: "#2b2620",
83
+ },
84
+ };
85
+
86
+ const OCEAN: Palette = {
87
+ // Deep navy — cyan accents
88
+ bg: "#0e1a26",
89
+ bgMsg: "#0a141d",
90
+ bgPanel: "#132433",
91
+ bgPanelAlt: "#182c3d",
92
+ bgInput: "#16293a",
93
+ bgHover: "#1e3346",
94
+ border: "#2f4a61",
95
+ borderFocus: "#4fc3f7",
96
+ fg: "#e3eef6",
97
+ fgDim: "#9db4c6",
98
+ fgMuted: "#6d8599",
99
+ primary: "#4fc3f7",
100
+ primarySoft: "#81d4fa",
101
+ secondary: "#80cbc4",
102
+ accent: "#ffb74d",
103
+ success: "#81c784",
104
+ warning: "#ffd54f",
105
+ error: "#e57373",
106
+ thinking: "#ffb74d",
107
+ syntax: {
108
+ kw: "#4fc3f7",
109
+ str: "#81c784",
110
+ com: "#6d8599",
111
+ num: "#ffd54f",
112
+ call: "#ffb74d",
113
+ plain: "#e3eef6",
114
+ },
115
+ };
116
+
117
+ const FOREST: Palette = {
118
+ // Dark evergreen — sage accents
119
+ bg: "#0f1a12",
120
+ bgMsg: "#0b130d",
121
+ bgPanel: "#152319",
122
+ bgPanelAlt: "#1b2d20",
123
+ bgInput: "#182a1e",
124
+ bgHover: "#223726",
125
+ border: "#33503a",
126
+ borderFocus: "#7bd88f",
127
+ fg: "#e8f2e9",
128
+ fgDim: "#9db3a0",
129
+ fgMuted: "#6b8270",
130
+ primary: "#7bd88f",
131
+ primarySoft: "#a3e5b2",
132
+ secondary: "#c6d8a8",
133
+ accent: "#e0c568",
134
+ success: "#9ccc65",
135
+ warning: "#d4c34f",
136
+ error: "#e08070",
137
+ thinking: "#c9a95c",
138
+ syntax: {
139
+ kw: "#7bd88f",
140
+ str: "#9ccc65",
141
+ com: "#6b8270",
142
+ num: "#d4c34f",
143
+ call: "#e0c568",
144
+ plain: "#e8f2e9",
145
+ },
146
+ };
147
+
148
+ const MIDNIGHT: Palette = {
149
+ // Near-black violet — lavender accents
150
+ bg: "#131019",
151
+ bgMsg: "#0f0c15",
152
+ bgPanel: "#1a1625",
153
+ bgPanelAlt: "#211c30",
154
+ bgInput: "#1d1830",
155
+ bgHover: "#2a2340",
156
+ border: "#3d3356",
157
+ borderFocus: "#a78bfa",
158
+ fg: "#ede9f5",
159
+ fgDim: "#a59bbd",
160
+ fgMuted: "#6f6690",
161
+ primary: "#a78bfa",
162
+ primarySoft: "#c4b5fd",
163
+ secondary: "#94d2bd",
164
+ accent: "#f6ad55",
165
+ success: "#9ae6b4",
166
+ warning: "#fde68a",
167
+ error: "#f87171",
168
+ thinking: "#f0abfc",
169
+ syntax: {
170
+ kw: "#a78bfa",
171
+ str: "#9ae6b4",
172
+ com: "#6f6690",
173
+ num: "#fde68a",
174
+ call: "#f6ad55",
175
+ plain: "#ede9f5",
176
+ },
177
+ };
178
+
179
+ const MONO: Palette = {
180
+ // Pure monochrome — greys only
181
+ bg: "#111111",
182
+ bgMsg: "#0d0d0d",
183
+ bgPanel: "#1a1a1a",
184
+ bgPanelAlt: "#242424",
185
+ bgInput: "#1c1c1c",
186
+ bgHover: "#2b2b2b",
187
+ border: "#3c3c3c",
188
+ borderFocus: "#e8e8e8",
189
+ fg: "#e8e8e8",
190
+ fgDim: "#9c9c9c",
191
+ fgMuted: "#666666",
192
+ primary: "#e8e8e8",
193
+ primarySoft: "#c4c4c4",
194
+ secondary: "#b8b8b8",
195
+ accent: "#cccccc",
196
+ success: "#9f9f9f",
197
+ warning: "#bdbdbd",
198
+ error: "#e0e0e0",
199
+ thinking: "#b0b0b0",
200
+ syntax: {
201
+ kw: "#e8e8e8",
202
+ str: "#b8b8b8",
203
+ com: "#666666",
204
+ num: "#bdbdbd",
205
+ call: "#cccccc",
206
+ plain: "#e8e8e8",
207
+ },
208
+ };
209
+
210
+ import { GENERATED_THEMES } from "./themes.generated.ts";
211
+
212
+ export const THEMES: Record<string, Theme> = {
213
+ loom: { id: "loom", label: "Loom Dark", desc: "Warm charcoal + terracotta (default)", palette: LOOM_DARK },
214
+ light: { id: "light", label: "Light", desc: "Parchment light + terracotta", palette: LIGHT },
215
+ ocean: { id: "ocean", label: "Ocean", desc: "Deep navy + cyan", palette: OCEAN },
216
+ forest: { id: "forest", label: "Forest", desc: "Dark evergreen + sage", palette: FOREST },
217
+ midnight: { id: "midnight", label: "Midnight", desc: "Violet-black + lavender", palette: MIDNIGHT },
218
+ mono: { id: "mono", label: "Mono", desc: "Pure greyscale", palette: MONO },
219
+ };
220
+
221
+ // Safety net: shiki-sourced themes sometimes carry transparent or
222
+ // background-identical border colors, which makes every UI grid line vanish.
223
+ // Accept a border only when it clears a real contrast bar against `bg`;
224
+ // otherwise derive one (lighten dark themes, darken light themes).
225
+ function visibleBorder(bg: string, border: string): string {
226
+ const hex = (h: string) => /^#?([0-9a-f]{6})/i.exec(String(h).trim())?.[1];
227
+ const lum = (h: string) => {
228
+ const m = hex(h);
229
+ if (!m) return 0;
230
+ const n = parseInt(m, 16);
231
+ return 0.299 * (n >> 16 & 255) + 0.587 * (n >> 8 & 255) + 0.114 * (n & 255);
232
+ };
233
+ const base = hex(bg) || "1e1e1e";
234
+ const raw = hex(border);
235
+ if (raw && Math.abs(lum("#" + raw) - lum("#" + base)) >= 28) return border;
236
+ const L = lum("#" + base);
237
+ // Channel shifts run red-green-blue order; a negative factor SUBTRACTS the
238
+ // requested proportion of each channel, and every channel clamps to 0-255
239
+ // so the derived border hex never overflows/underflows.
240
+ const mix = (f: number) => {
241
+ const hex2 = (v: number) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, "0");
242
+ const ch = (s: number) => {
243
+ const c = parseInt(base, 16) >> s & 255;
244
+ return f > 0 ? hex2(c + (255 - c) * f) : hex2(c - c * Math.abs(f));
245
+ };
246
+ return "#" + ch(16) + ch(8) + ch(0);
247
+ };
248
+ return L < 128 ? mix(0.38) : mix(-0.35);
249
+ }
250
+
251
+ // Stitch in the generated (shiki-sourced) themes after the local defaults.
252
+ // Ids from the registry win on collision — the six above always take priority.
253
+ // Each generated palette is validated on a COPY: missing fields (including
254
+ // syntax) are filled from LOOM_DARK and the border is derived on the copy, so
255
+ // the imported generated object is never mutated.
256
+ for (const t of GENERATED_THEMES) {
257
+ if (!THEMES[t.id]) {
258
+ const src: any = (t as Theme).palette || {};
259
+ const p: any = Object.assign({}, LOOM_DARK, src);
260
+ if (!p.syntax || typeof p.syntax !== "object") p.syntax = {};
261
+ p.syntax = Object.assign({}, LOOM_DARK.syntax, p.syntax);
262
+ p.border = visibleBorder(p.bg, p.border);
263
+ THEMES[t.id] = { id: t.id, label: t.label, desc: t.desc, palette: p };
264
+ }
265
+ }
266
+
267
+ function loadSavedTheme(): string {
268
+ const data = loadTuiJson();
269
+ if (typeof data.theme === "string" && THEMES[data.theme]) return data.theme;
270
+ return "loom";
271
+ }
272
+
273
+ export function saveThemePref(id: string) {
274
+ saveTuiJson({ theme: id });
275
+ }
276
+
277
+ const [themeId, setThemeId] = createSignal(loadSavedTheme());
278
+
279
+ export function setTheme(id: string) {
280
+ if (!THEMES[id]) return false;
281
+ setThemeId(id);
282
+ saveThemePref(id);
283
+ return true;
284
+ }
285
+
286
+ export function themeName() {
287
+ return themeId();
288
+ }
289
+
290
+ export function themeOptions(): { id: string; label: string; desc: string }[] {
291
+ return Object.values(THEMES).map(t => ({ id: t.id, label: t.label, desc: t.desc }));
292
+ }
293
+
294
+ export function palette(_name?: string): Palette {
295
+ return new Proxy(LOOM_DARK, {
296
+ get(_target, prop) {
297
+ const cur = THEMES[themeId()];
298
+ const p = (cur && cur.palette) || LOOM_DARK;
299
+ return (p as any)[prop as string];
300
+ },
301
+ }) as Palette;
302
+ }
303
+
304
+ export const VERSION = "1.2.0";
305
+
306
+ export const LOOM_LOGO = [
307
+ " ██╗ ██████╗ ██████╗ ███╗ ███╗ ██████╗ ██████╗ ██████╗ ███████╗",
308
+ " ██║ ██╔═══██╗██╔═══██╗████╗ ████║ ██╔════╝██╔═══██╗██╔══██╗██╔════╝",
309
+ " ██║ ██║ ██║██║ ██║██╔████╔██║ ██║ ██║ ██║██║ ██║█████╗ ",
310
+ " ██║ ██║ ██║██║ ██║██║╚██╔╝██║ ██║ ██║ ██║██║ ██║██╔══╝ ",
311
+ " ███████╗╚██████╔╝╚██████╔╝██║ ╚═╝ ██║ ╚██████╗██████╔╝██████╔╝███████╗",
312
+ " ╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝╚═════╝ ╚═════╝ ╚══════╝",
313
+ ];