tickmarkr 1.90.8 → 1.92.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.
- package/README.md +12 -9
- package/dist/adapters/catalog-remote.d.ts +11 -1
- package/dist/adapters/catalog-remote.js +169 -19
- package/dist/adapters/catalog.js +59 -0
- package/dist/adapters/model-lints.d.ts +37 -2
- package/dist/adapters/model-lints.js +180 -36
- package/dist/adapters/registry.js +32 -6
- package/dist/brand.d.ts +2 -0
- package/dist/brand.js +13 -0
- package/dist/cli/commands/doctor.d.ts +12 -0
- package/dist/cli/commands/doctor.js +90 -20
- package/dist/cli/commands/fleet-picker.js +1 -1
- package/dist/cli/commands/fleet.d.ts +4 -4
- package/dist/cli/commands/fleet.js +198 -37
- package/dist/cli/commands/init.js +6 -4
- package/dist/compile/native.js +5 -4
- package/dist/config/config.d.ts +34 -1
- package/dist/config/config.js +101 -5
- package/dist/config/fleet-overlay.d.ts +9 -2
- package/dist/config/fleet-overlay.js +159 -14
- package/dist/gates/acceptance.js +13 -4
- package/dist/route/preference.d.ts +1 -1
- package/dist/route/preference.js +12 -2
- package/dist/route/router.js +31 -2
- package/dist/run/daemon.js +1 -1
- package/dist/tui/ink/components.d.ts +16 -1
- package/dist/tui/ink/components.js +16 -2
- package/dist/tui/ink/fleet-app.d.ts +57 -8
- package/dist/tui/ink/fleet-app.js +1228 -727
- package/dist/tui/ink/frame.d.ts +62 -0
- package/dist/tui/ink/frame.js +204 -0
- package/dist/tui/ink/init-app.d.ts +2 -1
- package/dist/tui/ink/init-app.js +28 -100
- package/package.json +1 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export declare function inkOutput(output: NodeJS.WriteStream): NodeJS.WriteStream;
|
|
3
|
+
export declare function inkInput(input: NodeJS.ReadStream, initialInput: string[]): {
|
|
4
|
+
stream: NodeJS.ReadStream;
|
|
5
|
+
stop(): void;
|
|
6
|
+
};
|
|
7
|
+
/** Alt-screen guard: only the operator's real terminal, never injected test streams. */
|
|
8
|
+
export declare function enterAltScreen(output: NodeJS.WriteStream): () => void;
|
|
9
|
+
export declare const INK: {
|
|
10
|
+
readonly brand: "ansi256(41)";
|
|
11
|
+
readonly brandBright: "ansi256(84)";
|
|
12
|
+
readonly warn: "yellow";
|
|
13
|
+
readonly fail: "red";
|
|
14
|
+
};
|
|
15
|
+
/** 1048576 → "1m" · 262144 → "262k" · 200000 → "200k" · undefined → "" */
|
|
16
|
+
export declare function fmtCtx(tokens?: number): string;
|
|
17
|
+
/** ($3, $15) → "$3/15" — omp-style per-Mtok pair. Either side missing → "". */
|
|
18
|
+
export declare function fmtUsdPair(input?: number, output?: number): string;
|
|
19
|
+
/** 4169 → "4.2s" */
|
|
20
|
+
export declare function fmtMs(ms?: number): string;
|
|
21
|
+
/** Hard cell clamp with ellipsis — model ids are ASCII, plain length is the width. */
|
|
22
|
+
export declare function clip(text: string, width: number): string;
|
|
23
|
+
/** Path clamp that keeps the LAST segment — the distinguishing half of a deep model id.
|
|
24
|
+
* `prime-agent/prime-inference/anthropic/claude-fable-5` at 34 → `prime-agent/prime-i…/claude-fable-5`;
|
|
25
|
+
* end-clipping the same id hid exactly the part that told models apart (operator review, 80-col). */
|
|
26
|
+
export declare function clipPathTail(text: string, width: number): string;
|
|
27
|
+
export declare const padCell: (text: string, width: number) => string;
|
|
28
|
+
export declare const padCellStart: (text: string, width: number) => string;
|
|
29
|
+
export type KeyBind = {
|
|
30
|
+
key: string;
|
|
31
|
+
label: string;
|
|
32
|
+
};
|
|
33
|
+
/** One dim keybar line: `Enter assign · ↑/↓ move · Esc close` with bright keys. */
|
|
34
|
+
export declare function KeyBar({ binds }: {
|
|
35
|
+
binds: KeyBind[];
|
|
36
|
+
}): import("react").JSX.Element;
|
|
37
|
+
/** Search prompt row — brand cursor while the search mode is live; dim hint when idle and empty. */
|
|
38
|
+
export declare function SearchRow({ filter, active, hint }: {
|
|
39
|
+
filter: string;
|
|
40
|
+
active: boolean;
|
|
41
|
+
hint?: string;
|
|
42
|
+
}): import("react").JSX.Element;
|
|
43
|
+
/** Elision marker rows for windowed lists. */
|
|
44
|
+
export declare function ElisionMark({ count, side, hint }: {
|
|
45
|
+
count: number;
|
|
46
|
+
side: "above" | "below";
|
|
47
|
+
hint?: string;
|
|
48
|
+
}): import("react").JSX.Element | null;
|
|
49
|
+
/** Pointer cell: two columns everywhere so rows never shift when the cursor moves. */
|
|
50
|
+
export declare function Pointer({ on }: {
|
|
51
|
+
on: boolean;
|
|
52
|
+
}): import("react").JSX.Element;
|
|
53
|
+
/** Verdict/toggle glyph vocabulary, colored per the design contract. */
|
|
54
|
+
export declare function Glyph({ kind }: {
|
|
55
|
+
kind: "on" | "off" | "fail" | "warn" | "unknown" | "neutral";
|
|
56
|
+
}): import("react").JSX.Element;
|
|
57
|
+
/** Bordered overlay panel (rendered in the body region — the rail stays put). */
|
|
58
|
+
export declare function OverlayPanel({ title, children, width }: {
|
|
59
|
+
title: string;
|
|
60
|
+
children: ReactNode;
|
|
61
|
+
width?: number;
|
|
62
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { emitKeypressEvents } from "node:readline";
|
|
3
|
+
import { PassThrough } from "node:stream";
|
|
4
|
+
import { Box, Text } from "ink";
|
|
5
|
+
import { compactTokens, GLYPHS } from "../../brand.js";
|
|
6
|
+
// ── ink stream bridges ───────────────────────────────────────────────────────
|
|
7
|
+
// Hoisted from fleet-app.tsx / init-app.tsx (both files carried byte-identical
|
|
8
|
+
// copies with a "hoist on the third act" note — this is that hoist).
|
|
9
|
+
const escape = String.fromCharCode(27);
|
|
10
|
+
const inkBookkeepingWrites = {
|
|
11
|
+
"": true,
|
|
12
|
+
[`${escape}[?25l`]: true,
|
|
13
|
+
[`${escape}[?25h`]: true,
|
|
14
|
+
[`${escape}[?2026h`]: true,
|
|
15
|
+
[`${escape}[?2026l`]: true,
|
|
16
|
+
};
|
|
17
|
+
export function inkOutput(output) {
|
|
18
|
+
if (typeof output.on === "function" && typeof output.off === "function")
|
|
19
|
+
return output;
|
|
20
|
+
const facade = Object.create(output);
|
|
21
|
+
const write = output.write.bind(output);
|
|
22
|
+
facade.write = ((chunk, ...args) => {
|
|
23
|
+
const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
|
|
24
|
+
if (inkBookkeepingWrites[text])
|
|
25
|
+
return true;
|
|
26
|
+
return Reflect.apply(write, output, [chunk, ...args]);
|
|
27
|
+
});
|
|
28
|
+
facade.on = () => facade;
|
|
29
|
+
facade.off = () => facade;
|
|
30
|
+
return facade;
|
|
31
|
+
}
|
|
32
|
+
export function inkInput(input, initialInput) {
|
|
33
|
+
const productionInput = typeof input.ref === "function" && typeof input.unref === "function";
|
|
34
|
+
// Isolate Ink's listeners on a bridge so every editor exit can detach the
|
|
35
|
+
// one listener it owns from the operator's terminal. Older injected FleetIO
|
|
36
|
+
// streams can arrive as decoded keypress events, while real TTYs forward raw
|
|
37
|
+
// data and leave decoding entirely to Ink.
|
|
38
|
+
const stream = new PassThrough();
|
|
39
|
+
stream.isTTY = input.isTTY;
|
|
40
|
+
stream.setRawMode = input.setRawMode?.bind(input);
|
|
41
|
+
stream.ref = () => {
|
|
42
|
+
if (productionInput)
|
|
43
|
+
input.ref();
|
|
44
|
+
return stream;
|
|
45
|
+
};
|
|
46
|
+
stream.unref = () => {
|
|
47
|
+
if (productionInput)
|
|
48
|
+
input.unref();
|
|
49
|
+
return stream;
|
|
50
|
+
};
|
|
51
|
+
const queued = [...initialInput];
|
|
52
|
+
let active = true;
|
|
53
|
+
let scheduled;
|
|
54
|
+
const pump = () => {
|
|
55
|
+
scheduled = undefined;
|
|
56
|
+
if (!active)
|
|
57
|
+
return;
|
|
58
|
+
const next = queued.shift();
|
|
59
|
+
if (next === undefined)
|
|
60
|
+
return;
|
|
61
|
+
stream.write(next);
|
|
62
|
+
scheduled = setTimeout(pump, 0);
|
|
63
|
+
};
|
|
64
|
+
const onData = (chunk) => {
|
|
65
|
+
queued.push(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
|
|
66
|
+
scheduled ??= setTimeout(pump, 0);
|
|
67
|
+
};
|
|
68
|
+
const onKeypress = (sequence, key) => {
|
|
69
|
+
const token = key?.sequence ?? sequence;
|
|
70
|
+
if (token === undefined)
|
|
71
|
+
return;
|
|
72
|
+
queued.push(token);
|
|
73
|
+
scheduled ??= setTimeout(pump, 0);
|
|
74
|
+
};
|
|
75
|
+
if (productionInput) {
|
|
76
|
+
input.on("data", onData);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
emitKeypressEvents(input);
|
|
80
|
+
input.on("keypress", onKeypress);
|
|
81
|
+
}
|
|
82
|
+
input.resume();
|
|
83
|
+
if (queued.length > 0)
|
|
84
|
+
scheduled = setTimeout(pump, 0);
|
|
85
|
+
return {
|
|
86
|
+
stream: stream,
|
|
87
|
+
stop() {
|
|
88
|
+
active = false;
|
|
89
|
+
clearTimeout(scheduled);
|
|
90
|
+
if (productionInput)
|
|
91
|
+
input.off("data", onData);
|
|
92
|
+
else
|
|
93
|
+
input.off("keypress", onKeypress);
|
|
94
|
+
input.pause();
|
|
95
|
+
stream.end();
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/** Alt-screen guard: only the operator's real terminal, never injected test streams. */
|
|
100
|
+
export function enterAltScreen(output) {
|
|
101
|
+
if (output !== process.stdout || output.isTTY !== true)
|
|
102
|
+
return () => { };
|
|
103
|
+
output.write(`${escape}[?1049h${escape}[H`);
|
|
104
|
+
return () => output.write(`${escape}[?1049l`);
|
|
105
|
+
}
|
|
106
|
+
// ── palette (ink color names over the brand contract in src/brand.ts) ────────
|
|
107
|
+
// brand green ramp anchor 41 = ok/authed/selection accent; amber = attention;
|
|
108
|
+
// red = fail. Chrome is dim. Data is plain. (docs/codebase/CLI-DESIGN.md)
|
|
109
|
+
export const INK = {
|
|
110
|
+
brand: "ansi256(41)",
|
|
111
|
+
brandBright: "ansi256(84)",
|
|
112
|
+
warn: "yellow",
|
|
113
|
+
fail: "red",
|
|
114
|
+
};
|
|
115
|
+
// ── compact formatters for metadata columns ──────────────────────────────────
|
|
116
|
+
/** 1048576 → "1m" · 262144 → "262k" · 200000 → "200k" · undefined → "" */
|
|
117
|
+
export function fmtCtx(tokens) {
|
|
118
|
+
if (tokens === undefined || !Number.isFinite(tokens) || tokens <= 0)
|
|
119
|
+
return "";
|
|
120
|
+
return compactTokens(tokens);
|
|
121
|
+
}
|
|
122
|
+
const trimUsd = (n) => {
|
|
123
|
+
const fixed = n >= 10 ? String(Math.round(n)) : String(Math.round(n * 100) / 100);
|
|
124
|
+
return fixed.replace(/\.0+$/, "");
|
|
125
|
+
};
|
|
126
|
+
/** ($3, $15) → "$3/15" — omp-style per-Mtok pair. Either side missing → "". */
|
|
127
|
+
export function fmtUsdPair(input, output) {
|
|
128
|
+
if (input === undefined && output === undefined)
|
|
129
|
+
return "";
|
|
130
|
+
if (input === undefined || output === undefined)
|
|
131
|
+
return `$${trimUsd(input ?? output ?? 0)}`;
|
|
132
|
+
return `$${trimUsd(input)}/${trimUsd(output)}`;
|
|
133
|
+
}
|
|
134
|
+
/** 4169 → "4.2s" */
|
|
135
|
+
export function fmtMs(ms) {
|
|
136
|
+
if (ms === undefined || !Number.isFinite(ms) || ms < 0)
|
|
137
|
+
return "";
|
|
138
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
139
|
+
}
|
|
140
|
+
/** Hard cell clamp with ellipsis — model ids are ASCII, plain length is the width. */
|
|
141
|
+
export function clip(text, width) {
|
|
142
|
+
if (width <= 0)
|
|
143
|
+
return "";
|
|
144
|
+
if (text.length <= width)
|
|
145
|
+
return text;
|
|
146
|
+
if (width === 1)
|
|
147
|
+
return "…";
|
|
148
|
+
return `${text.slice(0, width - 1)}…`;
|
|
149
|
+
}
|
|
150
|
+
/** Path clamp that keeps the LAST segment — the distinguishing half of a deep model id.
|
|
151
|
+
* `prime-agent/prime-inference/anthropic/claude-fable-5` at 34 → `prime-agent/prime-i…/claude-fable-5`;
|
|
152
|
+
* end-clipping the same id hid exactly the part that told models apart (operator review, 80-col). */
|
|
153
|
+
export function clipPathTail(text, width) {
|
|
154
|
+
if (text.length <= width)
|
|
155
|
+
return text;
|
|
156
|
+
const cut = text.lastIndexOf("/");
|
|
157
|
+
if (cut === -1)
|
|
158
|
+
return clip(text, width);
|
|
159
|
+
const tail = text.slice(cut);
|
|
160
|
+
if (tail.length + 2 > width)
|
|
161
|
+
return clip(text, width); // no room for any head — fall back
|
|
162
|
+
return `${text.slice(0, width - tail.length - 1)}…${tail}`;
|
|
163
|
+
}
|
|
164
|
+
export const padCell = (text, width) => clip(text, width).padEnd(width);
|
|
165
|
+
export const padCellStart = (text, width) => clip(text, width).padStart(width);
|
|
166
|
+
/** One dim keybar line: `Enter assign · ↑/↓ move · Esc close` with bright keys. */
|
|
167
|
+
export function KeyBar({ binds }) {
|
|
168
|
+
return (_jsx(Text, { children: binds.map((bind, index) => (_jsxs(Text, { children: [index > 0 && _jsx(Text, { dimColor: true, children: " · " }), _jsx(Text, { children: bind.key }), _jsx(Text, { dimColor: true, children: ` ${bind.label}` })] }, bind.key + bind.label))) }));
|
|
169
|
+
}
|
|
170
|
+
/** Search prompt row — brand cursor while the search mode is live; dim hint when idle and empty. */
|
|
171
|
+
export function SearchRow({ filter, active, hint }) {
|
|
172
|
+
return (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: " " }), _jsx(Text, { color: active ? INK.brand : undefined, dimColor: !active, children: "> " }), filter === "" && !active && hint ? _jsx(Text, { dimColor: true, children: hint }) : _jsx(Text, { children: filter }), active ? _jsx(Text, { color: INK.brand, children: "█" }) : _jsx(Text, { children: " " })] }));
|
|
173
|
+
}
|
|
174
|
+
/** Elision marker rows for windowed lists. */
|
|
175
|
+
export function ElisionMark({ count, side, hint = "type to search" }) {
|
|
176
|
+
if (count <= 0)
|
|
177
|
+
return null;
|
|
178
|
+
return _jsx(Text, { dimColor: true, children: ` … ${count} ${side}${side === "below" ? ` — ${hint}` : ""}` });
|
|
179
|
+
}
|
|
180
|
+
/** Pointer cell: two columns everywhere so rows never shift when the cursor moves. */
|
|
181
|
+
export function Pointer({ on }) {
|
|
182
|
+
return on ? _jsx(Text, { color: INK.brand, children: `${GLYPHS.pointer} ` }) : _jsx(Text, { children: " " });
|
|
183
|
+
}
|
|
184
|
+
/** Verdict/toggle glyph vocabulary, colored per the design contract. */
|
|
185
|
+
export function Glyph({ kind }) {
|
|
186
|
+
switch (kind) {
|
|
187
|
+
case "on":
|
|
188
|
+
return _jsx(Text, { color: INK.brand, children: GLYPHS.toggleActive });
|
|
189
|
+
case "off":
|
|
190
|
+
return _jsx(Text, { dimColor: true, children: GLYPHS.toggleInactive });
|
|
191
|
+
case "fail":
|
|
192
|
+
return _jsx(Text, { color: INK.fail, children: GLYPHS.fail });
|
|
193
|
+
case "warn":
|
|
194
|
+
return _jsx(Text, { color: INK.warn, children: GLYPHS.attention });
|
|
195
|
+
case "unknown":
|
|
196
|
+
return _jsx(Text, { color: INK.warn, children: "?" });
|
|
197
|
+
default:
|
|
198
|
+
return _jsx(Text, { dimColor: true, children: GLYPHS.neutral });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/** Bordered overlay panel (rendered in the body region — the rail stays put). */
|
|
202
|
+
export function OverlayPanel({ title, children, width }) {
|
|
203
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderDimColor: true, paddingX: 1, width: width, alignSelf: "flex-start", children: [_jsx(Text, { bold: true, color: INK.brand, children: title }), children] }));
|
|
204
|
+
}
|
|
@@ -22,8 +22,9 @@ export type InitWizardResult = {
|
|
|
22
22
|
} | {
|
|
23
23
|
kind: "quit";
|
|
24
24
|
};
|
|
25
|
-
export declare function InitWizardApp({ fields }: {
|
|
25
|
+
export declare function InitWizardApp({ fields, frameColumns }: {
|
|
26
26
|
fields: InitWizardFields;
|
|
27
|
+
frameColumns?: number;
|
|
27
28
|
}): import("react").JSX.Element;
|
|
28
29
|
export declare function runInitWizardApp(opts: {
|
|
29
30
|
fields: InitWizardFields;
|
package/dist/tui/ink/init-app.js
CHANGED
|
@@ -1,103 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { emitKeypressEvents } from "node:readline";
|
|
3
|
-
import { PassThrough } from "node:stream";
|
|
4
2
|
import { Box, render, Text, useApp, useInput } from "ink";
|
|
5
3
|
import { useRef, useState } from "react";
|
|
6
|
-
import { GLYPHS } from "../../brand.js";
|
|
7
4
|
import { ToggleMark } from "./components.js";
|
|
5
|
+
import { clip, INK, inkInput, inkOutput, KeyBar, padCell, Pointer } from "./frame.js";
|
|
8
6
|
const DRIVERS = ["auto", "herdr", "subprocess"];
|
|
9
7
|
const VISIBILITY = ["pane", "headless"];
|
|
10
|
-
// inkOutput/inkInput replicate the compatibility bridges in fleet-app.tsx — they are
|
|
11
|
-
// not exported there and a sibling change owns that file, so importing was not an option when
|
|
12
|
-
// this act landed. If a third Ink act appears, hoist the pair into a shared module.
|
|
13
|
-
const escape = String.fromCharCode(27);
|
|
14
|
-
const inkBookkeepingWrites = {
|
|
15
|
-
"": true,
|
|
16
|
-
[`${escape}[?25l`]: true,
|
|
17
|
-
[`${escape}[?25h`]: true,
|
|
18
|
-
[`${escape}[?2026h`]: true,
|
|
19
|
-
[`${escape}[?2026l`]: true,
|
|
20
|
-
};
|
|
21
|
-
function inkOutput(output) {
|
|
22
|
-
if (typeof output.on === "function" && typeof output.off === "function")
|
|
23
|
-
return output;
|
|
24
|
-
const facade = Object.create(output);
|
|
25
|
-
const write = output.write.bind(output);
|
|
26
|
-
facade.write = ((chunk, ...args) => {
|
|
27
|
-
const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
|
|
28
|
-
if (inkBookkeepingWrites[text] === true)
|
|
29
|
-
return true;
|
|
30
|
-
return Reflect.apply(write, output, [chunk, ...args]);
|
|
31
|
-
});
|
|
32
|
-
facade.on = () => facade;
|
|
33
|
-
facade.off = () => facade;
|
|
34
|
-
return facade;
|
|
35
|
-
}
|
|
36
|
-
function inkInput(input, initialInput) {
|
|
37
|
-
const productionInput = typeof input.ref === "function" && typeof input.unref === "function";
|
|
38
|
-
// Isolate Ink's listeners on a bridge so every editor exit can detach the one listener it
|
|
39
|
-
// owns from the operator's terminal (see fleet-app.tsx for the full provenance).
|
|
40
|
-
const stream = new PassThrough();
|
|
41
|
-
stream.isTTY = input.isTTY;
|
|
42
|
-
stream.setRawMode = input.setRawMode?.bind(input);
|
|
43
|
-
stream.ref = () => {
|
|
44
|
-
if (productionInput)
|
|
45
|
-
input.ref();
|
|
46
|
-
return stream;
|
|
47
|
-
};
|
|
48
|
-
stream.unref = () => {
|
|
49
|
-
if (productionInput)
|
|
50
|
-
input.unref();
|
|
51
|
-
return stream;
|
|
52
|
-
};
|
|
53
|
-
const queued = [...initialInput];
|
|
54
|
-
let active = true;
|
|
55
|
-
let scheduled;
|
|
56
|
-
const pump = () => {
|
|
57
|
-
scheduled = undefined;
|
|
58
|
-
if (!active)
|
|
59
|
-
return;
|
|
60
|
-
const next = queued.shift();
|
|
61
|
-
if (next === undefined)
|
|
62
|
-
return;
|
|
63
|
-
stream.write(next);
|
|
64
|
-
scheduled = setTimeout(pump, 0);
|
|
65
|
-
};
|
|
66
|
-
const onData = (chunk) => {
|
|
67
|
-
queued.push(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
|
|
68
|
-
scheduled ??= setTimeout(pump, 0);
|
|
69
|
-
};
|
|
70
|
-
const onKeypress = (sequence, key) => {
|
|
71
|
-
const token = key?.sequence ?? sequence;
|
|
72
|
-
if (token === undefined)
|
|
73
|
-
return;
|
|
74
|
-
queued.push(token);
|
|
75
|
-
scheduled ??= setTimeout(pump, 0);
|
|
76
|
-
};
|
|
77
|
-
if (productionInput) {
|
|
78
|
-
input.on("data", onData);
|
|
79
|
-
}
|
|
80
|
-
else {
|
|
81
|
-
emitKeypressEvents(input);
|
|
82
|
-
input.on("keypress", onKeypress);
|
|
83
|
-
}
|
|
84
|
-
input.resume();
|
|
85
|
-
if (queued.length > 0)
|
|
86
|
-
scheduled = setTimeout(pump, 0);
|
|
87
|
-
return {
|
|
88
|
-
stream: stream,
|
|
89
|
-
stop() {
|
|
90
|
-
active = false;
|
|
91
|
-
clearTimeout(scheduled);
|
|
92
|
-
if (productionInput)
|
|
93
|
-
input.off("data", onData);
|
|
94
|
-
else
|
|
95
|
-
input.off("keypress", onKeypress);
|
|
96
|
-
input.pause();
|
|
97
|
-
stream.end();
|
|
98
|
-
},
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
8
|
// Three fields, one toggle, one action — a descriptor array, deliberately not a forms framework.
|
|
102
9
|
function buildRows(offerSkills) {
|
|
103
10
|
const rows = [
|
|
@@ -127,11 +34,9 @@ function buildRows(offerSkills) {
|
|
|
127
34
|
rows.push({ id: "continue", label: "Continue →", desc: "save these preferences and continue to discovery" });
|
|
128
35
|
return rows;
|
|
129
36
|
}
|
|
130
|
-
|
|
131
|
-
export function InitWizardApp({ fields }) {
|
|
37
|
+
export function InitWizardApp({ fields, frameColumns = 74 }) {
|
|
132
38
|
const { exit } = useApp();
|
|
133
39
|
const rows = buildRows(fields.offerSkills);
|
|
134
|
-
const sections = fields.offerSkills ? ["Run", "Skills"] : ["Run"];
|
|
135
40
|
const [cursor, setCursor] = useState(0);
|
|
136
41
|
const cursorRef = useRef(0);
|
|
137
42
|
const driverRef = useRef(fields.driver);
|
|
@@ -247,13 +152,31 @@ export function InitWizardApp({ fields }) {
|
|
|
247
152
|
return "";
|
|
248
153
|
}
|
|
249
154
|
};
|
|
250
|
-
const
|
|
251
|
-
|
|
155
|
+
const width = Math.max(58, Math.min(frameColumns, 100));
|
|
156
|
+
const rendered = [];
|
|
157
|
+
for (const [index, row] of rows.entries()) {
|
|
158
|
+
if (row.section && rows[index - 1]?.section !== row.section) {
|
|
159
|
+
if (index > 0)
|
|
160
|
+
rendered.push(_jsx(Text, { children: " " }, `${row.section}-gap`));
|
|
161
|
+
rendered.push(_jsx(Text, { dimColor: true, children: row.section }, row.section));
|
|
162
|
+
}
|
|
163
|
+
if (row.id === "continue")
|
|
164
|
+
rendered.push(_jsx(Text, { children: " " }, "continue-gap"));
|
|
165
|
+
rendered.push(_jsxs(Text, { children: [_jsx(Pointer, { on: index === cursor }), _jsx(Text, { bold: index === cursor, children: row.id === "continue" ? row.label : padCell(row.label, 26) }), _jsx(Text, { color: index === cursor ? INK.brand : undefined, children: valueOf(row) })] }, row.id));
|
|
166
|
+
}
|
|
167
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, color: INK.brand, children: " tickmarkr init" }), _jsx(Text, { dimColor: true, children: " · act 1 of 3" })] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderDimColor: true, paddingX: 1, children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, color: INK.brand, children: "Preferences" }), _jsx(Text, { dimColor: true, children: " › Discovery › Fleet" })] }), _jsx(Text, { children: " " }), rendered] }), _jsx(Text, { dimColor: true, children: ` ${clip(rows[cursor].desc, width - 2)}` }), _jsx(KeyBar, { binds: [
|
|
168
|
+
{ key: "Enter/Space", label: "change" },
|
|
169
|
+
{ key: "Tab", label: "section" },
|
|
170
|
+
{ key: "↑↓", label: "move" },
|
|
171
|
+
{ key: "Esc", label: "quit" },
|
|
172
|
+
] })] }));
|
|
252
173
|
}
|
|
253
174
|
export async function runInitWizardApp(opts) {
|
|
254
175
|
const bridgedInput = inkInput(opts.input, opts.initialInput ?? []);
|
|
255
176
|
const legacyOutput = typeof opts.output.on !== "function" || typeof opts.output.off !== "function";
|
|
256
|
-
|
|
177
|
+
// OBS-527: geometry is a live input — rebuild the element on resize (same pattern as fleet-app)
|
|
178
|
+
const wizardElement = () => _jsx(InitWizardApp, { fields: opts.fields, frameColumns: opts.output.columns ?? 74 });
|
|
179
|
+
const app = render(wizardElement(), {
|
|
257
180
|
stdin: bridgedInput.stream,
|
|
258
181
|
stdout: inkOutput(opts.output),
|
|
259
182
|
exitOnCtrlC: false,
|
|
@@ -262,10 +185,15 @@ export async function runInitWizardApp(opts) {
|
|
|
262
185
|
// fleet-app.tsx): disable Ink's render throttling only for that facade.
|
|
263
186
|
debug: legacyOutput,
|
|
264
187
|
});
|
|
188
|
+
const onResize = () => app.rerender(wizardElement());
|
|
189
|
+
if (!legacyOutput)
|
|
190
|
+
opts.output.on("resize", onResize);
|
|
265
191
|
try {
|
|
266
192
|
return await app.waitUntilExit();
|
|
267
193
|
}
|
|
268
194
|
finally {
|
|
195
|
+
if (!legacyOutput)
|
|
196
|
+
opts.output.off("resize", onResize);
|
|
269
197
|
app.unmount();
|
|
270
198
|
bridgedInput.stop();
|
|
271
199
|
}
|