@esso0428/pi-subagents 0.17.16 → 0.17.18
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 +11 -0
- package/ROADMAP.md +87 -0
- package/docs/post-0.17.6-agents-roster-viewer-spec.md +42 -0
- package/docs/post-0.17.6-durable-history-spec.md +41 -0
- package/docs/post-0.17.6-feature-specs.md +50 -0
- package/docs/post-0.17.6-nested-agents-spec.md +38 -0
- package/docs/post-0.17.6-recovery-shutdown-spec.md +46 -0
- package/docs/post-0.17.6-ui-latency-baseline-spec.md +46 -0
- package/docs/post-0.17.6-workflow-rpc-lifecycle-spec.md +54 -0
- package/package.json +1 -1
- package/src/agent-history.ts +2 -54
- package/src/agent-manager.ts +406 -1288
- package/src/agent-runner.ts +27 -251
- package/src/agent-types.ts +32 -188
- package/src/cross-extension-rpc.ts +20 -96
- package/src/custom-agents.ts +13 -170
- package/src/index.ts +480 -1920
- package/src/invocation-config.ts +3 -118
- package/src/model-resolver.ts +0 -18
- package/src/output-file.ts +6 -61
- package/src/prompts.ts +2 -45
- package/src/schedule.ts +14 -35
- package/src/settings.ts +2 -301
- package/src/status-note.ts +1 -66
- package/src/types.ts +10 -177
- package/src/ui/agent-widget.ts +57 -278
- package/src/ui/conversation-blocks.ts +0 -6
- package/src/ui/conversation-timeline.ts +25 -139
- package/src/ui/conversation-viewer.ts +48 -212
- package/src/ui/schedule-menu.ts +8 -9
- package/src/usage.ts +2 -109
- package/src/worktree.ts +55 -69
- package/dist/abortable.d.ts +0 -13
- package/dist/abortable.d.ts.map +0 -1
- package/dist/abortable.js +0 -43
- package/dist/abortable.js.map +0 -1
- package/dist/agent-color.d.ts +0 -36
- package/dist/agent-color.d.ts.map +0 -1
- package/dist/agent-color.js +0 -124
- package/dist/agent-color.js.map +0 -1
- package/dist/agent-file-toggle.d.ts +0 -126
- package/dist/agent-file-toggle.d.ts.map +0 -1
- package/dist/agent-file-toggle.js +0 -259
- package/dist/agent-file-toggle.js.map +0 -1
- package/dist/agent-history-list.d.ts +0 -19
- package/dist/agent-history-list.d.ts.map +0 -1
- package/dist/agent-history-list.js +0 -69
- package/dist/agent-history-list.js.map +0 -1
- package/dist/agent-history.d.ts +0 -35
- package/dist/agent-history.d.ts.map +0 -1
- package/dist/agent-history.js +0 -188
- package/dist/agent-history.js.map +0 -1
- package/dist/agent-manager.d.ts +0 -503
- package/dist/agent-manager.d.ts.map +0 -1
- package/dist/agent-manager.js +0 -1572
- package/dist/agent-manager.js.map +0 -1
- package/dist/agent-recovery.d.ts +0 -37
- package/dist/agent-recovery.d.ts.map +0 -1
- package/dist/agent-recovery.js +0 -171
- package/dist/agent-recovery.js.map +0 -1
- package/dist/agent-runner.d.ts +0 -303
- package/dist/agent-runner.d.ts.map +0 -1
- package/dist/agent-runner.js +0 -1003
- package/dist/agent-runner.js.map +0 -1
- package/dist/agent-types.d.ts +0 -120
- package/dist/agent-types.d.ts.map +0 -1
- package/dist/agent-types.js +0 -301
- package/dist/agent-types.js.map +0 -1
- package/dist/child-context.d.ts +0 -3
- package/dist/child-context.d.ts.map +0 -1
- package/dist/child-context.js +0 -13
- package/dist/child-context.js.map +0 -1
- package/dist/context.d.ts +0 -13
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -57
- package/dist/context.js.map +0 -1
- package/dist/cross-extension-rpc.d.ts +0 -67
- package/dist/cross-extension-rpc.d.ts.map +0 -1
- package/dist/cross-extension-rpc.js +0 -139
- package/dist/cross-extension-rpc.js.map +0 -1
- package/dist/custom-agents.d.ts +0 -55
- package/dist/custom-agents.d.ts.map +0 -1
- package/dist/custom-agents.js +0 -309
- package/dist/custom-agents.js.map +0 -1
- package/dist/default-agents.d.ts +0 -8
- package/dist/default-agents.d.ts.map +0 -1
- package/dist/default-agents.js +0 -123
- package/dist/default-agents.js.map +0 -1
- package/dist/enabled-models.d.ts +0 -50
- package/dist/enabled-models.d.ts.map +0 -1
- package/dist/enabled-models.js +0 -146
- package/dist/enabled-models.js.map +0 -1
- package/dist/env.d.ts +0 -7
- package/dist/env.d.ts.map +0 -1
- package/dist/env.js +0 -29
- package/dist/env.js.map +0 -1
- package/dist/group-join.d.ts +0 -33
- package/dist/group-join.d.ts.map +0 -1
- package/dist/group-join.js +0 -117
- package/dist/group-join.js.map +0 -1
- package/dist/index.d.ts +0 -51
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -3687
- package/dist/index.js.map +0 -1
- package/dist/invocation-config.d.ts +0 -108
- package/dist/invocation-config.d.ts.map +0 -1
- package/dist/invocation-config.js +0 -84
- package/dist/invocation-config.js.map +0 -1
- package/dist/memory.d.ts +0 -54
- package/dist/memory.d.ts.map +0 -1
- package/dist/memory.js +0 -166
- package/dist/memory.js.map +0 -1
- package/dist/mention-clone.d.ts +0 -88
- package/dist/mention-clone.d.ts.map +0 -1
- package/dist/mention-clone.js +0 -154
- package/dist/mention-clone.js.map +0 -1
- package/dist/mention.d.ts +0 -82
- package/dist/mention.d.ts.map +0 -1
- package/dist/mention.js +0 -132
- package/dist/mention.js.map +0 -1
- package/dist/model-resolver.d.ts +0 -37
- package/dist/model-resolver.d.ts.map +0 -1
- package/dist/model-resolver.js +0 -96
- package/dist/model-resolver.js.map +0 -1
- package/dist/model-scope.d.ts +0 -50
- package/dist/model-scope.d.ts.map +0 -1
- package/dist/model-scope.js +0 -49
- package/dist/model-scope.js.map +0 -1
- package/dist/nested-tools.d.ts +0 -57
- package/dist/nested-tools.d.ts.map +0 -1
- package/dist/nested-tools.js +0 -301
- package/dist/nested-tools.js.map +0 -1
- package/dist/nico-overrides.d.ts +0 -54
- package/dist/nico-overrides.d.ts.map +0 -1
- package/dist/nico-overrides.js +0 -170
- package/dist/nico-overrides.js.map +0 -1
- package/dist/output-file.d.ts +0 -44
- package/dist/output-file.d.ts.map +0 -1
- package/dist/output-file.js +0 -156
- package/dist/output-file.js.map +0 -1
- package/dist/prompts.d.ts +0 -56
- package/dist/prompts.d.ts.map +0 -1
- package/dist/prompts.js +0 -92
- package/dist/prompts.js.map +0 -1
- package/dist/schedule-store.d.ts +0 -39
- package/dist/schedule-store.d.ts.map +0 -1
- package/dist/schedule-store.js +0 -156
- package/dist/schedule-store.js.map +0 -1
- package/dist/schedule.d.ts +0 -110
- package/dist/schedule.d.ts.map +0 -1
- package/dist/schedule.js +0 -360
- package/dist/schedule.js.map +0 -1
- package/dist/settings.d.ts +0 -354
- package/dist/settings.d.ts.map +0 -1
- package/dist/settings.js +0 -247
- package/dist/settings.js.map +0 -1
- package/dist/skill-loader.d.ts +0 -25
- package/dist/skill-loader.d.ts.map +0 -1
- package/dist/skill-loader.js +0 -94
- package/dist/skill-loader.js.map +0 -1
- package/dist/status-note.d.ts +0 -62
- package/dist/status-note.d.ts.map +0 -1
- package/dist/status-note.js +0 -86
- package/dist/status-note.js.map +0 -1
- package/dist/structured-output.d.ts +0 -62
- package/dist/structured-output.d.ts.map +0 -1
- package/dist/structured-output.js +0 -113
- package/dist/structured-output.js.map +0 -1
- package/dist/types.d.ts +0 -372
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -6
- package/dist/types.js.map +0 -1
- package/dist/ui/agent-mention.d.ts +0 -83
- package/dist/ui/agent-mention.d.ts.map +0 -1
- package/dist/ui/agent-mention.js +0 -188
- package/dist/ui/agent-mention.js.map +0 -1
- package/dist/ui/agent-widget.d.ts +0 -241
- package/dist/ui/agent-widget.d.ts.map +0 -1
- package/dist/ui/agent-widget.js +0 -992
- package/dist/ui/agent-widget.js.map +0 -1
- package/dist/ui/ccstyle/diff/ansi-utils.d.ts +0 -11
- package/dist/ui/ccstyle/diff/ansi-utils.d.ts.map +0 -1
- package/dist/ui/ccstyle/diff/ansi-utils.js +0 -145
- package/dist/ui/ccstyle/diff/ansi-utils.js.map +0 -1
- package/dist/ui/ccstyle/diff/diff-presentation.d.ts +0 -12
- package/dist/ui/ccstyle/diff/diff-presentation.d.ts.map +0 -1
- package/dist/ui/ccstyle/diff/diff-presentation.js +0 -48
- package/dist/ui/ccstyle/diff/diff-presentation.js.map +0 -1
- package/dist/ui/ccstyle/diff/diff-renderer.d.ts +0 -46
- package/dist/ui/ccstyle/diff/diff-renderer.d.ts.map +0 -1
- package/dist/ui/ccstyle/diff/diff-renderer.js +0 -2049
- package/dist/ui/ccstyle/diff/diff-renderer.js.map +0 -1
- package/dist/ui/ccstyle/diff/line-width-safety.d.ts +0 -12
- package/dist/ui/ccstyle/diff/line-width-safety.d.ts.map +0 -1
- package/dist/ui/ccstyle/diff/line-width-safety.js +0 -58
- package/dist/ui/ccstyle/diff/line-width-safety.js.map +0 -1
- package/dist/ui/ccstyle/diff/render-utils.d.ts +0 -6
- package/dist/ui/ccstyle/diff/render-utils.d.ts.map +0 -1
- package/dist/ui/ccstyle/diff/render-utils.js +0 -25
- package/dist/ui/ccstyle/diff/render-utils.js.map +0 -1
- package/dist/ui/ccstyle/diff/shiki-highlight.d.ts +0 -19
- package/dist/ui/ccstyle/diff/shiki-highlight.d.ts.map +0 -1
- package/dist/ui/ccstyle/diff/shiki-highlight.js +0 -85
- package/dist/ui/ccstyle/diff/shiki-highlight.js.map +0 -1
- package/dist/ui/ccstyle/diff/types.d.ts +0 -22
- package/dist/ui/ccstyle/diff/types.d.ts.map +0 -1
- package/dist/ui/ccstyle/diff/types.js +0 -10
- package/dist/ui/ccstyle/diff/types.js.map +0 -1
- package/dist/ui/ccstyle/diff/write-display-utils.d.ts +0 -2
- package/dist/ui/ccstyle/diff/write-display-utils.d.ts.map +0 -1
- package/dist/ui/ccstyle/diff/write-display-utils.js +0 -12
- package/dist/ui/ccstyle/diff/write-display-utils.js.map +0 -1
- package/dist/ui/ccstyle/tool-renderer.d.ts +0 -17
- package/dist/ui/ccstyle/tool-renderer.d.ts.map +0 -1
- package/dist/ui/ccstyle/tool-renderer.js +0 -74
- package/dist/ui/ccstyle/tool-renderer.js.map +0 -1
- package/dist/ui/ccstyle/tool-result.d.ts +0 -44
- package/dist/ui/ccstyle/tool-result.d.ts.map +0 -1
- package/dist/ui/ccstyle/tool-result.js +0 -423
- package/dist/ui/ccstyle/tool-result.js.map +0 -1
- package/dist/ui/conversation-blocks.d.ts +0 -46
- package/dist/ui/conversation-blocks.d.ts.map +0 -1
- package/dist/ui/conversation-blocks.js +0 -313
- package/dist/ui/conversation-blocks.js.map +0 -1
- package/dist/ui/conversation-nvim.d.ts +0 -8
- package/dist/ui/conversation-nvim.d.ts.map +0 -1
- package/dist/ui/conversation-nvim.js +0 -117
- package/dist/ui/conversation-nvim.js.map +0 -1
- package/dist/ui/conversation-role.d.ts +0 -13
- package/dist/ui/conversation-role.d.ts.map +0 -1
- package/dist/ui/conversation-role.js +0 -39
- package/dist/ui/conversation-role.js.map +0 -1
- package/dist/ui/conversation-search.d.ts +0 -39
- package/dist/ui/conversation-search.d.ts.map +0 -1
- package/dist/ui/conversation-search.js +0 -124
- package/dist/ui/conversation-search.js.map +0 -1
- package/dist/ui/conversation-timeline.d.ts +0 -102
- package/dist/ui/conversation-timeline.d.ts.map +0 -1
- package/dist/ui/conversation-timeline.js +0 -555
- package/dist/ui/conversation-timeline.js.map +0 -1
- package/dist/ui/conversation-viewer.d.ts +0 -138
- package/dist/ui/conversation-viewer.d.ts.map +0 -1
- package/dist/ui/conversation-viewer.js +0 -1175
- package/dist/ui/conversation-viewer.js.map +0 -1
- package/dist/ui/schedule-menu.d.ts +0 -17
- package/dist/ui/schedule-menu.d.ts.map +0 -1
- package/dist/ui/schedule-menu.js +0 -95
- package/dist/ui/schedule-menu.js.map +0 -1
- package/dist/ui/select-item.d.ts +0 -28
- package/dist/ui/select-item.d.ts.map +0 -1
- package/dist/ui/select-item.js +0 -35
- package/dist/ui/select-item.js.map +0 -1
- package/dist/ui/viewer-keys.d.ts +0 -21
- package/dist/ui/viewer-keys.d.ts.map +0 -1
- package/dist/ui/viewer-keys.js +0 -18
- package/dist/ui/viewer-keys.js.map +0 -1
- package/dist/ui/workflow-card.d.ts +0 -176
- package/dist/ui/workflow-card.d.ts.map +0 -1
- package/dist/ui/workflow-card.js +0 -333
- package/dist/ui/workflow-card.js.map +0 -1
- package/dist/ui/workflow-dialog.d.ts +0 -306
- package/dist/ui/workflow-dialog.d.ts.map +0 -1
- package/dist/ui/workflow-dialog.js +0 -844
- package/dist/ui/workflow-dialog.js.map +0 -1
- package/dist/ui/workflow-menu.d.ts +0 -42
- package/dist/ui/workflow-menu.d.ts.map +0 -1
- package/dist/ui/workflow-menu.js +0 -127
- package/dist/ui/workflow-menu.js.map +0 -1
- package/dist/usage.d.ts +0 -136
- package/dist/usage.d.ts.map +0 -1
- package/dist/usage.js +0 -121
- package/dist/usage.js.map +0 -1
- package/dist/workflow/collisions.d.ts +0 -96
- package/dist/workflow/collisions.d.ts.map +0 -1
- package/dist/workflow/collisions.js +0 -89
- package/dist/workflow/collisions.js.map +0 -1
- package/dist/workflow/entry.d.ts +0 -33
- package/dist/workflow/entry.d.ts.map +0 -1
- package/dist/workflow/entry.js +0 -30
- package/dist/workflow/entry.js.map +0 -1
- package/dist/workflow/host.d.ts +0 -63
- package/dist/workflow/host.d.ts.map +0 -1
- package/dist/workflow/host.js +0 -363
- package/dist/workflow/host.js.map +0 -1
- package/dist/workflow/journal.d.ts +0 -98
- package/dist/workflow/journal.d.ts.map +0 -1
- package/dist/workflow/journal.js +0 -121
- package/dist/workflow/journal.js.map +0 -1
- package/dist/workflow/json-schema.d.ts +0 -52
- package/dist/workflow/json-schema.d.ts.map +0 -1
- package/dist/workflow/json-schema.js +0 -112
- package/dist/workflow/json-schema.js.map +0 -1
- package/dist/workflow/meta.d.ts +0 -68
- package/dist/workflow/meta.d.ts.map +0 -1
- package/dist/workflow/meta.js +0 -318
- package/dist/workflow/meta.js.map +0 -1
- package/dist/workflow/progress.d.ts +0 -225
- package/dist/workflow/progress.d.ts.map +0 -1
- package/dist/workflow/progress.js +0 -362
- package/dist/workflow/progress.js.map +0 -1
- package/dist/workflow/runtime.d.ts +0 -335
- package/dist/workflow/runtime.d.ts.map +0 -1
- package/dist/workflow/runtime.js +0 -831
- package/dist/workflow/runtime.js.map +0 -1
- package/dist/workflow/saved.d.ts +0 -91
- package/dist/workflow/saved.d.ts.map +0 -1
- package/dist/workflow/saved.js +0 -204
- package/dist/workflow/saved.js.map +0 -1
- package/dist/workflow/task.d.ts +0 -137
- package/dist/workflow/task.d.ts.map +0 -1
- package/dist/workflow/task.js +0 -208
- package/dist/workflow/task.js.map +0 -1
- package/dist/workflow/tool-description.d.ts +0 -39
- package/dist/workflow/tool-description.d.ts.map +0 -1
- package/dist/workflow/tool-description.js +0 -200
- package/dist/workflow/tool-description.js.map +0 -1
- package/dist/workflow/worker-source.d.ts +0 -48
- package/dist/workflow/worker-source.d.ts.map +0 -1
- package/dist/workflow/worker-source.js +0 -779
- package/dist/workflow/worker-source.js.map +0 -1
- package/dist/worktree.d.ts +0 -53
- package/dist/worktree.d.ts.map +0 -1
- package/dist/worktree.js +0 -165
- package/dist/worktree.js.map +0 -1
- package/dist/write-execution.d.ts +0 -42
- package/dist/write-execution.d.ts.map +0 -1
- package/dist/write-execution.js +0 -138
- package/dist/write-execution.js.map +0 -1
- package/dist/xml.d.ts +0 -11
- package/dist/xml.d.ts.map +0 -1
- package/dist/xml.js +0 -13
- package/dist/xml.js.map +0 -1
- package/src/abortable.ts +0 -43
- package/src/agent-color.ts +0 -161
- package/src/agent-file-toggle.ts +0 -269
- package/src/child-context.ts +0 -15
- package/src/mention-clone.ts +0 -196
- package/src/mention.ts +0 -141
- package/src/model-scope.ts +0 -70
- package/src/nested-tools.ts +0 -424
- package/src/structured-output.ts +0 -130
- package/src/ui/agent-mention.ts +0 -216
- package/src/ui/select-item.ts +0 -45
- package/src/ui/workflow-card.ts +0 -470
- package/src/ui/workflow-dialog.ts +0 -1115
- package/src/ui/workflow-menu.ts +0 -166
- package/src/workflow/collisions.ts +0 -123
- package/src/workflow/entry.ts +0 -47
- package/src/workflow/host.ts +0 -403
- package/src/workflow/journal.ts +0 -164
- package/src/workflow/json-schema.ts +0 -128
- package/src/workflow/meta.ts +0 -325
- package/src/workflow/progress.ts +0 -550
- package/src/workflow/runtime.ts +0 -1219
- package/src/workflow/saved.ts +0 -217
- package/src/workflow/task.ts +0 -302
- package/src/workflow/tool-description.ts +0 -200
- package/src/workflow/worker-source.ts +0 -781
- package/src/xml.ts +0 -13
package/dist/ui/agent-widget.js
DELETED
|
@@ -1,992 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* agent-widget.ts — Persistent widget showing running/completed agents above the editor.
|
|
3
|
-
*
|
|
4
|
-
* Displays a tree of agents with animated spinners, live stats, and activity descriptions.
|
|
5
|
-
* Uses the callback form of setWidget for themed rendering.
|
|
6
|
-
*/
|
|
7
|
-
import { Editor, isKeyRelease, Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
8
|
-
import { renderAgentName } from "../agent-color.js";
|
|
9
|
-
import { getConfig } from "../agent-types.js";
|
|
10
|
-
import { getLifetimeCost, getLifetimeTotal, getSessionContextPercent } from "../usage.js";
|
|
11
|
-
// ---- Constants ----
|
|
12
|
-
/** Maximum number of rendered lines before overflow collapse kicks in. */
|
|
13
|
-
export const MAX_WIDGET_LINES = 12;
|
|
14
|
-
/** Keep a small editor/input area visible below the above-editor widget. */
|
|
15
|
-
const MIN_EDITOR_LINES = 4;
|
|
16
|
-
/**
|
|
17
|
-
* Derive the widget ceiling from the terminal height while retaining the
|
|
18
|
-
* historical MAX_WIDGET_LINES ceiling. Very short terminals may have no room
|
|
19
|
-
* for the widget; that is preferable to consuming the editor/input area.
|
|
20
|
-
*/
|
|
21
|
-
export function getWidgetLineBudget(rows) {
|
|
22
|
-
if (!Number.isFinite(rows))
|
|
23
|
-
return MAX_WIDGET_LINES;
|
|
24
|
-
return Math.min(MAX_WIDGET_LINES, Math.max(0, Math.floor(rows) - MIN_EDITOR_LINES));
|
|
25
|
-
}
|
|
26
|
-
/** Braille spinner frames for animated running indicator. */
|
|
27
|
-
export const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
28
|
-
/** Statuses that indicate an error/non-success outcome (used for linger behavior and icon rendering). */
|
|
29
|
-
export const ERROR_STATUSES = new Set(["error", "aborted", "steered", "stopped"]);
|
|
30
|
-
/** Tool name → human-readable action for activity descriptions. */
|
|
31
|
-
const TOOL_DISPLAY = {
|
|
32
|
-
read: "reading",
|
|
33
|
-
bash: "running command",
|
|
34
|
-
edit: "editing",
|
|
35
|
-
write: "writing",
|
|
36
|
-
grep: "searching",
|
|
37
|
-
find: "finding files",
|
|
38
|
-
ls: "listing",
|
|
39
|
-
};
|
|
40
|
-
// ---- Formatting helpers ----
|
|
41
|
-
/** Apply foreground styling while restoring it after nested foreground/full ANSI resets. */
|
|
42
|
-
export function fgPreservingNestedStyles(theme, color, text) {
|
|
43
|
-
const styledEmpty = theme.fg(color, "");
|
|
44
|
-
const styleStart = styledEmpty.replace(/\u001b\[(?:0|39)m/g, "");
|
|
45
|
-
return theme.fg(color, text.replace(/\u001b\[(?:0|39)m/g, reset => `${reset}${styleStart}`));
|
|
46
|
-
}
|
|
47
|
-
/** Format a token count compactly: "33.8k token", "1.2M token". */
|
|
48
|
-
export function formatTokens(count) {
|
|
49
|
-
if (count >= 1_000_000)
|
|
50
|
-
return `${(count / 1_000_000).toFixed(1)}M token`;
|
|
51
|
-
if (count >= 1_000)
|
|
52
|
-
return `${(count / 1_000).toFixed(1)}k token`;
|
|
53
|
-
return `${count} token`;
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Token count with optional context-fill % and compaction-count annotations.
|
|
57
|
-
* Thresholds for percent: <70% dim, 70–85% warning, ≥85% error.
|
|
58
|
-
* Compaction count rendered as `⇊N` in dim.
|
|
59
|
-
*
|
|
60
|
-
* "12.3k token" — no annotations
|
|
61
|
-
* "12.3k token (45%)" — percent only
|
|
62
|
-
* "12.3k token (⇊2)" — compactions only (e.g. right after compact)
|
|
63
|
-
* "12.3k token (45% · ⇊2)" — both
|
|
64
|
-
*/
|
|
65
|
-
export function formatSessionTokens(tokens, percent, theme, compactions = 0) {
|
|
66
|
-
const tokenStr = formatTokens(tokens);
|
|
67
|
-
const annot = [];
|
|
68
|
-
if (percent !== null) {
|
|
69
|
-
const color = percent >= 85 ? "error" : percent >= 70 ? "warning" : "dim";
|
|
70
|
-
annot.push(theme.fg(color, `${Math.round(percent)}%`));
|
|
71
|
-
}
|
|
72
|
-
if (compactions > 0) {
|
|
73
|
-
annot.push(theme.fg("dim", `⇊${compactions}`));
|
|
74
|
-
}
|
|
75
|
-
if (annot.length === 0)
|
|
76
|
-
return tokenStr;
|
|
77
|
-
return `${tokenStr} (${annot.join(" · ")})`;
|
|
78
|
-
}
|
|
79
|
-
/** Format turn count with optional max limit: "↻5≤30" or "↻5". */
|
|
80
|
-
export function formatTurns(turnCount, maxTurns) {
|
|
81
|
-
return maxTurns != null ? `↻${turnCount}≤${maxTurns}` : `↻${turnCount}`;
|
|
82
|
-
}
|
|
83
|
-
/** Format milliseconds as human-readable duration. */
|
|
84
|
-
export function formatMs(ms) {
|
|
85
|
-
return `${(ms / 1000).toFixed(1)}s`;
|
|
86
|
-
}
|
|
87
|
-
/** Format duration from start/completed timestamps. */
|
|
88
|
-
export function formatDuration(startedAt, completedAt) {
|
|
89
|
-
if (completedAt)
|
|
90
|
-
return formatMs(completedAt - startedAt);
|
|
91
|
-
return `${formatMs(Date.now() - startedAt)} (running)`;
|
|
92
|
-
}
|
|
93
|
-
/** Format an estimated model cost without implying billing precision. */
|
|
94
|
-
export function formatCost(cost) {
|
|
95
|
-
if (!(cost > 0))
|
|
96
|
-
return "";
|
|
97
|
-
if (cost < 0.0001)
|
|
98
|
-
return "<$0.0001";
|
|
99
|
-
if (cost >= 1)
|
|
100
|
-
return `~$${cost.toFixed(2)}`;
|
|
101
|
-
const rounded = Number(cost.toFixed(4));
|
|
102
|
-
const decimals = (String(rounded).split(".")[1] ?? "").length;
|
|
103
|
-
return `~$${rounded.toFixed(Math.max(2, decimals))}`;
|
|
104
|
-
}
|
|
105
|
-
/** Get display name for any agent type (built-in or custom). */
|
|
106
|
-
export function getDisplayName(type) {
|
|
107
|
-
return getConfig(type).displayName;
|
|
108
|
-
}
|
|
109
|
-
/** Short label for prompt mode: "twin" for append, nothing for replace (the default). */
|
|
110
|
-
export function getPromptModeLabel(type) {
|
|
111
|
-
const config = getConfig(type);
|
|
112
|
-
return config.promptMode === "append" ? "twin" : undefined;
|
|
113
|
-
}
|
|
114
|
-
/** Mode label is not included — callers add it where they want it. */
|
|
115
|
-
export function buildInvocationTags(invocation) {
|
|
116
|
-
const tags = [];
|
|
117
|
-
if (!invocation)
|
|
118
|
-
return { tags };
|
|
119
|
-
const asked = (value, requested) => value && requested && requested !== value ? `${value} (asked ${requested})` : value;
|
|
120
|
-
const thinking = asked(invocation.thinking, invocation.requestedThinking);
|
|
121
|
-
if (thinking)
|
|
122
|
-
tags.push(`thinking: ${thinking}`);
|
|
123
|
-
if (invocation.isolated)
|
|
124
|
-
tags.push("isolated");
|
|
125
|
-
if (invocation.isolation === "worktree")
|
|
126
|
-
tags.push("worktree");
|
|
127
|
-
if (invocation.inheritContext)
|
|
128
|
-
tags.push("inherit context");
|
|
129
|
-
if (invocation.runInBackground)
|
|
130
|
-
tags.push("background");
|
|
131
|
-
if (invocation.maxTurns != null)
|
|
132
|
-
tags.push(`max turns: ${invocation.maxTurns}`);
|
|
133
|
-
return {
|
|
134
|
-
modelName: asked(invocation.modelName, invocation.requestedModel),
|
|
135
|
-
modelId: asked(invocation.modelId, invocation.requestedModel),
|
|
136
|
-
tags,
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
/** Normalize and truncate text so it can never add physical widget rows. */
|
|
140
|
-
function truncateLine(text, len = 60) {
|
|
141
|
-
const line = text.replace(/[\r\n]+/g, " ").replace(/\s+/g, " ").trim();
|
|
142
|
-
if (line.length <= len)
|
|
143
|
-
return line;
|
|
144
|
-
return line.slice(0, len) + "…";
|
|
145
|
-
}
|
|
146
|
-
/** Build a human-readable activity string from currently-running tools or response text. */
|
|
147
|
-
export function describeActivity(activeTools, responseText) {
|
|
148
|
-
if (activeTools.size > 0) {
|
|
149
|
-
const groups = new Map();
|
|
150
|
-
for (const toolName of activeTools.values()) {
|
|
151
|
-
const action = TOOL_DISPLAY[toolName] ?? toolName;
|
|
152
|
-
groups.set(action, (groups.get(action) ?? 0) + 1);
|
|
153
|
-
}
|
|
154
|
-
const parts = [];
|
|
155
|
-
for (const [action, count] of groups) {
|
|
156
|
-
if (count > 1) {
|
|
157
|
-
parts.push(`${action} ${count} ${action === "searching" ? "patterns" : "files"}`);
|
|
158
|
-
}
|
|
159
|
-
else {
|
|
160
|
-
parts.push(action);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
return parts.join(", ") + "…";
|
|
164
|
-
}
|
|
165
|
-
// No tools active — show truncated response text if available
|
|
166
|
-
if (responseText && responseText.trim().length > 0) {
|
|
167
|
-
return truncateLine(responseText);
|
|
168
|
-
}
|
|
169
|
-
return "thinking…";
|
|
170
|
-
}
|
|
171
|
-
// ---- Widget manager ----
|
|
172
|
-
export class AgentWidget {
|
|
173
|
-
manager;
|
|
174
|
-
agentActivity;
|
|
175
|
-
mode;
|
|
176
|
-
uiCtx;
|
|
177
|
-
widgetFrame = 0;
|
|
178
|
-
widgetInterval;
|
|
179
|
-
inputUnsub;
|
|
180
|
-
/** Whether arrow keys currently navigate the agent roster. */
|
|
181
|
-
navigationActive = false;
|
|
182
|
-
/** Stable identity of the selected row, so roster changes do not jump selection. */
|
|
183
|
-
selectedAgentId;
|
|
184
|
-
/** Last logical roster index of the selected row, used when it disappears. */
|
|
185
|
-
selectedRosterIndex = 0;
|
|
186
|
-
/** First logical row currently represented by the bounded viewport. */
|
|
187
|
-
viewportStart = 0;
|
|
188
|
-
/** Cached records and roster. Rebuilt by update(), never by an arrow press. */
|
|
189
|
-
cachedAgents = [];
|
|
190
|
-
cachedRoster = [];
|
|
191
|
-
rosterCacheKey;
|
|
192
|
-
rosterCacheInitialized = false;
|
|
193
|
-
/** Terminal history capability is a potentially filesystem-backed lookup. */
|
|
194
|
-
historyOpenabilityCache = new Map();
|
|
195
|
-
/** Whether the widget callback is currently registered with the TUI. */
|
|
196
|
-
widgetRegistered = false;
|
|
197
|
-
/** Cached TUI reference from widget factory callback, used for requestRender(). */
|
|
198
|
-
tui;
|
|
199
|
-
/** Last status bar text, used to avoid redundant setStatus calls. */
|
|
200
|
-
lastStatusText;
|
|
201
|
-
/** Snapshot of the state used for the last widget registration/render request. */
|
|
202
|
-
lastRenderKey;
|
|
203
|
-
openOptions;
|
|
204
|
-
showCost;
|
|
205
|
-
showModel;
|
|
206
|
-
constructor(manager, agentActivity,
|
|
207
|
-
/** Read live at render time. Selects which agents the widget shows. */
|
|
208
|
-
mode = () => "all", showCostOrOptions = () => false, showModel = () => false) {
|
|
209
|
-
this.manager = manager;
|
|
210
|
-
this.agentActivity = agentActivity;
|
|
211
|
-
this.mode = mode;
|
|
212
|
-
if (typeof showCostOrOptions === "function") {
|
|
213
|
-
this.showCost = showCostOrOptions;
|
|
214
|
-
this.openOptions = {};
|
|
215
|
-
}
|
|
216
|
-
else {
|
|
217
|
-
this.showCost = showCostOrOptions.showCost ?? (() => false);
|
|
218
|
-
this.openOptions = showCostOrOptions;
|
|
219
|
-
}
|
|
220
|
-
this.showModel = showModel;
|
|
221
|
-
}
|
|
222
|
-
/**
|
|
223
|
-
* Agents eligible for the widget, per the current `WidgetMode`:
|
|
224
|
-
* - `off`: none (the widget's existing empty-state path hides it entirely).
|
|
225
|
-
* - `background`: drop only agents *known* to be foreground
|
|
226
|
-
* (`isBackground === false`); keep everything else — background, queued,
|
|
227
|
-
* scheduled, or RPC-spawned (`undefined`). Keying off the `isBackground`
|
|
228
|
-
* record flag rather than the UI-only `invocation` snapshot (which only the
|
|
229
|
-
* Agent-tool path sets), and excluding rather than allow-listing, means
|
|
230
|
-
* only proven-foreground runs drop out — nothing else silently vanishes.
|
|
231
|
-
* - `all`: every agent.
|
|
232
|
-
*/
|
|
233
|
-
widgetAgentsFromManager() {
|
|
234
|
-
const all = this.manager.listAgents();
|
|
235
|
-
switch (this.mode()) {
|
|
236
|
-
case "off": return [];
|
|
237
|
-
case "background": return all.filter(a => a.isBackground !== false);
|
|
238
|
-
default: return all;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
/** Records currently visible to the widget; safe for render and input paths. */
|
|
242
|
-
widgetAgents() {
|
|
243
|
-
return this.rosterCacheInitialized ? this.cachedAgents : this.widgetAgentsFromManager();
|
|
244
|
-
}
|
|
245
|
-
canOpenHistory(record) {
|
|
246
|
-
return this.openOptions.canOpenHistory?.(record) ?? true;
|
|
247
|
-
}
|
|
248
|
-
historyOpenabilityKey(record) {
|
|
249
|
-
return JSON.stringify([
|
|
250
|
-
record.status,
|
|
251
|
-
record.completedAt ?? null,
|
|
252
|
-
record.transcriptPath ?? null,
|
|
253
|
-
record.session !== undefined,
|
|
254
|
-
]);
|
|
255
|
-
}
|
|
256
|
-
/**
|
|
257
|
-
* Cache terminal history capability by the record state that can affect it.
|
|
258
|
-
* The supplied predicate may call `existsSync`, so it must not run from a
|
|
259
|
-
* timer refresh, render-key calculation, or arrow-navigation path more than
|
|
260
|
-
* once for the same terminal record state.
|
|
261
|
-
*/
|
|
262
|
-
cachedCanOpenHistory(record) {
|
|
263
|
-
if (record.status === "running" || record.status === "queued")
|
|
264
|
-
return true;
|
|
265
|
-
const key = this.historyOpenabilityKey(record);
|
|
266
|
-
const cached = this.historyOpenabilityCache.get(record.id);
|
|
267
|
-
if (cached?.key === key)
|
|
268
|
-
return cached.value;
|
|
269
|
-
const value = this.canOpenHistory(record);
|
|
270
|
-
this.historyOpenabilityCache.set(record.id, { key, value });
|
|
271
|
-
return value;
|
|
272
|
-
}
|
|
273
|
-
/** Refresh the manager snapshot and openability cache at lifecycle/update time. */
|
|
274
|
-
refreshRoster() {
|
|
275
|
-
const agents = this.widgetAgentsFromManager();
|
|
276
|
-
const key = JSON.stringify({
|
|
277
|
-
mode: this.mode(),
|
|
278
|
-
agents: agents.map(record => [
|
|
279
|
-
record.id,
|
|
280
|
-
record.type,
|
|
281
|
-
record.description,
|
|
282
|
-
record.status,
|
|
283
|
-
record.completedAt ?? null,
|
|
284
|
-
record.startedAt,
|
|
285
|
-
record.toolUses,
|
|
286
|
-
record.error ?? null,
|
|
287
|
-
record.transcriptPath ?? null,
|
|
288
|
-
record.session !== undefined,
|
|
289
|
-
record.isBackground,
|
|
290
|
-
record.status !== "running" && record.status !== "queued"
|
|
291
|
-
? this.cachedCanOpenHistory(record)
|
|
292
|
-
: undefined,
|
|
293
|
-
]),
|
|
294
|
-
});
|
|
295
|
-
if (this.rosterCacheInitialized && key === this.rosterCacheKey)
|
|
296
|
-
return this.cachedRoster;
|
|
297
|
-
const finished = agents.filter(record => record.status !== "running" && record.status !== "queued"
|
|
298
|
-
&& record.completedAt !== undefined
|
|
299
|
-
&& this.cachedCanOpenHistory(record));
|
|
300
|
-
const running = agents.filter(record => record.status === "running");
|
|
301
|
-
const queued = agents.filter(record => record.status === "queued");
|
|
302
|
-
this.cachedAgents = agents;
|
|
303
|
-
this.cachedRoster = [...running, ...queued, ...finished];
|
|
304
|
-
this.rosterCacheKey = key;
|
|
305
|
-
this.rosterCacheInitialized = true;
|
|
306
|
-
return this.cachedRoster;
|
|
307
|
-
}
|
|
308
|
-
/** Set the UI context (grabbed from first tool execution). */
|
|
309
|
-
setUICtx(ctx) {
|
|
310
|
-
if (ctx === this.uiCtx)
|
|
311
|
-
return false;
|
|
312
|
-
// UICtx changed — the widget and input handler registered on the old
|
|
313
|
-
// context are gone. Re-register both on the next update().
|
|
314
|
-
this.inputUnsub?.();
|
|
315
|
-
this.inputUnsub = undefined;
|
|
316
|
-
this.uiCtx = ctx;
|
|
317
|
-
this.widgetRegistered = false;
|
|
318
|
-
this.tui = undefined;
|
|
319
|
-
this.lastStatusText = undefined;
|
|
320
|
-
this.lastRenderKey = undefined;
|
|
321
|
-
this.navigationActive = false;
|
|
322
|
-
this.selectedAgentId = undefined;
|
|
323
|
-
this.selectedRosterIndex = 0;
|
|
324
|
-
this.viewportStart = 0;
|
|
325
|
-
this.cachedAgents = [];
|
|
326
|
-
this.cachedRoster = [];
|
|
327
|
-
this.rosterCacheKey = undefined;
|
|
328
|
-
this.rosterCacheInitialized = false;
|
|
329
|
-
this.historyOpenabilityCache.clear();
|
|
330
|
-
// Print/RPC tests and lightweight embedders may provide only the widget
|
|
331
|
-
// surface; real interactive contexts always implement this hook.
|
|
332
|
-
if (typeof ctx.onTerminalInput === "function") {
|
|
333
|
-
this.inputUnsub = ctx.onTerminalInput(data => this.handleKey(data));
|
|
334
|
-
}
|
|
335
|
-
return true;
|
|
336
|
-
}
|
|
337
|
-
/** Request a render on the currently registered TUI without touching input. */
|
|
338
|
-
requestUiRefresh(force = false) {
|
|
339
|
-
if (!this.tui || typeof this.tui.requestRender !== "function")
|
|
340
|
-
return false;
|
|
341
|
-
this.tui.requestRender(force);
|
|
342
|
-
return true;
|
|
343
|
-
}
|
|
344
|
-
/** Called on each new turn (tool_execution_start). */
|
|
345
|
-
onTurnStart() {
|
|
346
|
-
this.update();
|
|
347
|
-
}
|
|
348
|
-
/** Keep the spinner/elapsed-time timer alive only while a visible agent runs. */
|
|
349
|
-
ensureTimer() {
|
|
350
|
-
if (!this.uiCtx)
|
|
351
|
-
return;
|
|
352
|
-
const roster = this.rosterCacheInitialized ? this.cachedRoster : this.refreshRoster();
|
|
353
|
-
if (!roster.some(a => a.status === "running"))
|
|
354
|
-
return;
|
|
355
|
-
if (!this.widgetInterval) {
|
|
356
|
-
this.widgetInterval = setInterval(() => this.update(true), 250);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
syncTimer(shouldRun) {
|
|
360
|
-
if (shouldRun) {
|
|
361
|
-
this.ensureTimer();
|
|
362
|
-
}
|
|
363
|
-
else if (this.widgetInterval) {
|
|
364
|
-
clearInterval(this.widgetInterval);
|
|
365
|
-
this.widgetInterval = undefined;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
/**
|
|
369
|
-
* Retained for the lifecycle call sites. Terminal visibility is determined
|
|
370
|
-
* by the manager record and the history capability, not a one-turn timer.
|
|
371
|
-
*/
|
|
372
|
-
markFinished(_agentId) { }
|
|
373
|
-
/** Retained for lifecycle call sites; terminal rows are record-driven now. */
|
|
374
|
-
markRunning(_agentId) { }
|
|
375
|
-
/**
|
|
376
|
-
* Records represented by selectable rows in the above-editor widget.
|
|
377
|
-
*
|
|
378
|
-
* Keep this order as the single source of truth for both rendering and key
|
|
379
|
-
* navigation. `listAgents()` is newest-first; active rows come first so the
|
|
380
|
-
* panel exposes currently useful work before terminal history.
|
|
381
|
-
*/
|
|
382
|
-
roster() {
|
|
383
|
-
return this.rosterCacheInitialized ? this.cachedRoster : this.refreshRoster();
|
|
384
|
-
}
|
|
385
|
-
/** True when pi's prompt editor owns the keyboard. */
|
|
386
|
-
editorHasFocus() {
|
|
387
|
-
const focused = this.tui?.focusedComponent;
|
|
388
|
-
return focused == null || focused instanceof Editor;
|
|
389
|
-
}
|
|
390
|
-
selectedIndexOf(records) {
|
|
391
|
-
if (!this.selectedAgentId)
|
|
392
|
-
return -1;
|
|
393
|
-
return records.findIndex(record => record.id === this.selectedAgentId);
|
|
394
|
-
}
|
|
395
|
-
deactivate() {
|
|
396
|
-
// Keep the selected row and viewport so re-entering navigation can resume
|
|
397
|
-
// where the user left off. Lifecycle resets (context, dispose, empty
|
|
398
|
-
// roster) clear this state explicitly instead of treating every exit as a
|
|
399
|
-
// reset.
|
|
400
|
-
this.navigationActive = false;
|
|
401
|
-
this.requestUiRefresh();
|
|
402
|
-
}
|
|
403
|
-
/** Resume navigation from the retained row, or select the first row. */
|
|
404
|
-
activate(records) {
|
|
405
|
-
this.navigationActive = true;
|
|
406
|
-
const selectedIndex = this.selectedIndexOf(records);
|
|
407
|
-
if (selectedIndex >= 0) {
|
|
408
|
-
this.selectedRosterIndex = selectedIndex;
|
|
409
|
-
}
|
|
410
|
-
else {
|
|
411
|
-
// The previously selected row may have been cleaned up while navigation
|
|
412
|
-
// was inactive. Resume at its old logical position, clamped to the new
|
|
413
|
-
// roster, rather than jumping back to the first row.
|
|
414
|
-
const fallbackIndex = Math.max(0, Math.min(this.selectedRosterIndex, records.length - 1));
|
|
415
|
-
this.selectedAgentId = records[fallbackIndex].id;
|
|
416
|
-
this.selectedRosterIndex = fallbackIndex;
|
|
417
|
-
this.viewportStart = Math.min(this.viewportStart, Math.max(0, records.length - 1));
|
|
418
|
-
}
|
|
419
|
-
this.requestUiRefresh();
|
|
420
|
-
}
|
|
421
|
-
/** Move the selected row, activating only from an empty focused editor. */
|
|
422
|
-
moveSelection(direction) {
|
|
423
|
-
const records = this.roster();
|
|
424
|
-
const ui = this.uiCtx;
|
|
425
|
-
if (records.length === 0 || !ui)
|
|
426
|
-
return false;
|
|
427
|
-
if (!this.navigationActive) {
|
|
428
|
-
if (direction !== 1 || !this.editorHasFocus() || (ui.getEditorText?.() ?? "") !== "")
|
|
429
|
-
return false;
|
|
430
|
-
this.activate(records);
|
|
431
|
-
return true;
|
|
432
|
-
}
|
|
433
|
-
const currentIndex = Math.max(0, this.selectedIndexOf(records));
|
|
434
|
-
if (direction === -1 && currentIndex === 0) {
|
|
435
|
-
this.deactivate();
|
|
436
|
-
return true;
|
|
437
|
-
}
|
|
438
|
-
const nextIndex = Math.max(0, Math.min(records.length - 1, currentIndex + direction));
|
|
439
|
-
this.selectedAgentId = records[nextIndex].id;
|
|
440
|
-
this.selectedRosterIndex = nextIndex;
|
|
441
|
-
this.requestUiRefresh();
|
|
442
|
-
return true;
|
|
443
|
-
}
|
|
444
|
-
openSelected() {
|
|
445
|
-
const record = this.roster().find(candidate => candidate.id === this.selectedAgentId);
|
|
446
|
-
this.deactivate();
|
|
447
|
-
if (!record)
|
|
448
|
-
return;
|
|
449
|
-
const mode = record.status === "running" || record.status === "queued" ? "live" : "history";
|
|
450
|
-
void this.openOptions.onOpen?.(record, mode);
|
|
451
|
-
}
|
|
452
|
-
/** Handle terminal input before it reaches the focused prompt editor. */
|
|
453
|
-
handleKey(data) {
|
|
454
|
-
if (!this.uiCtx || isKeyRelease(data))
|
|
455
|
-
return undefined;
|
|
456
|
-
if (!this.editorHasFocus()) {
|
|
457
|
-
if (this.navigationActive)
|
|
458
|
-
this.deactivate();
|
|
459
|
-
return undefined;
|
|
460
|
-
}
|
|
461
|
-
if (!this.navigationActive) {
|
|
462
|
-
const records = this.roster();
|
|
463
|
-
if (!matchesKey(data, "down") || (this.uiCtx.getEditorText?.() ?? "") !== "" || records.length === 0) {
|
|
464
|
-
return undefined;
|
|
465
|
-
}
|
|
466
|
-
this.activate(records);
|
|
467
|
-
return { consume: true };
|
|
468
|
-
}
|
|
469
|
-
if (matchesKey(data, "escape")) {
|
|
470
|
-
this.deactivate();
|
|
471
|
-
return { consume: true };
|
|
472
|
-
}
|
|
473
|
-
if (matchesKey(data, "up"))
|
|
474
|
-
return this.moveSelection(-1) ? { consume: true } : undefined;
|
|
475
|
-
if (matchesKey(data, "down"))
|
|
476
|
-
return this.moveSelection(1) ? { consume: true } : undefined;
|
|
477
|
-
if (matchesKey(data, Key.enter)) {
|
|
478
|
-
this.openSelected();
|
|
479
|
-
return { consume: true };
|
|
480
|
-
}
|
|
481
|
-
// Only ↑/↓ navigate. Other keys, including j/k/←/→, flow to the editor
|
|
482
|
-
// and leave navigation mode.
|
|
483
|
-
this.deactivate();
|
|
484
|
-
return undefined;
|
|
485
|
-
}
|
|
486
|
-
/** Render a finished agent line. */
|
|
487
|
-
renderFinishedLine(a, theme) {
|
|
488
|
-
const modeLabel = getPromptModeLabel(a.type);
|
|
489
|
-
const duration = formatMs((a.completedAt ?? Date.now()) - a.startedAt);
|
|
490
|
-
let icon;
|
|
491
|
-
let statusText;
|
|
492
|
-
if (a.status === "completed") {
|
|
493
|
-
icon = theme.fg("success", "✓");
|
|
494
|
-
statusText = "";
|
|
495
|
-
}
|
|
496
|
-
else if (a.status === "steered") {
|
|
497
|
-
icon = theme.fg("warning", "✓");
|
|
498
|
-
statusText = theme.fg("warning", " (turn limit)");
|
|
499
|
-
}
|
|
500
|
-
else if (a.status === "stopped") {
|
|
501
|
-
icon = theme.fg("dim", "■");
|
|
502
|
-
statusText = theme.fg("dim", " stopped");
|
|
503
|
-
}
|
|
504
|
-
else if (a.status === "error") {
|
|
505
|
-
icon = theme.fg("error", "✗");
|
|
506
|
-
const errMsg = a.error ? `: ${truncateLine(a.error)}` : "";
|
|
507
|
-
statusText = theme.fg("error", ` error${errMsg}`);
|
|
508
|
-
}
|
|
509
|
-
else {
|
|
510
|
-
// aborted
|
|
511
|
-
icon = theme.fg("error", "✗");
|
|
512
|
-
statusText = theme.fg("warning", " aborted");
|
|
513
|
-
}
|
|
514
|
-
const parts = [];
|
|
515
|
-
const activity = this.agentActivity.get(a.id);
|
|
516
|
-
if (activity)
|
|
517
|
-
parts.push(formatTurns(activity.turnCount, activity.maxTurns));
|
|
518
|
-
if (a.toolUses > 0)
|
|
519
|
-
parts.push(`${a.toolUses} tool use${a.toolUses === 1 ? "" : "s"}`);
|
|
520
|
-
const costText = this.showCost() && a.lifetimeUsage ? formatCost(getLifetimeCost(a.lifetimeUsage)) : "";
|
|
521
|
-
if (costText)
|
|
522
|
-
parts.push(costText);
|
|
523
|
-
parts.push(duration);
|
|
524
|
-
const modeTag = modeLabel ? ` ${theme.fg("dim", `(${modeLabel})`)}` : "";
|
|
525
|
-
const description = truncateLine(a.description);
|
|
526
|
-
return `${icon} ${renderAgentName(a.type, theme, { fallbackColor: "dim" })}${modeTag} ${theme.fg("dim", description)} ${theme.fg("dim", "·")} ${theme.fg("dim", parts.join(" · "))}${statusText}`;
|
|
527
|
-
}
|
|
528
|
-
/**
|
|
529
|
-
* Render the widget content. Called from the registered widget's render() callback,
|
|
530
|
-
* reading live state each time instead of capturing it in a closure.
|
|
531
|
-
*/
|
|
532
|
-
renderWidget(tui, theme) {
|
|
533
|
-
const roster = this.roster();
|
|
534
|
-
const running = roster.filter(a => a.status === "running");
|
|
535
|
-
const queued = roster.filter(a => a.status === "queued");
|
|
536
|
-
const finished = roster.filter(a => a.status !== "running" && a.status !== "queued");
|
|
537
|
-
const selectedId = this.navigationActive ? this.selectedAgentId : undefined;
|
|
538
|
-
const hasActive = running.length > 0 || queued.length > 0;
|
|
539
|
-
const hasFinished = finished.length > 0;
|
|
540
|
-
// Nothing to show — return empty (widget will be unregistered by update())
|
|
541
|
-
if (!hasActive && !hasFinished)
|
|
542
|
-
return [];
|
|
543
|
-
const w = tui.terminal.columns;
|
|
544
|
-
const maxLines = getWidgetLineBudget(tui.terminal.rows);
|
|
545
|
-
if (maxLines === 0)
|
|
546
|
-
return [];
|
|
547
|
-
const truncate = (line, width = w) => truncateToWidth(line, Math.max(0, width));
|
|
548
|
-
const headingColor = hasActive ? "accent" : "dim";
|
|
549
|
-
const headingIcon = hasActive ? "●" : "○";
|
|
550
|
-
const frame = SPINNER[this.widgetFrame % SPINNER.length];
|
|
551
|
-
const finishedLines = [];
|
|
552
|
-
for (const a of finished) {
|
|
553
|
-
const marker = a.id === selectedId ? theme.fg("accent", "●") : theme.fg("dim", "○");
|
|
554
|
-
finishedLines.push({
|
|
555
|
-
record: a,
|
|
556
|
-
lines: [truncate(theme.fg("dim", "├─") + ` ${marker} ` + this.renderFinishedLine(a, theme))],
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
|
-
const runningLines = []; // each entry is [header, activity]
|
|
560
|
-
for (const a of running) {
|
|
561
|
-
const modeLabel = getPromptModeLabel(a.type);
|
|
562
|
-
const modeTag = modeLabel ? ` ${theme.fg("dim", `(${modeLabel})`)}` : "";
|
|
563
|
-
const elapsed = formatMs(Date.now() - a.startedAt);
|
|
564
|
-
const bg = this.agentActivity.get(a.id);
|
|
565
|
-
const toolUses = bg?.toolUses ?? a.toolUses;
|
|
566
|
-
// Spend comes from the record, never from the activity tracker: the record
|
|
567
|
-
// survives the agent finishing and includes nested-child usage.
|
|
568
|
-
const tokens = getLifetimeTotal(a.lifetimeUsage);
|
|
569
|
-
const contextPercent = getSessionContextPercent(bg?.session);
|
|
570
|
-
const tokenText = tokens > 0 ? formatSessionTokens(tokens, contextPercent, theme, a.compactionCount) : "";
|
|
571
|
-
const costText = this.showCost() ? formatCost(getLifetimeCost(a.lifetimeUsage)) : "";
|
|
572
|
-
const parts = [];
|
|
573
|
-
if (this.showModel()) {
|
|
574
|
-
const { modelName, tags } = buildInvocationTags(a.invocation);
|
|
575
|
-
if (modelName)
|
|
576
|
-
parts.push(modelName);
|
|
577
|
-
const thinkingTag = tags.find(tag => tag.startsWith("thinking: "));
|
|
578
|
-
if (thinkingTag)
|
|
579
|
-
parts.push(thinkingTag);
|
|
580
|
-
}
|
|
581
|
-
if (bg)
|
|
582
|
-
parts.push(formatTurns(bg.turnCount, bg.maxTurns));
|
|
583
|
-
if (toolUses > 0)
|
|
584
|
-
parts.push(`${toolUses} tool use${toolUses === 1 ? "" : "s"}`);
|
|
585
|
-
if (tokenText)
|
|
586
|
-
parts.push(tokenText);
|
|
587
|
-
if (costText)
|
|
588
|
-
parts.push(costText);
|
|
589
|
-
parts.push(elapsed);
|
|
590
|
-
const statsText = parts.join(" · ");
|
|
591
|
-
const activity = bg ? describeActivity(bg.activeTools, bg.responseText) : "thinking…";
|
|
592
|
-
const marker = a.id === selectedId ? theme.fg("accent", "●") : theme.fg("dim", "○");
|
|
593
|
-
runningLines.push({
|
|
594
|
-
record: a,
|
|
595
|
-
lines: [
|
|
596
|
-
truncate(theme.fg("dim", "├─") + ` ${marker} ${theme.fg("accent", frame)} ${renderAgentName(a.type, theme, { bold: true })}${modeTag} ${theme.fg("muted", truncateLine(a.description))} ${theme.fg("dim", "·")} ${fgPreservingNestedStyles(theme, "dim", statsText)}`),
|
|
597
|
-
truncate(theme.fg("dim", "│ ") + ` ${theme.fg("dim", `⎿ ${truncateLine(activity)}`)}`),
|
|
598
|
-
],
|
|
599
|
-
});
|
|
600
|
-
}
|
|
601
|
-
const queuedLines = queued.map(a => {
|
|
602
|
-
const marker = a.id === selectedId ? theme.fg("accent", "●") : theme.fg("dim", "○");
|
|
603
|
-
return {
|
|
604
|
-
record: a,
|
|
605
|
-
lines: [truncate(theme.fg("dim", "├─") + ` ${marker} ${theme.fg("muted", "◦")} ${theme.fg("dim", `${getDisplayName(a.type)} ${truncateLine(a.description)} · queued`)}`)],
|
|
606
|
-
};
|
|
607
|
-
});
|
|
608
|
-
// Keep the idle widget compact: a queue is one logical status line until
|
|
609
|
-
// navigation starts. While navigating, expand it back to the same per-agent
|
|
610
|
-
// roster used by key handling so every selected queued record can render.
|
|
611
|
-
const queuedSummary = queued.length > 0 && !this.navigationActive
|
|
612
|
-
? [{
|
|
613
|
-
record: queued[0],
|
|
614
|
-
recordCount: queued.length,
|
|
615
|
-
lines: [truncate(theme.fg("dim", "├─") + ` ${theme.fg("dim", "○")} ${theme.fg("muted", "◦")} ${theme.fg("dim", `${queued.length} queued`)}`)],
|
|
616
|
-
}]
|
|
617
|
-
: [];
|
|
618
|
-
// Assemble with a responsive cap (heading + overflow indicator = 2
|
|
619
|
-
// reserved lines when content exceeds the available body budget).
|
|
620
|
-
const maxBody = maxLines - 1; // heading takes 1 line
|
|
621
|
-
const rows = [
|
|
622
|
-
...runningLines,
|
|
623
|
-
...(this.navigationActive ? queuedLines : queuedSummary),
|
|
624
|
-
...finishedLines,
|
|
625
|
-
];
|
|
626
|
-
const totalBody = rows.reduce((total, row) => total + row.lines.length, 0);
|
|
627
|
-
const heading = "Agents ↑↓ select · enter view · esc back";
|
|
628
|
-
const lines = [truncate(theme.fg(headingColor, headingIcon) + " " + theme.fg(headingColor, heading))];
|
|
629
|
-
if (maxLines === 1) {
|
|
630
|
-
// There is room only for the heading; do not consume the editor/input row.
|
|
631
|
-
return lines;
|
|
632
|
-
}
|
|
633
|
-
if (totalBody <= maxBody) {
|
|
634
|
-
this.viewportStart = 0;
|
|
635
|
-
for (const row of rows)
|
|
636
|
-
lines.push(...row.lines);
|
|
637
|
-
if (rows.length > 0) {
|
|
638
|
-
const lastRow = rows[rows.length - 1];
|
|
639
|
-
const lastStart = lines.length - lastRow.lines.length;
|
|
640
|
-
// Keep the selected row's bullet adjacent to the normal branch marker.
|
|
641
|
-
// This makes the active row unambiguous even when it is the last row.
|
|
642
|
-
const lastRowSelected = this.navigationActive && lastRow.record.id === selectedId;
|
|
643
|
-
if (!lastRowSelected)
|
|
644
|
-
lines[lastStart] = lines[lastStart].replace("├─", "└─");
|
|
645
|
-
if (lastRow.lines.length === 2) {
|
|
646
|
-
lines[lastStart + 1] = lines[lastStart + 1].replace("│ ", " ");
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
else if (!this.navigationActive && queuedSummary.length > 0) {
|
|
651
|
-
// In the idle view the queue is deliberately one summary row. Preserve
|
|
652
|
-
// that row before spending space on terminal history, and account for
|
|
653
|
-
// the real number of queued records if the summary itself cannot fit.
|
|
654
|
-
// Navigation expands the queue into per-record rows and uses the full
|
|
655
|
-
// contiguous viewport below.
|
|
656
|
-
const visibleRows = [];
|
|
657
|
-
const hiddenRows = [];
|
|
658
|
-
let bodyBudget = Math.max(0, maxBody - 1); // reserve the footer
|
|
659
|
-
let runningBudget = Math.max(0, bodyBudget - 1); // reserve queued summary
|
|
660
|
-
for (const row of runningLines) {
|
|
661
|
-
const height = row.lines.length;
|
|
662
|
-
if (height <= runningBudget) {
|
|
663
|
-
visibleRows.push(row);
|
|
664
|
-
runningBudget -= height;
|
|
665
|
-
bodyBudget -= height;
|
|
666
|
-
}
|
|
667
|
-
else {
|
|
668
|
-
hiddenRows.push(row);
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
const summaryRow = queuedSummary[0];
|
|
672
|
-
if (bodyBudget > 0) {
|
|
673
|
-
visibleRows.push(summaryRow);
|
|
674
|
-
bodyBudget -= summaryRow.lines.length;
|
|
675
|
-
}
|
|
676
|
-
else {
|
|
677
|
-
hiddenRows.push(summaryRow);
|
|
678
|
-
}
|
|
679
|
-
for (const row of finishedLines) {
|
|
680
|
-
const height = row.lines.length;
|
|
681
|
-
if (height <= bodyBudget) {
|
|
682
|
-
visibleRows.push(row);
|
|
683
|
-
bodyBudget -= height;
|
|
684
|
-
}
|
|
685
|
-
else {
|
|
686
|
-
hiddenRows.push(row);
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
for (const row of visibleRows)
|
|
690
|
-
lines.push(...row.lines);
|
|
691
|
-
if (visibleRows.length > 0) {
|
|
692
|
-
const lastRow = visibleRows[visibleRows.length - 1];
|
|
693
|
-
const lastStart = lines.length - lastRow.lines.length;
|
|
694
|
-
lines[lastStart] = lines[lastStart].replace("├─", "└─");
|
|
695
|
-
if (lastRow.lines.length === 2) {
|
|
696
|
-
lines[lastStart + 1] = lines[lastStart + 1].replace("│ ", " ");
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
const hiddenCount = (rows) => rows.reduce((total, row) => total + (row.recordCount ?? 1), 0);
|
|
700
|
-
const categoryCount = (status) => hiddenRows
|
|
701
|
-
.filter(row => row.record.status === status)
|
|
702
|
-
.reduce((total, row) => total + (row.recordCount ?? 1), 0);
|
|
703
|
-
const finishedCount = hiddenRows
|
|
704
|
-
.filter(row => row.record.status !== "running" && row.record.status !== "queued")
|
|
705
|
-
.reduce((total, row) => total + (row.recordCount ?? 1), 0);
|
|
706
|
-
const categoryCounts = [
|
|
707
|
-
["running", categoryCount("running")],
|
|
708
|
-
["queued", categoryCount("queued")],
|
|
709
|
-
["finished", finishedCount],
|
|
710
|
-
].filter(([, count]) => count > 0).map(([label, count]) => `${count} ${label}`);
|
|
711
|
-
const hidden = hiddenCount(hiddenRows);
|
|
712
|
-
lines.push(truncate(theme.fg("dim", "└─") + ` ${theme.fg("dim", `+${hidden} more (${categoryCounts.join(", ")})`)}`));
|
|
713
|
-
}
|
|
714
|
-
else {
|
|
715
|
-
// Reserve one line for a directional overflow summary. The viewport is
|
|
716
|
-
// a contiguous slice in roster order, so the same slice is navigable and
|
|
717
|
-
// renderable even when the selected row is currently hidden.
|
|
718
|
-
const baseContentBudget = Math.max(0, maxBody - 1);
|
|
719
|
-
const heightAt = (index) => rows[index]?.lines.length ?? 0;
|
|
720
|
-
const endFor = (start, budget) => {
|
|
721
|
-
let used = 0;
|
|
722
|
-
let end = start;
|
|
723
|
-
while (end < rows.length && used + heightAt(end) <= budget) {
|
|
724
|
-
used += heightAt(end++);
|
|
725
|
-
}
|
|
726
|
-
return end;
|
|
727
|
-
};
|
|
728
|
-
let start = Math.max(0, Math.min(this.viewportStart, Math.max(0, rows.length - 1)));
|
|
729
|
-
const selectedIndex = this.navigationActive && selectedId
|
|
730
|
-
? rows.findIndex(row => row.record.id === selectedId)
|
|
731
|
-
: -1;
|
|
732
|
-
if (selectedIndex >= 0) {
|
|
733
|
-
if (selectedIndex < start)
|
|
734
|
-
start = selectedIndex;
|
|
735
|
-
if (selectedIndex >= endFor(start, baseContentBudget))
|
|
736
|
-
start = selectedIndex;
|
|
737
|
-
}
|
|
738
|
-
// Keep both directional affordances in the viewport chrome: the top
|
|
739
|
-
// count sits below the heading and the bottom count sits below the
|
|
740
|
-
// bounded roster. Each consumes one row when that direction is hidden.
|
|
741
|
-
let showTopMore = start > 0;
|
|
742
|
-
let contentBudget = Math.max(0, maxBody - (showTopMore ? 2 : 1));
|
|
743
|
-
if (selectedIndex >= 0 && selectedIndex >= endFor(start, contentBudget)) {
|
|
744
|
-
start = selectedIndex;
|
|
745
|
-
showTopMore = start > 0;
|
|
746
|
-
contentBudget = Math.max(0, maxBody - (showTopMore ? 2 : 1));
|
|
747
|
-
}
|
|
748
|
-
this.viewportStart = start;
|
|
749
|
-
let used = 0;
|
|
750
|
-
let end = start;
|
|
751
|
-
const visibleBody = [];
|
|
752
|
-
while (end < rows.length && used + heightAt(end) <= contentBudget) {
|
|
753
|
-
visibleBody.push(...rows[end].lines);
|
|
754
|
-
used += heightAt(end);
|
|
755
|
-
end++;
|
|
756
|
-
}
|
|
757
|
-
// A selected two-line row must remain addressable even if only one body
|
|
758
|
-
// line is available. Showing its header is preferable to hiding it.
|
|
759
|
-
if (end === start && rows[start] && contentBudget > 0) {
|
|
760
|
-
visibleBody.push(rows[start].lines[0]);
|
|
761
|
-
end = start + 1;
|
|
762
|
-
}
|
|
763
|
-
const countRows = (selectedRows) => selectedRows.reduce((total, row) => total + (row.recordCount ?? 1), 0);
|
|
764
|
-
const hiddenBefore = countRows(rows.slice(0, start));
|
|
765
|
-
const hiddenAfter = countRows(rows.slice(end));
|
|
766
|
-
const hidden = hiddenBefore + hiddenAfter;
|
|
767
|
-
const hiddenRows = rows.filter((_row, index) => index < start || index >= end);
|
|
768
|
-
const categoryCount = (status) => hiddenRows
|
|
769
|
-
.filter(row => row.record.status === status)
|
|
770
|
-
.reduce((total, row) => total + (row.recordCount ?? 1), 0);
|
|
771
|
-
const categoryCounts = [
|
|
772
|
-
["running", categoryCount("running")],
|
|
773
|
-
["queued", categoryCount("queued")],
|
|
774
|
-
["finished", hiddenRows
|
|
775
|
-
.filter(row => row.record.status !== "running" && row.record.status !== "queued")
|
|
776
|
-
.reduce((total, row) => total + (row.recordCount ?? 1), 0)],
|
|
777
|
-
].filter(([, count]) => count > 0).map(([label, count]) => `${count} ${label}`);
|
|
778
|
-
const direction = [
|
|
779
|
-
...(hiddenBefore > 0 ? [`↑ ${hiddenBefore} more`] : []),
|
|
780
|
-
...(hiddenAfter > 0 ? [`↓ ${hiddenAfter} more`] : []),
|
|
781
|
-
].join(" · ");
|
|
782
|
-
const summary = `+${hidden} more (${direction}${categoryCounts.length > 0 ? `; ${categoryCounts.join(", ")}` : ""})`;
|
|
783
|
-
if (showTopMore)
|
|
784
|
-
lines.push(truncate(theme.fg("dim", `↑ ${hiddenBefore} more`)));
|
|
785
|
-
// Fill the bounded viewport so the scrollbar track has a stable height.
|
|
786
|
-
while (visibleBody.length < contentBudget)
|
|
787
|
-
visibleBody.push("");
|
|
788
|
-
const totalBody = rows.reduce((total, row) => total + row.lines.length, 0);
|
|
789
|
-
const startOffset = rows.slice(0, start).reduce((total, row) => total + row.lines.length, 0);
|
|
790
|
-
const trackHeight = Math.max(0, contentBudget);
|
|
791
|
-
const minThumbHeight = Math.min(2, trackHeight);
|
|
792
|
-
const thumbHeight = Math.max(minThumbHeight, Math.min(trackHeight, Math.round((trackHeight * trackHeight) / totalBody)));
|
|
793
|
-
const maxScrollTop = Math.max(0, totalBody - trackHeight);
|
|
794
|
-
const maxThumbTop = Math.max(0, trackHeight - thumbHeight);
|
|
795
|
-
const thumbOffset = maxScrollTop === 0
|
|
796
|
-
? 0
|
|
797
|
-
: Math.round((startOffset / maxScrollTop) * maxThumbTop);
|
|
798
|
-
const contentWidth = Math.max(0, w - 1);
|
|
799
|
-
for (let index = 0; index < visibleBody.length; index++) {
|
|
800
|
-
const isThumb = index >= thumbOffset && index < thumbOffset + thumbHeight;
|
|
801
|
-
const scrollbar = isThumb
|
|
802
|
-
? theme.fg("scrollbarThumb", this.navigationActive ? "█" : "┃")
|
|
803
|
-
: theme.fg("scrollbarTrack", "│");
|
|
804
|
-
const bodyLine = truncate(visibleBody[index] ?? "", contentWidth);
|
|
805
|
-
lines.push(bodyLine + " ".repeat(Math.max(0, contentWidth - visibleWidth(bodyLine))) + scrollbar);
|
|
806
|
-
}
|
|
807
|
-
// The directional count is outside the viewport, like ScrollView's
|
|
808
|
-
// surrounding chrome, and therefore does not consume scrollbar track.
|
|
809
|
-
if (hiddenAfter > 0) {
|
|
810
|
-
lines.push(truncate(theme.fg("dim", `↓ ${hiddenAfter} more · +${hidden} more (${categoryCounts.length > 0 ? categoryCounts.join(", ") : ""})`)));
|
|
811
|
-
}
|
|
812
|
-
else if (!showTopMore) {
|
|
813
|
-
lines.push(truncate(theme.fg("dim", "└─") + ` ${theme.fg("dim", summary)}`));
|
|
814
|
-
}
|
|
815
|
-
else {
|
|
816
|
-
// Keep a stable footer row when the selected viewport reaches the end.
|
|
817
|
-
lines.push(truncate(theme.fg("dim", "└─")));
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
return lines;
|
|
821
|
-
}
|
|
822
|
-
/** Build a render-relevant snapshot without capturing mutable records. */
|
|
823
|
-
renderKey(allAgents) {
|
|
824
|
-
const activities = allAgents.map(a => {
|
|
825
|
-
const activity = this.agentActivity.get(a.id);
|
|
826
|
-
return activity ? {
|
|
827
|
-
id: a.id,
|
|
828
|
-
activeTools: [...activity.activeTools.entries()],
|
|
829
|
-
toolUses: activity.toolUses,
|
|
830
|
-
responseText: activity.responseText,
|
|
831
|
-
turnCount: activity.turnCount,
|
|
832
|
-
maxTurns: activity.maxTurns,
|
|
833
|
-
lifetimeUsage: activity.lifetimeUsage,
|
|
834
|
-
contextPercent: getSessionContextPercent(activity.session),
|
|
835
|
-
} : undefined;
|
|
836
|
-
});
|
|
837
|
-
return JSON.stringify({
|
|
838
|
-
frame: this.widgetFrame,
|
|
839
|
-
agents: allAgents.map(a => ({
|
|
840
|
-
id: a.id,
|
|
841
|
-
type: a.type,
|
|
842
|
-
description: a.description,
|
|
843
|
-
status: a.status,
|
|
844
|
-
toolUses: a.toolUses,
|
|
845
|
-
startedAt: a.startedAt,
|
|
846
|
-
completedAt: a.completedAt,
|
|
847
|
-
error: a.error,
|
|
848
|
-
lifetimeUsage: a.lifetimeUsage,
|
|
849
|
-
compactionCount: a.compactionCount,
|
|
850
|
-
isBackground: a.isBackground,
|
|
851
|
-
hasSession: a.session !== undefined,
|
|
852
|
-
transcriptPath: a.transcriptPath,
|
|
853
|
-
openableHistory: this.cachedCanOpenHistory(a),
|
|
854
|
-
})),
|
|
855
|
-
activities,
|
|
856
|
-
navigationActive: this.navigationActive,
|
|
857
|
-
selectedAgentId: this.selectedAgentId,
|
|
858
|
-
viewportStart: this.viewportStart,
|
|
859
|
-
});
|
|
860
|
-
}
|
|
861
|
-
/** Force an immediate widget update. `advanceSpinner` is reserved for the timer. */
|
|
862
|
-
update(advanceSpinner = false) {
|
|
863
|
-
if (!this.uiCtx)
|
|
864
|
-
return;
|
|
865
|
-
// Refresh the manager snapshot once per lifecycle update. Arrow handling
|
|
866
|
-
// only changes selection and requests a render; it never scans the manager.
|
|
867
|
-
const roster = this.refreshRoster();
|
|
868
|
-
const allAgents = this.widgetAgents();
|
|
869
|
-
// Lightweight existence checks — full categorization happens in renderWidget()
|
|
870
|
-
let runningCount = 0;
|
|
871
|
-
let queuedCount = 0;
|
|
872
|
-
for (const a of roster) {
|
|
873
|
-
if (a.status === "running")
|
|
874
|
-
runningCount++;
|
|
875
|
-
else if (a.status === "queued")
|
|
876
|
-
queuedCount++;
|
|
877
|
-
}
|
|
878
|
-
const hasFinished = roster.some(a => a.status !== "running" && a.status !== "queued");
|
|
879
|
-
const hasActive = runningCount > 0 || queuedCount > 0;
|
|
880
|
-
// Nothing to show — clear widget
|
|
881
|
-
if (!hasActive && !hasFinished) {
|
|
882
|
-
if (this.widgetRegistered) {
|
|
883
|
-
this.uiCtx.setWidget("agents", undefined);
|
|
884
|
-
this.widgetRegistered = false;
|
|
885
|
-
this.tui = undefined;
|
|
886
|
-
}
|
|
887
|
-
if (this.lastStatusText !== undefined) {
|
|
888
|
-
this.uiCtx.setStatus("subagents", undefined);
|
|
889
|
-
this.lastStatusText = undefined;
|
|
890
|
-
}
|
|
891
|
-
this.lastRenderKey = undefined;
|
|
892
|
-
this.navigationActive = false;
|
|
893
|
-
this.selectedAgentId = undefined;
|
|
894
|
-
this.selectedRosterIndex = 0;
|
|
895
|
-
this.viewportStart = 0;
|
|
896
|
-
this.syncTimer(false);
|
|
897
|
-
return;
|
|
898
|
-
}
|
|
899
|
-
// Status bar — only call setStatus when the text actually changes
|
|
900
|
-
let newStatusText;
|
|
901
|
-
if (hasActive) {
|
|
902
|
-
const statusParts = [];
|
|
903
|
-
if (runningCount > 0)
|
|
904
|
-
statusParts.push(`${runningCount} running`);
|
|
905
|
-
if (queuedCount > 0)
|
|
906
|
-
statusParts.push(`${queuedCount} queued`);
|
|
907
|
-
const total = runningCount + queuedCount;
|
|
908
|
-
newStatusText = `${statusParts.join(", ")} agent${total === 1 ? "" : "s"}`;
|
|
909
|
-
}
|
|
910
|
-
if (newStatusText !== this.lastStatusText) {
|
|
911
|
-
this.uiCtx.setStatus("subagents", newStatusText);
|
|
912
|
-
this.lastStatusText = newStatusText;
|
|
913
|
-
}
|
|
914
|
-
// Spinner animation is driven only by the timer while a visible running
|
|
915
|
-
// agent exists. Event-driven updates refresh changed stats without making
|
|
916
|
-
// an otherwise idle widget advance.
|
|
917
|
-
if (advanceSpinner && runningCount > 0)
|
|
918
|
-
this.widgetFrame++;
|
|
919
|
-
this.syncTimer(runningCount > 0);
|
|
920
|
-
const selectedIndex = this.selectedIndexOf(roster);
|
|
921
|
-
if (selectedIndex >= 0) {
|
|
922
|
-
this.selectedRosterIndex = selectedIndex;
|
|
923
|
-
}
|
|
924
|
-
else if (this.navigationActive) {
|
|
925
|
-
if (roster.length === 0) {
|
|
926
|
-
this.navigationActive = false;
|
|
927
|
-
this.selectedAgentId = undefined;
|
|
928
|
-
this.selectedRosterIndex = 0;
|
|
929
|
-
this.viewportStart = 0;
|
|
930
|
-
}
|
|
931
|
-
else {
|
|
932
|
-
// Keep the selection near the row that disappeared. Both the saved
|
|
933
|
-
// logical index and viewport are clamped as the roster shrinks.
|
|
934
|
-
const fallbackIndex = Math.max(0, Math.min(this.selectedRosterIndex, roster.length - 1));
|
|
935
|
-
this.selectedAgentId = roster[fallbackIndex].id;
|
|
936
|
-
this.selectedRosterIndex = fallbackIndex;
|
|
937
|
-
this.viewportStart = Math.min(this.viewportStart, roster.length - 1);
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
const renderKey = this.renderKey(allAgents);
|
|
941
|
-
// Register widget callback once; subsequent updates use requestRender()
|
|
942
|
-
// which re-invokes render() without replacing the component (avoids layout thrashing).
|
|
943
|
-
if (!this.widgetRegistered) {
|
|
944
|
-
this.uiCtx.setWidget("agents", (tui, theme) => {
|
|
945
|
-
this.tui = tui;
|
|
946
|
-
return {
|
|
947
|
-
render: () => this.renderWidget(tui, theme),
|
|
948
|
-
invalidate: () => {
|
|
949
|
-
// Theme changed — force re-registration so factory captures fresh theme.
|
|
950
|
-
this.widgetRegistered = false;
|
|
951
|
-
this.tui = undefined;
|
|
952
|
-
this.lastRenderKey = undefined;
|
|
953
|
-
},
|
|
954
|
-
};
|
|
955
|
-
}, { placement: "aboveEditor" });
|
|
956
|
-
this.widgetRegistered = true;
|
|
957
|
-
}
|
|
958
|
-
else if (renderKey !== this.lastRenderKey) {
|
|
959
|
-
// Widget already registered — request a re-render only when visible state
|
|
960
|
-
// changed (or the spinner timer advanced).
|
|
961
|
-
this.tui?.requestRender();
|
|
962
|
-
}
|
|
963
|
-
this.lastRenderKey = renderKey;
|
|
964
|
-
}
|
|
965
|
-
dispose() {
|
|
966
|
-
if (this.widgetInterval) {
|
|
967
|
-
clearInterval(this.widgetInterval);
|
|
968
|
-
this.widgetInterval = undefined;
|
|
969
|
-
}
|
|
970
|
-
if (this.uiCtx) {
|
|
971
|
-
this.uiCtx.setWidget("agents", undefined);
|
|
972
|
-
this.uiCtx.setStatus("subagents", undefined);
|
|
973
|
-
}
|
|
974
|
-
this.inputUnsub?.();
|
|
975
|
-
this.inputUnsub = undefined;
|
|
976
|
-
this.widgetRegistered = false;
|
|
977
|
-
this.tui = undefined;
|
|
978
|
-
this.lastStatusText = undefined;
|
|
979
|
-
this.lastRenderKey = undefined;
|
|
980
|
-
this.navigationActive = false;
|
|
981
|
-
this.selectedAgentId = undefined;
|
|
982
|
-
this.selectedRosterIndex = 0;
|
|
983
|
-
this.viewportStart = 0;
|
|
984
|
-
this.cachedAgents = [];
|
|
985
|
-
this.cachedRoster = [];
|
|
986
|
-
this.rosterCacheKey = undefined;
|
|
987
|
-
this.rosterCacheInitialized = false;
|
|
988
|
-
this.historyOpenabilityCache.clear();
|
|
989
|
-
this.uiCtx = undefined;
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
//# sourceMappingURL=agent-widget.js.map
|