loom-agent 1.2.9 → 1.2.12
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/README.md +3 -3
- package/bin/loom-bun.js +31 -0
- package/bin/loom-tui.js +20 -49
- package/package.json +18 -10
- package/src/core/cli.js +14 -1
- package/src/core/session.js +6 -3
- package/src/tui/App.tsx +21 -7
- package/src/tui/components/ChatArea.tsx +8 -7
- package/src/tui/components/InputBar.tsx +2 -3
- package/src/tui/components/SplashScreen.tsx +14 -1
- package/src/tui/components/SubagentPanel.tsx +1 -1
- package/src/tui-entry.tsx +10 -0
- package/src/tui-open.tsx +10 -0
- package/src/tui-preload.js +125 -51
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Loom Code
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/loom-agent)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](#)
|
|
6
6
|
|
|
7
7
|
An AI-powered coding agent for the terminal with multi-provider support and a full terminal UI.
|
|
8
8
|
|
package/bin/loom-bun.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// npm invokes bin targets through Node on Windows, so re-launch under Bun
|
|
3
|
+
// when this file was not started by Bun itself.
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { spawnSync } = require("child_process");
|
|
6
|
+
|
|
7
|
+
if (!(typeof Bun !== "undefined" && process.versions.bun)) {
|
|
8
|
+
const result = spawnSync(process.platform === "win32" ? "bun.exe" : "bun", ["--conditions=browser", __filename, ...process.argv.slice(2)], {
|
|
9
|
+
stdio: "inherit",
|
|
10
|
+
cwd: process.cwd(),
|
|
11
|
+
env: process.env,
|
|
12
|
+
windowsHide: false,
|
|
13
|
+
});
|
|
14
|
+
if (result.error) {
|
|
15
|
+
console.error("[loom] Bun is required for the full CLI. Install it from https://bun.sh/");
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
process.exit(result.status == null ? 1 : result.status);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
process.env.BUN_CONFIG = path.join(__dirname, "..", "bunfig.toml");
|
|
22
|
+
process.title = "loom-code";
|
|
23
|
+
(async () => {
|
|
24
|
+
try {
|
|
25
|
+
const { main } = require("../src/core/cli.js");
|
|
26
|
+
await main();
|
|
27
|
+
} catch (err) {
|
|
28
|
+
console.error(err && err.message ? err.message : String(err));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
})();
|
package/bin/loom-tui.js
CHANGED
|
@@ -1,52 +1,23 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
const { spawnSync } = require(
|
|
6
|
-
const path = require('path');
|
|
7
|
-
const fs = require('fs');
|
|
8
|
-
const os = require('os');
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// npm invokes bin targets through Node on Windows, so re-launch under Bun
|
|
3
|
+
// when this file was not started by Bun itself.
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { spawnSync } = require("child_process");
|
|
9
6
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
const out = require('child_process').execSync('bun --version', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true }).trim();
|
|
24
|
-
if (out) return 'bun';
|
|
25
|
-
} catch {}
|
|
26
|
-
return null;
|
|
7
|
+
if (!(typeof Bun !== "undefined" && process.versions.bun)) {
|
|
8
|
+
const result = spawnSync(process.platform === "win32" ? "bun.exe" : "bun", ["--conditions=browser", __filename, ...process.argv.slice(2)], {
|
|
9
|
+
stdio: "inherit",
|
|
10
|
+
cwd: process.cwd(),
|
|
11
|
+
env: process.env,
|
|
12
|
+
windowsHide: false,
|
|
13
|
+
});
|
|
14
|
+
if (result.error) {
|
|
15
|
+
console.error("[loom] Bun is required for the TUI. Install it from https://bun.sh/");
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
process.exit(result.status == null ? 1 : result.status);
|
|
27
19
|
}
|
|
28
20
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
// is restored by the bootstrap shim before app code runs.
|
|
33
|
-
const pkgRoot = path.join(__dirname, '..');
|
|
34
|
-
process.env.LOOM_START_CWD = process.cwd();
|
|
35
|
-
process.env.LOOM_BIN_NAME = "loom";
|
|
36
|
-
const bun = findBun();
|
|
37
|
-
|
|
38
|
-
if (bun) {
|
|
39
|
-
// Pin Solid to its CLIENT build: from this spawn context bun can resolve
|
|
40
|
-
// solid-js under the "node" export condition (= SSR build, renders one
|
|
41
|
-
// static frame then ignores all signal updates -> frozen splash).
|
|
42
|
-
const result = spawnSync(bun, ['run', entry, ...process.argv.slice(2)], {
|
|
43
|
-
stdio: 'inherit',
|
|
44
|
-
cwd: pkgRoot,
|
|
45
|
-
env: process.env,
|
|
46
|
-
});
|
|
47
|
-
process.exit(result.status ?? 0);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
console.log('Loom TUI requires bun (https://bun.sh) to run the TSX/JSX pipeline.');
|
|
51
|
-
console.log('Install bun first, then use: bun run ' + entry);
|
|
52
|
-
process.exit(1);
|
|
21
|
+
process.env.BUN_CONFIG = path.join(__dirname, "..", "bunfig.toml");
|
|
22
|
+
require("../src/tui-preload.js");
|
|
23
|
+
(async () => { await import("../src/tui-open.tsx"); })();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loom-agent",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.12",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/toshalkumbhar8979-design/loomcode.git"
|
|
@@ -9,10 +9,14 @@
|
|
|
9
9
|
"bugs": {
|
|
10
10
|
"url": "https://github.com/toshalkumbhar8979-design/loomcode/issues"
|
|
11
11
|
},
|
|
12
|
-
"description": "Loom Code
|
|
12
|
+
"description": "Loom Code  AI-powered coding agent for the terminal. Multi-provider support including NVIDIA. OpenTUI interface.",
|
|
13
13
|
"main": "src/index.js",
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public",
|
|
16
|
+
"tag": "latest"
|
|
17
|
+
},
|
|
14
18
|
"bin": {
|
|
15
|
-
"loom": "bin/loom.js",
|
|
19
|
+
"loom": "bin/loom-bun.js",
|
|
16
20
|
"loom-tui": "bin/loom-tui.js"
|
|
17
21
|
},
|
|
18
22
|
"scripts": {
|
|
@@ -23,13 +27,13 @@
|
|
|
23
27
|
"test:unit": "bun test src/core/agents.test.js src/core/hooks.test.js src/core/custom-commands.test.js src/core/background-tasks.test.js src/core/memory.test.js src/core/subagent-log.test.js src/core/session.test.js src/tools/index.test.js src/providers/providers.test.js src/providers/caching.test.js src/providers/caching-thinking.test.js src/providers/registry.test.js src/mcp/mcp-client.test.js src/mcp/mcp-manager.test.js src/core/session-store.test.js src/skills/skills-manager.test.js src/core/usage.test.js src/core/permissions.test.js src/core/format.test.js src/acp/acp-server.test.js src/web/web-server.test.js src/tui/keybinds.test.ts",
|
|
24
28
|
"lint:core": "node node_modules/typescript/bin/tsc -p tsconfig.core.json",
|
|
25
29
|
"smoke:acp": "node scripts/acp-smoke.js",
|
|
26
|
-
"tui": "
|
|
30
|
+
"tui": "bun bin/loom-tui.js",
|
|
27
31
|
"tui:win": "\"%USERPROFILE%\\..\\bun\\bin\\bun.exe\" run src/tui-open.tsx",
|
|
28
32
|
"prepublishOnly": "npm test",
|
|
29
33
|
"lint": "tsc --noEmit"
|
|
30
34
|
},
|
|
31
35
|
"dependencies": {
|
|
32
|
-
"@anthropic-ai/sdk": "^0.
|
|
36
|
+
"@anthropic-ai/sdk": "^0.120.0",
|
|
33
37
|
"@opentui/core": "^0.5.1",
|
|
34
38
|
"@opentui/solid": "^0.5.1",
|
|
35
39
|
"@shikijs/themes": "4.4.3",
|
|
@@ -41,23 +45,26 @@
|
|
|
41
45
|
"dayjs": "^1.11.0",
|
|
42
46
|
"diff": "^7.0.0",
|
|
43
47
|
"dotenv": "^16.4.0",
|
|
44
|
-
"glob": "^
|
|
48
|
+
"glob": "^13.0.6",
|
|
45
49
|
"inquirer": "^12.1.0",
|
|
46
|
-
"openai": "^
|
|
50
|
+
"openai": "^5.23.2",
|
|
47
51
|
"ora": "^8.0.0",
|
|
48
52
|
"shell-quote": "^1.8.1",
|
|
49
|
-
"solid-js": "
|
|
53
|
+
"solid-js": "1.9.12",
|
|
50
54
|
"strip-ansi": "^7.1.0",
|
|
51
55
|
"tiktoken": "^1.0.22",
|
|
52
56
|
"uuid": "^11.0.0",
|
|
53
57
|
"wrap-ansi": "^9.0.0"
|
|
54
58
|
},
|
|
55
59
|
"optionalDependencies": {
|
|
56
|
-
"@opentui/core-win32-x64": "^0.5.1",
|
|
57
60
|
"@opentui/core-darwin-arm64": "^0.5.1",
|
|
58
61
|
"@opentui/core-darwin-x64": "^0.5.1",
|
|
62
|
+
"@opentui/core-linux-arm64": "^0.5.1",
|
|
59
63
|
"@opentui/core-linux-x64": "^0.5.1",
|
|
60
|
-
"@opentui/core-
|
|
64
|
+
"@opentui/core-win32-x64": "^0.5.1"
|
|
65
|
+
},
|
|
66
|
+
"overrides": {
|
|
67
|
+
"glob": "^13.0.6"
|
|
61
68
|
},
|
|
62
69
|
"devDependencies": {
|
|
63
70
|
"@types/bun": "^1.2.0",
|
|
@@ -66,6 +73,7 @@
|
|
|
66
73
|
"files": [
|
|
67
74
|
"bunfig.toml",
|
|
68
75
|
"tsconfig.json",
|
|
76
|
+
"bin/loom-bun.js",
|
|
69
77
|
"bin/loom.js",
|
|
70
78
|
"bin/loom-tui.js",
|
|
71
79
|
"src/**/*.js",
|
package/src/core/cli.js
CHANGED
|
@@ -554,6 +554,18 @@ if (args.includes('--help') || args.includes('-h')) {
|
|
|
554
554
|
|
|
555
555
|
if ((args.includes('--tui') || (!args.includes('--basic') && process.stdin.isTTY))) {
|
|
556
556
|
const canRaw = process.stdin.isTTY && typeof process.stdin.setRawMode === 'function';
|
|
557
|
+
// Fast path: when this process is ALREADY bun (launched via bin/loom-bun.js),
|
|
558
|
+
// import the TUI in-process — no spawn, no second console-mode negotiation.
|
|
559
|
+
const underBun = typeof Bun !== 'undefined' && !!process.versions.bun;
|
|
560
|
+
if (underBun && canRaw) {
|
|
561
|
+
try {
|
|
562
|
+
const tuiModule = '../tui-open.tsx';
|
|
563
|
+
await import(tuiModule);
|
|
564
|
+
return;
|
|
565
|
+
} catch (err) {
|
|
566
|
+
console.error('[loom] TUI failed: ' + (err && err.message ? err.message : err));
|
|
567
|
+
}
|
|
568
|
+
}
|
|
557
569
|
if (canRaw) {
|
|
558
570
|
// Launch new OpenTUI TUI via bun
|
|
559
571
|
const bunPath = findBun();
|
|
@@ -566,10 +578,11 @@ if (args.includes('--help') || args.includes('-h')) {
|
|
|
566
578
|
const pkgRoot = path.join(__dirname, '..', '..');
|
|
567
579
|
process.env.LOOM_START_CWD = process.cwd();
|
|
568
580
|
process.env.LOOM_BIN_NAME = "loom";
|
|
581
|
+
process.env.BUN_CONFIG = path.join(pkgRoot, "bunfig.toml");
|
|
569
582
|
// --conditions=browser pins Solid to its client build (see
|
|
570
583
|
// bin/loom-tui.js) — without it the SSR build loads and the TUI
|
|
571
584
|
// renders one static frame then never updates.
|
|
572
|
-
const tuiArgs = [tuiEntry];
|
|
585
|
+
const tuiArgs = ["--conditions=browser", tuiEntry];
|
|
573
586
|
if (sessionId) tuiArgs.push('-s', sessionId);
|
|
574
587
|
if (autoMode) tuiArgs.push('--auto');
|
|
575
588
|
const prompt = promptArgs.join(' ');
|
package/src/core/session.js
CHANGED
|
@@ -100,9 +100,12 @@ class Session {
|
|
|
100
100
|
this.todos = [];
|
|
101
101
|
this.compactCount = 0;
|
|
102
102
|
this.lastCompact = null;
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
|
|
103
|
+
// Let the TUI render before starting MCP discovery. The first warm-up can
|
|
104
|
+
// launch npx and download packages, so starting it in the constructor can
|
|
105
|
+
// make startup appear frozen before the renderer gets its first frame.
|
|
106
|
+
setTimeout(() => {
|
|
107
|
+
try { require('../mcp/mcp-client').warm(); } catch {}
|
|
108
|
+
}, 0);
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
setMode(mode) {
|
package/src/tui/App.tsx
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// useKeyboard handles ALL text input char-by-char.
|
|
3
3
|
import { onMount, onCleanup, createMemo, Show } from "solid-js";
|
|
4
4
|
import { useKeyboard, usePaste, useRenderer, useSelectionHandler } from "@opentui/solid";
|
|
5
|
+
import { createTimeline } from "@opentui/core";
|
|
5
6
|
import path from "path";
|
|
6
7
|
import fs from "fs";
|
|
7
8
|
import os from "os";
|
|
@@ -462,10 +463,10 @@ export function App(props: { initialPrompt?: string; resumeSession?: string; aut
|
|
|
462
463
|
if (resp.interrupted) {
|
|
463
464
|
// Keep the partial text in the bubble; the session already stored it,
|
|
464
465
|
// so the user can type "continue" to resume the task.
|
|
465
|
-
patchMessageAt(idx, { thinking: false, interrupted: true, thinkTime: Date.now() - t0, isError: false });
|
|
466
|
+
patchMessageAt(idx, { thinking: false, interrupted: true, thinkTime: Date.now() - t0, isError: false, parts: snapshotParts() });
|
|
466
467
|
appendMessage({ role: "system", content: "Interrupted \u2014 partial response kept. Type \"continue\" to resume the task." });
|
|
467
468
|
} else {
|
|
468
|
-
patchMessageAt(idx, { content: resp.content || "(no response)", thinking: false, thinkTime: Date.now() - t0, isError: isErr });
|
|
469
|
+
patchMessageAt(idx, { content: resp.content || "(no response)", thinking: false, thinkTime: Date.now() - t0, isError: isErr, parts: snapshotParts() });
|
|
469
470
|
}
|
|
470
471
|
if (!isErr && sess.mode === "plan") {
|
|
471
472
|
appendMessage({ role: "system", content: "Plan complete \u2014 press Tab to switch to Build, then send \"go\" to execute." });
|
|
@@ -475,15 +476,20 @@ export function App(props: { initialPrompt?: string; resumeSession?: string; aut
|
|
|
475
476
|
ensureTextPart("Error: " + String(e?.message || e).slice(0, 500));
|
|
476
477
|
flushStream();
|
|
477
478
|
var err = e || {};
|
|
478
|
-
patchMessageAt(idx, { content: "Error: " + String(err.message || err).slice(0, 500), thinking: false, isError: true, thinkTime: Date.now() - t0 });
|
|
479
|
+
patchMessageAt(idx, { content: "Error: " + String(err.message || err).slice(0, 500), thinking: false, isError: true, thinkTime: Date.now() - t0, parts: snapshotParts() });
|
|
479
480
|
}).finally(function() {
|
|
480
481
|
clearInterval(speedTimer);
|
|
481
482
|
if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
|
|
482
483
|
if (offTodos) { offTodos(); offTodos = null; }
|
|
483
484
|
// Freeze the subagent panel at its final state ("finished").
|
|
484
485
|
if (subAcc.agent) patchMessageAt(idx, { subagent: { agent: subAcc.agent, text: subAcc.text, log: subAcc.log, status: subAcc.status, done: true } });
|
|
485
|
-
|
|
486
|
-
|
|
486
|
+
// Let the settled assistant message render before the global turn state
|
|
487
|
+
// changes; otherwise the test renderer can sample the old frame after
|
|
488
|
+
// thinking() becomes false.
|
|
489
|
+
setTimeout(function() {
|
|
490
|
+
setThinking(false); setThinkStart(null); recomputeTodos(); refreshUsage();
|
|
491
|
+
flushQueueSoon();
|
|
492
|
+
}, 0);
|
|
487
493
|
});
|
|
488
494
|
} catch (e: any) {
|
|
489
495
|
// A provider that throws synchronously (bad key, malformed config) must
|
|
@@ -494,8 +500,10 @@ export function App(props: { initialPrompt?: string; resumeSession?: string; aut
|
|
|
494
500
|
clearInterval(speedTimer);
|
|
495
501
|
if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
|
|
496
502
|
if (offTodos) { offTodos(); offTodos = null; }
|
|
497
|
-
|
|
498
|
-
|
|
503
|
+
setTimeout(function() {
|
|
504
|
+
setThinking(false); setThinkStart(null); recomputeTodos(); refreshUsage();
|
|
505
|
+
flushQueueSoon();
|
|
506
|
+
}, 0);
|
|
499
507
|
}
|
|
500
508
|
}
|
|
501
509
|
|
|
@@ -1475,6 +1483,12 @@ export function App(props: { initialPrompt?: string; resumeSession?: string; aut
|
|
|
1475
1483
|
let _skillToastOff: (() => void) | null = null;
|
|
1476
1484
|
let _skillDoneOff: (() => void) | null = null;
|
|
1477
1485
|
onMount(function() {
|
|
1486
|
+
// Keep the renderer's frame pump alive. The engine only runs its
|
|
1487
|
+
// frame callback while there's a non-complete timeline registered;
|
|
1488
|
+
// without one it drops the renderer to "not live" after the first
|
|
1489
|
+
// paint and no further frames are flushed (frozen splash).
|
|
1490
|
+
createTimeline({ duration: Infinity, autoplay: true });
|
|
1491
|
+
|
|
1478
1492
|
refreshProviderState();
|
|
1479
1493
|
refreshUsage();
|
|
1480
1494
|
wireTodoEvents();
|
|
@@ -4,6 +4,7 @@ import { palette } from "../theme.ts";
|
|
|
4
4
|
import {
|
|
5
5
|
showThinking, userExpandedIdx, setUserExpandedIdx, sidebarVisible,
|
|
6
6
|
thoughtExpanded, setThoughtExpanded, thoughtClosed, setThoughtClosed,
|
|
7
|
+
thinking,
|
|
7
8
|
} from "../store.ts";
|
|
8
9
|
import { toolDisplay } from "../tool-display.ts";
|
|
9
10
|
import { formatDiffCount } from "../../core/file-diffs.js";
|
|
@@ -321,9 +322,9 @@ function PartList(props: { m: any; idx: number }) {
|
|
|
321
322
|
{parts().map((p: any, i: number) => {
|
|
322
323
|
if (p.type === "reasoning") {
|
|
323
324
|
if (!showThinking()) return null;
|
|
324
|
-
const live = !!m.thinking && i === lastReasonIdx();
|
|
325
|
-
const open = live ? !thoughtClosed().has(props.idx) : (thoughtExpanded().get(props.idx)?.has(i) ?? false);
|
|
326
|
-
blockPrev = open;
|
|
325
|
+
const live = () => !!m.thinking && thinking() && i === lastReasonIdx();
|
|
326
|
+
const open = () => live() ? !thoughtClosed().has(props.idx) : (thoughtExpanded().get(props.idx)?.has(i) ?? false);
|
|
327
|
+
blockPrev = open();
|
|
327
328
|
return <ThoughtPart p={p} i={i} idx={props.idx} live={live} open={open} />;
|
|
328
329
|
}
|
|
329
330
|
if (p.type === "tool") {
|
|
@@ -370,9 +371,9 @@ function deriveParts(m: any): any[] {
|
|
|
370
371
|
// below (click collapses it mid-turn); once the model moves on (a tool or text
|
|
371
372
|
// part arrives, or the turn ends) it settles to a clickable "+ Thought · Ns"
|
|
372
373
|
// line — opencode's minimal mode. Clicking toggles the body in either state.
|
|
373
|
-
function ThoughtPart(props: { p: any; i: number; idx: number; live: boolean; open: boolean }) {
|
|
374
|
+
function ThoughtPart(props: { p: any; i: number; idx: number; live: () => boolean; open: () => boolean }) {
|
|
374
375
|
const toggle = () => {
|
|
375
|
-
if (props.live) {
|
|
376
|
+
if (props.live()) {
|
|
376
377
|
const closed = new Set(thoughtClosed());
|
|
377
378
|
if (closed.has(props.idx)) closed.delete(props.idx); else closed.add(props.idx);
|
|
378
379
|
setThoughtClosed(closed);
|
|
@@ -392,9 +393,9 @@ function ThoughtPart(props: { p: any; i: number; idx: number; live: boolean; ope
|
|
|
392
393
|
onMouseUp={() => toggle()}
|
|
393
394
|
onKeyDown={(e: any) => { if (e.name === "return" || e.name === "space") toggle(); }}
|
|
394
395
|
>
|
|
395
|
-
{props.live ? <ThinkingLabel /> : (props.open ? "- " : "+ ") + "Thought" + (ms() ? " \u00B7 " + ms() : "")}
|
|
396
|
+
{props.live() ? <ThinkingLabel /> : (props.open() ? "- " : "+ ") + "Thought" + (ms() ? " \u00B7 " + ms() : "")}
|
|
396
397
|
</text>
|
|
397
|
-
<Show when={props.open}>
|
|
398
|
+
<Show when={props.open()}>
|
|
398
399
|
<box paddingLeft={2}>
|
|
399
400
|
<MdText md={String(props.p.text || "").slice(0, 20000)} />
|
|
400
401
|
</box>
|
|
@@ -345,9 +345,8 @@ export function InputBar() {
|
|
|
345
345
|
</box>
|
|
346
346
|
</box>
|
|
347
347
|
|
|
348
|
-
<box paddingX={1} flexDirection="row"
|
|
349
|
-
<text fg={ui.fgMuted}>{statusLine().cwd}</text>
|
|
350
|
-
<text fg={ui.fgMuted}>{statusLine().right}</text>
|
|
348
|
+
<box paddingX={1} flexDirection="row">
|
|
349
|
+
<text fg={ui.fgMuted}>{statusLine().cwd + " " + statusLine().right}</text>
|
|
351
350
|
</box>
|
|
352
351
|
</box>
|
|
353
352
|
);
|
|
@@ -1,8 +1,21 @@
|
|
|
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";
|
|
2
4
|
import { palette, LOOM_LOGO } from "../theme.ts";
|
|
3
5
|
import { providerName, modelName, providerKeyOk } from "../store.ts";
|
|
4
6
|
import { InputBar } from "./InputBar.tsx";
|
|
5
7
|
|
|
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
|
+
|
|
6
19
|
export function SplashScreen() {
|
|
7
20
|
const ui = palette("loom");
|
|
8
21
|
|
|
@@ -27,7 +40,7 @@ export function SplashScreen() {
|
|
|
27
40
|
</box>
|
|
28
41
|
|
|
29
42
|
<box marginTop={0}>
|
|
30
|
-
<text fg={ui.fgMuted}>{
|
|
43
|
+
<text fg={ui.fgMuted}>{LOOM_VERSION}</text>
|
|
31
44
|
</box>
|
|
32
45
|
|
|
33
46
|
<box marginY={1} width={74}>
|
|
@@ -104,7 +104,7 @@ export function SubagentPanel() {
|
|
|
104
104
|
// Set the DEFAULT model for the selected agent id ("provider/model-id").
|
|
105
105
|
const cur = all()[sel()];
|
|
106
106
|
if (!cur) return;
|
|
107
|
-
const { loadConfig, saveConfig } = require("
|
|
107
|
+
const { loadConfig, saveConfig } = require("../../config/settings.js");
|
|
108
108
|
const cfg = loadConfig();
|
|
109
109
|
cfg.agents = cfg.agents || {};
|
|
110
110
|
const curModel = (cfg.agents[cur.agentId] && cfg.agents[cur.agentId].model) || "";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Compile entry for the standalone executable (bun build --compile).
|
|
2
|
+
// Compiled binaries skip bunfig.toml entirely, so the preload-side effects
|
|
3
|
+
// (Windows VT/console-mode fix, UTF-8 codepage, crash black-box, optional
|
|
4
|
+
// LOOM_START_CWD restore) are imported here explicitly. The dynamic import
|
|
5
|
+
// keeps tui-open.tsx's module graph evaluating AFTER those effects run.
|
|
6
|
+
import "./tui-preload.js";
|
|
7
|
+
// Compiled binaries skip bunfig.toml, so the Solid JSX runtime loader must be
|
|
8
|
+
// registered explicitly before the app graph evaluates.
|
|
9
|
+
import "@opentui/solid/preload";
|
|
10
|
+
await import("./tui-open.tsx");
|
package/src/tui-open.tsx
CHANGED
|
@@ -8,6 +8,16 @@ import { defaultMcpInstall } from "./core/plugin-cmd.js";
|
|
|
8
8
|
globalThis.__loomTrace?.("entry", new Error("module imports evaluated"));
|
|
9
9
|
const args = process.argv.slice(2);
|
|
10
10
|
|
|
11
|
+
// Restore the user's project directory as the very first module-body
|
|
12
|
+
// statement — i.e. AFTER the entire import graph above has finished
|
|
13
|
+
// evaluating under Bun's original working directory (where bunfig.toml /
|
|
14
|
+
// tsconfig.json were discovered). Doing this any earlier breaks Solid's
|
|
15
|
+
// JSX transform; doing it here means every lazy process.cwd() call in app
|
|
16
|
+
// code still sees the project the user launched from.
|
|
17
|
+
if (process.env.LOOM_START_CWD) {
|
|
18
|
+
try { process.chdir(process.env.LOOM_START_CWD); } catch {}
|
|
19
|
+
}
|
|
20
|
+
|
|
11
21
|
// Windows: switch both console codepages to UTF-8 so the box-drawing logo
|
|
12
22
|
// and VT input sequences work even when launched without the bootstrap
|
|
13
23
|
// shim (bun run src/tui-open.tsx). No-op elsewhere / if FFI is unavailable.
|
package/src/tui-preload.js
CHANGED
|
@@ -1,51 +1,125 @@
|
|
|
1
|
-
// Loaded via bunfig.toml preload BEFORE any app code, so this replaces the
|
|
2
|
-
// old CJS bootstrap hop entirely: the launch chain stays a plain direct
|
|
3
|
-
// "bun src/tui-open.tsx" (identical shape to repo runs) while we still get
|
|
4
|
-
// to restore the user's project directory early and keep the crash
|
|
5
|
-
// black-box watching.
|
|
6
|
-
const fs = require("fs");
|
|
7
|
-
const os = require("os");
|
|
8
|
-
const path = require("path");
|
|
9
|
-
|
|
10
|
-
if (process.platform === "win32") {
|
|
11
|
-
// UTF-8 console codepage: legacy CPs render the box-drawing logo as "?"
|
|
12
|
-
// garbage and can mangle VT input sequences.
|
|
13
|
-
try {
|
|
14
|
-
const { dlopen } = require("bun:ffi");
|
|
15
|
-
const k32 = dlopen("kernel32.dll", {
|
|
16
|
-
SetConsoleOutputCP: { args: ["uint"], returns: "int" },
|
|
17
|
-
SetConsoleCP: { args: ["uint"], returns: "int" },
|
|
18
|
-
});
|
|
19
|
-
k32.symbols.SetConsoleOutputCP(65001);
|
|
20
|
-
k32.symbols.SetConsoleCP(65001);
|
|
21
|
-
} catch {}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
1
|
+
// Loaded via bunfig.toml preload BEFORE any app code, so this replaces the
|
|
2
|
+
// old CJS bootstrap hop entirely: the launch chain stays a plain direct
|
|
3
|
+
// "bun src/tui-open.tsx" (identical shape to repo runs) while we still get
|
|
4
|
+
// to restore the user's project directory early and keep the crash
|
|
5
|
+
// black-box watching.
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
|
|
10
|
+
if (process.platform === "win32") {
|
|
11
|
+
// UTF-8 console codepage: legacy CPs render the box-drawing logo as "?"
|
|
12
|
+
// garbage and can mangle VT input sequences.
|
|
13
|
+
try {
|
|
14
|
+
const { dlopen } = require("bun:ffi");
|
|
15
|
+
const k32 = dlopen("kernel32.dll", {
|
|
16
|
+
SetConsoleOutputCP: { args: ["uint"], returns: "int" },
|
|
17
|
+
SetConsoleCP: { args: ["uint"], returns: "int" },
|
|
18
|
+
});
|
|
19
|
+
k32.symbols.SetConsoleOutputCP(65001);
|
|
20
|
+
k32.symbols.SetConsoleCP(65001);
|
|
21
|
+
} catch {}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Windows VT-mode re-enable — OPT-IN via LOOM_FORCE_VT=1.
|
|
25
|
+
// Off by default: OpenTUI manages its own console modes, and forcing flags
|
|
26
|
+
// underneath it can desync its input parsing. Only enable when debugging
|
|
27
|
+
// console-mode issues on a specific terminal.
|
|
28
|
+
if (process.platform === "win32" && process.env.LOOM_FORCE_VT === "1") {
|
|
29
|
+
try {
|
|
30
|
+
const { dlopen } = require("bun:ffi");
|
|
31
|
+
const k32 = dlopen("kernel32.dll", {
|
|
32
|
+
GetStdHandle: { args: ["int"], returns: "ptr" },
|
|
33
|
+
GetConsoleMode: { args: ["ptr", "ptr"], returns: "int" },
|
|
34
|
+
SetConsoleMode: { args: ["ptr", "uint"], returns: "int" },
|
|
35
|
+
});
|
|
36
|
+
const STD_INPUT_HANDLE = -10;
|
|
37
|
+
const STD_OUTPUT_HANDLE = -11;
|
|
38
|
+
|
|
39
|
+
// Output: processed + wrap-at-EOL + VT processing (so ANSI repaints work).
|
|
40
|
+
const ENABLE_PROCESSED_OUTPUT = 0x0001;
|
|
41
|
+
const ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002;
|
|
42
|
+
const ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004;
|
|
43
|
+
// Input: raw-ish mode for OpenTUI — VT input + processed + mouse + window,
|
|
44
|
+
// with line/echo/quick-edit cleared so keys stream and don't echo.
|
|
45
|
+
const ENABLE_PROCESSED_INPUT = 0x0001;
|
|
46
|
+
const ENABLE_MOUSE_INPUT = 0x0010;
|
|
47
|
+
const ENABLE_WINDOW_INPUT = 0x0008;
|
|
48
|
+
const ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200;
|
|
49
|
+
const ENABLE_QUICK_EDIT_MODE = 0x0040;
|
|
50
|
+
const ENABLE_LINE_INPUT = 0x0002;
|
|
51
|
+
const ENABLE_ECHO_INPUT = 0x0004;
|
|
52
|
+
|
|
53
|
+
const outH = k32.symbols.GetStdHandle(STD_OUTPUT_HANDLE);
|
|
54
|
+
const inH = k32.symbols.GetStdHandle(STD_INPUT_HANDLE);
|
|
55
|
+
const modeBuf = new Uint32Array(1);
|
|
56
|
+
const modePtr = Bun.ptr(modeBuf);
|
|
57
|
+
|
|
58
|
+
if (outH && !outH.isNull && k32.symbols.GetConsoleMode(outH, modePtr)) {
|
|
59
|
+
const cur = modeBuf[0];
|
|
60
|
+
const next = cur | ENABLE_PROCESSED_OUTPUT | ENABLE_WRAP_AT_EOL_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING;
|
|
61
|
+
if (next !== cur) k32.symbols.SetConsoleMode(outH, next);
|
|
62
|
+
}
|
|
63
|
+
if (inH && !inH.isNull && k32.symbols.GetConsoleMode(inH, modePtr)) {
|
|
64
|
+
const cur = modeBuf[0];
|
|
65
|
+
const next = (cur | ENABLE_PROCESSED_INPUT | ENABLE_MOUSE_INPUT | ENABLE_WINDOW_INPUT | ENABLE_VIRTUAL_TERMINAL_INPUT)
|
|
66
|
+
& ~(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_QUICK_EDIT_MODE);
|
|
67
|
+
if (next !== cur) k32.symbols.SetConsoleMode(inH, next);
|
|
68
|
+
}
|
|
69
|
+
} catch {}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// NOTE: LOOM_START_CWD is deliberately NOT applied here. Changing the working
|
|
73
|
+
// directory before the app's module graph loads breaks how Bun resolves the
|
|
74
|
+
// Solid JSX transform (proven: identical entry paints 19KB from repo cwd,
|
|
75
|
+
// 0 bytes from a foreign cwd). The restore happens in tui-open.tsx instead —
|
|
76
|
+
// as a module-body statement, i.e. AFTER every import has finished loading.
|
|
77
|
+
|
|
78
|
+
const crashPath = () => path.join(os.homedir(), ".loom", "tui-crash.log");
|
|
79
|
+
const MAX_LOG_BYTES = 1024 * 1024; // 1 MB cap, then rotate to .old
|
|
80
|
+
function rotateIfNeeded() {
|
|
81
|
+
try {
|
|
82
|
+
const p = crashPath();
|
|
83
|
+
if (fs.existsSync(p) && fs.statSync(p).size > MAX_LOG_BYTES) {
|
|
84
|
+
fs.renameSync(p, p + ".old");
|
|
85
|
+
}
|
|
86
|
+
} catch {}
|
|
87
|
+
}
|
|
88
|
+
function record(kind, err) {
|
|
89
|
+
try {
|
|
90
|
+
fs.mkdirSync(path.dirname(crashPath()), { recursive: true });
|
|
91
|
+
rotateIfNeeded();
|
|
92
|
+
fs.appendFileSync(
|
|
93
|
+
crashPath(),
|
|
94
|
+
`[${new Date().toISOString()}] ${kind}: ${(err && (err.stack || err.message)) || String(err)}\n`
|
|
95
|
+
);
|
|
96
|
+
} catch {}
|
|
97
|
+
}
|
|
98
|
+
globalThis.__loomTrace = record;
|
|
99
|
+
process.on("uncaughtException", (e) => record("uncaughtException", e));
|
|
100
|
+
process.on("unhandledRejection", (r) => record("unhandledRejection", r));
|
|
101
|
+
|
|
102
|
+
// stdout byte-counter: frames flowing = counter climbs. This splits the two
|
|
103
|
+
// remaining frozen-splash suspects with certainty — if the counter climbs but
|
|
104
|
+
// the screen is frozen, the console is dropping VT repaints (mode flags); if
|
|
105
|
+
// the counter is flat, the renderer flush-loop itself is stalled.
|
|
106
|
+
let __stdoutBytes = 0;
|
|
107
|
+
const __origWrite = process.stdout.write.bind(process.stdout);
|
|
108
|
+
process.stdout.write = function (chunk, ...rest) {
|
|
109
|
+
try {
|
|
110
|
+
if (typeof chunk === "string") __stdoutBytes += Buffer.byteLength(chunk, "utf8");
|
|
111
|
+
else if (chunk && chunk.length) __stdoutBytes += chunk.length;
|
|
112
|
+
} catch {}
|
|
113
|
+
return __origWrite(chunk, ...rest);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const __hbStart = Date.now();
|
|
117
|
+
let __hbTick = 0;
|
|
118
|
+
let __hbScheduled = __hbStart;
|
|
119
|
+
const __hbTimer = setInterval(() => {
|
|
120
|
+
const now = Date.now();
|
|
121
|
+
record("heartbeat", new Error(`tick=${++__hbTick} lag=${now - __hbScheduled}ms uptime=${now - __hbStart}ms stdoutBytes=${__stdoutBytes}`));
|
|
122
|
+
__hbScheduled = now + 3000;
|
|
123
|
+
}, 3000);
|
|
124
|
+
try { __hbTimer.unref?.(); } catch {}
|
|
125
|
+
process.on("exit", () => { try { record("exit", new Error(`uptime=${Date.now() - __hbStart}ms ticks=${__hbTick} stdoutBytes=${__stdoutBytes}`)); } catch {} });
|