loom-agent 1.2.26 → 1.2.33
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 +25 -0
- package/bin/loom-bun.js +26 -11
- package/bin/loom-tui.js +27 -22
- package/package.json +4 -2
- package/scripts/postinstall.js +88 -0
- package/src/core/cli.js +10 -7
- package/src/tui/components/Modals.tsx +37 -21
- package/src/tui-preload.js +55 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,31 @@ All notable changes to **Loom Code** are documented here.
|
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [1.2.33] — fix frozen splash on global installs + stable model picker
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **Global npm installs froze on the splash screen** ("Build … no key", no
|
|
11
|
+
keyboard input) while repo checkouts worked. Root cause: the launch chain
|
|
12
|
+
started Bun at the package root and later ran `process.chdir()` back to the
|
|
13
|
+
user's project inside `tui-open.tsx`. That mid-flight chdir killed OpenTUI's
|
|
14
|
+
repaint/input pipeline after the first frame — signals kept updating and
|
|
15
|
+
Solid effects kept firing (verified with runtime instrumentation), but no
|
|
16
|
+
frame ever reached the terminal again.
|
|
17
|
+
- The launcher now registers the Solid JSX preloader with **absolute
|
|
18
|
+
`--preload` paths** and starts Bun **directly in the user's project
|
|
19
|
+
directory**, so no chdir ever happens: `bin/loom-bun.js`, `bin/loom-tui.js`,
|
|
20
|
+
the postinstall-rewritten shims, and the core-CLI TUI spawn
|
|
21
|
+
(`src/core/cli.js`) were all switched to that scheme.
|
|
22
|
+
- **Model picker jitter** — scrolling `/models` (and every `SelectModal`:
|
|
23
|
+
`/connect`, theme pickers, MCP preset picker) bounced the whole modal.
|
|
24
|
+
Section headers added an extra margin row, so the centered frame's height
|
|
25
|
+
flipped between 12/13/14 rows on every page of a header-heavy list. The
|
|
26
|
+
list window is now a fixed 12 rows (headers are plain one-row entries), the
|
|
27
|
+
window is computed once per change instead of mutating during render, and
|
|
28
|
+
mouse-hover no longer yanks the selection while a keyboard/wheel scroll is
|
|
29
|
+
settling. Regression test added (29b: modal title row must never move while
|
|
30
|
+
scrolling).
|
|
31
|
+
|
|
7
32
|
## [Unreleased]
|
|
8
33
|
|
|
9
34
|
### Added
|
package/bin/loom-bun.js
CHANGED
|
@@ -2,24 +2,31 @@
|
|
|
2
2
|
// npm invokes bin targets through Node on Windows, so re-launch under Bun
|
|
3
3
|
// when this file was not started by Bun itself.
|
|
4
4
|
const path = require("path");
|
|
5
|
+
const fs = require("fs");
|
|
5
6
|
const { spawnSync } = require("child_process");
|
|
6
7
|
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
8
|
+
// The Solid JSX transform must register in Bun's preload phase, so the
|
|
9
|
+
// preloads are passed as ABSOLUTE paths. That lets Bun start directly in the
|
|
10
|
+
// user's project directory — the old "spawn at the package root, then chdir
|
|
11
|
+
// to the project inside tui-open.tsx" dance froze the TUI after the first
|
|
12
|
+
// frame (splash paints once, then no repaints and no keyboard input; proven
|
|
13
|
+
// by A/B-launching the identical entry with and without the mid-flight
|
|
14
|
+
// process.chdir). No chdir ever happens now: LOOM_START_CWD always equals
|
|
15
|
+
// the starting directory, so the restore in tui-open.tsx is a no-op kept
|
|
16
|
+
// only as a safety net.
|
|
13
17
|
const pkgRoot = path.join(__dirname, "..");
|
|
14
18
|
const underBun = typeof Bun !== "undefined" && !!process.versions.bun;
|
|
15
|
-
|
|
16
|
-
if (!underBun || !inPkgRoot) {
|
|
19
|
+
if (!underBun) {
|
|
17
20
|
const result = spawnSync(
|
|
18
21
|
process.platform === "win32" ? "bun.exe" : "bun",
|
|
19
|
-
[
|
|
22
|
+
[
|
|
23
|
+
"--preload", path.join(pkgRoot, "src", "tui-preload.js"),
|
|
24
|
+
__filename,
|
|
25
|
+
...process.argv.slice(2),
|
|
26
|
+
],
|
|
20
27
|
{
|
|
21
28
|
stdio: "inherit",
|
|
22
|
-
cwd:
|
|
29
|
+
cwd: process.env.LOOM_START_CWD || process.cwd(),
|
|
23
30
|
env: { ...process.env, LOOM_START_CWD: process.env.LOOM_START_CWD || process.cwd() },
|
|
24
31
|
windowsHide: false,
|
|
25
32
|
}
|
|
@@ -33,6 +40,13 @@ if (!underBun || !inPkgRoot) {
|
|
|
33
40
|
process.title = "loom-code";
|
|
34
41
|
(async () => {
|
|
35
42
|
try {
|
|
43
|
+
// The npm bin entry bypasses src/index.js, so load dotenv here for both
|
|
44
|
+
// the package environment and the project from which `loom` was run.
|
|
45
|
+
const dotenv = require("dotenv");
|
|
46
|
+
dotenv.config({ path: path.join(__dirname, "..", ".env") });
|
|
47
|
+
const startCwd = process.env.LOOM_START_CWD || process.cwd();
|
|
48
|
+
const projectEnv = path.join(startCwd, ".env");
|
|
49
|
+
if (fs.existsSync(projectEnv)) dotenv.config({ path: projectEnv, override: false });
|
|
36
50
|
// The npm global shim can start Bun outside the package directory, so do
|
|
37
51
|
// not depend on bunfig.toml discovery for Windows console setup.
|
|
38
52
|
await import("../src/tui-preload.js");
|
|
@@ -41,6 +55,7 @@ process.title = "loom-code";
|
|
|
41
55
|
|| args.includes("--help") || args.includes("-h") || args.includes("--version") || args.includes("-v")
|
|
42
56
|
|| ["acp", "web", "attach", "graph"].includes(args[0]);
|
|
43
57
|
if (!coreMode) {
|
|
58
|
+
await import("@opentui/solid/preload");
|
|
44
59
|
await import("../src/tui-open.tsx");
|
|
45
60
|
return;
|
|
46
61
|
}
|
|
@@ -50,4 +65,4 @@ process.title = "loom-code";
|
|
|
50
65
|
console.error(err && err.message ? err.message : String(err));
|
|
51
66
|
process.exit(1);
|
|
52
67
|
}
|
|
53
|
-
})();
|
|
68
|
+
})();
|
package/bin/loom-tui.js
CHANGED
|
@@ -4,30 +4,35 @@
|
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const { spawnSync } = require("child_process");
|
|
6
6
|
|
|
7
|
-
//
|
|
8
|
-
//
|
|
7
|
+
// Preloads are ABSOLUTE paths so Bun starts directly in the user's project
|
|
8
|
+
// directory — see bin/loom-bun.js for why the package-root + chdir dance is
|
|
9
|
+
// gone (it froze the TUI after the first frame).
|
|
9
10
|
const pkgRoot = path.join(__dirname, "..");
|
|
10
11
|
const underBun = typeof Bun !== "undefined" && !!process.versions.bun;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
12
|
+
if (!underBun) {
|
|
13
|
+
const result = spawnSync(
|
|
14
|
+
process.platform === "win32" ? "bun.exe" : "bun",
|
|
15
|
+
[
|
|
16
|
+
"--preload", path.join(pkgRoot, "src", "tui-preload.js"),
|
|
17
|
+
__filename,
|
|
18
|
+
...process.argv.slice(2),
|
|
19
|
+
],
|
|
20
|
+
{
|
|
21
|
+
stdio: "inherit",
|
|
22
|
+
cwd: process.env.LOOM_START_CWD || process.cwd(),
|
|
23
|
+
env: { ...process.env, LOOM_START_CWD: process.env.LOOM_START_CWD || process.cwd() },
|
|
24
|
+
windowsHide: false,
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
if (result.error) {
|
|
28
|
+
console.error("[loom] Bun is required for the TUI. Install it from https://bun.sh/");
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
process.exit(result.status == null ? 1 : result.status);
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
(async () => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
await import("../src/tui-preload.js");
|
|
36
|
+
await import("@opentui/solid/preload");
|
|
37
|
+
await import("../src/tui-open.tsx");
|
|
38
|
+
})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loom-agent",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.33",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/toshalkumbhar8979-design/loomcode.git"
|
|
@@ -9,7 +9,7 @@
|
|
|
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
14
|
"publishConfig": {
|
|
15
15
|
"access": "public",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"tui": "bun bin/loom-tui.js",
|
|
31
31
|
"tui:win": "\"%USERPROFILE%\\..\\bun\\bin\\bun.exe\" run src/tui-open.tsx",
|
|
32
32
|
"prepublishOnly": "npm test",
|
|
33
|
+
"postinstall": "node scripts/postinstall.js",
|
|
33
34
|
"lint": "tsc --noEmit"
|
|
34
35
|
},
|
|
35
36
|
"dependencies": {
|
|
@@ -90,6 +91,7 @@
|
|
|
90
91
|
"LOOM.md",
|
|
91
92
|
"docs/acp.md",
|
|
92
93
|
"scripts/acp-smoke.js",
|
|
94
|
+
"scripts/postinstall.js",
|
|
93
95
|
"docs/web.md",
|
|
94
96
|
"src/web/index.html",
|
|
95
97
|
"src/web/graph-view.html"
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Postinstall: rewrite the npm-generated bin shims so `loom` starts ONE bun
|
|
2
|
+
// process directly IN THE USER'S PROJECT DIRECTORY. The Solid JSX preloader
|
|
3
|
+
// is registered with absolute --preload paths (the package-root cwd trick is
|
|
4
|
+
// gone — starting at the package root forced a later process.chdir back to
|
|
5
|
+
// the project inside tui-open.tsx, which froze the TUI after the first frame:
|
|
6
|
+
// splash painted once, then no repaints and no keyboard input).
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
|
|
10
|
+
const pkgRoot = path.resolve(__dirname, "..");
|
|
11
|
+
|
|
12
|
+
function shimDirs() {
|
|
13
|
+
const dirs = new Set();
|
|
14
|
+
if (path.basename(path.resolve(pkgRoot, "..")) !== "node_modules") return [];
|
|
15
|
+
dirs.add(path.resolve(pkgRoot, "..", ".."));
|
|
16
|
+
dirs.add(path.join(path.resolve(pkgRoot, ".."), ".bin"));
|
|
17
|
+
return [...dirs].filter(function(d) {
|
|
18
|
+
try {
|
|
19
|
+
return fs.existsSync(path.join(d, "loom.cmd")) || fs.existsSync(path.join(d, "loom"));
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function cmdShim(pkg, script) {
|
|
27
|
+
var pkgEscaped = pkg.replace(/\//g, "\\");
|
|
28
|
+
return [
|
|
29
|
+
"@ECHO off",
|
|
30
|
+
"SETLOCAL",
|
|
31
|
+
'SET "LOOM_START_CWD=%CD%"',
|
|
32
|
+
'IF EXIST "%~dp0bun.exe" (SET "_prog=%~dp0bun.exe") ELSE (SET "_prog=bun")',
|
|
33
|
+
'"%_prog%" --preload "%~dp0' + pkgEscaped + '\\src\\tui-preload.js" "%~dp0' + pkgEscaped + '\\bin\\' + script + '" %*',
|
|
34
|
+
"EXIT /b %ERRORLEVEL%",
|
|
35
|
+
""
|
|
36
|
+
].join("\r\n");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function psShim(pkg, script) {
|
|
40
|
+
return [
|
|
41
|
+
"#!/usr/bin/env pwsh",
|
|
42
|
+
"# rewritten by loom-agent postinstall (no-cwd-change TUI launch)",
|
|
43
|
+
"$env:LOOM_START_CWD = (Get-Location).Path",
|
|
44
|
+
"$pkg = Join-Path $PSScriptRoot '" + pkg + "'",
|
|
45
|
+
"& bun --preload (Join-Path $pkg 'src/tui-preload.js') (Join-Path $pkg 'bin/" + script + "') @args",
|
|
46
|
+
"exit $LASTEXITCODE",
|
|
47
|
+
""
|
|
48
|
+
].join("\n");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function shShim(pkg, script) {
|
|
52
|
+
return [
|
|
53
|
+
"#!/bin/sh",
|
|
54
|
+
"# rewritten by loom-agent postinstall (no-cwd-change TUI launch)",
|
|
55
|
+
'LOOM_START_CWD="$(pwd)"',
|
|
56
|
+
"export LOOM_START_CWD",
|
|
57
|
+
'PKG="$(dirname "$0")/' + pkg + '"',
|
|
58
|
+
'exec bun --preload "$PKG/src/tui-preload.js" "$PKG/bin/' + script + '" "$@"',
|
|
59
|
+
""
|
|
60
|
+
].join("\n");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
for (var i = 0; i < shimDirs().length; i++) {
|
|
65
|
+
var dir = shimDirs()[i];
|
|
66
|
+
var relPkg = path.relative(dir, pkgRoot).split(path.sep).join("/");
|
|
67
|
+
if (!relPkg || relPkg.startsWith("..")) continue;
|
|
68
|
+
for (var j = 0; j < 2; j++) {
|
|
69
|
+
var name = j === 0 ? "loom" : "loom-tui";
|
|
70
|
+
var script = name === "loom" ? "loom-bun.js" : "loom-tui.js";
|
|
71
|
+
var targets = [
|
|
72
|
+
[name + ".cmd", cmdShim(relPkg, script)],
|
|
73
|
+
[name + ".ps1", psShim(relPkg, script)],
|
|
74
|
+
[name, shShim(relPkg, script)]
|
|
75
|
+
];
|
|
76
|
+
for (var k = 0; k < targets.length; k++) {
|
|
77
|
+
var file = targets[k][0];
|
|
78
|
+
var content = targets[k][1];
|
|
79
|
+
var p = path.join(dir, file);
|
|
80
|
+
if (!fs.existsSync(p)) continue;
|
|
81
|
+
fs.writeFileSync(p, content, { mode: 0o755 });
|
|
82
|
+
console.log("[loom-agent] rewrote shim " + p);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} catch (err) {
|
|
87
|
+
console.warn("[loom-agent] shim rewrite skipped: " + (err && err.message));
|
|
88
|
+
}
|
package/src/core/cli.js
CHANGED
|
@@ -38,7 +38,7 @@ class LoomCLI {
|
|
|
38
38
|
terminal: true,
|
|
39
39
|
});
|
|
40
40
|
console.log(LOOM_BASE);
|
|
41
|
-
console.log(`\n Loom Code v1.2.
|
|
41
|
+
console.log(`\n Loom Code v1.2.28 — AI Coding Agent for the terminal`);
|
|
42
42
|
console.log(` Press Ctrl+C or ESC to interrupt | /help for commands\n`);
|
|
43
43
|
|
|
44
44
|
process.stdin.on('keypress', (str, key) => {
|
|
@@ -575,19 +575,22 @@ if (args.includes('--help') || args.includes('-h')) {
|
|
|
575
575
|
const tuiEntry = path.join(__dirname, '..', 'tui-open.tsx');
|
|
576
576
|
if (bunPath && fs.existsSync(tuiEntry)) {
|
|
577
577
|
const { spawnSync } = require('child_process');
|
|
578
|
-
//
|
|
579
|
-
//
|
|
580
|
-
//
|
|
578
|
+
// The Solid JSX preloader is registered inside src/tui-preload.js
|
|
579
|
+
// (passed as an ABSOLUTE --preload path), so bun starts directly in
|
|
580
|
+
// the user's project directory. The old "start at the package root,
|
|
581
|
+
// chdir to the project inside tui-open" dance froze the TUI after
|
|
582
|
+
// the first frame (no repaints, no keyboard input) — see
|
|
583
|
+
// bin/loom-bun.js.
|
|
581
584
|
const pkgRoot = path.join(__dirname, '..', '..');
|
|
585
|
+
const tuiPreload = path.join(pkgRoot, 'src', 'tui-preload.js');
|
|
582
586
|
process.env.LOOM_START_CWD = process.cwd();
|
|
583
587
|
process.env.LOOM_BIN_NAME = "loom";
|
|
584
|
-
|
|
585
|
-
const tuiArgs = [tuiEntry];
|
|
588
|
+
const tuiArgs = ['--preload', tuiPreload, tuiEntry];
|
|
586
589
|
if (sessionId) tuiArgs.push('-s', sessionId);
|
|
587
590
|
if (autoMode) tuiArgs.push('--auto');
|
|
588
591
|
const prompt = promptArgs.join(' ');
|
|
589
592
|
if (prompt) tuiArgs.push(...prompt.split(/\s+/));
|
|
590
|
-
process.exit(spawnSync(bunPath, tuiArgs, { stdio: 'inherit', cwd:
|
|
593
|
+
process.exit(spawnSync(bunPath, tuiArgs, { stdio: 'inherit', cwd: process.cwd(), env: process.env }).status ?? 0);
|
|
591
594
|
}
|
|
592
595
|
console.error('[loom] bun not found — full TUI requires bun (https://bun.sh/). Falling back to line-mode REPL.');
|
|
593
596
|
console.error('[loom] Use --basic to skip this warning.\n');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Modals -- provider picker, model picker, key input, base URL, settings, palette.
|
|
2
|
-
import { createSignal, onMount } from "solid-js";
|
|
2
|
+
import { createSignal, createMemo, onMount } from "solid-js";
|
|
3
3
|
import { useKeyboard, usePaste } from "@opentui/solid";
|
|
4
4
|
import { palette } from "../theme.ts";
|
|
5
5
|
import * as kbs from "../keybinds.ts";
|
|
@@ -206,17 +206,25 @@ export function SelectModal(props: {
|
|
|
206
206
|
// Start on the first real row (not a header).
|
|
207
207
|
setIndex(firstSelectable());
|
|
208
208
|
|
|
209
|
+
// Hover must not fight scrolling: any keyboard/wheel/search index change
|
|
210
|
+
// locks hover-selection briefly, so the list sliding under a stationary
|
|
211
|
+
// cursor cannot yank the selection back (this feedback loop read as
|
|
212
|
+
// "jitter" while scrolling the model picker).
|
|
213
|
+
let hoverLockUntil = 0;
|
|
214
|
+
const lockHover = () => { hoverLockUntil = Date.now() + 250; };
|
|
215
|
+
const setIndexByKey = (fn: (i: number) => number) => { setIndex(fn); lockHover(); };
|
|
216
|
+
|
|
209
217
|
const nav = kbNav();
|
|
210
218
|
|
|
211
219
|
useKeyboard(key => {
|
|
212
220
|
const ks = kbs.keyString(key);
|
|
213
221
|
if (kbs.is("modal_cancel", ks)) { closeModal(); if (props.onCancel) props.onCancel(); return; }
|
|
214
|
-
if (kbs.dialogIs("dialog_select_prev", ks)) {
|
|
215
|
-
if (kbs.dialogIs("dialog_select_next", ks)) {
|
|
216
|
-
if (kbs.dialogIs("dialog_select_page_up", ks)) {
|
|
217
|
-
if (kbs.dialogIs("dialog_select_page_down", ks)) {
|
|
218
|
-
if (kbs.dialogIs("dialog_select_home", ks)) {
|
|
219
|
-
if (kbs.dialogIs("dialog_select_end", ks)) {
|
|
222
|
+
if (kbs.dialogIs("dialog_select_prev", ks)) { setIndexByKey(i => stepSelectable(i, -1)); firePreview(); return; }
|
|
223
|
+
if (kbs.dialogIs("dialog_select_next", ks)) { setIndexByKey(i => stepSelectable(i, 1)); firePreview(); return; }
|
|
224
|
+
if (kbs.dialogIs("dialog_select_page_up", ks)) { setIndexByKey(i => pageJump(i, -1)); firePreview(); return; }
|
|
225
|
+
if (kbs.dialogIs("dialog_select_page_down", ks)) { setIndexByKey(i => pageJump(i, 1)); firePreview(); return; }
|
|
226
|
+
if (kbs.dialogIs("dialog_select_home", ks)) { setIndexByKey(firstSelectable); firePreview(); return; }
|
|
227
|
+
if (kbs.dialogIs("dialog_select_end", ks)) { setIndexByKey(lastSelectable); firePreview(); return; }
|
|
220
228
|
if (kbs.dialogIs("dialog_select_submit", ks)) {
|
|
221
229
|
const opt = filtered()[index()];
|
|
222
230
|
if (!opt || opt.isHeader) return;
|
|
@@ -226,11 +234,11 @@ export function SelectModal(props: {
|
|
|
226
234
|
if (props.searchable) {
|
|
227
235
|
// Reset to 0, not firstSelectable(): setQ is batched, so firstSelectable()
|
|
228
236
|
// would read the STALE list and land past its end (dead arrows/blank row).
|
|
229
|
-
if (key.name === "backspace") { setQ(v => v.slice(0, -1));
|
|
237
|
+
if (key.name === "backspace") { setQ(v => v.slice(0, -1)); setIndexByKey(() => 0); firePreview(); return; }
|
|
230
238
|
const s = key.sequence;
|
|
231
239
|
if (!key.ctrl && !key.meta && s && s.length <= 10 && s !== "\r" && s !== "\n") {
|
|
232
240
|
setQ(v => v + s);
|
|
233
|
-
|
|
241
|
+
setIndexByKey(() => 0);
|
|
234
242
|
firePreview();
|
|
235
243
|
return;
|
|
236
244
|
}
|
|
@@ -246,7 +254,7 @@ export function SelectModal(props: {
|
|
|
246
254
|
if (j === i) break;
|
|
247
255
|
i = j;
|
|
248
256
|
}
|
|
249
|
-
|
|
257
|
+
setIndexByKey(() => i);
|
|
250
258
|
};
|
|
251
259
|
const clickRow = (i: number) => {
|
|
252
260
|
if (i !== index()) {
|
|
@@ -257,12 +265,14 @@ export function SelectModal(props: {
|
|
|
257
265
|
if (o?.isHeader) return;
|
|
258
266
|
props.onPick(o?.value, o);
|
|
259
267
|
};
|
|
260
|
-
|
|
261
|
-
|
|
268
|
+
// Window of 12 rows, computed ONCE per reactive change (the old version
|
|
269
|
+
// mutated `winStart` from inside the JSX — called three times per render).
|
|
270
|
+
let lastStart = 0;
|
|
271
|
+
const win = createMemo(() => {
|
|
262
272
|
const total = filtered().length;
|
|
263
|
-
|
|
264
|
-
return { total, start:
|
|
265
|
-
};
|
|
273
|
+
lastStart = windowFor(index(), total, 12, lastStart);
|
|
274
|
+
return { total, start: lastStart, items: filtered().slice(lastStart, lastStart + 12) };
|
|
275
|
+
});
|
|
266
276
|
const rangeSub = () => {
|
|
267
277
|
const w = win();
|
|
268
278
|
if (w.total <= 12) return "";
|
|
@@ -271,21 +281,27 @@ export function SelectModal(props: {
|
|
|
271
281
|
|
|
272
282
|
return (
|
|
273
283
|
<ModalFrame title={props.title} subtitle={(props.searchable ? "search: " + (q() || "_") + rangeSub() : rangeSub())} footer={nav.prev + "/" + nav.next + " navigate | " + nav.submit + " select | wheel scroll | " + nav.cancel + " cancel" + (props.searchable ? " | type to search" : "")}>
|
|
274
|
-
|
|
284
|
+
{/* Fixed height: the modal frame must not resize while scrolling.
|
|
285
|
+
Headers used to add an extra margin row, so the centered modal
|
|
286
|
+
bounced between 12/13/14 rows on every page of a header-heavy list
|
|
287
|
+
(the model picker) — the "jitter". Every item is now exactly one
|
|
288
|
+
row and the window is always 12 rows tall. */}
|
|
289
|
+
<box onMouseScroll={scrollBy} height={12} flexShrink={0}>
|
|
275
290
|
{win().items.map((opt, i) => {
|
|
276
291
|
const abs = win().start + i;
|
|
277
292
|
if (opt.isHeader) return (
|
|
278
|
-
<text fg={ui.secondary}
|
|
293
|
+
<text fg={ui.secondary}>
|
|
279
294
|
{opt.header + ":"}
|
|
280
295
|
</text>
|
|
281
296
|
);
|
|
282
297
|
const active = abs === index();
|
|
283
298
|
return (
|
|
284
299
|
<box
|
|
285
|
-
|
|
286
|
-
// Hover moves the selection (live theme preview via onPreview)
|
|
287
|
-
//
|
|
288
|
-
|
|
300
|
+
flexDirection="row" paddingLeft={2}
|
|
301
|
+
// Hover moves the selection (live theme preview via onPreview) —
|
|
302
|
+
// but only for genuine pointer movement, never while a
|
|
303
|
+
// keyboard/wheel scroll is settling (see hoverLockUntil).
|
|
304
|
+
onMouseOver={() => { if (Date.now() >= hoverLockUntil && abs !== index()) { setIndex(abs); firePreview(); } }}
|
|
289
305
|
onMouseDown={() => setIndex(abs)}
|
|
290
306
|
onMouseUp={() => clickRow(abs)}
|
|
291
307
|
>
|
package/src/tui-preload.js
CHANGED
|
@@ -51,6 +51,61 @@ globalThis.__loomTrace = record;
|
|
|
51
51
|
process.on("uncaughtException", (e) => record("uncaughtException", e));
|
|
52
52
|
process.on("unhandledRejection", (r) => record("unhandledRejection", r));
|
|
53
53
|
|
|
54
|
+
// Global npm installs live INSIDE node_modules, and @opentui/solid's loader
|
|
55
|
+
// filter deliberately skips every node_modules path — so for installs the
|
|
56
|
+
// app's own TSX would fall through to Bun's default React JSX transform and
|
|
57
|
+
// crash at startup ("Cannot find module 'react/jsx-dev-runtime'").
|
|
58
|
+
//
|
|
59
|
+
// This preload is THE single registration point for the TUI launch chain
|
|
60
|
+
// (shims and respawns pass ONLY this file via --preload, with an absolute
|
|
61
|
+
// path), so it registers both plugins itself:
|
|
62
|
+
// 1. The Solid JSX plugin — via the bare "@opentui/solid/bun-plugin"
|
|
63
|
+
// specifier, which resolves by walking up from this file and therefore
|
|
64
|
+
// works whether the dependency is nested inside the package or hoisted
|
|
65
|
+
// to the install root. Idempotent (symbol-guarded upstream).
|
|
66
|
+
// 2. A supplemental loader scoped to THIS package's src directory only —
|
|
67
|
+
// real dependencies are never touched. It is a no-op in repo checkouts,
|
|
68
|
+
// where the solid plugin (non-node_modules paths) already handles these
|
|
69
|
+
// files first.
|
|
70
|
+
if (typeof Bun !== "undefined" && Bun.plugin) {
|
|
71
|
+
try {
|
|
72
|
+
require("@opentui/solid/bun-plugin").ensureSolidTransformPlugin();
|
|
73
|
+
} catch {}
|
|
74
|
+
if (!globalThis.__loomAppTsxPlugin) {
|
|
75
|
+
try {
|
|
76
|
+
globalThis.__loomAppTsxPlugin = true;
|
|
77
|
+
const pkgSrc = __dirname; // tui-preload.js lives in src/
|
|
78
|
+
const pkgRoot = path.join(pkgSrc, "..");
|
|
79
|
+
const esc = pkgSrc.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
80
|
+
// Layout-proof transform lookup: nested (npm i -g default observed)
|
|
81
|
+
// and hoisted (top-level install node_modules) candidates.
|
|
82
|
+
const candidates = [
|
|
83
|
+
path.join(pkgRoot, "node_modules", "@opentui", "solid", "scripts", "solid-transform.js"),
|
|
84
|
+
path.join(pkgRoot, "..", "..", "@opentui", "solid", "scripts", "solid-transform.js"),
|
|
85
|
+
];
|
|
86
|
+
let transformSolidSource = null;
|
|
87
|
+
for (const c of candidates) {
|
|
88
|
+
try { transformSolidSource = require(c).transformSolidSource; break; } catch {}
|
|
89
|
+
}
|
|
90
|
+
if (transformSolidSource) {
|
|
91
|
+
Bun.plugin({
|
|
92
|
+
name: "loom-app-solid-tsx",
|
|
93
|
+
setup(build) {
|
|
94
|
+
build.onLoad({ filter: new RegExp("^" + esc + "[\\\\/].+\\.tsx$") }, async (args) => {
|
|
95
|
+
const code = await Bun.file(args.path).text();
|
|
96
|
+
const contents = await transformSolidSource(code, {
|
|
97
|
+
filename: args.path,
|
|
98
|
+
moduleName: "@opentui/solid",
|
|
99
|
+
});
|
|
100
|
+
return { contents, loader: "js" };
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
} catch {}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
54
109
|
// stdout byte-counter: frames flowing = counter climbs. This splits the two
|
|
55
110
|
// remaining frozen-splash suspects with certainty — if the counter climbs but
|
|
56
111
|
// the screen is frozen, the console is dropping VT repaints (mode flags); if
|