@juliusbrussee/caveman-tui 0.65.2
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 +767 -0
- package/dist/autocomplete.d.ts +52 -0
- package/dist/autocomplete.d.ts.map +1 -0
- package/dist/autocomplete.js +623 -0
- package/dist/autocomplete.js.map +1 -0
- package/dist/chord.d.ts +57 -0
- package/dist/chord.d.ts.map +1 -0
- package/dist/chord.js +97 -0
- package/dist/chord.js.map +1 -0
- package/dist/color-depth.d.ts +17 -0
- package/dist/color-depth.d.ts.map +1 -0
- package/dist/color-depth.js +147 -0
- package/dist/color-depth.js.map +1 -0
- package/dist/components/Chapters.d.ts +41 -0
- package/dist/components/Chapters.d.ts.map +1 -0
- package/dist/components/Chapters.js +103 -0
- package/dist/components/Chapters.js.map +1 -0
- package/dist/components/DiffView.d.ts +75 -0
- package/dist/components/DiffView.d.ts.map +1 -0
- package/dist/components/DiffView.js +170 -0
- package/dist/components/DiffView.js.map +1 -0
- package/dist/components/StatusLine.d.ts +135 -0
- package/dist/components/StatusLine.d.ts.map +1 -0
- package/dist/components/StatusLine.js +133 -0
- package/dist/components/StatusLine.js.map +1 -0
- package/dist/components/SubagentOverlay.d.ts +63 -0
- package/dist/components/SubagentOverlay.d.ts.map +1 -0
- package/dist/components/SubagentOverlay.js +124 -0
- package/dist/components/SubagentOverlay.js.map +1 -0
- package/dist/components/box.d.ts +22 -0
- package/dist/components/box.d.ts.map +1 -0
- package/dist/components/box.js +104 -0
- package/dist/components/box.js.map +1 -0
- package/dist/components/cancellable-loader.d.ts +22 -0
- package/dist/components/cancellable-loader.d.ts.map +1 -0
- package/dist/components/cancellable-loader.js +35 -0
- package/dist/components/cancellable-loader.js.map +1 -0
- package/dist/components/editor.d.ts +244 -0
- package/dist/components/editor.d.ts.map +1 -0
- package/dist/components/editor.js +1861 -0
- package/dist/components/editor.js.map +1 -0
- package/dist/components/grouped-select-list.d.ts +60 -0
- package/dist/components/grouped-select-list.d.ts.map +1 -0
- package/dist/components/grouped-select-list.js +312 -0
- package/dist/components/grouped-select-list.js.map +1 -0
- package/dist/components/image.d.ts +28 -0
- package/dist/components/image.d.ts.map +1 -0
- package/dist/components/image.js +69 -0
- package/dist/components/image.js.map +1 -0
- package/dist/components/input.d.ts +37 -0
- package/dist/components/input.d.ts.map +1 -0
- package/dist/components/input.js +426 -0
- package/dist/components/input.js.map +1 -0
- package/dist/components/loader.d.ts +26 -0
- package/dist/components/loader.d.ts.map +1 -0
- package/dist/components/loader.js +67 -0
- package/dist/components/loader.js.map +1 -0
- package/dist/components/markdown.d.ts +95 -0
- package/dist/components/markdown.d.ts.map +1 -0
- package/dist/components/markdown.js +663 -0
- package/dist/components/markdown.js.map +1 -0
- package/dist/components/select-list.d.ts +50 -0
- package/dist/components/select-list.d.ts.map +1 -0
- package/dist/components/select-list.js +159 -0
- package/dist/components/select-list.js.map +1 -0
- package/dist/components/settings-list.d.ts +50 -0
- package/dist/components/settings-list.d.ts.map +1 -0
- package/dist/components/settings-list.js +185 -0
- package/dist/components/settings-list.js.map +1 -0
- package/dist/components/spacer.d.ts +12 -0
- package/dist/components/spacer.d.ts.map +1 -0
- package/dist/components/spacer.js +23 -0
- package/dist/components/spacer.js.map +1 -0
- package/dist/components/spinner.d.ts +35 -0
- package/dist/components/spinner.d.ts.map +1 -0
- package/dist/components/spinner.js +77 -0
- package/dist/components/spinner.js.map +1 -0
- package/dist/components/streaming-markdown.d.ts +39 -0
- package/dist/components/streaming-markdown.d.ts.map +1 -0
- package/dist/components/streaming-markdown.js +137 -0
- package/dist/components/streaming-markdown.js.map +1 -0
- package/dist/components/text.d.ts +19 -0
- package/dist/components/text.d.ts.map +1 -0
- package/dist/components/text.js +89 -0
- package/dist/components/text.js.map +1 -0
- package/dist/components/truncated-text.d.ts +13 -0
- package/dist/components/truncated-text.d.ts.map +1 -0
- package/dist/components/truncated-text.js +51 -0
- package/dist/components/truncated-text.js.map +1 -0
- package/dist/editor-component.d.ts +39 -0
- package/dist/editor-component.d.ts.map +1 -0
- package/dist/editor-component.js +2 -0
- package/dist/editor-component.js.map +1 -0
- package/dist/fuzzy.d.ts +16 -0
- package/dist/fuzzy.d.ts.map +1 -0
- package/dist/fuzzy.js +107 -0
- package/dist/fuzzy.js.map +1 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +59 -0
- package/dist/index.js.map +1 -0
- package/dist/keybindings.d.ts +193 -0
- package/dist/keybindings.d.ts.map +1 -0
- package/dist/keybindings.js +174 -0
- package/dist/keybindings.js.map +1 -0
- package/dist/keys.d.ts +170 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +1124 -0
- package/dist/keys.js.map +1 -0
- package/dist/kill-ring.d.ts +28 -0
- package/dist/kill-ring.d.ts.map +1 -0
- package/dist/kill-ring.js +44 -0
- package/dist/kill-ring.js.map +1 -0
- package/dist/notifications.d.ts +35 -0
- package/dist/notifications.d.ts.map +1 -0
- package/dist/notifications.js +62 -0
- package/dist/notifications.js.map +1 -0
- package/dist/osc52.d.ts +28 -0
- package/dist/osc52.d.ts.map +1 -0
- package/dist/osc52.js +53 -0
- package/dist/osc52.js.map +1 -0
- package/dist/scroll-buffer.d.ts +67 -0
- package/dist/scroll-buffer.d.ts.map +1 -0
- package/dist/scroll-buffer.js +222 -0
- package/dist/scroll-buffer.js.map +1 -0
- package/dist/spinners.d.ts +26 -0
- package/dist/spinners.d.ts.map +1 -0
- package/dist/spinners.js +136 -0
- package/dist/spinners.js.map +1 -0
- package/dist/stdin-buffer.d.ts +48 -0
- package/dist/stdin-buffer.d.ts.map +1 -0
- package/dist/stdin-buffer.js +317 -0
- package/dist/stdin-buffer.js.map +1 -0
- package/dist/sync-output.d.ts +58 -0
- package/dist/sync-output.d.ts.map +1 -0
- package/dist/sync-output.js +79 -0
- package/dist/sync-output.js.map +1 -0
- package/dist/terminal-detect.d.ts +66 -0
- package/dist/terminal-detect.d.ts.map +1 -0
- package/dist/terminal-detect.js +315 -0
- package/dist/terminal-detect.js.map +1 -0
- package/dist/terminal-image.d.ts +68 -0
- package/dist/terminal-image.d.ts.map +1 -0
- package/dist/terminal-image.js +288 -0
- package/dist/terminal-image.js.map +1 -0
- package/dist/terminal.d.ts +105 -0
- package/dist/terminal.d.ts.map +1 -0
- package/dist/terminal.js +427 -0
- package/dist/terminal.js.map +1 -0
- package/dist/tui.d.ts +268 -0
- package/dist/tui.d.ts.map +1 -0
- package/dist/tui.js +1161 -0
- package/dist/tui.js.map +1 -0
- package/dist/undo-stack.d.ts +17 -0
- package/dist/undo-stack.d.ts.map +1 -0
- package/dist/undo-stack.js +25 -0
- package/dist/undo-stack.js.map +1 -0
- package/dist/utils.d.ts +78 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +960 -0
- package/dist/utils.js.map +1 -0
- package/package.json +59 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { truncateToWidth, visibleWidth } from "../utils.js";
|
|
2
|
+
/**
|
|
3
|
+
* Build the default (terse) status line. Format:
|
|
4
|
+
* <model> · <cwd-tail>
|
|
5
|
+
*/
|
|
6
|
+
export function renderDefault(ctx) {
|
|
7
|
+
const model = ctx.model.display_name ?? ctx.model.id;
|
|
8
|
+
return `${model} · ${tailPath(ctx.workspace.current_dir, 2)}`;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Build the detailed status line. Format:
|
|
12
|
+
* <model> · <branch>(<dirty>) · <cwd-tail> · q:<n> · $<cost>
|
|
13
|
+
*/
|
|
14
|
+
export function renderDetailed(ctx) {
|
|
15
|
+
const parts = [];
|
|
16
|
+
const model = ctx.model.display_name ?? ctx.model.id;
|
|
17
|
+
parts.push(model);
|
|
18
|
+
const branch = ctx.cave?.branch;
|
|
19
|
+
if (branch) {
|
|
20
|
+
parts.push(`${branch}${ctx.cave?.gitDirty ? "*" : ""}`);
|
|
21
|
+
}
|
|
22
|
+
parts.push(tailPath(ctx.workspace.current_dir, 2));
|
|
23
|
+
const queued = ctx.cave?.queuedMessages ?? 0;
|
|
24
|
+
if (queued > 0)
|
|
25
|
+
parts.push(`q:${queued}`);
|
|
26
|
+
if (ctx.cost && ctx.cost.total_cost_usd > 0) {
|
|
27
|
+
parts.push(`$${ctx.cost.total_cost_usd.toFixed(4)}`);
|
|
28
|
+
}
|
|
29
|
+
if (ctx.exceeds_200k_tokens)
|
|
30
|
+
parts.push("⚠ 200k");
|
|
31
|
+
return parts.join(" · ");
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Truncate a filesystem path to the trailing N components.
|
|
35
|
+
*
|
|
36
|
+
* tailPath("/a/b/c/d", 2) → "c/d"
|
|
37
|
+
* tailPath("/usr", 2) → "/usr"
|
|
38
|
+
*/
|
|
39
|
+
export function tailPath(path, components) {
|
|
40
|
+
if (!path)
|
|
41
|
+
return "";
|
|
42
|
+
const parts = path.split(/[\\/]/).filter(Boolean);
|
|
43
|
+
if (parts.length <= components)
|
|
44
|
+
return path;
|
|
45
|
+
return parts.slice(-components).join("/");
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Resolve a status line settings block to text. Synchronous helpers only —
|
|
49
|
+
* the `command` type is async and lives on the StatusLine component below.
|
|
50
|
+
*/
|
|
51
|
+
export function renderStatusLineSync(settings, ctx) {
|
|
52
|
+
const type = settings.type ?? "default";
|
|
53
|
+
if (type === "detailed")
|
|
54
|
+
return { text: renderDetailed(ctx), source: "detailed" };
|
|
55
|
+
if (type === "command") {
|
|
56
|
+
// Sync caller cannot run the command; surface the default.
|
|
57
|
+
return { text: renderDefault(ctx), source: "default" };
|
|
58
|
+
}
|
|
59
|
+
return { text: renderDefault(ctx), source: "default" };
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Validate and coerce an unknown settings.json `statusLine` value into a
|
|
63
|
+
* concrete StatusLineSettings. Returns `undefined` only when the value is
|
|
64
|
+
* malformed in a way that cannot be safely rendered.
|
|
65
|
+
*/
|
|
66
|
+
export function parseStatusLineSettings(raw) {
|
|
67
|
+
if (raw === undefined || raw === null)
|
|
68
|
+
return undefined;
|
|
69
|
+
if (typeof raw !== "object" || Array.isArray(raw))
|
|
70
|
+
return undefined;
|
|
71
|
+
const o = raw;
|
|
72
|
+
const out = {};
|
|
73
|
+
if (typeof o.type === "string") {
|
|
74
|
+
if (o.type === "command" || o.type === "default" || o.type === "detailed") {
|
|
75
|
+
out.type = o.type;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
out.type = "default";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (typeof o.command === "string" && o.command.trim().length > 0) {
|
|
82
|
+
out.command = o.command;
|
|
83
|
+
}
|
|
84
|
+
if (typeof o.padding === "number" && Number.isFinite(o.padding) && o.padding >= 0) {
|
|
85
|
+
out.padding = Math.floor(o.padding);
|
|
86
|
+
}
|
|
87
|
+
// If type is "command" but no command field, downgrade to "default".
|
|
88
|
+
if (out.type === "command" && !out.command) {
|
|
89
|
+
out.type = "default";
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
const IDENTITY_THEME = {
|
|
94
|
+
bg: (s) => s,
|
|
95
|
+
muted: (s) => s,
|
|
96
|
+
error: (s) => s,
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Single-line status component. Renders the current cached text. Callers
|
|
100
|
+
* call `setText()` when the renderer produces a new result.
|
|
101
|
+
*/
|
|
102
|
+
export class StatusLine {
|
|
103
|
+
text = "";
|
|
104
|
+
source = "default";
|
|
105
|
+
theme;
|
|
106
|
+
padding;
|
|
107
|
+
constructor(options = {}) {
|
|
108
|
+
this.theme = options.theme ?? IDENTITY_THEME;
|
|
109
|
+
this.padding = options.padding ?? 0;
|
|
110
|
+
}
|
|
111
|
+
setText(result) {
|
|
112
|
+
this.text = sanitizeOneLine(result.text);
|
|
113
|
+
this.source = result.source;
|
|
114
|
+
}
|
|
115
|
+
invalidate() {
|
|
116
|
+
// Stateless render — nothing to clear.
|
|
117
|
+
}
|
|
118
|
+
render(width) {
|
|
119
|
+
const pad = " ".repeat(this.padding);
|
|
120
|
+
const inner = `${pad}${this.text}`;
|
|
121
|
+
const truncated = truncateToWidth(inner, width);
|
|
122
|
+
// Pad to width so the status line owns its row even with diff backgrounds.
|
|
123
|
+
const w = visibleWidth(truncated);
|
|
124
|
+
const padded = w < width ? truncated + " ".repeat(width - w) : truncated;
|
|
125
|
+
const styled = this.source === "command-failed" ? this.theme.error(padded) : this.theme.bg(padded);
|
|
126
|
+
return [styled];
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/** Strip newlines/carriage returns; status line is single-row by contract. */
|
|
130
|
+
export function sanitizeOneLine(s) {
|
|
131
|
+
return s.replace(/[\r\n]+/g, " ").trim();
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=StatusLine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StatusLine.js","sourceRoot":"","sources":["../../src/components/StatusLine.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Status line — Claude Code v2.1.119 compatible.\n *\n * Schema (from `settings.json`):\n *\n * {\n * \"statusLine\": {\n * \"type\": \"command\" | \"default\" | \"detailed\",\n * \"command\": \"/path/to/script.sh\", // when type === \"command\"\n * \"padding\": 0\n * }\n * }\n *\n * When `type === \"command\"`, cave invokes the binary with a JSON context\n * payload on stdin and renders stdout (single-line, ANSI-stripped to\n * terminal width). The schema is identical to Claude Code so a user pasting\n * `~/.claude/settings.json#statusLine` into `~/.cave/settings.json` Just\n * Works.\n */\nimport type { Component } from \"../tui.js\";\nimport { truncateToWidth, visibleWidth } from \"../utils.js\";\n\n/**\n * Claude-Code-shaped statusLine setting. The `type` field is the union\n * authority; unknown types fall back to \"default\".\n */\nexport interface StatusLineSettings {\n\ttype?: \"command\" | \"default\" | \"detailed\";\n\t/** Shell command to run (only when `type === \"command\"`). */\n\tcommand?: string;\n\t/** Left padding in cells. */\n\tpadding?: number;\n}\n\n/**\n * JSON context that cave pipes to a `command`-type status line on stdin.\n * Matches Claude Code's documented shape closely enough that scripts written\n * for Claude Code work with cave (and vice versa).\n */\nexport interface StatusLineContext {\n\thook_event_name: \"Status\";\n\tsession_id: string;\n\ttranscript_path?: string;\n\tcwd: string;\n\tmodel: { id: string; display_name?: string };\n\tworkspace: { current_dir: string; project_dir: string };\n\tversion?: string;\n\toutput_style?: { name: string };\n\tcost?: { total_cost_usd: number; total_duration_ms: number };\n\texceeds_200k_tokens?: boolean;\n\t/** Cave-specific extras. Claude Code ignores keys it doesn't know. */\n\tcave?: {\n\t\tbranch?: string;\n\t\tgitDirty?: boolean;\n\t\tqueuedMessages?: number;\n\t\ttokensIn?: number;\n\t\ttokensOut?: number;\n\t};\n}\n\n/** Result of rendering — separated from Component so callers can compose. */\nexport interface StatusLineResult {\n\ttext: string;\n\t/** Where the text came from. Useful for surfacing errors in /doctor. */\n\tsource: \"default\" | \"detailed\" | \"command\" | \"command-failed\";\n\tstderr?: string;\n}\n\n/**\n * Build the default (terse) status line. Format:\n * <model> · <cwd-tail>\n */\nexport function renderDefault(ctx: StatusLineContext): string {\n\tconst model = ctx.model.display_name ?? ctx.model.id;\n\treturn `${model} · ${tailPath(ctx.workspace.current_dir, 2)}`;\n}\n\n/**\n * Build the detailed status line. Format:\n * <model> · <branch>(<dirty>) · <cwd-tail> · q:<n> · $<cost>\n */\nexport function renderDetailed(ctx: StatusLineContext): string {\n\tconst parts: string[] = [];\n\tconst model = ctx.model.display_name ?? ctx.model.id;\n\tparts.push(model);\n\n\tconst branch = ctx.cave?.branch;\n\tif (branch) {\n\t\tparts.push(`${branch}${ctx.cave?.gitDirty ? \"*\" : \"\"}`);\n\t}\n\n\tparts.push(tailPath(ctx.workspace.current_dir, 2));\n\n\tconst queued = ctx.cave?.queuedMessages ?? 0;\n\tif (queued > 0) parts.push(`q:${queued}`);\n\n\tif (ctx.cost && ctx.cost.total_cost_usd > 0) {\n\t\tparts.push(`$${ctx.cost.total_cost_usd.toFixed(4)}`);\n\t}\n\n\tif (ctx.exceeds_200k_tokens) parts.push(\"⚠ 200k\");\n\n\treturn parts.join(\" · \");\n}\n\n/**\n * Truncate a filesystem path to the trailing N components.\n *\n * tailPath(\"/a/b/c/d\", 2) → \"c/d\"\n * tailPath(\"/usr\", 2) → \"/usr\"\n */\nexport function tailPath(path: string, components: number): string {\n\tif (!path) return \"\";\n\tconst parts = path.split(/[\\\\/]/).filter(Boolean);\n\tif (parts.length <= components) return path;\n\treturn parts.slice(-components).join(\"/\");\n}\n\n/**\n * Resolve a status line settings block to text. Synchronous helpers only —\n * the `command` type is async and lives on the StatusLine component below.\n */\nexport function renderStatusLineSync(settings: StatusLineSettings, ctx: StatusLineContext): StatusLineResult {\n\tconst type = settings.type ?? \"default\";\n\tif (type === \"detailed\") return { text: renderDetailed(ctx), source: \"detailed\" };\n\tif (type === \"command\") {\n\t\t// Sync caller cannot run the command; surface the default.\n\t\treturn { text: renderDefault(ctx), source: \"default\" };\n\t}\n\treturn { text: renderDefault(ctx), source: \"default\" };\n}\n\n/**\n * Validate and coerce an unknown settings.json `statusLine` value into a\n * concrete StatusLineSettings. Returns `undefined` only when the value is\n * malformed in a way that cannot be safely rendered.\n */\nexport function parseStatusLineSettings(raw: unknown): StatusLineSettings | undefined {\n\tif (raw === undefined || raw === null) return undefined;\n\tif (typeof raw !== \"object\" || Array.isArray(raw)) return undefined;\n\tconst o = raw as Record<string, unknown>;\n\n\tconst out: StatusLineSettings = {};\n\n\tif (typeof o.type === \"string\") {\n\t\tif (o.type === \"command\" || o.type === \"default\" || o.type === \"detailed\") {\n\t\t\tout.type = o.type;\n\t\t} else {\n\t\t\tout.type = \"default\";\n\t\t}\n\t}\n\tif (typeof o.command === \"string\" && o.command.trim().length > 0) {\n\t\tout.command = o.command;\n\t}\n\tif (typeof o.padding === \"number\" && Number.isFinite(o.padding) && o.padding >= 0) {\n\t\tout.padding = Math.floor(o.padding);\n\t}\n\n\t// If type is \"command\" but no command field, downgrade to \"default\".\n\tif (out.type === \"command\" && !out.command) {\n\t\tout.type = \"default\";\n\t}\n\n\treturn out;\n}\n\nexport interface StatusLineRenderer {\n\t/**\n\t * Async render — runs the configured command if any. Implementations are\n\t * provided by the coding-agent (which has access to bash-executor); the\n\t * TUI component takes a renderer and only does layout.\n\t */\n\trender(ctx: StatusLineContext): Promise<StatusLineResult>;\n}\n\nexport interface StatusLineComponentTheme {\n\tbg: (text: string) => string;\n\tmuted: (text: string) => string;\n\terror: (text: string) => string;\n}\n\nconst IDENTITY_THEME: StatusLineComponentTheme = {\n\tbg: (s) => s,\n\tmuted: (s) => s,\n\terror: (s) => s,\n};\n\n/**\n * Single-line status component. Renders the current cached text. Callers\n * call `setText()` when the renderer produces a new result.\n */\nexport class StatusLine implements Component {\n\tprivate text = \"\";\n\tprivate source: StatusLineResult[\"source\"] = \"default\";\n\tprivate theme: StatusLineComponentTheme;\n\tprivate padding: number;\n\n\tconstructor(options: { theme?: StatusLineComponentTheme; padding?: number } = {}) {\n\t\tthis.theme = options.theme ?? IDENTITY_THEME;\n\t\tthis.padding = options.padding ?? 0;\n\t}\n\n\tsetText(result: StatusLineResult): void {\n\t\tthis.text = sanitizeOneLine(result.text);\n\t\tthis.source = result.source;\n\t}\n\n\tinvalidate(): void {\n\t\t// Stateless render — nothing to clear.\n\t}\n\n\trender(width: number): string[] {\n\t\tconst pad = \" \".repeat(this.padding);\n\t\tconst inner = `${pad}${this.text}`;\n\t\tconst truncated = truncateToWidth(inner, width);\n\t\t// Pad to width so the status line owns its row even with diff backgrounds.\n\t\tconst w = visibleWidth(truncated);\n\t\tconst padded = w < width ? truncated + \" \".repeat(width - w) : truncated;\n\t\tconst styled = this.source === \"command-failed\" ? this.theme.error(padded) : this.theme.bg(padded);\n\t\treturn [styled];\n\t}\n}\n\n/** Strip newlines/carriage returns; status line is single-row by contract. */\nexport function sanitizeOneLine(s: string): string {\n\treturn s.replace(/[\\r\\n]+/g, \" \").trim();\n}\n"]}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subagent observability overlay.
|
|
3
|
+
*
|
|
4
|
+
* F2 toggles a live tree of running subagents (current tool, token spend,
|
|
5
|
+
* elapsed). Until WS6 ships the Task tool + global subagent registry this
|
|
6
|
+
* overlay subscribes to a no-op registry that emits an empty snapshot. Once
|
|
7
|
+
* WS6 lands, `setRegistry()` will receive a real registry and the overlay
|
|
8
|
+
* begins rendering rows.
|
|
9
|
+
*/
|
|
10
|
+
import type { Component } from "../tui.js";
|
|
11
|
+
export interface SubagentSnapshot {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
currentTool?: string;
|
|
15
|
+
tokensIn: number;
|
|
16
|
+
tokensOut: number;
|
|
17
|
+
elapsedMs: number;
|
|
18
|
+
status: "running" | "done" | "error" | "queued";
|
|
19
|
+
depth?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface SubagentRegistry {
|
|
22
|
+
/** Read current snapshot of every active subagent. WS6 wires this. */
|
|
23
|
+
list(): SubagentSnapshot[];
|
|
24
|
+
/** Subscribe to changes. Returns an unsubscribe function. */
|
|
25
|
+
subscribe(listener: () => void): () => void;
|
|
26
|
+
}
|
|
27
|
+
/** Empty registry used until WS6 lands the Task tool. */
|
|
28
|
+
export declare const NULL_SUBAGENT_REGISTRY: SubagentRegistry;
|
|
29
|
+
export interface SubagentOverlayTheme {
|
|
30
|
+
border: (text: string) => string;
|
|
31
|
+
header: (text: string) => string;
|
|
32
|
+
row: (text: string) => string;
|
|
33
|
+
muted: (text: string) => string;
|
|
34
|
+
accent: (text: string) => string;
|
|
35
|
+
error: (text: string) => string;
|
|
36
|
+
}
|
|
37
|
+
export interface SubagentOverlayOptions {
|
|
38
|
+
registry?: SubagentRegistry;
|
|
39
|
+
theme?: SubagentOverlayTheme;
|
|
40
|
+
maxRows?: number;
|
|
41
|
+
}
|
|
42
|
+
export declare class SubagentOverlay implements Component {
|
|
43
|
+
private registry;
|
|
44
|
+
private theme;
|
|
45
|
+
private maxRows;
|
|
46
|
+
private unsubscribe?;
|
|
47
|
+
private redraw?;
|
|
48
|
+
constructor(options?: SubagentOverlayOptions);
|
|
49
|
+
/** Wire a new registry (e.g. when WS6 lands and registers the real one). */
|
|
50
|
+
setRegistry(registry: SubagentRegistry): void;
|
|
51
|
+
/** Bind a redraw callback so registry events trigger a TUI re-render. */
|
|
52
|
+
bindRedraw(redraw: () => void): void;
|
|
53
|
+
dispose(): void;
|
|
54
|
+
invalidate(): void;
|
|
55
|
+
render(width: number): string[];
|
|
56
|
+
private formatRow;
|
|
57
|
+
private statusBadge;
|
|
58
|
+
private layoutRow;
|
|
59
|
+
private truncateVisible;
|
|
60
|
+
private padRight;
|
|
61
|
+
}
|
|
62
|
+
export declare function formatElapsed(ms: number): string;
|
|
63
|
+
//# sourceMappingURL=SubagentOverlay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SubagentOverlay.d.ts","sourceRoot":"","sources":["../../src/components/SubagentOverlay.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAG3C,MAAM,WAAW,gBAAgB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,SAAS,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;IAChD,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAChC,sEAAsE;IACtE,IAAI,IAAI,gBAAgB,EAAE,CAAC;IAC3B,6DAA6D;IAC7D,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;CAC5C;AAED,yDAAyD;AACzD,eAAO,MAAM,sBAAsB,EAAE,gBAGpC,CAAC;AAEF,MAAM,WAAW,oBAAoB;IACpC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACjC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACjC,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAC9B,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAChC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACjC,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CAChC;AAWD,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,KAAK,CAAC,EAAE,oBAAoB,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,eAAgB,YAAW,SAAS;IAChD,OAAO,CAAC,QAAQ,CAAmB;IACnC,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,WAAW,CAAC,CAAa;IACjC,OAAO,CAAC,MAAM,CAAC,CAAa;IAE5B,YAAY,OAAO,GAAE,sBAA2B,EAI/C;IAED,4EAA4E;IAC5E,WAAW,CAAC,QAAQ,EAAE,gBAAgB,GAAG,IAAI,CAI5C;IAED,yEAAyE;IACzE,UAAU,CAAC,MAAM,EAAE,MAAM,IAAI,GAAG,IAAI,CAKnC;IAED,OAAO,IAAI,IAAI,CAId;IAED,UAAU,IAAI,IAAI,CAEjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAoB9B;IAED,OAAO,CAAC,SAAS;IAWjB,OAAO,CAAC,WAAW;IAanB,OAAO,CAAC,SAAS;IAWjB,OAAO,CAAC,eAAe;IAOvB,OAAO,CAAC,QAAQ;CAKhB;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAOhD","sourcesContent":["/**\n * Subagent observability overlay.\n *\n * F2 toggles a live tree of running subagents (current tool, token spend,\n * elapsed). Until WS6 ships the Task tool + global subagent registry this\n * overlay subscribes to a no-op registry that emits an empty snapshot. Once\n * WS6 lands, `setRegistry()` will receive a real registry and the overlay\n * begins rendering rows.\n */\nimport type { Component } from \"../tui.js\";\nimport { visibleWidth } from \"../utils.js\";\n\nexport interface SubagentSnapshot {\n\tid: string;\n\tname: string;\n\tcurrentTool?: string;\n\ttokensIn: number;\n\ttokensOut: number;\n\telapsedMs: number;\n\tstatus: \"running\" | \"done\" | \"error\" | \"queued\";\n\tdepth?: number;\n}\n\nexport interface SubagentRegistry {\n\t/** Read current snapshot of every active subagent. WS6 wires this. */\n\tlist(): SubagentSnapshot[];\n\t/** Subscribe to changes. Returns an unsubscribe function. */\n\tsubscribe(listener: () => void): () => void;\n}\n\n/** Empty registry used until WS6 lands the Task tool. */\nexport const NULL_SUBAGENT_REGISTRY: SubagentRegistry = {\n\tlist: () => [],\n\tsubscribe: () => () => {},\n};\n\nexport interface SubagentOverlayTheme {\n\tborder: (text: string) => string;\n\theader: (text: string) => string;\n\trow: (text: string) => string;\n\tmuted: (text: string) => string;\n\taccent: (text: string) => string;\n\terror: (text: string) => string;\n}\n\nconst IDENTITY: SubagentOverlayTheme = {\n\tborder: (s) => s,\n\theader: (s) => s,\n\trow: (s) => s,\n\tmuted: (s) => s,\n\taccent: (s) => s,\n\terror: (s) => s,\n};\n\nexport interface SubagentOverlayOptions {\n\tregistry?: SubagentRegistry;\n\ttheme?: SubagentOverlayTheme;\n\tmaxRows?: number;\n}\n\nexport class SubagentOverlay implements Component {\n\tprivate registry: SubagentRegistry;\n\tprivate theme: SubagentOverlayTheme;\n\tprivate maxRows: number;\n\tprivate unsubscribe?: () => void;\n\tprivate redraw?: () => void;\n\n\tconstructor(options: SubagentOverlayOptions = {}) {\n\t\tthis.registry = options.registry ?? NULL_SUBAGENT_REGISTRY;\n\t\tthis.theme = options.theme ?? IDENTITY;\n\t\tthis.maxRows = options.maxRows ?? 12;\n\t}\n\n\t/** Wire a new registry (e.g. when WS6 lands and registers the real one). */\n\tsetRegistry(registry: SubagentRegistry): void {\n\t\tif (this.unsubscribe) this.unsubscribe();\n\t\tthis.registry = registry;\n\t\tthis.unsubscribe = registry.subscribe(() => this.redraw?.());\n\t}\n\n\t/** Bind a redraw callback so registry events trigger a TUI re-render. */\n\tbindRedraw(redraw: () => void): void {\n\t\tthis.redraw = redraw;\n\t\tif (!this.unsubscribe) {\n\t\t\tthis.unsubscribe = this.registry.subscribe(() => this.redraw?.());\n\t\t}\n\t}\n\n\tdispose(): void {\n\t\tif (this.unsubscribe) this.unsubscribe();\n\t\tthis.unsubscribe = undefined;\n\t\tthis.redraw = undefined;\n\t}\n\n\tinvalidate(): void {\n\t\t// Stateless render — nothing to clear.\n\t}\n\n\trender(width: number): string[] {\n\t\tconst snapshot = this.registry.list();\n\t\tif (snapshot.length === 0) {\n\t\t\treturn [\n\t\t\t\tthis.theme.header(this.padRight(\"Subagents\", width)),\n\t\t\t\tthis.theme.muted(this.padRight(\"(none running — F2 to dismiss)\", width)),\n\t\t\t];\n\t\t}\n\n\t\tconst rows: string[] = [];\n\t\trows.push(this.theme.header(this.padRight(`Subagents (${snapshot.length})`, width)));\n\n\t\tconst visible = snapshot.slice(0, this.maxRows);\n\t\tfor (const sa of visible) {\n\t\t\trows.push(this.formatRow(sa, width));\n\t\t}\n\t\tif (snapshot.length > this.maxRows) {\n\t\t\trows.push(this.theme.muted(this.padRight(`… +${snapshot.length - this.maxRows} more`, width)));\n\t\t}\n\t\treturn rows;\n\t}\n\n\tprivate formatRow(sa: SubagentSnapshot, width: number): string {\n\t\tconst indent = \" \".repeat(sa.depth ?? 0);\n\t\tconst status = this.statusBadge(sa.status);\n\t\tconst tool = sa.currentTool ? `[${sa.currentTool}]` : \"\";\n\t\tconst tokens = `${sa.tokensIn}/${sa.tokensOut}`;\n\t\tconst elapsed = formatElapsed(sa.elapsedMs);\n\t\tconst left = `${indent}${status} ${sa.name} ${tool}`;\n\t\tconst right = `${tokens} ${elapsed}`;\n\t\treturn this.theme.row(this.layoutRow(left, right, width));\n\t}\n\n\tprivate statusBadge(status: SubagentSnapshot[\"status\"]): string {\n\t\tswitch (status) {\n\t\t\tcase \"running\":\n\t\t\t\treturn this.theme.accent(\"●\");\n\t\t\tcase \"done\":\n\t\t\t\treturn this.theme.muted(\"○\");\n\t\t\tcase \"error\":\n\t\t\t\treturn this.theme.error(\"✗\");\n\t\t\tcase \"queued\":\n\t\t\t\treturn this.theme.muted(\"◌\");\n\t\t}\n\t}\n\n\tprivate layoutRow(left: string, right: string, width: number): string {\n\t\tconst leftW = visibleWidth(left);\n\t\tconst rightW = visibleWidth(right);\n\t\tconst gap = Math.max(1, width - leftW - rightW);\n\t\tif (leftW + rightW + 1 > width) {\n\t\t\t// Truncate left to make room.\n\t\t\treturn `${this.truncateVisible(left, Math.max(0, width - rightW - 1))} ${right}`;\n\t\t}\n\t\treturn `${left}${\" \".repeat(gap)}${right}`;\n\t}\n\n\tprivate truncateVisible(s: string, max: number): string {\n\t\tif (visibleWidth(s) <= max) return s;\n\t\t// Coarse truncation; ANSI-aware truncation lives in utils but for the\n\t\t// overlay row we never embed ANSI (theme is applied to the whole row).\n\t\treturn `${s.slice(0, Math.max(0, max - 1))}…`;\n\t}\n\n\tprivate padRight(s: string, width: number): string {\n\t\tconst w = visibleWidth(s);\n\t\tif (w >= width) return s;\n\t\treturn s + \" \".repeat(width - w);\n\t}\n}\n\nexport function formatElapsed(ms: number): string {\n\tif (ms < 1000) return `${ms}ms`;\n\tconst s = Math.floor(ms / 1000);\n\tif (s < 60) return `${s}s`;\n\tconst m = Math.floor(s / 60);\n\tconst rs = s % 60;\n\treturn `${m}m${rs.toString().padStart(2, \"0\")}s`;\n}\n"]}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { visibleWidth } from "../utils.js";
|
|
2
|
+
/** Empty registry used until WS6 lands the Task tool. */
|
|
3
|
+
export const NULL_SUBAGENT_REGISTRY = {
|
|
4
|
+
list: () => [],
|
|
5
|
+
subscribe: () => () => { },
|
|
6
|
+
};
|
|
7
|
+
const IDENTITY = {
|
|
8
|
+
border: (s) => s,
|
|
9
|
+
header: (s) => s,
|
|
10
|
+
row: (s) => s,
|
|
11
|
+
muted: (s) => s,
|
|
12
|
+
accent: (s) => s,
|
|
13
|
+
error: (s) => s,
|
|
14
|
+
};
|
|
15
|
+
export class SubagentOverlay {
|
|
16
|
+
registry;
|
|
17
|
+
theme;
|
|
18
|
+
maxRows;
|
|
19
|
+
unsubscribe;
|
|
20
|
+
redraw;
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
this.registry = options.registry ?? NULL_SUBAGENT_REGISTRY;
|
|
23
|
+
this.theme = options.theme ?? IDENTITY;
|
|
24
|
+
this.maxRows = options.maxRows ?? 12;
|
|
25
|
+
}
|
|
26
|
+
/** Wire a new registry (e.g. when WS6 lands and registers the real one). */
|
|
27
|
+
setRegistry(registry) {
|
|
28
|
+
if (this.unsubscribe)
|
|
29
|
+
this.unsubscribe();
|
|
30
|
+
this.registry = registry;
|
|
31
|
+
this.unsubscribe = registry.subscribe(() => this.redraw?.());
|
|
32
|
+
}
|
|
33
|
+
/** Bind a redraw callback so registry events trigger a TUI re-render. */
|
|
34
|
+
bindRedraw(redraw) {
|
|
35
|
+
this.redraw = redraw;
|
|
36
|
+
if (!this.unsubscribe) {
|
|
37
|
+
this.unsubscribe = this.registry.subscribe(() => this.redraw?.());
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
dispose() {
|
|
41
|
+
if (this.unsubscribe)
|
|
42
|
+
this.unsubscribe();
|
|
43
|
+
this.unsubscribe = undefined;
|
|
44
|
+
this.redraw = undefined;
|
|
45
|
+
}
|
|
46
|
+
invalidate() {
|
|
47
|
+
// Stateless render — nothing to clear.
|
|
48
|
+
}
|
|
49
|
+
render(width) {
|
|
50
|
+
const snapshot = this.registry.list();
|
|
51
|
+
if (snapshot.length === 0) {
|
|
52
|
+
return [
|
|
53
|
+
this.theme.header(this.padRight("Subagents", width)),
|
|
54
|
+
this.theme.muted(this.padRight("(none running — F2 to dismiss)", width)),
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
const rows = [];
|
|
58
|
+
rows.push(this.theme.header(this.padRight(`Subagents (${snapshot.length})`, width)));
|
|
59
|
+
const visible = snapshot.slice(0, this.maxRows);
|
|
60
|
+
for (const sa of visible) {
|
|
61
|
+
rows.push(this.formatRow(sa, width));
|
|
62
|
+
}
|
|
63
|
+
if (snapshot.length > this.maxRows) {
|
|
64
|
+
rows.push(this.theme.muted(this.padRight(`… +${snapshot.length - this.maxRows} more`, width)));
|
|
65
|
+
}
|
|
66
|
+
return rows;
|
|
67
|
+
}
|
|
68
|
+
formatRow(sa, width) {
|
|
69
|
+
const indent = " ".repeat(sa.depth ?? 0);
|
|
70
|
+
const status = this.statusBadge(sa.status);
|
|
71
|
+
const tool = sa.currentTool ? `[${sa.currentTool}]` : "";
|
|
72
|
+
const tokens = `${sa.tokensIn}/${sa.tokensOut}`;
|
|
73
|
+
const elapsed = formatElapsed(sa.elapsedMs);
|
|
74
|
+
const left = `${indent}${status} ${sa.name} ${tool}`;
|
|
75
|
+
const right = `${tokens} ${elapsed}`;
|
|
76
|
+
return this.theme.row(this.layoutRow(left, right, width));
|
|
77
|
+
}
|
|
78
|
+
statusBadge(status) {
|
|
79
|
+
switch (status) {
|
|
80
|
+
case "running":
|
|
81
|
+
return this.theme.accent("●");
|
|
82
|
+
case "done":
|
|
83
|
+
return this.theme.muted("○");
|
|
84
|
+
case "error":
|
|
85
|
+
return this.theme.error("✗");
|
|
86
|
+
case "queued":
|
|
87
|
+
return this.theme.muted("◌");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
layoutRow(left, right, width) {
|
|
91
|
+
const leftW = visibleWidth(left);
|
|
92
|
+
const rightW = visibleWidth(right);
|
|
93
|
+
const gap = Math.max(1, width - leftW - rightW);
|
|
94
|
+
if (leftW + rightW + 1 > width) {
|
|
95
|
+
// Truncate left to make room.
|
|
96
|
+
return `${this.truncateVisible(left, Math.max(0, width - rightW - 1))} ${right}`;
|
|
97
|
+
}
|
|
98
|
+
return `${left}${" ".repeat(gap)}${right}`;
|
|
99
|
+
}
|
|
100
|
+
truncateVisible(s, max) {
|
|
101
|
+
if (visibleWidth(s) <= max)
|
|
102
|
+
return s;
|
|
103
|
+
// Coarse truncation; ANSI-aware truncation lives in utils but for the
|
|
104
|
+
// overlay row we never embed ANSI (theme is applied to the whole row).
|
|
105
|
+
return `${s.slice(0, Math.max(0, max - 1))}…`;
|
|
106
|
+
}
|
|
107
|
+
padRight(s, width) {
|
|
108
|
+
const w = visibleWidth(s);
|
|
109
|
+
if (w >= width)
|
|
110
|
+
return s;
|
|
111
|
+
return s + " ".repeat(width - w);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
export function formatElapsed(ms) {
|
|
115
|
+
if (ms < 1000)
|
|
116
|
+
return `${ms}ms`;
|
|
117
|
+
const s = Math.floor(ms / 1000);
|
|
118
|
+
if (s < 60)
|
|
119
|
+
return `${s}s`;
|
|
120
|
+
const m = Math.floor(s / 60);
|
|
121
|
+
const rs = s % 60;
|
|
122
|
+
return `${m}m${rs.toString().padStart(2, "0")}s`;
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=SubagentOverlay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SubagentOverlay.js","sourceRoot":"","sources":["../../src/components/SubagentOverlay.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Subagent observability overlay.\n *\n * F2 toggles a live tree of running subagents (current tool, token spend,\n * elapsed). Until WS6 ships the Task tool + global subagent registry this\n * overlay subscribes to a no-op registry that emits an empty snapshot. Once\n * WS6 lands, `setRegistry()` will receive a real registry and the overlay\n * begins rendering rows.\n */\nimport type { Component } from \"../tui.js\";\nimport { visibleWidth } from \"../utils.js\";\n\nexport interface SubagentSnapshot {\n\tid: string;\n\tname: string;\n\tcurrentTool?: string;\n\ttokensIn: number;\n\ttokensOut: number;\n\telapsedMs: number;\n\tstatus: \"running\" | \"done\" | \"error\" | \"queued\";\n\tdepth?: number;\n}\n\nexport interface SubagentRegistry {\n\t/** Read current snapshot of every active subagent. WS6 wires this. */\n\tlist(): SubagentSnapshot[];\n\t/** Subscribe to changes. Returns an unsubscribe function. */\n\tsubscribe(listener: () => void): () => void;\n}\n\n/** Empty registry used until WS6 lands the Task tool. */\nexport const NULL_SUBAGENT_REGISTRY: SubagentRegistry = {\n\tlist: () => [],\n\tsubscribe: () => () => {},\n};\n\nexport interface SubagentOverlayTheme {\n\tborder: (text: string) => string;\n\theader: (text: string) => string;\n\trow: (text: string) => string;\n\tmuted: (text: string) => string;\n\taccent: (text: string) => string;\n\terror: (text: string) => string;\n}\n\nconst IDENTITY: SubagentOverlayTheme = {\n\tborder: (s) => s,\n\theader: (s) => s,\n\trow: (s) => s,\n\tmuted: (s) => s,\n\taccent: (s) => s,\n\terror: (s) => s,\n};\n\nexport interface SubagentOverlayOptions {\n\tregistry?: SubagentRegistry;\n\ttheme?: SubagentOverlayTheme;\n\tmaxRows?: number;\n}\n\nexport class SubagentOverlay implements Component {\n\tprivate registry: SubagentRegistry;\n\tprivate theme: SubagentOverlayTheme;\n\tprivate maxRows: number;\n\tprivate unsubscribe?: () => void;\n\tprivate redraw?: () => void;\n\n\tconstructor(options: SubagentOverlayOptions = {}) {\n\t\tthis.registry = options.registry ?? NULL_SUBAGENT_REGISTRY;\n\t\tthis.theme = options.theme ?? IDENTITY;\n\t\tthis.maxRows = options.maxRows ?? 12;\n\t}\n\n\t/** Wire a new registry (e.g. when WS6 lands and registers the real one). */\n\tsetRegistry(registry: SubagentRegistry): void {\n\t\tif (this.unsubscribe) this.unsubscribe();\n\t\tthis.registry = registry;\n\t\tthis.unsubscribe = registry.subscribe(() => this.redraw?.());\n\t}\n\n\t/** Bind a redraw callback so registry events trigger a TUI re-render. */\n\tbindRedraw(redraw: () => void): void {\n\t\tthis.redraw = redraw;\n\t\tif (!this.unsubscribe) {\n\t\t\tthis.unsubscribe = this.registry.subscribe(() => this.redraw?.());\n\t\t}\n\t}\n\n\tdispose(): void {\n\t\tif (this.unsubscribe) this.unsubscribe();\n\t\tthis.unsubscribe = undefined;\n\t\tthis.redraw = undefined;\n\t}\n\n\tinvalidate(): void {\n\t\t// Stateless render — nothing to clear.\n\t}\n\n\trender(width: number): string[] {\n\t\tconst snapshot = this.registry.list();\n\t\tif (snapshot.length === 0) {\n\t\t\treturn [\n\t\t\t\tthis.theme.header(this.padRight(\"Subagents\", width)),\n\t\t\t\tthis.theme.muted(this.padRight(\"(none running — F2 to dismiss)\", width)),\n\t\t\t];\n\t\t}\n\n\t\tconst rows: string[] = [];\n\t\trows.push(this.theme.header(this.padRight(`Subagents (${snapshot.length})`, width)));\n\n\t\tconst visible = snapshot.slice(0, this.maxRows);\n\t\tfor (const sa of visible) {\n\t\t\trows.push(this.formatRow(sa, width));\n\t\t}\n\t\tif (snapshot.length > this.maxRows) {\n\t\t\trows.push(this.theme.muted(this.padRight(`… +${snapshot.length - this.maxRows} more`, width)));\n\t\t}\n\t\treturn rows;\n\t}\n\n\tprivate formatRow(sa: SubagentSnapshot, width: number): string {\n\t\tconst indent = \" \".repeat(sa.depth ?? 0);\n\t\tconst status = this.statusBadge(sa.status);\n\t\tconst tool = sa.currentTool ? `[${sa.currentTool}]` : \"\";\n\t\tconst tokens = `${sa.tokensIn}/${sa.tokensOut}`;\n\t\tconst elapsed = formatElapsed(sa.elapsedMs);\n\t\tconst left = `${indent}${status} ${sa.name} ${tool}`;\n\t\tconst right = `${tokens} ${elapsed}`;\n\t\treturn this.theme.row(this.layoutRow(left, right, width));\n\t}\n\n\tprivate statusBadge(status: SubagentSnapshot[\"status\"]): string {\n\t\tswitch (status) {\n\t\t\tcase \"running\":\n\t\t\t\treturn this.theme.accent(\"●\");\n\t\t\tcase \"done\":\n\t\t\t\treturn this.theme.muted(\"○\");\n\t\t\tcase \"error\":\n\t\t\t\treturn this.theme.error(\"✗\");\n\t\t\tcase \"queued\":\n\t\t\t\treturn this.theme.muted(\"◌\");\n\t\t}\n\t}\n\n\tprivate layoutRow(left: string, right: string, width: number): string {\n\t\tconst leftW = visibleWidth(left);\n\t\tconst rightW = visibleWidth(right);\n\t\tconst gap = Math.max(1, width - leftW - rightW);\n\t\tif (leftW + rightW + 1 > width) {\n\t\t\t// Truncate left to make room.\n\t\t\treturn `${this.truncateVisible(left, Math.max(0, width - rightW - 1))} ${right}`;\n\t\t}\n\t\treturn `${left}${\" \".repeat(gap)}${right}`;\n\t}\n\n\tprivate truncateVisible(s: string, max: number): string {\n\t\tif (visibleWidth(s) <= max) return s;\n\t\t// Coarse truncation; ANSI-aware truncation lives in utils but for the\n\t\t// overlay row we never embed ANSI (theme is applied to the whole row).\n\t\treturn `${s.slice(0, Math.max(0, max - 1))}…`;\n\t}\n\n\tprivate padRight(s: string, width: number): string {\n\t\tconst w = visibleWidth(s);\n\t\tif (w >= width) return s;\n\t\treturn s + \" \".repeat(width - w);\n\t}\n}\n\nexport function formatElapsed(ms: number): string {\n\tif (ms < 1000) return `${ms}ms`;\n\tconst s = Math.floor(ms / 1000);\n\tif (s < 60) return `${s}s`;\n\tconst m = Math.floor(s / 60);\n\tconst rs = s % 60;\n\treturn `${m}m${rs.toString().padStart(2, \"0\")}s`;\n}\n"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Component } from "../tui.js";
|
|
2
|
+
/**
|
|
3
|
+
* Box component - a container that applies padding and background to all children
|
|
4
|
+
*/
|
|
5
|
+
export declare class Box implements Component {
|
|
6
|
+
children: Component[];
|
|
7
|
+
private paddingX;
|
|
8
|
+
private paddingY;
|
|
9
|
+
private bgFn?;
|
|
10
|
+
private cache?;
|
|
11
|
+
constructor(paddingX?: number, paddingY?: number, bgFn?: (text: string) => string);
|
|
12
|
+
addChild(component: Component): void;
|
|
13
|
+
removeChild(component: Component): void;
|
|
14
|
+
clear(): void;
|
|
15
|
+
setBgFn(bgFn?: (text: string) => string): void;
|
|
16
|
+
private invalidateCache;
|
|
17
|
+
private matchCache;
|
|
18
|
+
invalidate(): void;
|
|
19
|
+
render(width: number): string[];
|
|
20
|
+
private applyBg;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=box.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"box.d.ts","sourceRoot":"","sources":["../../src/components/box.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAU3C;;GAEG;AACH,qBAAa,GAAI,YAAW,SAAS;IACpC,QAAQ,EAAE,SAAS,EAAE,CAAM;IAC3B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,IAAI,CAAC,CAA2B;IAGxC,OAAO,CAAC,KAAK,CAAC,CAAc;IAE5B,YAAY,QAAQ,SAAI,EAAE,QAAQ,SAAI,EAAE,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,EAItE;IAED,QAAQ,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAGnC;IAED,WAAW,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAMtC;IAED,KAAK,IAAI,IAAI,CAGZ;IAED,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAG7C;IAED,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,UAAU;IAWlB,UAAU,IAAI,IAAI,CAKjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAmD9B;IAED,OAAO,CAAC,OAAO;CAUf","sourcesContent":["import type { Component } from \"../tui.js\";\nimport { applyBackgroundToLine, visibleWidth } from \"../utils.js\";\n\ntype RenderCache = {\n\tchildLines: string[];\n\twidth: number;\n\tbgSample: string | undefined;\n\tlines: string[];\n};\n\n/**\n * Box component - a container that applies padding and background to all children\n */\nexport class Box implements Component {\n\tchildren: Component[] = [];\n\tprivate paddingX: number;\n\tprivate paddingY: number;\n\tprivate bgFn?: (text: string) => string;\n\n\t// Cache for rendered output\n\tprivate cache?: RenderCache;\n\n\tconstructor(paddingX = 1, paddingY = 1, bgFn?: (text: string) => string) {\n\t\tthis.paddingX = paddingX;\n\t\tthis.paddingY = paddingY;\n\t\tthis.bgFn = bgFn;\n\t}\n\n\taddChild(component: Component): void {\n\t\tthis.children.push(component);\n\t\tthis.invalidateCache();\n\t}\n\n\tremoveChild(component: Component): void {\n\t\tconst index = this.children.indexOf(component);\n\t\tif (index !== -1) {\n\t\t\tthis.children.splice(index, 1);\n\t\t\tthis.invalidateCache();\n\t\t}\n\t}\n\n\tclear(): void {\n\t\tthis.children = [];\n\t\tthis.invalidateCache();\n\t}\n\n\tsetBgFn(bgFn?: (text: string) => string): void {\n\t\tthis.bgFn = bgFn;\n\t\t// Don't invalidate here - we'll detect bgFn changes by sampling output\n\t}\n\n\tprivate invalidateCache(): void {\n\t\tthis.cache = undefined;\n\t}\n\n\tprivate matchCache(width: number, childLines: string[], bgSample: string | undefined): boolean {\n\t\tconst cache = this.cache;\n\t\treturn (\n\t\t\t!!cache &&\n\t\t\tcache.width === width &&\n\t\t\tcache.bgSample === bgSample &&\n\t\t\tcache.childLines.length === childLines.length &&\n\t\t\tcache.childLines.every((line, i) => line === childLines[i])\n\t\t);\n\t}\n\n\tinvalidate(): void {\n\t\tthis.invalidateCache();\n\t\tfor (const child of this.children) {\n\t\t\tchild.invalidate?.();\n\t\t}\n\t}\n\n\trender(width: number): string[] {\n\t\tif (this.children.length === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\tconst contentWidth = Math.max(1, width - this.paddingX * 2);\n\t\tconst leftPad = \" \".repeat(this.paddingX);\n\n\t\t// Render all children\n\t\tconst childLines: string[] = [];\n\t\tfor (const child of this.children) {\n\t\t\tconst lines = child.render(contentWidth);\n\t\t\tfor (const line of lines) {\n\t\t\t\tchildLines.push(leftPad + line);\n\t\t\t}\n\t\t}\n\n\t\tif (childLines.length === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\t// Check if bgFn output changed by sampling\n\t\tconst bgSample = this.bgFn ? this.bgFn(\"test\") : undefined;\n\n\t\t// Check cache validity\n\t\tif (this.matchCache(width, childLines, bgSample)) {\n\t\t\treturn this.cache!.lines;\n\t\t}\n\n\t\t// Apply background and padding\n\t\tconst result: string[] = [];\n\n\t\t// Top padding\n\t\tfor (let i = 0; i < this.paddingY; i++) {\n\t\t\tresult.push(this.applyBg(\"\", width));\n\t\t}\n\n\t\t// Content\n\t\tfor (const line of childLines) {\n\t\t\tresult.push(this.applyBg(line, width));\n\t\t}\n\n\t\t// Bottom padding\n\t\tfor (let i = 0; i < this.paddingY; i++) {\n\t\t\tresult.push(this.applyBg(\"\", width));\n\t\t}\n\n\t\t// Update cache\n\t\tthis.cache = { childLines, width, bgSample, lines: result };\n\n\t\treturn result;\n\t}\n\n\tprivate applyBg(line: string, width: number): string {\n\t\tconst visLen = visibleWidth(line);\n\t\tconst padNeeded = Math.max(0, width - visLen);\n\t\tconst padded = line + \" \".repeat(padNeeded);\n\n\t\tif (this.bgFn) {\n\t\t\treturn applyBackgroundToLine(padded, width, this.bgFn);\n\t\t}\n\t\treturn padded;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { applyBackgroundToLine, visibleWidth } from "../utils.js";
|
|
2
|
+
/**
|
|
3
|
+
* Box component - a container that applies padding and background to all children
|
|
4
|
+
*/
|
|
5
|
+
export class Box {
|
|
6
|
+
children = [];
|
|
7
|
+
paddingX;
|
|
8
|
+
paddingY;
|
|
9
|
+
bgFn;
|
|
10
|
+
// Cache for rendered output
|
|
11
|
+
cache;
|
|
12
|
+
constructor(paddingX = 1, paddingY = 1, bgFn) {
|
|
13
|
+
this.paddingX = paddingX;
|
|
14
|
+
this.paddingY = paddingY;
|
|
15
|
+
this.bgFn = bgFn;
|
|
16
|
+
}
|
|
17
|
+
addChild(component) {
|
|
18
|
+
this.children.push(component);
|
|
19
|
+
this.invalidateCache();
|
|
20
|
+
}
|
|
21
|
+
removeChild(component) {
|
|
22
|
+
const index = this.children.indexOf(component);
|
|
23
|
+
if (index !== -1) {
|
|
24
|
+
this.children.splice(index, 1);
|
|
25
|
+
this.invalidateCache();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
clear() {
|
|
29
|
+
this.children = [];
|
|
30
|
+
this.invalidateCache();
|
|
31
|
+
}
|
|
32
|
+
setBgFn(bgFn) {
|
|
33
|
+
this.bgFn = bgFn;
|
|
34
|
+
// Don't invalidate here - we'll detect bgFn changes by sampling output
|
|
35
|
+
}
|
|
36
|
+
invalidateCache() {
|
|
37
|
+
this.cache = undefined;
|
|
38
|
+
}
|
|
39
|
+
matchCache(width, childLines, bgSample) {
|
|
40
|
+
const cache = this.cache;
|
|
41
|
+
return (!!cache &&
|
|
42
|
+
cache.width === width &&
|
|
43
|
+
cache.bgSample === bgSample &&
|
|
44
|
+
cache.childLines.length === childLines.length &&
|
|
45
|
+
cache.childLines.every((line, i) => line === childLines[i]));
|
|
46
|
+
}
|
|
47
|
+
invalidate() {
|
|
48
|
+
this.invalidateCache();
|
|
49
|
+
for (const child of this.children) {
|
|
50
|
+
child.invalidate?.();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
render(width) {
|
|
54
|
+
if (this.children.length === 0) {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
const contentWidth = Math.max(1, width - this.paddingX * 2);
|
|
58
|
+
const leftPad = " ".repeat(this.paddingX);
|
|
59
|
+
// Render all children
|
|
60
|
+
const childLines = [];
|
|
61
|
+
for (const child of this.children) {
|
|
62
|
+
const lines = child.render(contentWidth);
|
|
63
|
+
for (const line of lines) {
|
|
64
|
+
childLines.push(leftPad + line);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (childLines.length === 0) {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
// Check if bgFn output changed by sampling
|
|
71
|
+
const bgSample = this.bgFn ? this.bgFn("test") : undefined;
|
|
72
|
+
// Check cache validity
|
|
73
|
+
if (this.matchCache(width, childLines, bgSample)) {
|
|
74
|
+
return this.cache.lines;
|
|
75
|
+
}
|
|
76
|
+
// Apply background and padding
|
|
77
|
+
const result = [];
|
|
78
|
+
// Top padding
|
|
79
|
+
for (let i = 0; i < this.paddingY; i++) {
|
|
80
|
+
result.push(this.applyBg("", width));
|
|
81
|
+
}
|
|
82
|
+
// Content
|
|
83
|
+
for (const line of childLines) {
|
|
84
|
+
result.push(this.applyBg(line, width));
|
|
85
|
+
}
|
|
86
|
+
// Bottom padding
|
|
87
|
+
for (let i = 0; i < this.paddingY; i++) {
|
|
88
|
+
result.push(this.applyBg("", width));
|
|
89
|
+
}
|
|
90
|
+
// Update cache
|
|
91
|
+
this.cache = { childLines, width, bgSample, lines: result };
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
applyBg(line, width) {
|
|
95
|
+
const visLen = visibleWidth(line);
|
|
96
|
+
const padNeeded = Math.max(0, width - visLen);
|
|
97
|
+
const padded = line + " ".repeat(padNeeded);
|
|
98
|
+
if (this.bgFn) {
|
|
99
|
+
return applyBackgroundToLine(padded, width, this.bgFn);
|
|
100
|
+
}
|
|
101
|
+
return padded;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=box.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"box.js","sourceRoot":"","sources":["../../src/components/box.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Component } from \"../tui.js\";\nimport { applyBackgroundToLine, visibleWidth } from \"../utils.js\";\n\ntype RenderCache = {\n\tchildLines: string[];\n\twidth: number;\n\tbgSample: string | undefined;\n\tlines: string[];\n};\n\n/**\n * Box component - a container that applies padding and background to all children\n */\nexport class Box implements Component {\n\tchildren: Component[] = [];\n\tprivate paddingX: number;\n\tprivate paddingY: number;\n\tprivate bgFn?: (text: string) => string;\n\n\t// Cache for rendered output\n\tprivate cache?: RenderCache;\n\n\tconstructor(paddingX = 1, paddingY = 1, bgFn?: (text: string) => string) {\n\t\tthis.paddingX = paddingX;\n\t\tthis.paddingY = paddingY;\n\t\tthis.bgFn = bgFn;\n\t}\n\n\taddChild(component: Component): void {\n\t\tthis.children.push(component);\n\t\tthis.invalidateCache();\n\t}\n\n\tremoveChild(component: Component): void {\n\t\tconst index = this.children.indexOf(component);\n\t\tif (index !== -1) {\n\t\t\tthis.children.splice(index, 1);\n\t\t\tthis.invalidateCache();\n\t\t}\n\t}\n\n\tclear(): void {\n\t\tthis.children = [];\n\t\tthis.invalidateCache();\n\t}\n\n\tsetBgFn(bgFn?: (text: string) => string): void {\n\t\tthis.bgFn = bgFn;\n\t\t// Don't invalidate here - we'll detect bgFn changes by sampling output\n\t}\n\n\tprivate invalidateCache(): void {\n\t\tthis.cache = undefined;\n\t}\n\n\tprivate matchCache(width: number, childLines: string[], bgSample: string | undefined): boolean {\n\t\tconst cache = this.cache;\n\t\treturn (\n\t\t\t!!cache &&\n\t\t\tcache.width === width &&\n\t\t\tcache.bgSample === bgSample &&\n\t\t\tcache.childLines.length === childLines.length &&\n\t\t\tcache.childLines.every((line, i) => line === childLines[i])\n\t\t);\n\t}\n\n\tinvalidate(): void {\n\t\tthis.invalidateCache();\n\t\tfor (const child of this.children) {\n\t\t\tchild.invalidate?.();\n\t\t}\n\t}\n\n\trender(width: number): string[] {\n\t\tif (this.children.length === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\tconst contentWidth = Math.max(1, width - this.paddingX * 2);\n\t\tconst leftPad = \" \".repeat(this.paddingX);\n\n\t\t// Render all children\n\t\tconst childLines: string[] = [];\n\t\tfor (const child of this.children) {\n\t\t\tconst lines = child.render(contentWidth);\n\t\t\tfor (const line of lines) {\n\t\t\t\tchildLines.push(leftPad + line);\n\t\t\t}\n\t\t}\n\n\t\tif (childLines.length === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\t// Check if bgFn output changed by sampling\n\t\tconst bgSample = this.bgFn ? this.bgFn(\"test\") : undefined;\n\n\t\t// Check cache validity\n\t\tif (this.matchCache(width, childLines, bgSample)) {\n\t\t\treturn this.cache!.lines;\n\t\t}\n\n\t\t// Apply background and padding\n\t\tconst result: string[] = [];\n\n\t\t// Top padding\n\t\tfor (let i = 0; i < this.paddingY; i++) {\n\t\t\tresult.push(this.applyBg(\"\", width));\n\t\t}\n\n\t\t// Content\n\t\tfor (const line of childLines) {\n\t\t\tresult.push(this.applyBg(line, width));\n\t\t}\n\n\t\t// Bottom padding\n\t\tfor (let i = 0; i < this.paddingY; i++) {\n\t\t\tresult.push(this.applyBg(\"\", width));\n\t\t}\n\n\t\t// Update cache\n\t\tthis.cache = { childLines, width, bgSample, lines: result };\n\n\t\treturn result;\n\t}\n\n\tprivate applyBg(line: string, width: number): string {\n\t\tconst visLen = visibleWidth(line);\n\t\tconst padNeeded = Math.max(0, width - visLen);\n\t\tconst padded = line + \" \".repeat(padNeeded);\n\n\t\tif (this.bgFn) {\n\t\t\treturn applyBackgroundToLine(padded, width, this.bgFn);\n\t\t}\n\t\treturn padded;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Loader } from "./loader.js";
|
|
2
|
+
/**
|
|
3
|
+
* Loader that can be cancelled with Escape.
|
|
4
|
+
* Extends Loader with an AbortSignal for cancelling async operations.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* const loader = new CancellableLoader(tui, cyan, dim, "Working...");
|
|
8
|
+
* loader.onAbort = () => done(null);
|
|
9
|
+
* doWork(loader.signal).then(done);
|
|
10
|
+
*/
|
|
11
|
+
export declare class CancellableLoader extends Loader {
|
|
12
|
+
private abortController;
|
|
13
|
+
/** Called when user presses Escape */
|
|
14
|
+
onAbort?: () => void;
|
|
15
|
+
/** AbortSignal that is aborted when user presses Escape */
|
|
16
|
+
get signal(): AbortSignal;
|
|
17
|
+
/** Whether the loader was aborted */
|
|
18
|
+
get aborted(): boolean;
|
|
19
|
+
handleInput(data: string): void;
|
|
20
|
+
dispose(): void;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=cancellable-loader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cancellable-loader.d.ts","sourceRoot":"","sources":["../../src/components/cancellable-loader.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC;;;;;;;;GAQG;AACH,qBAAa,iBAAkB,SAAQ,MAAM;IAC5C,OAAO,CAAC,eAAe,CAAyB;IAEhD,sCAAsC;IACtC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IAErB,2DAA2D;IAC3D,IAAI,MAAM,IAAI,WAAW,CAExB;IAED,qCAAqC;IACrC,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAM9B;IAED,OAAO,IAAI,IAAI,CAEd;CACD","sourcesContent":["import { getKeybindings } from \"../keybindings.js\";\nimport { Loader } from \"./loader.js\";\n\n/**\n * Loader that can be cancelled with Escape.\n * Extends Loader with an AbortSignal for cancelling async operations.\n *\n * @example\n * const loader = new CancellableLoader(tui, cyan, dim, \"Working...\");\n * loader.onAbort = () => done(null);\n * doWork(loader.signal).then(done);\n */\nexport class CancellableLoader extends Loader {\n\tprivate abortController = new AbortController();\n\n\t/** Called when user presses Escape */\n\tonAbort?: () => void;\n\n\t/** AbortSignal that is aborted when user presses Escape */\n\tget signal(): AbortSignal {\n\t\treturn this.abortController.signal;\n\t}\n\n\t/** Whether the loader was aborted */\n\tget aborted(): boolean {\n\t\treturn this.abortController.signal.aborted;\n\t}\n\n\thandleInput(data: string): void {\n\t\tconst kb = getKeybindings();\n\t\tif (kb.matches(data, \"tui.select.cancel\")) {\n\t\t\tthis.abortController.abort();\n\t\t\tthis.onAbort?.();\n\t\t}\n\t}\n\n\tdispose(): void {\n\t\tthis.stop();\n\t}\n}\n"]}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { getKeybindings } from "../keybindings.js";
|
|
2
|
+
import { Loader } from "./loader.js";
|
|
3
|
+
/**
|
|
4
|
+
* Loader that can be cancelled with Escape.
|
|
5
|
+
* Extends Loader with an AbortSignal for cancelling async operations.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const loader = new CancellableLoader(tui, cyan, dim, "Working...");
|
|
9
|
+
* loader.onAbort = () => done(null);
|
|
10
|
+
* doWork(loader.signal).then(done);
|
|
11
|
+
*/
|
|
12
|
+
export class CancellableLoader extends Loader {
|
|
13
|
+
abortController = new AbortController();
|
|
14
|
+
/** Called when user presses Escape */
|
|
15
|
+
onAbort;
|
|
16
|
+
/** AbortSignal that is aborted when user presses Escape */
|
|
17
|
+
get signal() {
|
|
18
|
+
return this.abortController.signal;
|
|
19
|
+
}
|
|
20
|
+
/** Whether the loader was aborted */
|
|
21
|
+
get aborted() {
|
|
22
|
+
return this.abortController.signal.aborted;
|
|
23
|
+
}
|
|
24
|
+
handleInput(data) {
|
|
25
|
+
const kb = getKeybindings();
|
|
26
|
+
if (kb.matches(data, "tui.select.cancel")) {
|
|
27
|
+
this.abortController.abort();
|
|
28
|
+
this.onAbort?.();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
dispose() {
|
|
32
|
+
this.stop();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=cancellable-loader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cancellable-loader.js","sourceRoot":"","sources":["../../src/components/cancellable-loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC;;;;;;;;GAQG;AACH,MAAM,OAAO,iBAAkB,SAAQ,MAAM;IACpC,eAAe,GAAG,IAAI,eAAe,EAAE,CAAC;IAEhD,sCAAsC;IACtC,OAAO,CAAc;IAErB,2DAA2D;IAC3D,IAAI,MAAM,GAAgB;QACzB,OAAO,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC;IAAA,CACnC;IAED,qCAAqC;IACrC,IAAI,OAAO,GAAY;QACtB,OAAO,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,OAAO,CAAC;IAAA,CAC3C;IAED,WAAW,CAAC,IAAY,EAAQ;QAC/B,MAAM,EAAE,GAAG,cAAc,EAAE,CAAC;QAC5B,IAAI,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,mBAAmB,CAAC,EAAE,CAAC;YAC3C,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,CAAC;YAC7B,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC;QAClB,CAAC;IAAA,CACD;IAED,OAAO,GAAS;QACf,IAAI,CAAC,IAAI,EAAE,CAAC;IAAA,CACZ;CACD","sourcesContent":["import { getKeybindings } from \"../keybindings.js\";\nimport { Loader } from \"./loader.js\";\n\n/**\n * Loader that can be cancelled with Escape.\n * Extends Loader with an AbortSignal for cancelling async operations.\n *\n * @example\n * const loader = new CancellableLoader(tui, cyan, dim, \"Working...\");\n * loader.onAbort = () => done(null);\n * doWork(loader.signal).then(done);\n */\nexport class CancellableLoader extends Loader {\n\tprivate abortController = new AbortController();\n\n\t/** Called when user presses Escape */\n\tonAbort?: () => void;\n\n\t/** AbortSignal that is aborted when user presses Escape */\n\tget signal(): AbortSignal {\n\t\treturn this.abortController.signal;\n\t}\n\n\t/** Whether the loader was aborted */\n\tget aborted(): boolean {\n\t\treturn this.abortController.signal.aborted;\n\t}\n\n\thandleInput(data: string): void {\n\t\tconst kb = getKeybindings();\n\t\tif (kb.matches(data, \"tui.select.cancel\")) {\n\t\t\tthis.abortController.abort();\n\t\t\tthis.onAbort?.();\n\t\t}\n\t}\n\n\tdispose(): void {\n\t\tthis.stop();\n\t}\n}\n"]}
|