tickmarkr 1.64.0 → 1.66.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/dist/adapters/claude-code.js +3 -0
- package/dist/adapters/codex.js +3 -0
- package/dist/adapters/cursor-agent.js +2 -0
- package/dist/adapters/grok.js +2 -0
- package/dist/adapters/kimi.js +3 -0
- package/dist/adapters/opencode.js +3 -0
- package/dist/adapters/pi.js +2 -0
- package/dist/adapters/prompt.d.ts +4 -0
- package/dist/adapters/prompt.js +28 -2
- package/dist/adapters/registry.d.ts +3 -0
- package/dist/adapters/registry.js +38 -0
- package/dist/adapters/types.d.ts +4 -0
- package/dist/cli/commands/doctor.js +4 -1
- package/dist/cli/commands/status.js +12 -4
- package/dist/cli/commands/ui.d.ts +5 -0
- package/dist/cli/commands/ui.js +13 -0
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +3 -1
- package/dist/config/config.js +4 -1
- package/dist/run/activity.d.ts +15 -0
- package/dist/run/activity.js +96 -0
- package/dist/run/consult.js +4 -1
- package/dist/run/daemon.js +35 -1
- package/dist/run/stall.d.ts +7 -3
- package/dist/run/stall.js +71 -3
- package/dist/tui/app.d.ts +40 -0
- package/dist/tui/app.js +130 -0
- package/dist/tui/engine.d.ts +52 -0
- package/dist/tui/engine.js +92 -0
- package/dist/tui/frame.d.ts +22 -0
- package/dist/tui/frame.js +56 -0
- package/dist/tui/input.d.ts +41 -0
- package/dist/tui/input.js +100 -0
- package/dist/tui/views/fleet-view.d.ts +69 -0
- package/dist/tui/views/fleet-view.js +243 -0
- package/dist/tui/views/preview-view.d.ts +7 -0
- package/dist/tui/views/preview-view.js +80 -0
- package/dist/tui/views/profile-view.d.ts +15 -0
- package/dist/tui/views/profile-view.js +93 -0
- package/dist/tui/views/routing-view.d.ts +29 -0
- package/dist/tui/views/routing-view.js +98 -0
- package/package.json +1 -1
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type OutputStream } from "../tui/engine.js";
|
|
2
|
+
import type { InputStream } from "../tui/input.js";
|
|
3
|
+
export type View = {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
render(props: {
|
|
7
|
+
cols: number;
|
|
8
|
+
rows: number;
|
|
9
|
+
}): string[];
|
|
10
|
+
};
|
|
11
|
+
export type StudioOptions = {
|
|
12
|
+
input: InputStream;
|
|
13
|
+
output: OutputStream;
|
|
14
|
+
};
|
|
15
|
+
export declare class StudioApp {
|
|
16
|
+
private engine;
|
|
17
|
+
private views;
|
|
18
|
+
private active;
|
|
19
|
+
private showingHelp;
|
|
20
|
+
private notice;
|
|
21
|
+
private _lines;
|
|
22
|
+
private resolveExit;
|
|
23
|
+
readonly exited: Promise<void>;
|
|
24
|
+
constructor(opts: StudioOptions);
|
|
25
|
+
/** Current line model last rendered or prepared. */
|
|
26
|
+
get lines(): string[];
|
|
27
|
+
/** Labels of every registered view, in order. */
|
|
28
|
+
get viewLabels(): string[];
|
|
29
|
+
/** Enter the alternate screen and start routing input. */
|
|
30
|
+
start(): void;
|
|
31
|
+
/** Restore the terminal and release the input stream. */
|
|
32
|
+
stop(): void;
|
|
33
|
+
private bindKeys;
|
|
34
|
+
private setView;
|
|
35
|
+
private showNotice;
|
|
36
|
+
private paint;
|
|
37
|
+
private buildFrame;
|
|
38
|
+
private renderTabBar;
|
|
39
|
+
private renderHelp;
|
|
40
|
+
}
|
package/dist/tui/app.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { TerminalEngine } from "../tui/engine.js";
|
|
2
|
+
import { createFleetView } from "./views/fleet-view.js";
|
|
3
|
+
import { createPreviewView } from "./views/preview-view.js";
|
|
4
|
+
import { createProfileView } from "./views/profile-view.js";
|
|
5
|
+
import { createRoutingView } from "./views/routing-view.js";
|
|
6
|
+
const MUTATION_KEYS = ["r", "s", "m", "return"];
|
|
7
|
+
const READONLY_NOTICE = "read-only in v1.66 — write path ships in v1.67";
|
|
8
|
+
export class StudioApp {
|
|
9
|
+
engine;
|
|
10
|
+
views;
|
|
11
|
+
active = 0;
|
|
12
|
+
showingHelp = false;
|
|
13
|
+
notice = null;
|
|
14
|
+
_lines = [];
|
|
15
|
+
resolveExit = null;
|
|
16
|
+
exited;
|
|
17
|
+
constructor(opts) {
|
|
18
|
+
this.engine = new TerminalEngine({
|
|
19
|
+
input: opts.input,
|
|
20
|
+
output: opts.output,
|
|
21
|
+
onResize: () => this.paint(),
|
|
22
|
+
});
|
|
23
|
+
this.views = [createFleetView(), createRoutingView(), createPreviewView(), createProfileView()];
|
|
24
|
+
this.exited = new Promise((resolve) => {
|
|
25
|
+
this.resolveExit = resolve;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/** Current line model last rendered or prepared. */
|
|
29
|
+
get lines() {
|
|
30
|
+
return this._lines;
|
|
31
|
+
}
|
|
32
|
+
/** Labels of every registered view, in order. */
|
|
33
|
+
get viewLabels() {
|
|
34
|
+
return this.views.map((v) => v.label);
|
|
35
|
+
}
|
|
36
|
+
/** Enter the alternate screen and start routing input. */
|
|
37
|
+
start() {
|
|
38
|
+
this.bindKeys();
|
|
39
|
+
this.paint();
|
|
40
|
+
this.engine.start(this._lines);
|
|
41
|
+
}
|
|
42
|
+
/** Restore the terminal and release the input stream. */
|
|
43
|
+
stop() {
|
|
44
|
+
this.engine.stop();
|
|
45
|
+
if (this.resolveExit) {
|
|
46
|
+
this.resolveExit();
|
|
47
|
+
this.resolveExit = null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
bindKeys() {
|
|
51
|
+
// view switching
|
|
52
|
+
// Tab is decoded as the raw "\t" sequence by the micro-engine.
|
|
53
|
+
this.engine.key("\t", () => this.setView((this.active + 1) % this.views.length));
|
|
54
|
+
for (let i = 1; i <= this.views.length; i++) {
|
|
55
|
+
const idx = i - 1;
|
|
56
|
+
this.engine.key(String(i), () => this.setView(idx));
|
|
57
|
+
}
|
|
58
|
+
// help overlay
|
|
59
|
+
this.engine.key("?", () => {
|
|
60
|
+
this.showingHelp = true;
|
|
61
|
+
this.paint();
|
|
62
|
+
});
|
|
63
|
+
// dismissal / quit
|
|
64
|
+
this.engine.key("escape", () => {
|
|
65
|
+
if (this.showingHelp) {
|
|
66
|
+
this.showingHelp = false;
|
|
67
|
+
this.paint();
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
this.stop();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
this.engine.key("q", () => this.stop());
|
|
74
|
+
// mutation keys — read-only until the v1.67 write path
|
|
75
|
+
for (const key of MUTATION_KEYS) {
|
|
76
|
+
this.engine.key(key, () => this.showNotice());
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
setView(idx) {
|
|
80
|
+
this.active = idx;
|
|
81
|
+
this.showingHelp = false;
|
|
82
|
+
this.notice = null;
|
|
83
|
+
this.paint();
|
|
84
|
+
}
|
|
85
|
+
showNotice() {
|
|
86
|
+
this.notice = READONLY_NOTICE;
|
|
87
|
+
this.paint();
|
|
88
|
+
}
|
|
89
|
+
paint() {
|
|
90
|
+
this._lines = this.buildFrame();
|
|
91
|
+
this.engine.render(this._lines);
|
|
92
|
+
}
|
|
93
|
+
buildFrame() {
|
|
94
|
+
const { cols, rows } = this.engine.size;
|
|
95
|
+
const lines = [];
|
|
96
|
+
lines.push(this.renderTabBar());
|
|
97
|
+
if (this.showingHelp) {
|
|
98
|
+
lines.push(...this.renderHelp());
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
const view = this.views[this.active];
|
|
102
|
+
lines.push(...view.render({ cols, rows: Math.max(rows - 2, 1) }));
|
|
103
|
+
}
|
|
104
|
+
if (this.notice) {
|
|
105
|
+
lines.push(this.notice);
|
|
106
|
+
}
|
|
107
|
+
return lines;
|
|
108
|
+
}
|
|
109
|
+
renderTabBar() {
|
|
110
|
+
const parts = ["tickmarkr ui ──"];
|
|
111
|
+
for (let i = 0; i < this.views.length; i++) {
|
|
112
|
+
const view = this.views[i];
|
|
113
|
+
const marker = i === this.active ? `*${view.label}` : view.label;
|
|
114
|
+
parts.push(`[${i + 1}]${marker}`);
|
|
115
|
+
}
|
|
116
|
+
return parts.join(" ");
|
|
117
|
+
}
|
|
118
|
+
renderHelp() {
|
|
119
|
+
return [
|
|
120
|
+
"Key bindings",
|
|
121
|
+
"1-4 switch view",
|
|
122
|
+
"tab cycle views",
|
|
123
|
+
"? show this help",
|
|
124
|
+
"esc / q quit",
|
|
125
|
+
"r re-probe (read-only)",
|
|
126
|
+
"s save (read-only)",
|
|
127
|
+
"m mode (read-only)",
|
|
128
|
+
];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type Key, type KeyHandler, type InputStream } from "./input.js";
|
|
2
|
+
export type { Key, KeyHandler };
|
|
3
|
+
/** Writable stream with the small surface the engine needs. */
|
|
4
|
+
export interface OutputStream {
|
|
5
|
+
isTTY?: boolean;
|
|
6
|
+
columns?: number;
|
|
7
|
+
rows?: number;
|
|
8
|
+
write: (chunk: string) => unknown;
|
|
9
|
+
on?(event: "resize", listener: () => void): this;
|
|
10
|
+
off?(event: "resize", listener: () => void): this;
|
|
11
|
+
}
|
|
12
|
+
export interface EngineOptions {
|
|
13
|
+
input: InputStream;
|
|
14
|
+
output: OutputStream;
|
|
15
|
+
onResize?: (cols: number, rows: number) => void;
|
|
16
|
+
onError?: (error: unknown) => void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Dependency-free terminal engine: alternate-screen line-diff renderer, named-key routing,
|
|
20
|
+
* resize tracking, and terminal restoration on every exit path including thrown handlers.
|
|
21
|
+
*/
|
|
22
|
+
export declare class TerminalEngine {
|
|
23
|
+
private opts;
|
|
24
|
+
private frame;
|
|
25
|
+
private input;
|
|
26
|
+
private lastLines;
|
|
27
|
+
private _cols;
|
|
28
|
+
private _rows;
|
|
29
|
+
private running;
|
|
30
|
+
private userHandlers;
|
|
31
|
+
constructor(opts: EngineOptions);
|
|
32
|
+
/** Enter the alternate screen, begin listening for keys and resize events. */
|
|
33
|
+
start(lines: string[]): void;
|
|
34
|
+
/** Restore the terminal and stop listening. Safe to call multiple times. */
|
|
35
|
+
stop(): void;
|
|
36
|
+
/** Update the line model; only changed lines are repainted. */
|
|
37
|
+
render(lines: string[]): void;
|
|
38
|
+
/** Register a handler for a named key. */
|
|
39
|
+
key(name: string, handler: KeyHandler): void;
|
|
40
|
+
/** Unregister a handler. */
|
|
41
|
+
unkey(name: string, handler: KeyHandler): void;
|
|
42
|
+
/** Trigger a full repaint at the new size. */
|
|
43
|
+
resize(cols: number, rows: number): void;
|
|
44
|
+
/** Current terminal size as last seen or reported by a resize event. */
|
|
45
|
+
get size(): {
|
|
46
|
+
cols: number;
|
|
47
|
+
rows: number;
|
|
48
|
+
};
|
|
49
|
+
private onResize;
|
|
50
|
+
private attachResize;
|
|
51
|
+
private detachResize;
|
|
52
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Frame } from "./frame.js";
|
|
2
|
+
import { InputRouter } from "./input.js";
|
|
3
|
+
/**
|
|
4
|
+
* Dependency-free terminal engine: alternate-screen line-diff renderer, named-key routing,
|
|
5
|
+
* resize tracking, and terminal restoration on every exit path including thrown handlers.
|
|
6
|
+
*/
|
|
7
|
+
export class TerminalEngine {
|
|
8
|
+
opts;
|
|
9
|
+
frame;
|
|
10
|
+
input;
|
|
11
|
+
lastLines = [];
|
|
12
|
+
_cols;
|
|
13
|
+
_rows;
|
|
14
|
+
running = false;
|
|
15
|
+
userHandlers = new Map();
|
|
16
|
+
constructor(opts) {
|
|
17
|
+
this.opts = opts;
|
|
18
|
+
this.frame = new Frame(opts.output);
|
|
19
|
+
this.input = new InputRouter(opts.input);
|
|
20
|
+
this._cols = opts.output.columns ?? 80;
|
|
21
|
+
this._rows = opts.output.rows ?? 24;
|
|
22
|
+
}
|
|
23
|
+
/** Enter the alternate screen, begin listening for keys and resize events. */
|
|
24
|
+
start(lines) {
|
|
25
|
+
if (this.running)
|
|
26
|
+
return;
|
|
27
|
+
this.running = true;
|
|
28
|
+
this.lastLines = lines.slice();
|
|
29
|
+
this.frame.enter(this.lastLines);
|
|
30
|
+
this.input.start();
|
|
31
|
+
this.attachResize();
|
|
32
|
+
}
|
|
33
|
+
/** Restore the terminal and stop listening. Safe to call multiple times. */
|
|
34
|
+
stop() {
|
|
35
|
+
if (!this.running)
|
|
36
|
+
return;
|
|
37
|
+
this.detachResize();
|
|
38
|
+
this.input.stop();
|
|
39
|
+
this.frame.exit();
|
|
40
|
+
this.running = false;
|
|
41
|
+
}
|
|
42
|
+
/** Update the line model; only changed lines are repainted. */
|
|
43
|
+
render(lines) {
|
|
44
|
+
this.lastLines = lines.slice();
|
|
45
|
+
this.frame.render(this.lastLines);
|
|
46
|
+
}
|
|
47
|
+
/** Register a handler for a named key. */
|
|
48
|
+
key(name, handler) {
|
|
49
|
+
const wrapped = async (k) => {
|
|
50
|
+
try {
|
|
51
|
+
await handler(k);
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
this.stop();
|
|
55
|
+
this.opts.onError?.(e);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
this.userHandlers.set(handler, wrapped);
|
|
59
|
+
this.input.on(name, wrapped);
|
|
60
|
+
}
|
|
61
|
+
/** Unregister a handler. */
|
|
62
|
+
unkey(name, handler) {
|
|
63
|
+
const wrapped = this.userHandlers.get(handler);
|
|
64
|
+
if (wrapped) {
|
|
65
|
+
this.input.off(name, wrapped);
|
|
66
|
+
this.userHandlers.delete(handler);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** Trigger a full repaint at the new size. */
|
|
70
|
+
resize(cols, rows) {
|
|
71
|
+
this._cols = cols;
|
|
72
|
+
this._rows = rows;
|
|
73
|
+
this.frame.resize(this.lastLines);
|
|
74
|
+
this.opts.onResize?.(cols, rows);
|
|
75
|
+
}
|
|
76
|
+
/** Current terminal size as last seen or reported by a resize event. */
|
|
77
|
+
get size() {
|
|
78
|
+
return { cols: this._cols, rows: this._rows };
|
|
79
|
+
}
|
|
80
|
+
onResize = () => {
|
|
81
|
+
this._cols = this.opts.output.columns ?? this._cols;
|
|
82
|
+
this._rows = this.opts.output.rows ?? this._rows;
|
|
83
|
+
this.frame.resize(this.lastLines);
|
|
84
|
+
this.opts.onResize?.(this._cols, this._rows);
|
|
85
|
+
};
|
|
86
|
+
attachResize() {
|
|
87
|
+
this.opts.output.on?.("resize", this.onResize);
|
|
88
|
+
}
|
|
89
|
+
detachResize() {
|
|
90
|
+
this.opts.output.off?.("resize", this.onResize);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** A write target — anything with a write(chunk: string) method. */
|
|
2
|
+
export interface OutputSink {
|
|
3
|
+
write: (chunk: string) => unknown;
|
|
4
|
+
}
|
|
5
|
+
/** Frame state + diff rendering. */
|
|
6
|
+
export declare class Frame {
|
|
7
|
+
private out;
|
|
8
|
+
private prev;
|
|
9
|
+
private active;
|
|
10
|
+
constructor(out: OutputSink);
|
|
11
|
+
/** Enter the alternate screen, hide the cursor, and paint the initial frame. */
|
|
12
|
+
enter(lines: string[]): void;
|
|
13
|
+
/**
|
|
14
|
+
* Paint only the lines that differ from the previous model. Lines are addressed with
|
|
15
|
+
* absolute cursor positioning; unchanged rows produce no output.
|
|
16
|
+
*/
|
|
17
|
+
render(lines: string[]): void;
|
|
18
|
+
/** Invalidate the cached frame so the next render writes every line. */
|
|
19
|
+
resize(lines: string[]): void;
|
|
20
|
+
/** Restore the terminal: show cursor and return to the normal screen buffer. */
|
|
21
|
+
exit(): void;
|
|
22
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Line-diff renderer for the alternate screen. Maintains the previous line model and
|
|
2
|
+
// repaints only rows that changed since the last render, keeping terminal traffic small.
|
|
3
|
+
/** Frame state + diff rendering. */
|
|
4
|
+
export class Frame {
|
|
5
|
+
out;
|
|
6
|
+
prev = [];
|
|
7
|
+
active = false;
|
|
8
|
+
constructor(out) {
|
|
9
|
+
this.out = out;
|
|
10
|
+
}
|
|
11
|
+
/** Enter the alternate screen, hide the cursor, and paint the initial frame. */
|
|
12
|
+
enter(lines) {
|
|
13
|
+
this.active = true;
|
|
14
|
+
this.prev = [];
|
|
15
|
+
this.out.write("\x1b[?1049h"); // alternate screen buffer
|
|
16
|
+
this.out.write("\x1b[?25l"); // hide cursor
|
|
17
|
+
this.render(lines);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Paint only the lines that differ from the previous model. Lines are addressed with
|
|
21
|
+
* absolute cursor positioning; unchanged rows produce no output.
|
|
22
|
+
*/
|
|
23
|
+
render(lines) {
|
|
24
|
+
if (!this.active)
|
|
25
|
+
return;
|
|
26
|
+
const curr = lines.slice();
|
|
27
|
+
const max = Math.max(this.prev.length, curr.length);
|
|
28
|
+
for (let i = 0; i < max; i++) {
|
|
29
|
+
if (this.prev[i] !== curr[i]) {
|
|
30
|
+
this.out.write(`\x1b[${i + 1};1H`); // row i+1, column 1
|
|
31
|
+
this.out.write(curr[i] ?? "");
|
|
32
|
+
this.out.write("\x1b[K"); // clear to end of line
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (curr.length < this.prev.length) {
|
|
36
|
+
// The frame shrank — clear everything below the new last line.
|
|
37
|
+
this.out.write(`\x1b[${curr.length + 1};1H`);
|
|
38
|
+
this.out.write("\x1b[J");
|
|
39
|
+
}
|
|
40
|
+
this.prev = curr;
|
|
41
|
+
}
|
|
42
|
+
/** Invalidate the cached frame so the next render writes every line. */
|
|
43
|
+
resize(lines) {
|
|
44
|
+
this.prev = [];
|
|
45
|
+
this.render(lines);
|
|
46
|
+
}
|
|
47
|
+
/** Restore the terminal: show cursor and return to the normal screen buffer. */
|
|
48
|
+
exit() {
|
|
49
|
+
if (!this.active)
|
|
50
|
+
return;
|
|
51
|
+
this.out.write("\x1b[?25h"); // show cursor
|
|
52
|
+
this.out.write("\x1b[?1049l"); // normal screen buffer
|
|
53
|
+
this.active = false;
|
|
54
|
+
this.prev = [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** Decoded keypress shape. */
|
|
2
|
+
export interface Key {
|
|
3
|
+
name?: string;
|
|
4
|
+
sequence?: string;
|
|
5
|
+
ctrl?: boolean;
|
|
6
|
+
meta?: boolean;
|
|
7
|
+
shift?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/** Handler for a named key. May be async. */
|
|
10
|
+
export type KeyHandler = (key: Key) => void | Promise<void>;
|
|
11
|
+
/** Readable stream with the small TTY surface the engine needs. */
|
|
12
|
+
export interface InputStream extends NodeJS.ReadableStream {
|
|
13
|
+
isTTY?: boolean;
|
|
14
|
+
setRawMode?: (mode: boolean) => unknown;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Decode the next key at the start of a byte buffer. Returns the decoded key and
|
|
18
|
+
* how many bytes were consumed. Undefined when more bytes are needed.
|
|
19
|
+
*/
|
|
20
|
+
export declare function decodeOneKey(buf: Buffer): {
|
|
21
|
+
key: Key;
|
|
22
|
+
consumed: number;
|
|
23
|
+
} | undefined;
|
|
24
|
+
/** Routes decoded keypress events to handlers registered by key name. */
|
|
25
|
+
export declare class InputRouter {
|
|
26
|
+
private input;
|
|
27
|
+
private handlers;
|
|
28
|
+
private started;
|
|
29
|
+
private buffer;
|
|
30
|
+
constructor(input: InputStream);
|
|
31
|
+
/** Register a handler for a named key (e.g. "up", "escape", "q"). */
|
|
32
|
+
on(name: string, handler: KeyHandler): void;
|
|
33
|
+
/** Remove a previously registered handler. */
|
|
34
|
+
off(name: string, handler: KeyHandler): void;
|
|
35
|
+
/** Start decoding keypresses from the input stream. Idempotent. */
|
|
36
|
+
start(): void;
|
|
37
|
+
/** Stop decoding and release the stream. Idempotent. */
|
|
38
|
+
stop(): void;
|
|
39
|
+
private onData;
|
|
40
|
+
private dispatch;
|
|
41
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decode the next key at the start of a byte buffer. Returns the decoded key and
|
|
3
|
+
* how many bytes were consumed. Undefined when more bytes are needed.
|
|
4
|
+
*/
|
|
5
|
+
export function decodeOneKey(buf) {
|
|
6
|
+
if (buf.length === 0)
|
|
7
|
+
return undefined;
|
|
8
|
+
const b0 = buf[0];
|
|
9
|
+
if (b0 === 0x1b) {
|
|
10
|
+
// CSI arrow sequences: ESC [ A/B/C/D. Keep an incomplete CSI buffered so
|
|
11
|
+
// arbitrary stream chunking cannot turn an arrow into Escape + plain keys.
|
|
12
|
+
if (buf[1] === 0x5b) {
|
|
13
|
+
if (buf.length < 3)
|
|
14
|
+
return undefined;
|
|
15
|
+
const name = buf[2] === 0x41 ? "up"
|
|
16
|
+
: buf[2] === 0x42 ? "down"
|
|
17
|
+
: buf[2] === 0x43 ? "right"
|
|
18
|
+
: buf[2] === 0x44 ? "left"
|
|
19
|
+
: undefined;
|
|
20
|
+
if (name) {
|
|
21
|
+
return { key: { name, sequence: buf.subarray(0, 3).toString("binary") }, consumed: 3 };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// A lone ESC (or ESC followed by something we do not recognise) is Escape.
|
|
25
|
+
return { key: { name: "escape", sequence: "\x1b" }, consumed: 1 };
|
|
26
|
+
}
|
|
27
|
+
if (b0 === 0x0d || b0 === 0x0a) {
|
|
28
|
+
return { key: { name: "return", sequence: String.fromCharCode(b0) }, consumed: 1 };
|
|
29
|
+
}
|
|
30
|
+
if (b0 === 0x03) {
|
|
31
|
+
return { key: { name: "c", ctrl: true, sequence: "\x03" }, consumed: 1 };
|
|
32
|
+
}
|
|
33
|
+
if (b0 >= 0x20 && b0 < 0x7f) {
|
|
34
|
+
return { key: { name: String.fromCharCode(b0), sequence: String.fromCharCode(b0) }, consumed: 1 };
|
|
35
|
+
}
|
|
36
|
+
// Unknown byte: consume it without a name so it does not block the stream.
|
|
37
|
+
return { key: { sequence: String.fromCharCode(b0) }, consumed: 1 };
|
|
38
|
+
}
|
|
39
|
+
/** Routes decoded keypress events to handlers registered by key name. */
|
|
40
|
+
export class InputRouter {
|
|
41
|
+
input;
|
|
42
|
+
handlers = new Map();
|
|
43
|
+
started = false;
|
|
44
|
+
buffer = Buffer.alloc(0);
|
|
45
|
+
constructor(input) {
|
|
46
|
+
this.input = input;
|
|
47
|
+
}
|
|
48
|
+
/** Register a handler for a named key (e.g. "up", "escape", "q"). */
|
|
49
|
+
on(name, handler) {
|
|
50
|
+
let set = this.handlers.get(name);
|
|
51
|
+
if (!set) {
|
|
52
|
+
set = new Set();
|
|
53
|
+
this.handlers.set(name, set);
|
|
54
|
+
}
|
|
55
|
+
set.add(handler);
|
|
56
|
+
}
|
|
57
|
+
/** Remove a previously registered handler. */
|
|
58
|
+
off(name, handler) {
|
|
59
|
+
this.handlers.get(name)?.delete(handler);
|
|
60
|
+
}
|
|
61
|
+
/** Start decoding keypresses from the input stream. Idempotent. */
|
|
62
|
+
start() {
|
|
63
|
+
if (this.started)
|
|
64
|
+
return;
|
|
65
|
+
this.started = true;
|
|
66
|
+
this.input.setRawMode?.(true);
|
|
67
|
+
this.input.on("data", this.onData);
|
|
68
|
+
this.input.resume();
|
|
69
|
+
}
|
|
70
|
+
/** Stop decoding and release the stream. Idempotent. */
|
|
71
|
+
stop() {
|
|
72
|
+
if (!this.started)
|
|
73
|
+
return;
|
|
74
|
+
this.input.off("data", this.onData);
|
|
75
|
+
this.input.setRawMode?.(false);
|
|
76
|
+
this.input.pause();
|
|
77
|
+
this.started = false;
|
|
78
|
+
}
|
|
79
|
+
onData = (chunk) => {
|
|
80
|
+
const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "binary");
|
|
81
|
+
this.buffer = Buffer.concat([this.buffer, data]);
|
|
82
|
+
while (this.buffer.length > 0) {
|
|
83
|
+
const result = decodeOneKey(this.buffer);
|
|
84
|
+
if (!result)
|
|
85
|
+
break;
|
|
86
|
+
this.dispatch(result.key);
|
|
87
|
+
this.buffer = this.buffer.subarray(result.consumed);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
dispatch(key) {
|
|
91
|
+
const name = key.name ?? key.sequence;
|
|
92
|
+
if (!name)
|
|
93
|
+
return;
|
|
94
|
+
const set = this.handlers.get(name);
|
|
95
|
+
if (!set)
|
|
96
|
+
return;
|
|
97
|
+
for (const handler of set)
|
|
98
|
+
void handler(key);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { View } from "../app.js";
|
|
2
|
+
import type { AuthHealth } from "../../adapters/types.js";
|
|
3
|
+
import type { Tier } from "../../config/config.js";
|
|
4
|
+
import type { TelemetryRow } from "../../run/journal.js";
|
|
5
|
+
/** One discovered adapter and its classified models (cfg.tiers via channels()). */
|
|
6
|
+
export type FleetRosterEntry = {
|
|
7
|
+
adapter: string;
|
|
8
|
+
models: {
|
|
9
|
+
model: string;
|
|
10
|
+
tier: Tier;
|
|
11
|
+
channel: "sub" | "api";
|
|
12
|
+
}[];
|
|
13
|
+
};
|
|
14
|
+
/** Provenance notes harvested from overlay `# comment`s (fleet-overlay.ts shapes). */
|
|
15
|
+
export type FleetProvenanceNotes = {
|
|
16
|
+
denyAdapters?: Record<string, string>;
|
|
17
|
+
denyModels?: Record<string, string>;
|
|
18
|
+
};
|
|
19
|
+
/** Everything the roster renders — loaded by the caller, never by the render path. */
|
|
20
|
+
export type FleetViewData = {
|
|
21
|
+
adapters: FleetRosterEntry[];
|
|
22
|
+
/** Doctor cache contents (doctor.json), keyed by adapter id. */
|
|
23
|
+
health: Record<string, AuthHealth>;
|
|
24
|
+
/** Age of the doctor cache in ms; null when no probe data exists. */
|
|
25
|
+
doctorAgeMs: number | null;
|
|
26
|
+
denyAdapters: string[];
|
|
27
|
+
/** routing.deny.models entries in channelKey form ("adapter:model"). */
|
|
28
|
+
denyModels: string[];
|
|
29
|
+
notes?: FleetProvenanceNotes;
|
|
30
|
+
/** Telemetry folded from recent run journals (readAllTelemetry); runId optional. */
|
|
31
|
+
telemetry: (TelemetryRow & {
|
|
32
|
+
runId?: string;
|
|
33
|
+
})[];
|
|
34
|
+
};
|
|
35
|
+
/** Per-channel health digest, folded from journal telemetry rows only. */
|
|
36
|
+
export type ChannelHealth = {
|
|
37
|
+
key: string;
|
|
38
|
+
tasks: number;
|
|
39
|
+
attempts: number;
|
|
40
|
+
done: number;
|
|
41
|
+
failed: number;
|
|
42
|
+
human: number;
|
|
43
|
+
overruns: number;
|
|
44
|
+
quotaFailovers: number;
|
|
45
|
+
/** Sum of OBSERVED gateFails; null when no row carried the field (unobserved, never 0). */
|
|
46
|
+
gateFails: number | null;
|
|
47
|
+
medianDurationMs: number | null;
|
|
48
|
+
lastOutcome: TelemetryRow["outcome"] | null;
|
|
49
|
+
};
|
|
50
|
+
/** Fold journal telemetry rows into a per-channel health digest keyed by channelKey
|
|
51
|
+
* ("adapter:model"). The digest derives from journal telemetry (TelemetryRow, the rows the
|
|
52
|
+
* daemon appends to each run's telemetry.jsonl) rather than any new state file: this module
|
|
53
|
+
* reads nothing and writes nothing — the caller folds rows out of the existing run journals
|
|
54
|
+
* (readAllTelemetry) and the fold is a pure function of them, same telemetry in, same digest
|
|
55
|
+
* out. */
|
|
56
|
+
export declare function foldChannelHealth(rows: (TelemetryRow & {
|
|
57
|
+
runId?: string;
|
|
58
|
+
})[]): Map<string, ChannelHealth>;
|
|
59
|
+
/** The studio View plus the roster's own key seam. The T2 shell binds only view-level keys, so
|
|
60
|
+
* expand/collapse and selection are driven through key() — the same names the engine decodes. */
|
|
61
|
+
export type FleetView = View & {
|
|
62
|
+
/** Handle a decoded key name ("up" | "down" | "left" | "right" | "space" | "return"). */
|
|
63
|
+
key(name: string): void;
|
|
64
|
+
/** Whether an adapter's model rows are currently shown. */
|
|
65
|
+
expanded(adapter: string): boolean;
|
|
66
|
+
/** channelKey of the cursor's channel, or null when the cursor sits on an adapter row. */
|
|
67
|
+
selectedChannel(): string | null;
|
|
68
|
+
};
|
|
69
|
+
export declare function createFleetView(data?: FleetViewData): FleetView;
|