tickmarkr 1.91.0 → 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.
@@ -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;
@@ -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
- const HINT_BAR = "Enter/Space to change · Tab to jump sections · ↑/↓ to move · Enter on Continue · Esc to quit";
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 currentSection = rows.slice(0, cursor + 1).reduce((section, row) => row.section ?? section, rows[0].section);
251
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "tickmarkr init" }), _jsxs(Text, { children: [_jsx(Text, { bold: true, children: "Preferences" }), _jsx(Text, { dimColor: true, children: " \u00B7 Discovery \u00B7 Fleet" })] }), _jsx(Text, { dimColor: true, children: "act 1 of 3 \u2014 run preferences for this repo" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Box, { flexDirection: "column", width: 10, marginRight: 2, children: sections.map((section) => (_jsx(Text, { bold: section === currentSection, dimColor: section !== currentSection, children: section }, section))) }), _jsx(Box, { flexDirection: "column", children: rows.map((row, index) => (_jsxs(Text, { bold: index === cursor, children: [index === cursor ? `${GLYPHS.pointer} ` : " ", row.id === "continue" ? row.label : row.label.padEnd(24), valueOf(row)] }, row.id))) })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: rows[cursor].desc }), _jsx(Text, { dimColor: true, children: HINT_BAR })] })] }));
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
- const app = render(_jsx(InitWizardApp, { fields: opts.fields }), {
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tickmarkr",
3
- "version": "1.91.0",
3
+ "version": "1.92.0",
4
4
  "description": "Spec in, verified work out.",
5
5
  "type": "module",
6
6
  "license": "MIT",