@astralyn/pi 0.84.4 → 0.84.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +57 -0
- package/dist/cli/args.d.ts +1 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +11 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/auth-command.d.ts.map +1 -1
- package/dist/cli/auth-command.js +5 -4
- package/dist/cli/auth-command.js.map +1 -1
- package/dist/core/agent-session.d.ts +6 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +11 -6
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/experimental.d.ts +4 -0
- package/dist/core/experimental.d.ts.map +1 -1
- package/dist/core/experimental.js +4 -0
- package/dist/core/experimental.js.map +1 -1
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +1 -0
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +10 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +19 -1
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/keybindings.d.ts +29 -0
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +4 -0
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/model-config.d.ts +5 -0
- package/dist/core/model-config.d.ts.map +1 -1
- package/dist/core/model-config.js +1 -0
- package/dist/core/model-config.js.map +1 -1
- package/dist/core/model-registry.d.ts +2 -0
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +4 -0
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/project-trust.js +2 -2
- package/dist/core/project-trust.js.map +1 -1
- package/dist/core/sdk.d.ts +5 -3
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +2 -1
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.js +2 -2
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +6 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +12 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/system-prompt.js +1 -1
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/bash.d.ts +7 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +4 -2
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +2 -0
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/find.js +1 -1
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.js +1 -1
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +2 -0
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js +2 -0
- package/dist/core/tools/write.js.map +1 -1
- package/dist/extensions/background/index.d.ts +149 -0
- package/dist/extensions/background/index.d.ts.map +1 -0
- package/dist/extensions/background/index.js +666 -0
- package/dist/extensions/background/index.js.map +1 -0
- package/dist/extensions/background/manager.d.ts +95 -0
- package/dist/extensions/background/manager.d.ts.map +1 -0
- package/dist/extensions/background/manager.js +421 -0
- package/dist/extensions/background/manager.js.map +1 -0
- package/dist/extensions/background/registry.d.ts +220 -0
- package/dist/extensions/background/registry.d.ts.map +1 -0
- package/dist/extensions/background/registry.js +589 -0
- package/dist/extensions/background/registry.js.map +1 -0
- package/dist/extensions/background/render.d.ts +50 -0
- package/dist/extensions/background/render.d.ts.map +1 -0
- package/dist/extensions/background/render.js +260 -0
- package/dist/extensions/background/render.js.map +1 -0
- package/dist/extensions/index.d.ts.map +1 -1
- package/dist/extensions/index.js +2 -0
- package/dist/extensions/index.js.map +1 -1
- package/dist/extensions/subagent/activity.d.ts +3 -6
- package/dist/extensions/subagent/activity.d.ts.map +1 -1
- package/dist/extensions/subagent/activity.js +9 -93
- package/dist/extensions/subagent/activity.js.map +1 -1
- package/dist/extensions/subagent/agents-command.d.ts.map +1 -1
- package/dist/extensions/subagent/agents-command.js +270 -169
- package/dist/extensions/subagent/agents-command.js.map +1 -1
- package/dist/extensions/subagent/agents.d.ts +5 -6
- package/dist/extensions/subagent/agents.d.ts.map +1 -1
- package/dist/extensions/subagent/agents.js +28 -192
- package/dist/extensions/subagent/agents.js.map +1 -1
- package/dist/extensions/subagent/budget.d.ts +15 -0
- package/dist/extensions/subagent/budget.d.ts.map +1 -0
- package/dist/extensions/subagent/budget.js +96 -0
- package/dist/extensions/subagent/budget.js.map +1 -0
- package/dist/extensions/subagent/cancellation.d.ts +29 -0
- package/dist/extensions/subagent/cancellation.d.ts.map +1 -0
- package/dist/extensions/subagent/cancellation.js +73 -0
- package/dist/extensions/subagent/cancellation.js.map +1 -0
- package/dist/extensions/subagent/constants.d.ts +4 -5
- package/dist/extensions/subagent/constants.d.ts.map +1 -1
- package/dist/extensions/subagent/constants.js +11 -7
- package/dist/extensions/subagent/constants.js.map +1 -1
- package/dist/extensions/subagent/index.d.ts +0 -3
- package/dist/extensions/subagent/index.d.ts.map +1 -1
- package/dist/extensions/subagent/index.js +59 -208
- package/dist/extensions/subagent/index.js.map +1 -1
- package/dist/extensions/subagent/model-selection.d.ts +16 -0
- package/dist/extensions/subagent/model-selection.d.ts.map +1 -0
- package/dist/extensions/subagent/model-selection.js +51 -0
- package/dist/extensions/subagent/model-selection.js.map +1 -0
- package/dist/extensions/subagent/render.d.ts +7 -2
- package/dist/extensions/subagent/render.d.ts.map +1 -1
- package/dist/extensions/subagent/render.js +219 -576
- package/dist/extensions/subagent/render.js.map +1 -1
- package/dist/extensions/subagent/resolve.d.ts +2 -2
- package/dist/extensions/subagent/resolve.d.ts.map +1 -1
- package/dist/extensions/subagent/resolve.js +24 -35
- package/dist/extensions/subagent/resolve.js.map +1 -1
- package/dist/extensions/subagent/runner.d.ts +4 -6
- package/dist/extensions/subagent/runner.d.ts.map +1 -1
- package/dist/extensions/subagent/runner.js +101 -316
- package/dist/extensions/subagent/runner.js.map +1 -1
- package/dist/extensions/subagent/schema.d.ts +4 -10
- package/dist/extensions/subagent/schema.d.ts.map +1 -1
- package/dist/extensions/subagent/schema.js +8 -13
- package/dist/extensions/subagent/schema.js.map +1 -1
- package/dist/extensions/subagent/sdk-runner.d.ts +13 -11
- package/dist/extensions/subagent/sdk-runner.d.ts.map +1 -1
- package/dist/extensions/subagent/sdk-runner.js +169 -161
- package/dist/extensions/subagent/sdk-runner.js.map +1 -1
- package/dist/extensions/subagent/settings.d.ts +2 -2
- package/dist/extensions/subagent/settings.d.ts.map +1 -1
- package/dist/extensions/subagent/settings.js +27 -14
- package/dist/extensions/subagent/settings.js.map +1 -1
- package/dist/extensions/subagent/state.d.ts +89 -0
- package/dist/extensions/subagent/state.d.ts.map +1 -0
- package/dist/extensions/subagent/state.js +294 -0
- package/dist/extensions/subagent/state.js.map +1 -0
- package/dist/extensions/subagent/text.d.ts +7 -0
- package/dist/extensions/subagent/text.d.ts.map +1 -0
- package/dist/extensions/subagent/text.js +91 -0
- package/dist/extensions/subagent/text.js.map +1 -0
- package/dist/extensions/subagent/types.d.ts +16 -43
- package/dist/extensions/subagent/types.d.ts.map +1 -1
- package/dist/extensions/subagent/types.js.map +1 -1
- package/dist/extensions/subagent/ui/choices.d.ts +36 -0
- package/dist/extensions/subagent/ui/choices.d.ts.map +1 -0
- package/dist/extensions/subagent/ui/choices.js +54 -0
- package/dist/extensions/subagent/ui/choices.js.map +1 -0
- package/dist/extensions/subagent/ui/model-list.d.ts +4 -3
- package/dist/extensions/subagent/ui/model-list.d.ts.map +1 -1
- package/dist/extensions/subagent/ui/model-list.js +20 -22
- package/dist/extensions/subagent/ui/model-list.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +5 -1
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +2 -1
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +3 -1
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +10 -0
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +8 -2
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +77 -25
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/model-catalog-refresh.d.ts +7 -0
- package/dist/modes/interactive/model-catalog-refresh.d.ts.map +1 -0
- package/dist/modes/interactive/model-catalog-refresh.js +36 -0
- package/dist/modes/interactive/model-catalog-refresh.js.map +1 -0
- package/dist/modes/interactive/theme/dark.json +2 -0
- package/dist/modes/interactive/theme/light.json +2 -0
- package/dist/modes/interactive/theme/theme-controller.d.ts +10 -2
- package/dist/modes/interactive/theme/theme-controller.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme-controller.js +23 -10
- package/dist/modes/interactive/theme/theme-controller.js.map +1 -1
- package/dist/modes/interactive/theme/theme-schema.json +9 -1
- package/dist/modes/interactive/theme/theme.d.ts +5 -3
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +12 -2
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/json-event.d.ts +4 -1
- package/dist/modes/json-event.d.ts.map +1 -1
- package/dist/modes/json-event.js +5 -2
- package/dist/modes/json-event.js.map +1 -1
- package/dist/package-manager-cli.js +1 -1
- package/dist/package-manager-cli.js.map +1 -1
- package/dist/server/create-harness.d.ts.map +1 -1
- package/dist/server/create-harness.js +2 -0
- package/dist/server/create-harness.js.map +1 -1
- package/dist/utils/tools-manager.d.ts +10 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +14 -19
- package/dist/utils/tools-manager.js.map +1 -1
- package/docs/bundled/README.md +1 -0
- package/docs/bundled/extensions/background.md +99 -0
- package/docs/bundled/extensions/subagent.md +83 -71
- package/docs/compaction.md +6 -6
- package/docs/custom-provider.md +1 -1
- package/docs/docs.json +4 -0
- package/docs/environment-variables.md +8 -2
- package/docs/extensions.md +19 -2
- package/docs/json.md +6 -4
- package/docs/keybindings.md +9 -2
- package/docs/rpc.md +15 -4
- package/docs/sdk.md +9 -0
- package/docs/settings.md +31 -0
- package/docs/terminal-setup.md +23 -2
- package/docs/themes.md +24 -3
- package/docs/tui.md +2 -2
- package/docs/usage.md +4 -3
- package/examples/README.md +1 -1
- package/examples/extensions/README.md +0 -1
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +170 -221
- package/package.json +6 -6
- package/dist/extensions/subagent/display-name.d.ts +0 -6
- package/dist/extensions/subagent/display-name.d.ts.map +0 -1
- package/dist/extensions/subagent/display-name.js +0 -13
- package/dist/extensions/subagent/display-name.js.map +0 -1
- package/dist/extensions/subagent/retry.d.ts +0 -17
- package/dist/extensions/subagent/retry.d.ts.map +0 -1
- package/dist/extensions/subagent/retry.js +0 -41
- package/dist/extensions/subagent/retry.js.map +0 -1
- package/dist/extensions/subagent/ui/profile-editor.d.ts +0 -54
- package/dist/extensions/subagent/ui/profile-editor.d.ts.map +0 -1
- package/dist/extensions/subagent/ui/profile-editor.js +0 -154
- package/dist/extensions/subagent/ui/profile-editor.js.map +0 -1
- package/dist/extensions/subagent/ui/profile-picker.d.ts +0 -32
- package/dist/extensions/subagent/ui/profile-picker.d.ts.map +0 -1
- package/dist/extensions/subagent/ui/profile-picker.js +0 -149
- package/dist/extensions/subagent/ui/profile-picker.js.map +0 -1
- package/examples/extensions/subagent/README.md +0 -175
- package/examples/extensions/subagent/agents/planner.md +0 -37
- package/examples/extensions/subagent/agents/reviewer.md +0 -35
- package/examples/extensions/subagent/agents/scout.md +0 -50
- package/examples/extensions/subagent/agents/worker.md +0 -24
- package/examples/extensions/subagent/agents.ts +0 -126
- package/examples/extensions/subagent/index.ts +0 -1009
- package/examples/extensions/subagent/prompts/implement-and-review.md +0 -10
- package/examples/extensions/subagent/prompts/implement.md +0 -10
- package/examples/extensions/subagent/prompts/scout-and-plan.md +0 -9
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /bg task manager — an inline, /model-style menu that mounts in the editor
|
|
3
|
+
* slot. Two views in one component: a task list, and a live output viewport
|
|
4
|
+
* for the selected task (Enter to open, Esc to go back). The host dependency
|
|
5
|
+
* is narrowed to three callbacks so the component stays testable.
|
|
6
|
+
*/
|
|
7
|
+
import { stripTerminalSequences, truncateToWidth } from "@earendil-works/pi-tui";
|
|
8
|
+
import { formatSize } from "../../core/tools/truncate.js";
|
|
9
|
+
import { keyLabel } from "../../modes/interactive/components/keybinding-hints.js";
|
|
10
|
+
import { sanitizeBinaryOutput } from "../../utils/shell.js";
|
|
11
|
+
import { firstCommandLine, formatDuration } from "./registry.js";
|
|
12
|
+
import { statusColor, statusGlyph } from "./render.js";
|
|
13
|
+
const POLL_INTERVAL_MS = 1000;
|
|
14
|
+
const VIEW_TAIL_BYTES = 128 * 1024;
|
|
15
|
+
const MAX_VIEW_LINES = 2000;
|
|
16
|
+
const LIST_MAX_VISIBLE = 10;
|
|
17
|
+
const LIST_MIN_VISIBLE = 3;
|
|
18
|
+
/** Rows outside the task list itself (borders, title, counter, footer, padding). */
|
|
19
|
+
const LIST_RESERVED_ROWS = 14;
|
|
20
|
+
/** Truncate to an exact visible width, padding short lines with spaces. */
|
|
21
|
+
function padLine(line, width) {
|
|
22
|
+
return truncateToWidth(line, width, "…", true);
|
|
23
|
+
}
|
|
24
|
+
export class BackgroundTasksMenu {
|
|
25
|
+
constructor(options) {
|
|
26
|
+
this.view = "list";
|
|
27
|
+
this.tasks = [];
|
|
28
|
+
this.listScrollTop = 0;
|
|
29
|
+
this.follow = true;
|
|
30
|
+
this.tailOffsetLines = 0;
|
|
31
|
+
/** Last tick's render signature; unchanged means nothing visible moved. */
|
|
32
|
+
this.lastSignature = "";
|
|
33
|
+
this.readBusy = false;
|
|
34
|
+
this.disposed = false;
|
|
35
|
+
this._focused = false;
|
|
36
|
+
this.tui = options.tui;
|
|
37
|
+
this.theme = options.theme;
|
|
38
|
+
this.keybindings = options.keybindings;
|
|
39
|
+
this.host = options.host;
|
|
40
|
+
this.onClose = options.onClose;
|
|
41
|
+
this.pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
42
|
+
this.tasks = this.host.listTasks();
|
|
43
|
+
this.selectedTaskId = this.tasks[0]?.id;
|
|
44
|
+
void this.tick();
|
|
45
|
+
this.pollTimer = setInterval(() => void this.tick(), this.pollIntervalMs);
|
|
46
|
+
}
|
|
47
|
+
get focused() {
|
|
48
|
+
return this._focused;
|
|
49
|
+
}
|
|
50
|
+
set focused(value) {
|
|
51
|
+
this._focused = value;
|
|
52
|
+
}
|
|
53
|
+
invalidate() {
|
|
54
|
+
// No cached render state; the poller drives refresh.
|
|
55
|
+
}
|
|
56
|
+
dispose() {
|
|
57
|
+
this.disposed = true;
|
|
58
|
+
if (this.pollTimer !== undefined) {
|
|
59
|
+
clearInterval(this.pollTimer);
|
|
60
|
+
this.pollTimer = undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
handleInput(data) {
|
|
64
|
+
if (this.killFeedback !== undefined) {
|
|
65
|
+
this.killFeedback = undefined;
|
|
66
|
+
this.killFeedbackTaskId = undefined;
|
|
67
|
+
this.tui.requestRender();
|
|
68
|
+
}
|
|
69
|
+
if (this.keybindings.matches(data, "tui.select.cancel")) {
|
|
70
|
+
if (this.view === "detail") {
|
|
71
|
+
this.view = "list";
|
|
72
|
+
this.tui.requestRender();
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
this.onClose();
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (this.keybindings.matches(data, "app.backgroundTasks.kill")) {
|
|
80
|
+
this.killSelected();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (this.view === "list")
|
|
84
|
+
this.handleListInput(data);
|
|
85
|
+
else
|
|
86
|
+
this.handleDetailInput(data);
|
|
87
|
+
}
|
|
88
|
+
handleListInput(data) {
|
|
89
|
+
if (this.keybindings.matches(data, "tui.select.up")) {
|
|
90
|
+
this.moveSelection(-1);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (this.keybindings.matches(data, "tui.select.down")) {
|
|
94
|
+
this.moveSelection(1);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (this.keybindings.matches(data, "tui.select.confirm")) {
|
|
98
|
+
if (!this.selectedTask())
|
|
99
|
+
return;
|
|
100
|
+
this.view = "detail";
|
|
101
|
+
this.resetFollow();
|
|
102
|
+
this.tui.requestRender();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
handleDetailInput(data) {
|
|
106
|
+
if (this.keybindings.matches(data, "tui.select.pageUp")) {
|
|
107
|
+
this.scrollDetail(Math.max(1, this.detailBodyHeight()));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (this.keybindings.matches(data, "tui.select.pageDown")) {
|
|
111
|
+
if (!this.follow)
|
|
112
|
+
this.scrollDetail(-Math.max(1, this.detailBodyHeight()));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (this.keybindings.matches(data, "tui.select.up")) {
|
|
116
|
+
this.scrollDetail(1);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (this.keybindings.matches(data, "tui.select.down")) {
|
|
120
|
+
if (!this.follow)
|
|
121
|
+
this.scrollDetail(-1);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/** Scroll the detail viewport by `lines` (positive = back through history). */
|
|
125
|
+
scrollDetail(lines) {
|
|
126
|
+
if (lines > 0) {
|
|
127
|
+
const max = this.maxTailOffset();
|
|
128
|
+
if (max === 0)
|
|
129
|
+
return; // Nothing above the viewport to scroll back to.
|
|
130
|
+
this.follow = false;
|
|
131
|
+
this.tailOffsetLines = Math.min(max, this.tailOffsetLines + lines);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
if (this.follow)
|
|
135
|
+
return;
|
|
136
|
+
this.tailOffsetLines = Math.max(0, this.tailOffsetLines + lines);
|
|
137
|
+
if (this.tailOffsetLines === 0) {
|
|
138
|
+
this.follow = true;
|
|
139
|
+
void this.refreshViewport();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
this.tui.requestRender();
|
|
143
|
+
}
|
|
144
|
+
render(width) {
|
|
145
|
+
const body = this.view === "list" ? this.renderList(width) : this.renderDetail(width);
|
|
146
|
+
const border = this.theme.fg("border", "─".repeat(Math.max(1, width)));
|
|
147
|
+
return [border, ...body, padLine(this.footerLine(), width), border];
|
|
148
|
+
}
|
|
149
|
+
// ── list view ──────────────────────────────────────────────────────────
|
|
150
|
+
/** Task rows the terminal can spare; short windows trade rows for the transcript. */
|
|
151
|
+
listMaxVisible() {
|
|
152
|
+
return Math.min(LIST_MAX_VISIBLE, Math.max(LIST_MIN_VISIBLE, this.tui.terminal.rows - LIST_RESERVED_ROWS));
|
|
153
|
+
}
|
|
154
|
+
renderList(width) {
|
|
155
|
+
const lines = [];
|
|
156
|
+
const counts = this.countsLabel();
|
|
157
|
+
const title = counts ? `Background tasks — ${counts}` : "Background tasks";
|
|
158
|
+
lines.push(padLine(this.theme.fg("accent", this.theme.bold(title)), width));
|
|
159
|
+
if (this.tasks.length === 0) {
|
|
160
|
+
lines.push(padLine(this.theme.fg("muted", " (no tasks)"), width));
|
|
161
|
+
return lines;
|
|
162
|
+
}
|
|
163
|
+
const selected = this.selectedTask();
|
|
164
|
+
const maxVisible = this.listMaxVisible();
|
|
165
|
+
const visible = this.tasks.slice(this.listScrollTop, this.listScrollTop + maxVisible);
|
|
166
|
+
let seenRunning = false;
|
|
167
|
+
for (const task of visible) {
|
|
168
|
+
// listTasks() orders running first, finished after: one separator at the boundary.
|
|
169
|
+
if (seenRunning && task.status !== "running") {
|
|
170
|
+
lines.push(padLine(this.theme.fg("muted", " ── finished ──"), width));
|
|
171
|
+
seenRunning = false;
|
|
172
|
+
}
|
|
173
|
+
if (task.status === "running")
|
|
174
|
+
seenRunning = true;
|
|
175
|
+
lines.push(this.renderTaskRow(task, task === selected, width));
|
|
176
|
+
}
|
|
177
|
+
if (this.tasks.length > maxVisible) {
|
|
178
|
+
const index = this.tasks.findIndex((task) => task.id === this.selectedTaskId);
|
|
179
|
+
lines.push(padLine(this.theme.fg("muted", ` (${index + 1}/${this.tasks.length})`), width));
|
|
180
|
+
}
|
|
181
|
+
return lines;
|
|
182
|
+
}
|
|
183
|
+
renderTaskRow(task, isSelected, width) {
|
|
184
|
+
const duration = formatDuration((task.endedAt ?? Date.now()) - task.startedAt);
|
|
185
|
+
const label = task.description
|
|
186
|
+
? `${task.description} — ${firstCommandLine(task.command)}`
|
|
187
|
+
: firstCommandLine(task.command);
|
|
188
|
+
if (isSelected) {
|
|
189
|
+
// Pad to full width before styling so the selection background spans the row.
|
|
190
|
+
const plain = padLine(`→ ${statusGlyph(task.status, task.stalled)} ${task.id} ${duration} ${label}`, width);
|
|
191
|
+
return this.theme.bg("selectedBg", this.theme.fg("text", plain));
|
|
192
|
+
}
|
|
193
|
+
const glyph = this.theme.fg(statusColor(task.status, task.stalled), statusGlyph(task.status, task.stalled));
|
|
194
|
+
const id = this.theme.fg("accent", task.id);
|
|
195
|
+
const rest = this.theme.fg("muted", `${duration} ${label}`);
|
|
196
|
+
return padLine(` ${glyph} ${id} ${rest}`, width);
|
|
197
|
+
}
|
|
198
|
+
countsLabel() {
|
|
199
|
+
let running = 0;
|
|
200
|
+
for (const task of this.tasks)
|
|
201
|
+
if (task.status === "running")
|
|
202
|
+
running++;
|
|
203
|
+
const ended = this.tasks.length - running;
|
|
204
|
+
const parts = [];
|
|
205
|
+
if (running > 0)
|
|
206
|
+
parts.push(`${running} running`);
|
|
207
|
+
if (ended > 0)
|
|
208
|
+
parts.push(`${ended} done`);
|
|
209
|
+
return parts.join(" · ");
|
|
210
|
+
}
|
|
211
|
+
// ── detail view ────────────────────────────────────────────────────────
|
|
212
|
+
renderDetail(width) {
|
|
213
|
+
const task = this.selectedTask();
|
|
214
|
+
const lines = [padLine(this.detailHeader(task), width)];
|
|
215
|
+
const height = this.detailBodyHeight();
|
|
216
|
+
const cache = task && this.tailCache?.taskId === task.id ? this.tailCache : undefined;
|
|
217
|
+
if (cache?.error) {
|
|
218
|
+
lines.push(padLine(this.theme.fg("error", `Cannot read output: ${cache.error}`), width));
|
|
219
|
+
for (let row = 1; row < height; row++)
|
|
220
|
+
lines.push(padLine("", width));
|
|
221
|
+
return lines;
|
|
222
|
+
}
|
|
223
|
+
const content = cache?.lines ?? [];
|
|
224
|
+
const fromEnd = this.follow ? 0 : this.tailOffsetLines;
|
|
225
|
+
const start = content.length - height - fromEnd;
|
|
226
|
+
for (let row = 0; row < height; row++) {
|
|
227
|
+
const line = content[start + row];
|
|
228
|
+
lines.push(line === undefined ? padLine("", width) : padLine(this.theme.fg("toolOutput", line), width));
|
|
229
|
+
}
|
|
230
|
+
return lines;
|
|
231
|
+
}
|
|
232
|
+
detailHeader(task) {
|
|
233
|
+
if (!task)
|
|
234
|
+
return this.theme.fg("muted", "(no task selected)");
|
|
235
|
+
const duration = formatDuration((task.endedAt ?? Date.now()) - task.startedAt);
|
|
236
|
+
const exit = task.exitCode !== undefined && task.exitCode !== null ? ` exit ${task.exitCode}` : "";
|
|
237
|
+
const resumeLabel = keyLabel("tui.select.pageDown", { keybindings: this.keybindings });
|
|
238
|
+
const paused = this.follow ? "" : resumeLabel ? ` — paused (${resumeLabel} to follow)` : " — paused";
|
|
239
|
+
const fileName = task.outputPath.replace(/\\/g, "/").split("/").at(-1) ?? task.outputPath;
|
|
240
|
+
const glyph = this.theme.fg(statusColor(task.status, task.stalled), statusGlyph(task.status, task.stalled));
|
|
241
|
+
const statusLabel = task.stalled ? "running, waiting for input" : task.status;
|
|
242
|
+
const header = `${task.id} · ${statusLabel}${exit} · ${duration} · ${formatSize(task.outputBytes)} · ${fileName}${paused}`;
|
|
243
|
+
return `${glyph} ${this.theme.fg("muted", header)}`;
|
|
244
|
+
}
|
|
245
|
+
detailBodyHeight() {
|
|
246
|
+
// Inline component: keep the chat transcript visible above the menu.
|
|
247
|
+
return Math.min(20, Math.max(4, this.tui.terminal.rows - 8));
|
|
248
|
+
}
|
|
249
|
+
// ── shared behavior ────────────────────────────────────────────────────
|
|
250
|
+
footerLine() {
|
|
251
|
+
if (this.killFeedback)
|
|
252
|
+
return this.theme.fg("warning", this.killFeedback);
|
|
253
|
+
const opts = { keybindings: this.keybindings };
|
|
254
|
+
const parts = this.view === "list"
|
|
255
|
+
? [
|
|
256
|
+
`${keyLabel("tui.select.up", opts)}/${keyLabel("tui.select.down", opts)} select`,
|
|
257
|
+
`${keyLabel("tui.select.confirm", opts)} output`,
|
|
258
|
+
`${keyLabel("app.backgroundTasks.kill", opts)} kill`,
|
|
259
|
+
`${keyLabel("tui.select.cancel", opts)} close`,
|
|
260
|
+
]
|
|
261
|
+
: [
|
|
262
|
+
`${keyLabel("tui.select.up", opts)}/${keyLabel("tui.select.down", opts)} scroll`,
|
|
263
|
+
`${keyLabel("tui.select.pageUp", opts)}/${keyLabel("tui.select.pageDown", opts)} page`,
|
|
264
|
+
`${keyLabel("app.backgroundTasks.kill", opts)} kill`,
|
|
265
|
+
`${keyLabel("tui.select.cancel", opts)} back`,
|
|
266
|
+
];
|
|
267
|
+
return this.theme.fg("dim", parts.join(" · "));
|
|
268
|
+
}
|
|
269
|
+
selectedTask() {
|
|
270
|
+
return this.tasks.find((task) => task.id === this.selectedTaskId);
|
|
271
|
+
}
|
|
272
|
+
moveSelection(offset) {
|
|
273
|
+
if (this.tasks.length === 0)
|
|
274
|
+
return;
|
|
275
|
+
const current = this.tasks.findIndex((task) => task.id === this.selectedTaskId);
|
|
276
|
+
const next = (current + offset + this.tasks.length) % this.tasks.length;
|
|
277
|
+
const task = this.tasks[next];
|
|
278
|
+
if (!task)
|
|
279
|
+
return;
|
|
280
|
+
this.selectedTaskId = task.id;
|
|
281
|
+
if (next < this.listScrollTop)
|
|
282
|
+
this.listScrollTop = next;
|
|
283
|
+
if (next >= this.listScrollTop + this.listMaxVisible()) {
|
|
284
|
+
this.listScrollTop = next - this.listMaxVisible() + 1;
|
|
285
|
+
}
|
|
286
|
+
this.resetFollow();
|
|
287
|
+
this.tui.requestRender();
|
|
288
|
+
}
|
|
289
|
+
resetFollow() {
|
|
290
|
+
this.follow = true;
|
|
291
|
+
this.tailOffsetLines = 0;
|
|
292
|
+
this.tailCache = undefined;
|
|
293
|
+
void this.refreshViewport();
|
|
294
|
+
}
|
|
295
|
+
currentLines() {
|
|
296
|
+
const task = this.selectedTask();
|
|
297
|
+
const cache = this.tailCache;
|
|
298
|
+
return task && cache && cache.taskId === task.id ? cache.lines : [];
|
|
299
|
+
}
|
|
300
|
+
maxTailOffset() {
|
|
301
|
+
return Math.max(0, this.currentLines().length - this.detailBodyHeight());
|
|
302
|
+
}
|
|
303
|
+
killSelected() {
|
|
304
|
+
const task = this.selectedTask();
|
|
305
|
+
if (!task)
|
|
306
|
+
return;
|
|
307
|
+
if (task.status !== "running") {
|
|
308
|
+
this.killFeedback = `${task.id} is not running`;
|
|
309
|
+
this.tui.requestRender();
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
this.host.killTask(task.id);
|
|
313
|
+
// Terminal state lands on the next poll; the feedback expires with it (tick).
|
|
314
|
+
this.killFeedback = `stopping ${task.id}…`;
|
|
315
|
+
this.killFeedbackTaskId = task.id;
|
|
316
|
+
this.tui.requestRender();
|
|
317
|
+
}
|
|
318
|
+
async tick() {
|
|
319
|
+
if (this.disposed)
|
|
320
|
+
return;
|
|
321
|
+
this.tasks = this.host.listTasks();
|
|
322
|
+
if (!this.selectedTask() && this.tasks[0])
|
|
323
|
+
this.selectedTaskId = this.tasks[0].id;
|
|
324
|
+
this.expireKillFeedback();
|
|
325
|
+
if (this.renderSignature() === this.lastSignature)
|
|
326
|
+
return; // Nothing visible changed.
|
|
327
|
+
if (this.view === "detail" && this.follow && this.detailOutputChanged()) {
|
|
328
|
+
await this.refreshViewport();
|
|
329
|
+
}
|
|
330
|
+
this.lastSignature = this.renderSignature();
|
|
331
|
+
this.tui.requestRender();
|
|
332
|
+
}
|
|
333
|
+
/** Drop "stopping" feedback once its task has settled. */
|
|
334
|
+
expireKillFeedback() {
|
|
335
|
+
if (this.killFeedbackTaskId === undefined)
|
|
336
|
+
return;
|
|
337
|
+
const task = this.tasks.find((candidate) => candidate.id === this.killFeedbackTaskId);
|
|
338
|
+
if (!task || task.status !== "running") {
|
|
339
|
+
this.killFeedback = undefined;
|
|
340
|
+
this.killFeedbackTaskId = undefined;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
/** Everything the rendered output depends on; equal signatures need no redraw. */
|
|
344
|
+
renderSignature() {
|
|
345
|
+
const parts = [
|
|
346
|
+
this.view,
|
|
347
|
+
this.selectedTaskId ?? "",
|
|
348
|
+
String(this.listScrollTop),
|
|
349
|
+
String(this.follow),
|
|
350
|
+
String(this.tailOffsetLines),
|
|
351
|
+
this.killFeedback ?? "",
|
|
352
|
+
this.tailCache
|
|
353
|
+
? `${this.tailCache.taskId}:${this.tailCache.lines.length}:${this.tailCache.totalBytes}:${this.tailCache.error ?? ""}:${String(this.tailCache.settledRead ?? false)}`
|
|
354
|
+
: "",
|
|
355
|
+
];
|
|
356
|
+
for (const task of this.tasks) {
|
|
357
|
+
const duration = formatDuration((task.endedAt ?? Date.now()) - task.startedAt);
|
|
358
|
+
parts.push(`${task.id}:${task.status}:${String(task.stalled)}:${task.outputBytes}:${task.exitCode ?? ""}:${duration}`);
|
|
359
|
+
}
|
|
360
|
+
return parts.join("|");
|
|
361
|
+
}
|
|
362
|
+
/** True when the detail viewport must re-read the output file this tick. */
|
|
363
|
+
detailOutputChanged() {
|
|
364
|
+
const task = this.selectedTask();
|
|
365
|
+
if (!task)
|
|
366
|
+
return false;
|
|
367
|
+
const cache = this.tailCache;
|
|
368
|
+
if (!cache || cache.taskId !== task.id)
|
|
369
|
+
return true;
|
|
370
|
+
if (cache.error !== undefined)
|
|
371
|
+
return true; // Keep retrying; failed reads are cheap.
|
|
372
|
+
if (task.status !== "running") {
|
|
373
|
+
// Terminal task: the file is final — read it once, then never again.
|
|
374
|
+
return cache.settledRead !== true;
|
|
375
|
+
}
|
|
376
|
+
return cache.totalBytes !== task.outputBytes;
|
|
377
|
+
}
|
|
378
|
+
async refreshViewport() {
|
|
379
|
+
if (this.readBusy)
|
|
380
|
+
return;
|
|
381
|
+
const task = this.selectedTask();
|
|
382
|
+
if (!task)
|
|
383
|
+
return;
|
|
384
|
+
this.readBusy = true;
|
|
385
|
+
try {
|
|
386
|
+
const slice = await this.host.readSlice(task.outputPath, { mode: "tail", maxBytes: VIEW_TAIL_BYTES });
|
|
387
|
+
if (this.disposed || this.selectedTaskId !== task.id || !this.follow)
|
|
388
|
+
return;
|
|
389
|
+
this.tailCache = {
|
|
390
|
+
taskId: task.id,
|
|
391
|
+
lines: toViewLines(slice.text),
|
|
392
|
+
totalBytes: slice.totalBytes,
|
|
393
|
+
settledRead: task.status !== "running",
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
catch (error) {
|
|
397
|
+
if (this.disposed || this.selectedTaskId !== task.id)
|
|
398
|
+
return;
|
|
399
|
+
this.tailCache = {
|
|
400
|
+
taskId: task.id,
|
|
401
|
+
lines: [],
|
|
402
|
+
totalBytes: task.outputBytes,
|
|
403
|
+
error: error instanceof Error ? error.message : String(error),
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
finally {
|
|
407
|
+
this.readBusy = false;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
function toViewLines(text) {
|
|
412
|
+
// Strip ANSI first: sanitize would remove the escape bytes alone, leaving
|
|
413
|
+
// orphaned "[31m" fragments that strip can no longer see.
|
|
414
|
+
const lines = sanitizeBinaryOutput(stripTerminalSequences(text)).split("\n");
|
|
415
|
+
// Drop the trailing empty line produced by a final newline, then bound the
|
|
416
|
+
// window: a 128KB slice of one-byte lines must never become 128k rows.
|
|
417
|
+
while (lines.length > 0 && lines[lines.length - 1] === "")
|
|
418
|
+
lines.pop();
|
|
419
|
+
return lines.slice(-MAX_VIEW_LINES);
|
|
420
|
+
}
|
|
421
|
+
//# sourceMappingURL=manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manager.js","sourceRoot":"","sources":["../../../src/extensions/background/manager.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * /bg task manager — an inline, /model-style menu that mounts in the editor\n * slot. Two views in one component: a task list, and a live output viewport\n * for the selected task (Enter to open, Esc to go back). The host dependency\n * is narrowed to three callbacks so the component stays testable.\n */\n\nimport { type Component, type Focusable, stripTerminalSequences, truncateToWidth } from \"@earendil-works/pi-tui\";\nimport type { KeybindingsManager } from \"../../core/keybindings.ts\";\nimport { formatSize } from \"../../core/tools/truncate.ts\";\nimport { keyLabel } from \"../../modes/interactive/components/keybinding-hints.ts\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.ts\";\nimport { sanitizeBinaryOutput } from \"../../utils/shell.ts\";\nimport { type BgTask, firstCommandLine, formatDuration, type OutputSlice } from \"./registry.ts\";\nimport { statusColor, statusGlyph } from \"./render.ts\";\n\nconst POLL_INTERVAL_MS = 1000;\nconst VIEW_TAIL_BYTES = 128 * 1024;\nconst MAX_VIEW_LINES = 2000;\nconst LIST_MAX_VISIBLE = 10;\nconst LIST_MIN_VISIBLE = 3;\n/** Rows outside the task list itself (borders, title, counter, footer, padding). */\nconst LIST_RESERVED_ROWS = 14;\n\nexport interface BackgroundManagerHost {\n\tlistTasks(): BgTask[];\n\tkillTask(id: string): { killed: boolean };\n\treadSlice(filePath: string, options: { mode: \"tail\"; maxBytes: number }): Promise<OutputSlice>;\n}\n\ninterface MenuTui {\n\trequestRender(): void;\n\tterminal: { rows: number; columns: number };\n}\n\nexport interface BackgroundTasksMenuOptions {\n\ttui: MenuTui;\n\ttheme: Theme;\n\tkeybindings: Pick<KeybindingsManager, \"matches\" | \"getKeys\">;\n\thost: BackgroundManagerHost;\n\tonClose: () => void;\n\tpollIntervalMs?: number;\n}\n\ninterface TailCache {\n\ttaskId: string;\n\tlines: string[];\n\ttotalBytes: number;\n\t/** True once a terminal task's final output has been read; skips re-reads. */\n\tsettledRead?: boolean;\n\terror?: string;\n}\n\n/** Truncate to an exact visible width, padding short lines with spaces. */\nfunction padLine(line: string, width: number): string {\n\treturn truncateToWidth(line, width, \"…\", true);\n}\n\nexport class BackgroundTasksMenu implements Component, Focusable {\n\tprivate readonly tui: MenuTui;\n\tprivate readonly theme: Theme;\n\tprivate readonly keybindings: Pick<KeybindingsManager, \"matches\" | \"getKeys\">;\n\tprivate readonly host: BackgroundManagerHost;\n\tprivate readonly onClose: () => void;\n\tprivate readonly pollIntervalMs: number;\n\n\tprivate view: \"list\" | \"detail\" = \"list\";\n\tprivate tasks: BgTask[] = [];\n\tprivate selectedTaskId: string | undefined;\n\tprivate listScrollTop = 0;\n\tprivate follow = true;\n\tprivate tailOffsetLines = 0;\n\tprivate tailCache: TailCache | undefined;\n\tprivate killFeedback: string | undefined;\n\t/** Task whose settle expires killFeedback; undefined = persists until the next key. */\n\tprivate killFeedbackTaskId: string | undefined;\n\t/** Last tick's render signature; unchanged means nothing visible moved. */\n\tprivate lastSignature = \"\";\n\tprivate pollTimer: ReturnType<typeof setInterval> | undefined;\n\tprivate readBusy = false;\n\tprivate disposed = false;\n\tprivate _focused = false;\n\n\tconstructor(options: BackgroundTasksMenuOptions) {\n\t\tthis.tui = options.tui;\n\t\tthis.theme = options.theme;\n\t\tthis.keybindings = options.keybindings;\n\t\tthis.host = options.host;\n\t\tthis.onClose = options.onClose;\n\t\tthis.pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;\n\n\t\tthis.tasks = this.host.listTasks();\n\t\tthis.selectedTaskId = this.tasks[0]?.id;\n\t\tvoid this.tick();\n\t\tthis.pollTimer = setInterval(() => void this.tick(), this.pollIntervalMs);\n\t}\n\n\tget focused(): boolean {\n\t\treturn this._focused;\n\t}\n\n\tset focused(value: boolean) {\n\t\tthis._focused = value;\n\t}\n\n\tinvalidate(): void {\n\t\t// No cached render state; the poller drives refresh.\n\t}\n\n\tdispose(): void {\n\t\tthis.disposed = true;\n\t\tif (this.pollTimer !== undefined) {\n\t\t\tclearInterval(this.pollTimer);\n\t\t\tthis.pollTimer = undefined;\n\t\t}\n\t}\n\n\thandleInput(data: string): void {\n\t\tif (this.killFeedback !== undefined) {\n\t\t\tthis.killFeedback = undefined;\n\t\t\tthis.killFeedbackTaskId = undefined;\n\t\t\tthis.tui.requestRender();\n\t\t}\n\t\tif (this.keybindings.matches(data, \"tui.select.cancel\")) {\n\t\t\tif (this.view === \"detail\") {\n\t\t\t\tthis.view = \"list\";\n\t\t\t\tthis.tui.requestRender();\n\t\t\t} else {\n\t\t\t\tthis.onClose();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tif (this.keybindings.matches(data, \"app.backgroundTasks.kill\")) {\n\t\t\tthis.killSelected();\n\t\t\treturn;\n\t\t}\n\t\tif (this.view === \"list\") this.handleListInput(data);\n\t\telse this.handleDetailInput(data);\n\t}\n\n\tprivate handleListInput(data: string): void {\n\t\tif (this.keybindings.matches(data, \"tui.select.up\")) {\n\t\t\tthis.moveSelection(-1);\n\t\t\treturn;\n\t\t}\n\t\tif (this.keybindings.matches(data, \"tui.select.down\")) {\n\t\t\tthis.moveSelection(1);\n\t\t\treturn;\n\t\t}\n\t\tif (this.keybindings.matches(data, \"tui.select.confirm\")) {\n\t\t\tif (!this.selectedTask()) return;\n\t\t\tthis.view = \"detail\";\n\t\t\tthis.resetFollow();\n\t\t\tthis.tui.requestRender();\n\t\t}\n\t}\n\n\tprivate handleDetailInput(data: string): void {\n\t\tif (this.keybindings.matches(data, \"tui.select.pageUp\")) {\n\t\t\tthis.scrollDetail(Math.max(1, this.detailBodyHeight()));\n\t\t\treturn;\n\t\t}\n\t\tif (this.keybindings.matches(data, \"tui.select.pageDown\")) {\n\t\t\tif (!this.follow) this.scrollDetail(-Math.max(1, this.detailBodyHeight()));\n\t\t\treturn;\n\t\t}\n\t\tif (this.keybindings.matches(data, \"tui.select.up\")) {\n\t\t\tthis.scrollDetail(1);\n\t\t\treturn;\n\t\t}\n\t\tif (this.keybindings.matches(data, \"tui.select.down\")) {\n\t\t\tif (!this.follow) this.scrollDetail(-1);\n\t\t}\n\t}\n\n\t/** Scroll the detail viewport by `lines` (positive = back through history). */\n\tprivate scrollDetail(lines: number): void {\n\t\tif (lines > 0) {\n\t\t\tconst max = this.maxTailOffset();\n\t\t\tif (max === 0) return; // Nothing above the viewport to scroll back to.\n\t\t\tthis.follow = false;\n\t\t\tthis.tailOffsetLines = Math.min(max, this.tailOffsetLines + lines);\n\t\t} else {\n\t\t\tif (this.follow) return;\n\t\t\tthis.tailOffsetLines = Math.max(0, this.tailOffsetLines + lines);\n\t\t\tif (this.tailOffsetLines === 0) {\n\t\t\t\tthis.follow = true;\n\t\t\t\tvoid this.refreshViewport();\n\t\t\t}\n\t\t}\n\t\tthis.tui.requestRender();\n\t}\n\n\trender(width: number): string[] {\n\t\tconst body = this.view === \"list\" ? this.renderList(width) : this.renderDetail(width);\n\t\tconst border = this.theme.fg(\"border\", \"─\".repeat(Math.max(1, width)));\n\t\treturn [border, ...body, padLine(this.footerLine(), width), border];\n\t}\n\n\t// ── list view ──────────────────────────────────────────────────────────\n\n\t/** Task rows the terminal can spare; short windows trade rows for the transcript. */\n\tprivate listMaxVisible(): number {\n\t\treturn Math.min(LIST_MAX_VISIBLE, Math.max(LIST_MIN_VISIBLE, this.tui.terminal.rows - LIST_RESERVED_ROWS));\n\t}\n\n\tprivate renderList(width: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tconst counts = this.countsLabel();\n\t\tconst title = counts ? `Background tasks — ${counts}` : \"Background tasks\";\n\t\tlines.push(padLine(this.theme.fg(\"accent\", this.theme.bold(title)), width));\n\n\t\tif (this.tasks.length === 0) {\n\t\t\tlines.push(padLine(this.theme.fg(\"muted\", \" (no tasks)\"), width));\n\t\t\treturn lines;\n\t\t}\n\n\t\tconst selected = this.selectedTask();\n\t\tconst maxVisible = this.listMaxVisible();\n\t\tconst visible = this.tasks.slice(this.listScrollTop, this.listScrollTop + maxVisible);\n\t\tlet seenRunning = false;\n\t\tfor (const task of visible) {\n\t\t\t// listTasks() orders running first, finished after: one separator at the boundary.\n\t\t\tif (seenRunning && task.status !== \"running\") {\n\t\t\t\tlines.push(padLine(this.theme.fg(\"muted\", \" ── finished ──\"), width));\n\t\t\t\tseenRunning = false;\n\t\t\t}\n\t\t\tif (task.status === \"running\") seenRunning = true;\n\t\t\tlines.push(this.renderTaskRow(task, task === selected, width));\n\t\t}\n\t\tif (this.tasks.length > maxVisible) {\n\t\t\tconst index = this.tasks.findIndex((task) => task.id === this.selectedTaskId);\n\t\t\tlines.push(padLine(this.theme.fg(\"muted\", ` (${index + 1}/${this.tasks.length})`), width));\n\t\t}\n\t\treturn lines;\n\t}\n\n\tprivate renderTaskRow(task: BgTask, isSelected: boolean, width: number): string {\n\t\tconst duration = formatDuration((task.endedAt ?? Date.now()) - task.startedAt);\n\t\tconst label = task.description\n\t\t\t? `${task.description} — ${firstCommandLine(task.command)}`\n\t\t\t: firstCommandLine(task.command);\n\t\tif (isSelected) {\n\t\t\t// Pad to full width before styling so the selection background spans the row.\n\t\t\tconst plain = padLine(`→ ${statusGlyph(task.status, task.stalled)} ${task.id} ${duration} ${label}`, width);\n\t\t\treturn this.theme.bg(\"selectedBg\", this.theme.fg(\"text\", plain));\n\t\t}\n\t\tconst glyph = this.theme.fg(statusColor(task.status, task.stalled), statusGlyph(task.status, task.stalled));\n\t\tconst id = this.theme.fg(\"accent\", task.id);\n\t\tconst rest = this.theme.fg(\"muted\", `${duration} ${label}`);\n\t\treturn padLine(` ${glyph} ${id} ${rest}`, width);\n\t}\n\n\tprivate countsLabel(): string {\n\t\tlet running = 0;\n\t\tfor (const task of this.tasks) if (task.status === \"running\") running++;\n\t\tconst ended = this.tasks.length - running;\n\t\tconst parts: string[] = [];\n\t\tif (running > 0) parts.push(`${running} running`);\n\t\tif (ended > 0) parts.push(`${ended} done`);\n\t\treturn parts.join(\" · \");\n\t}\n\n\t// ── detail view ────────────────────────────────────────────────────────\n\n\tprivate renderDetail(width: number): string[] {\n\t\tconst task = this.selectedTask();\n\t\tconst lines: string[] = [padLine(this.detailHeader(task), width)];\n\t\tconst height = this.detailBodyHeight();\n\t\tconst cache = task && this.tailCache?.taskId === task.id ? this.tailCache : undefined;\n\t\tif (cache?.error) {\n\t\t\tlines.push(padLine(this.theme.fg(\"error\", `Cannot read output: ${cache.error}`), width));\n\t\t\tfor (let row = 1; row < height; row++) lines.push(padLine(\"\", width));\n\t\t\treturn lines;\n\t\t}\n\t\tconst content = cache?.lines ?? [];\n\t\tconst fromEnd = this.follow ? 0 : this.tailOffsetLines;\n\t\tconst start = content.length - height - fromEnd;\n\t\tfor (let row = 0; row < height; row++) {\n\t\t\tconst line = content[start + row];\n\t\t\tlines.push(line === undefined ? padLine(\"\", width) : padLine(this.theme.fg(\"toolOutput\", line), width));\n\t\t}\n\t\treturn lines;\n\t}\n\n\tprivate detailHeader(task: BgTask | undefined): string {\n\t\tif (!task) return this.theme.fg(\"muted\", \"(no task selected)\");\n\t\tconst duration = formatDuration((task.endedAt ?? Date.now()) - task.startedAt);\n\t\tconst exit = task.exitCode !== undefined && task.exitCode !== null ? ` exit ${task.exitCode}` : \"\";\n\t\tconst resumeLabel = keyLabel(\"tui.select.pageDown\", { keybindings: this.keybindings });\n\t\tconst paused = this.follow ? \"\" : resumeLabel ? ` — paused (${resumeLabel} to follow)` : \" — paused\";\n\t\tconst fileName = task.outputPath.replace(/\\\\/g, \"/\").split(\"/\").at(-1) ?? task.outputPath;\n\t\tconst glyph = this.theme.fg(statusColor(task.status, task.stalled), statusGlyph(task.status, task.stalled));\n\t\tconst statusLabel = task.stalled ? \"running, waiting for input\" : task.status;\n\t\tconst header = `${task.id} · ${statusLabel}${exit} · ${duration} · ${formatSize(task.outputBytes)} · ${fileName}${paused}`;\n\t\treturn `${glyph} ${this.theme.fg(\"muted\", header)}`;\n\t}\n\n\tprivate detailBodyHeight(): number {\n\t\t// Inline component: keep the chat transcript visible above the menu.\n\t\treturn Math.min(20, Math.max(4, this.tui.terminal.rows - 8));\n\t}\n\n\t// ── shared behavior ────────────────────────────────────────────────────\n\n\tprivate footerLine(): string {\n\t\tif (this.killFeedback) return this.theme.fg(\"warning\", this.killFeedback);\n\t\tconst opts = { keybindings: this.keybindings };\n\t\tconst parts =\n\t\t\tthis.view === \"list\"\n\t\t\t\t? [\n\t\t\t\t\t\t`${keyLabel(\"tui.select.up\", opts)}/${keyLabel(\"tui.select.down\", opts)} select`,\n\t\t\t\t\t\t`${keyLabel(\"tui.select.confirm\", opts)} output`,\n\t\t\t\t\t\t`${keyLabel(\"app.backgroundTasks.kill\", opts)} kill`,\n\t\t\t\t\t\t`${keyLabel(\"tui.select.cancel\", opts)} close`,\n\t\t\t\t\t]\n\t\t\t\t: [\n\t\t\t\t\t\t`${keyLabel(\"tui.select.up\", opts)}/${keyLabel(\"tui.select.down\", opts)} scroll`,\n\t\t\t\t\t\t`${keyLabel(\"tui.select.pageUp\", opts)}/${keyLabel(\"tui.select.pageDown\", opts)} page`,\n\t\t\t\t\t\t`${keyLabel(\"app.backgroundTasks.kill\", opts)} kill`,\n\t\t\t\t\t\t`${keyLabel(\"tui.select.cancel\", opts)} back`,\n\t\t\t\t\t];\n\t\treturn this.theme.fg(\"dim\", parts.join(\" · \"));\n\t}\n\n\tprivate selectedTask(): BgTask | undefined {\n\t\treturn this.tasks.find((task) => task.id === this.selectedTaskId);\n\t}\n\n\tprivate moveSelection(offset: -1 | 1): void {\n\t\tif (this.tasks.length === 0) return;\n\t\tconst current = this.tasks.findIndex((task) => task.id === this.selectedTaskId);\n\t\tconst next = (current + offset + this.tasks.length) % this.tasks.length;\n\t\tconst task = this.tasks[next];\n\t\tif (!task) return;\n\t\tthis.selectedTaskId = task.id;\n\t\tif (next < this.listScrollTop) this.listScrollTop = next;\n\t\tif (next >= this.listScrollTop + this.listMaxVisible()) {\n\t\t\tthis.listScrollTop = next - this.listMaxVisible() + 1;\n\t\t}\n\t\tthis.resetFollow();\n\t\tthis.tui.requestRender();\n\t}\n\n\tprivate resetFollow(): void {\n\t\tthis.follow = true;\n\t\tthis.tailOffsetLines = 0;\n\t\tthis.tailCache = undefined;\n\t\tvoid this.refreshViewport();\n\t}\n\n\tprivate currentLines(): string[] {\n\t\tconst task = this.selectedTask();\n\t\tconst cache = this.tailCache;\n\t\treturn task && cache && cache.taskId === task.id ? cache.lines : [];\n\t}\n\n\tprivate maxTailOffset(): number {\n\t\treturn Math.max(0, this.currentLines().length - this.detailBodyHeight());\n\t}\n\n\tprivate killSelected(): void {\n\t\tconst task = this.selectedTask();\n\t\tif (!task) return;\n\t\tif (task.status !== \"running\") {\n\t\t\tthis.killFeedback = `${task.id} is not running`;\n\t\t\tthis.tui.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tthis.host.killTask(task.id);\n\t\t// Terminal state lands on the next poll; the feedback expires with it (tick).\n\t\tthis.killFeedback = `stopping ${task.id}…`;\n\t\tthis.killFeedbackTaskId = task.id;\n\t\tthis.tui.requestRender();\n\t}\n\n\tprivate async tick(): Promise<void> {\n\t\tif (this.disposed) return;\n\t\tthis.tasks = this.host.listTasks();\n\t\tif (!this.selectedTask() && this.tasks[0]) this.selectedTaskId = this.tasks[0].id;\n\t\tthis.expireKillFeedback();\n\t\tif (this.renderSignature() === this.lastSignature) return; // Nothing visible changed.\n\t\tif (this.view === \"detail\" && this.follow && this.detailOutputChanged()) {\n\t\t\tawait this.refreshViewport();\n\t\t}\n\t\tthis.lastSignature = this.renderSignature();\n\t\tthis.tui.requestRender();\n\t}\n\n\t/** Drop \"stopping\" feedback once its task has settled. */\n\tprivate expireKillFeedback(): void {\n\t\tif (this.killFeedbackTaskId === undefined) return;\n\t\tconst task = this.tasks.find((candidate) => candidate.id === this.killFeedbackTaskId);\n\t\tif (!task || task.status !== \"running\") {\n\t\t\tthis.killFeedback = undefined;\n\t\t\tthis.killFeedbackTaskId = undefined;\n\t\t}\n\t}\n\n\t/** Everything the rendered output depends on; equal signatures need no redraw. */\n\tprivate renderSignature(): string {\n\t\tconst parts = [\n\t\t\tthis.view,\n\t\t\tthis.selectedTaskId ?? \"\",\n\t\t\tString(this.listScrollTop),\n\t\t\tString(this.follow),\n\t\t\tString(this.tailOffsetLines),\n\t\t\tthis.killFeedback ?? \"\",\n\t\t\tthis.tailCache\n\t\t\t\t? `${this.tailCache.taskId}:${this.tailCache.lines.length}:${this.tailCache.totalBytes}:${this.tailCache.error ?? \"\"}:${String(this.tailCache.settledRead ?? false)}`\n\t\t\t\t: \"\",\n\t\t];\n\t\tfor (const task of this.tasks) {\n\t\t\tconst duration = formatDuration((task.endedAt ?? Date.now()) - task.startedAt);\n\t\t\tparts.push(\n\t\t\t\t`${task.id}:${task.status}:${String(task.stalled)}:${task.outputBytes}:${task.exitCode ?? \"\"}:${duration}`,\n\t\t\t);\n\t\t}\n\t\treturn parts.join(\"|\");\n\t}\n\n\t/** True when the detail viewport must re-read the output file this tick. */\n\tprivate detailOutputChanged(): boolean {\n\t\tconst task = this.selectedTask();\n\t\tif (!task) return false;\n\t\tconst cache = this.tailCache;\n\t\tif (!cache || cache.taskId !== task.id) return true;\n\t\tif (cache.error !== undefined) return true; // Keep retrying; failed reads are cheap.\n\t\tif (task.status !== \"running\") {\n\t\t\t// Terminal task: the file is final — read it once, then never again.\n\t\t\treturn cache.settledRead !== true;\n\t\t}\n\t\treturn cache.totalBytes !== task.outputBytes;\n\t}\n\n\tprivate async refreshViewport(): Promise<void> {\n\t\tif (this.readBusy) return;\n\t\tconst task = this.selectedTask();\n\t\tif (!task) return;\n\t\tthis.readBusy = true;\n\t\ttry {\n\t\t\tconst slice = await this.host.readSlice(task.outputPath, { mode: \"tail\", maxBytes: VIEW_TAIL_BYTES });\n\t\t\tif (this.disposed || this.selectedTaskId !== task.id || !this.follow) return;\n\t\t\tthis.tailCache = {\n\t\t\t\ttaskId: task.id,\n\t\t\t\tlines: toViewLines(slice.text),\n\t\t\t\ttotalBytes: slice.totalBytes,\n\t\t\t\tsettledRead: task.status !== \"running\",\n\t\t\t};\n\t\t} catch (error) {\n\t\t\tif (this.disposed || this.selectedTaskId !== task.id) return;\n\t\t\tthis.tailCache = {\n\t\t\t\ttaskId: task.id,\n\t\t\t\tlines: [],\n\t\t\t\ttotalBytes: task.outputBytes,\n\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t};\n\t\t} finally {\n\t\t\tthis.readBusy = false;\n\t\t}\n\t}\n}\n\nfunction toViewLines(text: string): string[] {\n\t// Strip ANSI first: sanitize would remove the escape bytes alone, leaving\n\t// orphaned \"[31m\" fragments that strip can no longer see.\n\tconst lines = sanitizeBinaryOutput(stripTerminalSequences(text)).split(\"\\n\");\n\t// Drop the trailing empty line produced by a final newline, then bound the\n\t// window: a 128KB slice of one-byte lines must never become 128k rows.\n\twhile (lines.length > 0 && lines[lines.length - 1] === \"\") lines.pop();\n\treturn lines.slice(-MAX_VIEW_LINES);\n}\n"]}
|