@oh-my-pi/pi-coding-agent 17.2.8 → 17.2.10
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 +94 -1
- package/dist/{CHANGELOG-0b0w17se.md → CHANGELOG-rhwzpexa.md} +94 -1
- package/dist/cli.js +13216 -15614
- package/dist/types/async/job-manager.d.ts +12 -0
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +13 -0
- package/dist/types/discovery/agents.d.ts +11 -0
- package/dist/types/extensibility/custom-commands/types.d.ts +5 -5
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -1
- package/dist/types/extensibility/extensions/loader.d.ts +2 -2
- package/dist/types/extensibility/extensions/types.d.ts +34 -8
- package/dist/types/extensibility/hooks/types.d.ts +5 -5
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +10 -0
- package/dist/types/hindsight/state.d.ts +0 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/lsp/utils.d.ts +8 -0
- package/dist/types/main.d.ts +1 -1
- package/dist/types/modes/acp/acp-agent.d.ts +1 -1
- package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -1
- package/dist/types/modes/acp/acp-event-mapper.d.ts +1 -1
- package/dist/types/modes/acp/acp-mode.d.ts +1 -1
- package/dist/types/modes/components/agent-hub-projection.d.ts +38 -0
- package/dist/types/modes/components/agent-hub-renderer.d.ts +41 -0
- package/dist/types/modes/components/agent-hub.d.ts +12 -5
- package/dist/types/modes/components/model-browser.d.ts +9 -1
- package/dist/types/modes/interactive-mode.d.ts +8 -1
- package/dist/types/modes/session-observer-registry.d.ts +2 -0
- package/dist/types/registry/agent-lifecycle.d.ts +1 -1
- package/dist/types/registry/agent-registry.d.ts +47 -0
- package/dist/types/registry/persisted-agents.d.ts +3 -1
- package/dist/types/session/agent-session-types.d.ts +7 -0
- package/dist/types/session/agent-session.d.ts +8 -4
- package/dist/types/session/session-entries.d.ts +10 -0
- package/dist/types/session/session-loader.d.ts +11 -1
- package/dist/types/session/session-manager.d.ts +9 -1
- package/dist/types/session/session-tools.d.ts +10 -1
- package/dist/types/session/turn-recovery.d.ts +6 -2
- package/dist/types/slash-commands/acp-builtins.d.ts +1 -1
- package/dist/types/slash-commands/available-commands.d.ts +1 -1
- package/dist/types/task/executor.d.ts +10 -0
- package/dist/types/task/index.d.ts +2 -3
- package/dist/types/task/read-only-policy.d.ts +3 -0
- package/dist/types/task/structured-subagent.d.ts +2 -0
- package/dist/types/task/types.d.ts +4 -0
- package/dist/types/tools/bash.d.ts +3 -3
- package/dist/types/tools/browser/launch.d.ts +3 -0
- package/dist/types/tools/browser/tab-worker.d.ts +2 -2
- package/dist/types/tools/path-utils.d.ts +8 -0
- package/dist/types/tools/read.d.ts +3 -5
- package/dist/types/tools/run-scope.d.ts +27 -1
- package/dist/types/tools/shell-tokenize.d.ts +21 -3
- package/dist/types/tools/terminal-output.d.ts +1 -1
- package/dist/types/utils/changelog.d.ts +3 -2
- package/dist/types/utils/late-cleanup.d.ts +2 -0
- package/dist/types/utils/turndown.d.ts +1 -1
- package/dist/types/vibe/runtime.d.ts +4 -3
- package/dist/types/web/scrapers/readthedocs.d.ts +0 -3
- package/dist/types/web/search/providers/browser-headers.d.ts +2 -3
- package/examples/custom-tools/README.md +1 -1
- package/examples/extensions/README.md +1 -1
- package/examples/extensions/api-demo.ts +5 -6
- package/examples/extensions/chalk-logger.ts +1 -1
- package/examples/extensions/hello.ts +2 -2
- package/examples/extensions/reload-runtime.ts +1 -1
- package/examples/extensions/tools.ts +2 -2
- package/examples/extensions/with-deps/index.ts +1 -1
- package/examples/sdk/06-extensions.ts +1 -1
- package/package.json +15 -31
- package/scripts/bundle-dist.ts +1 -11
- package/scripts/omp +11 -2
- package/src/advisor/runtime.ts +54 -0
- package/src/async/job-manager.ts +26 -0
- package/src/cli/agents-cli.ts +1 -1
- package/src/cli/args.ts +24 -2
- package/src/cli/auth-broker-cli.ts +1 -1
- package/src/cli/auth-gateway-cli.ts +1 -1
- package/src/cli/bench-cli.ts +1 -1
- package/src/cli/claude-trace-cli.ts +1 -1
- package/src/cli/config-cli.ts +1 -1
- package/src/cli/dry-balance-cli.ts +1 -1
- package/src/cli/file-processor.ts +1 -1
- package/src/cli/flag-tables.ts +4 -0
- package/src/cli/grep-cli.ts +1 -1
- package/src/cli/grievances-cli.ts +1 -1
- package/src/cli/help-extra.ts +1 -1
- package/src/cli/models-cli.ts +1 -1
- package/src/cli/plugin-cli.ts +1 -1
- package/src/cli/read-cli.ts +1 -1
- package/src/cli/setup-cli.ts +1 -1
- package/src/cli/shell-cli.ts +1 -1
- package/src/cli/ssh-cli.ts +1 -1
- package/src/cli/stats-cli.ts +4 -4
- package/src/cli/tiny-models-cli.ts +1 -1
- package/src/cli/ttsr-cli.ts +1 -1
- package/src/cli/update-cli.ts +3 -57
- package/src/cli/usage-cli.ts +1 -1
- package/src/cli/web-search-cli.ts +1 -1
- package/src/cli/worktree-cli.ts +1 -1
- package/src/commands/say.ts +1 -1
- package/src/commands/token.ts +1 -1
- package/src/commit/agentic/agent.ts +1 -1
- package/src/commit/cli.ts +1 -1
- package/src/config/config-file.ts +2 -2
- package/src/config/keybindings.ts +3 -3
- package/src/config/model-discovery.ts +36 -2
- package/src/config/model-registry.ts +4 -1
- package/src/config/model-resolver.ts +54 -7
- package/src/discovery/agents.ts +73 -3
- package/src/discovery/builtin-rules/ts-no-inline-cast-access.md +5 -4
- package/src/discovery/builtin-rules/ts-no-tiny-functions.md +1 -1
- package/src/discovery/claude.ts +8 -5
- package/src/discovery/cursor.ts +8 -5
- package/src/discovery/gemini.ts +11 -8
- package/src/discovery/vscode.ts +1 -0
- package/src/discovery/windsurf.ts +4 -2
- package/src/edit/hashline/filesystem.ts +7 -7
- package/src/eval/agent-bridge.ts +1 -3
- package/src/eval/jl/prelude.jl +4 -4
- package/src/eval/js/shared/prelude.txt +2 -2
- package/src/eval/py/prelude.py +0 -3
- package/src/eval/py/runner.py +38 -1
- package/src/eval/rb/prelude.rb +1 -2
- package/src/extensibility/custom-commands/loader.ts +4 -4
- package/src/extensibility/custom-commands/types.ts +5 -5
- package/src/extensibility/custom-tools/loader.ts +4 -4
- package/src/extensibility/custom-tools/types.ts +1 -1
- package/src/extensibility/extensions/loader.ts +7 -6
- package/src/extensibility/extensions/types.ts +39 -8
- package/src/extensibility/hooks/loader.ts +4 -5
- package/src/extensibility/hooks/types.ts +5 -5
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +23 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +53 -0
- package/src/hindsight/state.ts +2 -22
- package/src/index.ts +2 -2
- package/src/internal-urls/memory-protocol.ts +33 -0
- package/src/launch/terminal-output.ts +1 -1
- package/src/lsp/client.ts +2 -2
- package/src/lsp/utils.ts +29 -0
- package/src/main.ts +85 -21
- package/src/markit/converters/docx.ts +1 -1
- package/src/markit/converters/epub.ts +1 -1
- package/src/markit/converters/pptx.ts +1 -1
- package/src/markit/converters/xlsx.ts +1 -1
- package/src/modes/acp/acp-agent.ts +7 -6
- package/src/modes/acp/acp-client-bridge.ts +1 -1
- package/src/modes/acp/acp-event-mapper.ts +1 -1
- package/src/modes/acp/acp-mode.ts +1 -1
- package/src/modes/components/agent-hub-projection.ts +248 -0
- package/src/modes/components/agent-hub-renderer.ts +194 -0
- package/src/modes/components/agent-hub.ts +670 -192
- package/src/modes/components/agent-transcript-viewer.ts +1 -3
- package/src/modes/components/assistant-message.ts +1 -1
- package/src/modes/components/bordered-loader.ts +1 -1
- package/src/modes/components/custom-editor.ts +2 -2
- package/src/modes/components/extensions/inspector-panel.ts +11 -6
- package/src/modes/components/footer.ts +1 -3
- package/src/modes/components/hook-input.ts +1 -1
- package/src/modes/components/hook-selector.ts +2 -2
- package/src/modes/components/model-browser.ts +11 -3
- package/src/modes/components/model-hub.ts +4 -1
- package/src/modes/components/session-selector.ts +2 -2
- package/src/modes/components/status-line/segments.ts +4 -3
- package/src/modes/controllers/event-controller.ts +188 -7
- package/src/modes/controllers/extension-ui-controller.ts +4 -3
- package/src/modes/controllers/mcp-command-controller.ts +81 -20
- package/src/modes/controllers/selector-controller.ts +40 -49
- package/src/modes/controllers/streaming-reveal.ts +1 -1
- package/src/modes/interactive-mode.ts +13 -2
- package/src/modes/print-mode.ts +29 -12
- package/src/modes/rpc/rpc-mode.ts +2 -2
- package/src/modes/runtime-init.ts +1 -1
- package/src/modes/session-observer-registry.ts +5 -0
- package/src/modes/theme/theme.ts +2 -2
- package/src/prompts/tools/computer.md +1 -1
- package/src/registry/agent-lifecycle.ts +35 -10
- package/src/registry/agent-registry.ts +65 -2
- package/src/registry/persisted-agents.ts +341 -16
- package/src/sdk.ts +11 -5
- package/src/session/agent-session-types.ts +8 -0
- package/src/session/agent-session.ts +177 -188
- package/src/session/session-entries.ts +10 -0
- package/src/session/session-listing.ts +1 -1
- package/src/session/session-loader.ts +74 -8
- package/src/session/session-manager.ts +22 -2
- package/src/session/session-paths.ts +105 -110
- package/src/session/session-tools.ts +28 -1
- package/src/session/settings-stream-fn.ts +7 -3
- package/src/session/turn-recovery.ts +236 -18
- package/src/session/unexpected-stop-classifier.ts +12 -3
- package/src/slash-commands/acp-builtins.ts +1 -1
- package/src/slash-commands/available-commands.ts +1 -1
- package/src/slash-commands/helpers/stats-dashboard.ts +2 -1
- package/src/task/agents.ts +1 -1
- package/src/task/executor.ts +173 -47
- package/src/task/index.ts +9 -28
- package/src/task/isolation-runner.ts +43 -12
- package/src/task/persisted-revive.ts +12 -5
- package/src/task/read-only-policy.ts +27 -0
- package/src/task/structured-subagent.ts +31 -6
- package/src/task/types.ts +4 -0
- package/src/tiny/worker.ts +1 -1
- package/src/tools/auto-generated-guard.ts +1 -1
- package/src/tools/bash-interactive.ts +4 -4
- package/src/tools/bash-interceptor.ts +8 -4
- package/src/tools/bash.ts +6 -6
- package/src/tools/browser/cmux/cmux-tab.ts +66 -18
- package/src/tools/browser/launch.ts +76 -19
- package/src/tools/browser/readable.ts +9 -9
- package/src/tools/browser/tab-supervisor.ts +12 -3
- package/src/tools/browser/tab-worker-entry.ts +1 -1
- package/src/tools/browser/tab-worker.ts +101 -12
- package/src/tools/fetch.ts +1 -1
- package/src/tools/glob.ts +4 -2
- package/src/tools/path-utils.ts +22 -5
- package/src/tools/read.ts +22 -5
- package/src/tools/run-scope.ts +290 -4
- package/src/tools/shell-tokenize.ts +38 -9
- package/src/tools/terminal-output.ts +1 -1
- package/src/tools/todo.ts +1 -1
- package/src/utils/changelog.ts +5 -4
- package/src/utils/clipboard.ts +10 -2
- package/src/utils/external-editor.ts +4 -2
- package/src/utils/jj.ts +1 -1
- package/src/utils/late-cleanup.ts +17 -0
- package/src/utils/turndown.ts +1 -2
- package/src/vibe/runtime.ts +202 -58
- package/src/web/scrapers/arxiv.ts +1 -1
- package/src/web/scrapers/go-pkg.ts +1 -1
- package/src/web/scrapers/hackage.ts +1 -12
- package/src/web/scrapers/iacr.ts +1 -1
- package/src/web/scrapers/readthedocs.ts +2 -1
- package/src/web/scrapers/twitter.ts +2 -2
- package/src/web/scrapers/types.ts +1 -1
- package/src/web/scrapers/wikipedia.ts +1 -1
- package/src/web/search/providers/browser-headers.ts +12 -39
- package/src/web/search/providers/codex.ts +3 -26
- package/src/web/search/providers/duckduckgo.ts +1 -1
- package/src/web/search/providers/ecosia.ts +2 -2
- package/src/web/search/providers/exa.ts +1 -1
- package/src/web/search/providers/firecrawl.ts +1 -1
- package/src/web/search/providers/google.ts +1 -1
- package/src/web/search/providers/mojeek.ts +2 -2
- package/src/web/search/providers/perplexity.ts +1 -1
- package/src/web/search/providers/public.ts +1 -1
- package/src/web/search/providers/startpage.ts +2 -2
- package/src/markit/converters/mammoth.d.ts +0 -24
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* One overlay, two views:
|
|
5
5
|
* - Table view: every registered agent except Main (Main IS the ambient
|
|
6
6
|
* chat), live from the global AgentRegistry — status, unread irc count,
|
|
7
|
-
* current/last task, last activity.
|
|
8
|
-
* `r` revives a parked agent, `x` aborts + releases one.
|
|
7
|
+
* current/last task, last activity. Navigate with keys, wheel, hover, and
|
|
8
|
+
* click; `r` revives a parked agent, `x` aborts + releases one.
|
|
9
9
|
* - Chat view: per-agent transcript (incremental session-file tail, absorbed
|
|
10
10
|
* from the old session observer overlay) plus an input line. Submitting
|
|
11
11
|
* revives a parked agent, then prompts/steers it; the message lands in the
|
|
@@ -13,108 +13,80 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Replaces the old SessionObserverOverlayComponent (ctrl+s observer).
|
|
15
15
|
*/
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
|
|
16
|
+
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
17
|
+
import {
|
|
18
|
+
Container,
|
|
19
|
+
matchesKey,
|
|
20
|
+
type OverlayHandle,
|
|
21
|
+
padding,
|
|
22
|
+
routeSelectListMouse,
|
|
23
|
+
routeSgrMouseInput,
|
|
24
|
+
type SelectListMouseTarget,
|
|
25
|
+
type TUI,
|
|
26
|
+
visibleWidth,
|
|
27
|
+
wrapTextWithAnsi,
|
|
28
|
+
} from "@oh-my-pi/pi-tui";
|
|
29
|
+
import { formatAge, formatNumber, getProjectDir, logger } from "@oh-my-pi/pi-utils";
|
|
19
30
|
import type { KeyId } from "../../config/keybindings";
|
|
31
|
+
import type { Settings } from "../../config/settings";
|
|
20
32
|
import type { MessageRenderer } from "../../extensibility/extensions/types";
|
|
21
33
|
import { IrcBus } from "../../irc/bus";
|
|
22
34
|
import { AgentLifecycleManager } from "../../registry/agent-lifecycle";
|
|
23
35
|
import { type AgentRef, AgentRegistry, type AgentStatus, MAIN_AGENT_ID } from "../../registry/agent-registry";
|
|
24
36
|
import { registerPersistedSubagents } from "../../registry/persisted-agents";
|
|
25
37
|
import { USER_INTERRUPT_LABEL } from "../../session/messages";
|
|
26
|
-
import {
|
|
27
|
-
import { replaceTabs, TRUNCATE_LENGTHS, truncateToWidth } from "../../tools/render-utils";
|
|
38
|
+
import { shortenPath, truncateToWidth } from "../../tools/render-utils";
|
|
28
39
|
import type { ObservableSession, SessionObserverRegistry } from "../session-observer-registry";
|
|
29
40
|
import { theme } from "../theme/theme";
|
|
30
41
|
import { matchesSelectDown, matchesSelectUp } from "../utils/keybinding-matchers";
|
|
42
|
+
import {
|
|
43
|
+
type AgentMetrics,
|
|
44
|
+
type AggregateMetrics,
|
|
45
|
+
aggregateMetrics,
|
|
46
|
+
progressMetrics,
|
|
47
|
+
projectAgentTree,
|
|
48
|
+
STATUS_ORDER,
|
|
49
|
+
} from "./agent-hub-projection";
|
|
50
|
+
import {
|
|
51
|
+
clampHubLine,
|
|
52
|
+
contextGauge,
|
|
53
|
+
formatChildIds,
|
|
54
|
+
formatCost,
|
|
55
|
+
formatMetricDuration,
|
|
56
|
+
formatMetrics,
|
|
57
|
+
formatRoleBadge,
|
|
58
|
+
modelBadge,
|
|
59
|
+
type RosterRender,
|
|
60
|
+
sanitizeDisplayText,
|
|
61
|
+
sanitizeLine,
|
|
62
|
+
statusGlyph,
|
|
63
|
+
statusText,
|
|
64
|
+
treeBranch,
|
|
65
|
+
} from "./agent-hub-renderer";
|
|
31
66
|
import { AgentTranscriptViewer } from "./agent-transcript-viewer";
|
|
32
|
-
import {
|
|
67
|
+
import {
|
|
68
|
+
bottomBorder,
|
|
69
|
+
divider,
|
|
70
|
+
dividerSplit,
|
|
71
|
+
row,
|
|
72
|
+
splitBodyWidth,
|
|
73
|
+
splitRow,
|
|
74
|
+
topBorder,
|
|
75
|
+
topBorderSplit,
|
|
76
|
+
} from "./overlay-box";
|
|
33
77
|
|
|
34
|
-
|
|
78
|
+
type HubViewMode = "roster" | "tree";
|
|
79
|
+
|
|
80
|
+
/** Refresh cadence for the relative-time column. */
|
|
35
81
|
const AGE_TICK_MS = 5_000;
|
|
36
82
|
const DATA_CHANGE_RENDER_COALESCE_MS = 100;
|
|
37
83
|
/** Double-tap window for the table's left-left "close hub" gesture. */
|
|
38
84
|
const LEFT_TAP_WINDOW_MS = 500;
|
|
39
85
|
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
/** Sanitize a line for TUI display: replace tabs, then truncate to viewport width. */
|
|
46
|
-
function sanitizeLine(text: string, maxWidth?: number): string {
|
|
47
|
-
const singleLine = replaceTabs(text).replace(/[\r\n]+/g, " ");
|
|
48
|
-
return truncateToWidth(singleLine, maxWidth ?? contentWidth());
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function clampHubLine(line: string, width: number): string {
|
|
52
|
-
return truncateToWidth(line.replace(/[\r\n]+/g, " "), Math.max(1, width - 2), Ellipsis.Omit);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const STATUS_ORDER: Record<AgentStatus, number> = { running: 0, idle: 1, parked: 2, aborted: 3 };
|
|
56
|
-
|
|
57
|
-
/** Status glyph, colored per theme status conventions. The title-line counts spell out the words. */
|
|
58
|
-
function statusGlyph(status: AgentStatus): string {
|
|
59
|
-
switch (status) {
|
|
60
|
-
case "running":
|
|
61
|
-
return theme.fg("accent", theme.status.running);
|
|
62
|
-
case "idle":
|
|
63
|
-
return theme.fg("success", theme.status.enabled);
|
|
64
|
-
case "parked":
|
|
65
|
-
return theme.fg("muted", theme.status.shadowed);
|
|
66
|
-
case "aborted":
|
|
67
|
-
return theme.fg("error", theme.status.aborted);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Model id + thinking level (`sonnet-4-6 ◒ high`), level colored per theme. */
|
|
72
|
-
function formatModelBadge(modelId: string, level: ThinkingLevel | undefined): string {
|
|
73
|
-
const model = theme.fg("muted", replaceTabs(modelId));
|
|
74
|
-
if (!level || level === ThinkingLevel.Off || level === ThinkingLevel.Inherit) return model;
|
|
75
|
-
const display = theme.thinking[level as keyof typeof theme.thinking] ?? level;
|
|
76
|
-
return `${model} ${theme.getThinkingBorderColor(level)(display)}`;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Format a resolved selector, preserving provider identity when requested. */
|
|
80
|
-
function formatResolvedModelBadge(resolved: string, preserveProvider = false): string {
|
|
81
|
-
// Model ids may themselves contain colons (`qwen3:14b`), so only treat the
|
|
82
|
-
// suffix as a thinking level when it parses as one.
|
|
83
|
-
const colon = resolved.lastIndexOf(":");
|
|
84
|
-
const level = colon >= 0 ? parseThinkingLevel(resolved.slice(colon + 1)) : undefined;
|
|
85
|
-
const selector = level !== undefined ? resolved.slice(0, colon) : resolved;
|
|
86
|
-
const label = preserveProvider ? selector : selector.slice(selector.indexOf("/") + 1);
|
|
87
|
-
return formatModelBadge(label, level);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Active model + reasoning level for a hub row: live session state when the
|
|
92
|
-
* agent is attached, else the executor-reported `resolvedModel` selector
|
|
93
|
-
* (`provider/id`, optionally `:<level>`). Active retry fallbacks retain their
|
|
94
|
-
* provider and carry an explicit marker. Undefined when no model is known
|
|
95
|
-
* (e.g. a parked historical agent restored from disk).
|
|
96
|
-
*/
|
|
97
|
-
function modelBadge(ref: AgentRef, observed: ObservableSession | undefined): string | undefined {
|
|
98
|
-
const progress = observed?.progress;
|
|
99
|
-
// Prefer the live session's own resolved fallback selector; else honor the
|
|
100
|
-
// executor-reported fallback flag. The latter covers observer-only rows (no
|
|
101
|
-
// live session) AND live rows whose fallback armed no session retry state —
|
|
102
|
-
// e.g. the Fireworks Fast → base degrade, which emits `retry_fallback_applied`
|
|
103
|
-
// without populating `#activeRetryFallback`, so `retryFallbackModel` is undefined.
|
|
104
|
-
const fallbackSelector =
|
|
105
|
-
ref.session?.retryFallbackModel ?? (progress?.resolvedModelIsFallback ? progress.resolvedModel : undefined);
|
|
106
|
-
if (fallbackSelector) {
|
|
107
|
-
return `${theme.fg("warning", "fallback →")} ${formatResolvedModelBadge(fallbackSelector, true)}`;
|
|
108
|
-
}
|
|
109
|
-
const model = ref.session?.model;
|
|
110
|
-
if (model) {
|
|
111
|
-
const level = model.thinking ? ref.session?.thinkingLevel : undefined;
|
|
112
|
-
return formatModelBadge(model.id, level);
|
|
113
|
-
}
|
|
114
|
-
const resolved = progress?.resolvedModel;
|
|
115
|
-
return resolved ? formatResolvedModelBadge(resolved) : undefined;
|
|
116
|
-
}
|
|
117
|
-
|
|
86
|
+
/** Two-pane mode needs a useful roster and a readable inspector. */
|
|
87
|
+
const SPLIT_MIN_WIDTH = 96;
|
|
88
|
+
const DETAIL_MIN_WIDTH = 34;
|
|
89
|
+
const ROSTER_MIN_WIDTH = 48;
|
|
118
90
|
/** Result of one host-backed transcript read for the Agent Hub viewer. */
|
|
119
91
|
export interface AgentHubRemoteTranscript {
|
|
120
92
|
text: string;
|
|
@@ -135,6 +107,8 @@ export interface AgentHubRemote {
|
|
|
135
107
|
export interface AgentHubDeps {
|
|
136
108
|
/** Progress/status snapshot source (task lifecycle + progress channels). */
|
|
137
109
|
observers: SessionObserverRegistry;
|
|
110
|
+
/** Production settings used to resolve textual model-role tags. */
|
|
111
|
+
settings?: Settings;
|
|
138
112
|
/** Keys that toggle the hub closed from inside (app.agents.hub + app.session.observe). */
|
|
139
113
|
hubKeys: KeyId[];
|
|
140
114
|
onDone: () => void;
|
|
@@ -162,13 +136,15 @@ export interface AgentHubDeps {
|
|
|
162
136
|
focusAgent?: (id: string) => Promise<void>;
|
|
163
137
|
/** Current main session file; used to seed parked historical subagents after restart. */
|
|
164
138
|
sessionFile?: string | null;
|
|
139
|
+
|
|
165
140
|
/** Collab guest: route actions/transcripts to the host instead of local sessions. */
|
|
166
141
|
remote?: AgentHubRemote;
|
|
167
142
|
}
|
|
168
143
|
|
|
169
|
-
export class AgentHubOverlayComponent extends Container {
|
|
144
|
+
export class AgentHubOverlayComponent extends Container implements SelectListMouseTarget {
|
|
170
145
|
#registry: AgentRegistry;
|
|
171
146
|
#observers: SessionObserverRegistry;
|
|
147
|
+
#settings: Settings | undefined;
|
|
172
148
|
#irc: IrcBus;
|
|
173
149
|
#lifecycle: () => AgentLifecycleManager;
|
|
174
150
|
#onDone: () => void;
|
|
@@ -178,17 +154,54 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
178
154
|
#ageTimer: NodeJS.Timeout | undefined;
|
|
179
155
|
#dataChangeTimer?: NodeJS.Timeout;
|
|
180
156
|
#remote: AgentHubRemote | undefined;
|
|
157
|
+
#disposed = false;
|
|
181
158
|
/** Resolves after persisted historical subagents have been registered and rows refreshed. */
|
|
182
159
|
readonly persistedSubagentsReady: Promise<void>;
|
|
160
|
+
/** Prevent the async persisted-session scan from flashing a false empty state. */
|
|
161
|
+
#loadingPersistedSubagents = false;
|
|
183
162
|
|
|
184
163
|
// Table state
|
|
185
164
|
#rows: AgentRef[] = [];
|
|
165
|
+
#statusCounts: Record<AgentStatus, number> = { running: 0, idle: 0, parked: 0, aborted: 0 };
|
|
186
166
|
#selectedRow = 0;
|
|
167
|
+
#hoveredRow: number | null = null;
|
|
168
|
+
/** Per-render screen-line to agent-row map, shared by click and hover routing. */
|
|
169
|
+
#hitRows: Array<number | undefined> = [];
|
|
187
170
|
#notice: string | undefined;
|
|
188
171
|
/** Captured row order from the first refresh; keeps the hub stable while open. */
|
|
189
172
|
#rowOrder: Map<string, number> | undefined;
|
|
173
|
+
#nextRowOrder = 0;
|
|
190
174
|
/** Double-tap window state for the table's left-left "close hub" gesture. */
|
|
191
175
|
#lastLeftTap = 0;
|
|
176
|
+
/** Operational ordering by default; tree mode groups descendants under their spawner. */
|
|
177
|
+
#viewMode: HubViewMode = "roster";
|
|
178
|
+
#treeDepthById = new Map<string, number>();
|
|
179
|
+
#treeParentById = new Map<string, string>();
|
|
180
|
+
#treeLastSiblingById = new Map<string, boolean>();
|
|
181
|
+
/** Current observer index and summary data, rebuilt on source changes rather than every paint. */
|
|
182
|
+
#observedById = new Map<string, ObservableSession>();
|
|
183
|
+
#aggregate: AggregateMetrics = {
|
|
184
|
+
tokens: 0,
|
|
185
|
+
requests: 0,
|
|
186
|
+
tools: 0,
|
|
187
|
+
cost: 0,
|
|
188
|
+
durationMs: 0,
|
|
189
|
+
durationKind: "active",
|
|
190
|
+
reportedAgents: 0,
|
|
191
|
+
activeDurationAgents: 0,
|
|
192
|
+
};
|
|
193
|
+
#childrenByParent = new Map<string, AgentRef[]>();
|
|
194
|
+
/** Transcript-derived fallback stats are sampled only on the bounded age cadence. */
|
|
195
|
+
#sessionMetrics = new WeakMap<object, { metrics: AgentMetrics | undefined }>();
|
|
196
|
+
/** Avoid a cadence-time row scan for the common persisted-only roster. */
|
|
197
|
+
#hasFallbackLiveSessions = false;
|
|
198
|
+
/** On narrow terminals Tab replaces the roster with the selected-agent inspector. */
|
|
199
|
+
#narrowDetailsOpen = false;
|
|
200
|
+
#lastRenderWasSplit = false;
|
|
201
|
+
#lastSplitRosterWidth: number | undefined;
|
|
202
|
+
/** Scroll offset for the selected-agent inspector when its content overflows. */
|
|
203
|
+
#detailScrollOffset = 0;
|
|
204
|
+
#detailAgentId: string | undefined;
|
|
192
205
|
|
|
193
206
|
// Transcript-viewer launch deps (passed through to AgentTranscriptViewer).
|
|
194
207
|
#ui: TUI;
|
|
@@ -208,6 +221,7 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
208
221
|
super();
|
|
209
222
|
this.#registry = deps.registry ?? AgentRegistry.global();
|
|
210
223
|
this.#observers = deps.observers;
|
|
224
|
+
this.#settings = deps.settings;
|
|
211
225
|
this.#irc = deps.irc ?? IrcBus.global();
|
|
212
226
|
// Lazy: the lifecycle global self-constructs against the global
|
|
213
227
|
// registry, so only touch it when revive/kill actually needs it.
|
|
@@ -216,6 +230,7 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
216
230
|
this.#requestRender = deps.requestRender;
|
|
217
231
|
this.#hubKeys = deps.hubKeys;
|
|
218
232
|
this.#remote = deps.remote;
|
|
233
|
+
this.#loadingPersistedSubagents = !this.#remote && Boolean(deps.sessionFile?.endsWith(".jsonl"));
|
|
219
234
|
this.#ui =
|
|
220
235
|
deps.ui ??
|
|
221
236
|
({
|
|
@@ -232,19 +247,29 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
232
247
|
|
|
233
248
|
this.#unsubscribers.push(this.#registry.onChange(() => this.#scheduleDataChange()));
|
|
234
249
|
this.#unsubscribers.push(this.#observers.onChange(() => this.#scheduleDataChange()));
|
|
235
|
-
this.#ageTimer = setInterval(() =>
|
|
250
|
+
this.#ageTimer = setInterval(() => {
|
|
251
|
+
if (this.#hasFallbackLiveSessions) {
|
|
252
|
+
this.#refreshAggregate(true);
|
|
253
|
+
}
|
|
254
|
+
this.#requestRender();
|
|
255
|
+
}, AGE_TICK_MS);
|
|
236
256
|
this.#ageTimer.unref?.();
|
|
237
257
|
|
|
238
258
|
this.persistedSubagentsReady = this.#remote
|
|
239
259
|
? Promise.resolve()
|
|
240
|
-
: registerPersistedSubagents(this.#registry, deps.sessionFile
|
|
260
|
+
: registerPersistedSubagents(this.#registry, deps.sessionFile, {
|
|
261
|
+
shouldContinue: () => !this.#disposed,
|
|
262
|
+
})
|
|
263
|
+
.then(() => {
|
|
264
|
+
if (!this.#disposed) this.#refreshRows();
|
|
265
|
+
})
|
|
241
266
|
.catch((error: unknown) => {
|
|
242
267
|
logger.warn("Failed to register persisted subagents", { error });
|
|
243
268
|
})
|
|
244
|
-
.
|
|
245
|
-
this.#
|
|
246
|
-
|
|
247
|
-
|
|
269
|
+
.finally(() => {
|
|
270
|
+
this.#loadingPersistedSubagents = false;
|
|
271
|
+
if (!this.#disposed) this.#requestRender();
|
|
272
|
+
});
|
|
248
273
|
this.#refreshRows();
|
|
249
274
|
}
|
|
250
275
|
|
|
@@ -258,7 +283,9 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
258
283
|
}
|
|
259
284
|
|
|
260
285
|
/** Tear down every subscription and timer. Called by the overlay owner on close. */
|
|
261
|
-
dispose(): void {
|
|
286
|
+
override dispose(): void {
|
|
287
|
+
if (this.#disposed) return;
|
|
288
|
+
this.#disposed = true;
|
|
262
289
|
for (const unsubscribe of this.#unsubscribers.splice(0)) unsubscribe();
|
|
263
290
|
if (this.#ageTimer) {
|
|
264
291
|
clearInterval(this.#ageTimer);
|
|
@@ -272,10 +299,28 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
272
299
|
}
|
|
273
300
|
|
|
274
301
|
override render(width: number): readonly string[] {
|
|
275
|
-
|
|
302
|
+
const termHeight = this.#ui.terminal?.rows || process.stdout.rows || 40;
|
|
303
|
+
const frame = this.#renderTable(width, termHeight).map(line => clampHubLine(line, width));
|
|
304
|
+
if (frame.length <= termHeight) return frame;
|
|
305
|
+
|
|
306
|
+
// A tiny terminal can leave less room than the fixed chrome needs. Keep
|
|
307
|
+
// the title and footer visible instead of spilling into scrollback.
|
|
308
|
+
const footerLines = Math.min(3, frame.length);
|
|
309
|
+
const bodyEnd = Math.max(0, termHeight - footerLines);
|
|
310
|
+
return [...frame.slice(0, bodyEnd), ...frame.slice(-footerLines)].slice(0, termHeight);
|
|
276
311
|
}
|
|
277
312
|
|
|
278
313
|
handleInput(keyData: string): void {
|
|
314
|
+
if (
|
|
315
|
+
routeSgrMouseInput(keyData, event => {
|
|
316
|
+
const split = this.#lastSplitRosterWidth;
|
|
317
|
+
if (split !== undefined && event.wheel === null && event.col > split + 2) return false;
|
|
318
|
+
return routeSelectListMouse(this, event, event.row);
|
|
319
|
+
})
|
|
320
|
+
) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
279
324
|
// The hub/observe keys always close the overlay (toggle semantics)
|
|
280
325
|
for (const key of this.#hubKeys) {
|
|
281
326
|
if (matchesKey(keyData, key)) {
|
|
@@ -306,11 +351,12 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
306
351
|
* restored when the viewer closes. No-op without a real TUI (render-only test stub).
|
|
307
352
|
*/
|
|
308
353
|
openChat(id: string): void {
|
|
309
|
-
if (!this.#registry.get(id)) return;
|
|
354
|
+
if (this.#disposed || !this.#registry.get(id)) return;
|
|
310
355
|
if (typeof this.#ui.showOverlay !== "function") return;
|
|
311
356
|
this.#closeTranscriptOverlay();
|
|
312
357
|
this.#notice = undefined;
|
|
313
|
-
|
|
358
|
+
let viewer: AgentTranscriptViewer;
|
|
359
|
+
viewer = new AgentTranscriptViewer({
|
|
314
360
|
agentId: id,
|
|
315
361
|
registry: this.#registry,
|
|
316
362
|
remote: this.#remote,
|
|
@@ -325,10 +371,11 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
325
371
|
expandKeys: this.#expandKeys,
|
|
326
372
|
hubKeys: this.#hubKeys,
|
|
327
373
|
requestRender: this.#requestRender,
|
|
328
|
-
onClose: () => this.#closeTranscriptOverlay(),
|
|
374
|
+
onClose: () => this.#closeTranscriptOverlay(viewer),
|
|
329
375
|
onHubClose: () => {
|
|
330
|
-
this.#
|
|
331
|
-
this.#
|
|
376
|
+
if (this.#disposed) return;
|
|
377
|
+
this.#closeTranscriptOverlay(viewer);
|
|
378
|
+
if (!this.#disposed) this.#onDone();
|
|
332
379
|
},
|
|
333
380
|
});
|
|
334
381
|
this.#transcriptViewer = viewer;
|
|
@@ -338,13 +385,19 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
338
385
|
}
|
|
339
386
|
|
|
340
387
|
/** Close and dispose the transcript overlay, restoring focus to the hub table. */
|
|
341
|
-
#closeTranscriptOverlay(): void {
|
|
342
|
-
this.#
|
|
388
|
+
#closeTranscriptOverlay(expectedViewer?: AgentTranscriptViewer): void {
|
|
389
|
+
if (expectedViewer && this.#transcriptViewer !== expectedViewer) return;
|
|
390
|
+
const overlay = this.#transcriptOverlay;
|
|
391
|
+
const viewer = this.#transcriptViewer;
|
|
392
|
+
if (!overlay && !viewer) return;
|
|
393
|
+
overlay?.hide();
|
|
343
394
|
this.#transcriptOverlay = undefined;
|
|
344
|
-
|
|
395
|
+
viewer?.dispose();
|
|
345
396
|
this.#transcriptViewer = undefined;
|
|
346
|
-
if (
|
|
347
|
-
|
|
397
|
+
if (!this.#disposed) {
|
|
398
|
+
if (typeof this.#ui.setFocus === "function") this.#ui.setFocus(this);
|
|
399
|
+
this.#requestRender();
|
|
400
|
+
}
|
|
348
401
|
}
|
|
349
402
|
|
|
350
403
|
// ========================================================================
|
|
@@ -368,123 +421,459 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
368
421
|
#refreshRows(): void {
|
|
369
422
|
const selectedId = this.#rows[this.#selectedRow]?.id;
|
|
370
423
|
const refs = this.#registry.list().filter(ref => ref.id !== MAIN_AGENT_ID);
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
424
|
+
this.#observedById = new Map();
|
|
425
|
+
for (const session of this.#observers.getSessions()) this.#observedById.set(session.id, session);
|
|
426
|
+
const rowOrder = this.#rowOrder;
|
|
427
|
+
let rosterRows: AgentRef[];
|
|
428
|
+
if (!rowOrder) {
|
|
429
|
+
rosterRows = refs.sort(
|
|
430
|
+
(a, b) =>
|
|
431
|
+
STATUS_ORDER[a.status] - STATUS_ORDER[b.status] ||
|
|
432
|
+
b.lastActivity - a.lastActivity ||
|
|
433
|
+
a.id.localeCompare(b.id),
|
|
376
434
|
);
|
|
377
|
-
this.#rowOrder = new Map(
|
|
435
|
+
this.#rowOrder = new Map();
|
|
436
|
+
for (const ref of rosterRows) this.#rowOrder.set(ref.id, this.#nextRowOrder++);
|
|
378
437
|
} else {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
if (statusDiff !== 0) return statusDiff;
|
|
385
|
-
const aOrder = this.#rowOrder!.get(a.id) ?? Number.MAX_SAFE_INTEGER;
|
|
386
|
-
const bOrder = this.#rowOrder!.get(b.id) ?? Number.MAX_SAFE_INTEGER;
|
|
387
|
-
return aOrder - bOrder;
|
|
388
|
-
});
|
|
389
|
-
for (const ref of this.#rows) {
|
|
390
|
-
if (!this.#rowOrder.has(ref.id)) {
|
|
391
|
-
this.#rowOrder.set(ref.id, this.#rowOrder.size);
|
|
392
|
-
}
|
|
438
|
+
rosterRows = refs.sort(
|
|
439
|
+
(a, b) => (rowOrder.get(a.id) ?? Number.MAX_SAFE_INTEGER) - (rowOrder.get(b.id) ?? Number.MAX_SAFE_INTEGER),
|
|
440
|
+
);
|
|
441
|
+
for (const ref of rosterRows) {
|
|
442
|
+
if (!rowOrder.has(ref.id)) rowOrder.set(ref.id, this.#nextRowOrder++);
|
|
393
443
|
}
|
|
394
444
|
}
|
|
395
445
|
|
|
446
|
+
if (this.#viewMode === "tree") {
|
|
447
|
+
const tree = projectAgentTree(rosterRows);
|
|
448
|
+
this.#rows = tree.rows;
|
|
449
|
+
this.#treeDepthById = tree.depthById;
|
|
450
|
+
this.#treeParentById = tree.parentById;
|
|
451
|
+
this.#treeLastSiblingById = tree.lastSiblingById;
|
|
452
|
+
} else {
|
|
453
|
+
this.#rows = rosterRows;
|
|
454
|
+
this.#treeDepthById.clear();
|
|
455
|
+
this.#treeParentById.clear();
|
|
456
|
+
this.#treeLastSiblingById.clear();
|
|
457
|
+
}
|
|
396
458
|
const keptIndex = selectedId ? this.#rows.findIndex(ref => ref.id === selectedId) : -1;
|
|
397
459
|
this.#selectedRow = keptIndex >= 0 ? keptIndex : Math.min(this.#selectedRow, Math.max(0, this.#rows.length - 1));
|
|
460
|
+
const detailAgentId = this.#rows[this.#selectedRow]?.id;
|
|
461
|
+
if (detailAgentId !== this.#detailAgentId) {
|
|
462
|
+
this.#detailAgentId = detailAgentId;
|
|
463
|
+
this.#detailScrollOffset = 0;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
this.#childrenByParent.clear();
|
|
467
|
+
for (const ref of rosterRows) {
|
|
468
|
+
const parent = ref.parentId ?? MAIN_AGENT_ID;
|
|
469
|
+
const children = this.#childrenByParent.get(parent);
|
|
470
|
+
if (children) children.push(ref);
|
|
471
|
+
else this.#childrenByParent.set(parent, [ref]);
|
|
472
|
+
}
|
|
473
|
+
this.#statusCounts = { running: 0, idle: 0, parked: 0, aborted: 0 };
|
|
474
|
+
for (const ref of rosterRows) this.#statusCounts[ref.status]++;
|
|
475
|
+
this.#refreshAggregate();
|
|
398
476
|
}
|
|
399
477
|
|
|
400
|
-
#
|
|
401
|
-
|
|
478
|
+
#metricsFor(ref: AgentRef, observed: ObservableSession | undefined): AgentMetrics | undefined {
|
|
479
|
+
if (observed?.progress) return progressMetrics(observed);
|
|
480
|
+
if (ref.history?.metrics) return ref.history.metrics;
|
|
481
|
+
const session = this.#fallbackStatsSession(ref, observed);
|
|
482
|
+
return session ? this.#sessionMetrics.get(session)?.metrics : undefined;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
#fallbackStatsSession(
|
|
486
|
+
ref: AgentRef,
|
|
487
|
+
observed: ObservableSession | undefined,
|
|
488
|
+
): NonNullable<AgentRef["session"]> | undefined {
|
|
489
|
+
if (observed?.progress) return undefined;
|
|
490
|
+
const session = ref.session;
|
|
491
|
+
return session && typeof session.getSessionStats === "function" ? session : undefined;
|
|
402
492
|
}
|
|
403
493
|
|
|
404
494
|
// ========================================================================
|
|
405
495
|
// Table view
|
|
406
496
|
// ========================================================================
|
|
407
497
|
|
|
408
|
-
#renderTable(width: number): string[] {
|
|
498
|
+
#renderTable(width: number, termHeight: number): string[] {
|
|
499
|
+
this.#hitRows.length = 0;
|
|
500
|
+
const contentRows = Math.max(1, termHeight - 4);
|
|
501
|
+
const observedById = this.#observedById;
|
|
502
|
+
const split = this.#splitRosterWidth(width);
|
|
503
|
+
this.#lastRenderWasSplit = split !== undefined;
|
|
504
|
+
this.#lastSplitRosterWidth = split;
|
|
505
|
+
const selected = this.#rows[this.#selectedRow];
|
|
409
506
|
const lines: string[] = [];
|
|
410
|
-
lines.push(...new DynamicBorder().render(width));
|
|
411
|
-
const counts = this.#statusSummary();
|
|
412
|
-
lines.push(` ${theme.fg("accent", "Agent Hub")}${counts ? theme.fg("dim", `${theme.sep.dot}${counts}`) : ""}`);
|
|
413
|
-
lines.push(...new DynamicBorder().render(width));
|
|
414
507
|
|
|
415
|
-
if (
|
|
416
|
-
|
|
508
|
+
if (split !== undefined) {
|
|
509
|
+
const detailWidth = splitBodyWidth(width, split);
|
|
510
|
+
const roster = this.#renderRosterPanel(split, contentRows, observedById);
|
|
511
|
+
const details = this.#renderDetailPanel(selected, detailWidth, contentRows, observedById);
|
|
512
|
+
lines.push(topBorderSplit(width, "Agent Hub", split));
|
|
513
|
+
for (let i = 0; i < contentRows; i++) {
|
|
514
|
+
const hit = roster.hitRows[i];
|
|
515
|
+
if (hit !== undefined) this.#hitRows[lines.length] = hit;
|
|
516
|
+
lines.push(splitRow(roster.lines[i] ?? "", details[i] ?? "", width, split));
|
|
517
|
+
}
|
|
518
|
+
lines.push(dividerSplit(width, split));
|
|
519
|
+
lines.push(row(this.#footer(false, Math.max(1, width - 4)), width));
|
|
520
|
+
lines.push(bottomBorder(width));
|
|
521
|
+
return lines;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const innerWidth = Math.max(1, width - 4);
|
|
525
|
+
if (this.#narrowDetailsOpen && selected) {
|
|
526
|
+
const details = this.#renderDetailPanel(selected, innerWidth, contentRows, observedById);
|
|
527
|
+
lines.push(topBorder(width, `Agent Hub · ${selected.id}`));
|
|
528
|
+
for (const detail of details) lines.push(row(detail, width));
|
|
417
529
|
} else {
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
530
|
+
const roster = this.#renderRosterPanel(innerWidth, contentRows, observedById);
|
|
531
|
+
lines.push(topBorder(width, "Agent Hub"));
|
|
532
|
+
for (let i = 0; i < contentRows; i++) {
|
|
533
|
+
const hit = roster.hitRows[i];
|
|
534
|
+
if (hit !== undefined) this.#hitRows[lines.length] = hit;
|
|
535
|
+
lines.push(row(roster.lines[i] ?? "", width));
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
lines.push(divider(width));
|
|
539
|
+
lines.push(row(this.#footer(this.#narrowDetailsOpen, innerWidth), width));
|
|
540
|
+
lines.push(bottomBorder(width));
|
|
541
|
+
return lines;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
#splitRosterWidth(width: number): number | undefined {
|
|
545
|
+
if (width < SPLIT_MIN_WIDTH) return undefined;
|
|
546
|
+
const rosterWidth = Math.max(ROSTER_MIN_WIDTH, Math.min(Math.floor(width * 0.58), width - DETAIL_MIN_WIDTH - 7));
|
|
547
|
+
return splitBodyWidth(width, rosterWidth) >= DETAIL_MIN_WIDTH ? rosterWidth : undefined;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
#footer(showingNarrowDetails: boolean, availableWidth: number): string {
|
|
551
|
+
const nextView = this.#viewMode === "roster" ? "by parent" : "flat";
|
|
552
|
+
if (showingNarrowDetails) {
|
|
553
|
+
return theme.fg("dim", `Tab:roster PgUp/PgDn:scroll Enter:open t:${nextView} Esc:roster`);
|
|
554
|
+
}
|
|
555
|
+
if (availableWidth < 96) {
|
|
556
|
+
return theme.fg("dim", `j/k:select Enter:open t:${nextView} Tab:details r/x:manage Esc:close`);
|
|
557
|
+
}
|
|
558
|
+
return theme.fg(
|
|
559
|
+
"dim",
|
|
560
|
+
`j/k/wheel:select PgUp/PgDn:details Enter/click:open t:${nextView} r:revive x:kill Esc:close`,
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
#renderRosterPanel(width: number, rows: number, observedById: ReadonlyMap<string, ObservableSession>): RosterRender {
|
|
565
|
+
const lines = this.#summaryLines(width);
|
|
566
|
+
const hitRows: Array<number | undefined> = Array.from({ length: lines.length });
|
|
567
|
+
if (rows >= 8) {
|
|
568
|
+
lines.push("");
|
|
569
|
+
hitRows.push(undefined);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const noticeLines = this.#notice ? [theme.fg("error", sanitizeLine(this.#notice, Math.max(10, width)))] : [];
|
|
573
|
+
const budget = Math.max(0, rows - lines.length - noticeLines.length);
|
|
574
|
+
if (this.#rows.length === 0) {
|
|
575
|
+
if (this.#loadingPersistedSubagents) {
|
|
576
|
+
if (budget > 0) {
|
|
577
|
+
lines.push(`${statusGlyph("running")} ${theme.fg("accent", "Loading saved agents…")}`);
|
|
578
|
+
hitRows.push(undefined);
|
|
579
|
+
}
|
|
580
|
+
} else {
|
|
581
|
+
const emptyState = [
|
|
582
|
+
`${theme.fg("muted", theme.status.shadowed)} ${theme.bold("No agents in this session")}`,
|
|
583
|
+
theme.fg("dim", "Finished, parked, and killed subagents remain with the session that created them."),
|
|
584
|
+
theme.fg("dim", "Resume that session with omp-dev --continue, or spawn a task here."),
|
|
585
|
+
];
|
|
586
|
+
for (const line of emptyState.slice(0, budget)) {
|
|
587
|
+
lines.push(line);
|
|
588
|
+
hitRows.push(undefined);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
} else if (budget > 0) {
|
|
592
|
+
const window = this.#renderRosterWindow(width, budget, observedById);
|
|
593
|
+
lines.push(...window.lines);
|
|
594
|
+
hitRows.push(...window.hitRows);
|
|
595
|
+
}
|
|
596
|
+
for (const notice of noticeLines) {
|
|
597
|
+
lines.push(notice);
|
|
598
|
+
hitRows.push(undefined);
|
|
599
|
+
}
|
|
600
|
+
while (lines.length < rows) {
|
|
601
|
+
lines.push("");
|
|
602
|
+
hitRows.push(undefined);
|
|
603
|
+
}
|
|
604
|
+
return { lines: lines.slice(0, rows), hitRows: hitRows.slice(0, rows) };
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
#renderRosterWindow(
|
|
608
|
+
width: number,
|
|
609
|
+
budget: number,
|
|
610
|
+
_observedById: ReadonlyMap<string, ObservableSession>,
|
|
611
|
+
): RosterRender {
|
|
612
|
+
const lines: string[] = [];
|
|
613
|
+
const hitRows: Array<number | undefined> = [];
|
|
614
|
+
const rendered = new Map<number, string[]>();
|
|
615
|
+
const entryAt = (index: number): string[] => {
|
|
616
|
+
const cached = rendered.get(index);
|
|
617
|
+
if (cached) return cached;
|
|
618
|
+
const entry = this.#renderEntry(
|
|
619
|
+
this.#rows[index],
|
|
620
|
+
index === this.#selectedRow,
|
|
621
|
+
width,
|
|
622
|
+
this.#observableFor(this.#rows[index].id),
|
|
623
|
+
index === this.#hoveredRow,
|
|
624
|
+
);
|
|
625
|
+
rendered.set(index, entry);
|
|
626
|
+
return entry;
|
|
627
|
+
};
|
|
628
|
+
const appendEntry = (index: number, entry = entryAt(index)): void => {
|
|
629
|
+
for (const line of entry) {
|
|
630
|
+
lines.push(line);
|
|
631
|
+
hitRows.push(index);
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
let start = this.#selectedRow;
|
|
636
|
+
let end = this.#selectedRow + 1;
|
|
637
|
+
let used = entryAt(this.#selectedRow).length;
|
|
638
|
+
if (used > budget) {
|
|
639
|
+
appendEntry(this.#selectedRow, entryAt(this.#selectedRow).slice(0, budget));
|
|
640
|
+
return { lines, hitRows };
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Grow a window around the selection. Only visible entries are rendered,
|
|
644
|
+
// so the 5,000-agent Hub retains bounded paint cost.
|
|
645
|
+
for (let grew = true; grew; ) {
|
|
646
|
+
grew = false;
|
|
647
|
+
if (end < this.#rows.length) {
|
|
648
|
+
const next = entryAt(end);
|
|
649
|
+
if (used + next.length <= budget) {
|
|
650
|
+
used += next.length;
|
|
431
651
|
end++;
|
|
432
652
|
grew = true;
|
|
433
653
|
}
|
|
434
|
-
|
|
654
|
+
}
|
|
655
|
+
if (start > 0) {
|
|
656
|
+
const previous = entryAt(start - 1);
|
|
657
|
+
if (used + previous.length <= budget) {
|
|
435
658
|
start--;
|
|
436
|
-
used +=
|
|
659
|
+
used += previous.length;
|
|
437
660
|
grew = true;
|
|
438
661
|
}
|
|
439
662
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
663
|
+
}
|
|
664
|
+
// Overflow labels consume real rows. Trim the farthest visible neighbors
|
|
665
|
+
// before painting them so the selected entry and both labels fit.
|
|
666
|
+
for (
|
|
667
|
+
let markerRows = Number(start > 0) + Number(end < this.#rows.length);
|
|
668
|
+
used + markerRows > budget && start < end;
|
|
669
|
+
markerRows = Number(start > 0) + Number(end < this.#rows.length)
|
|
670
|
+
) {
|
|
671
|
+
if (end - 1 > this.#selectedRow) {
|
|
672
|
+
end--;
|
|
673
|
+
used -= entryAt(end).length;
|
|
674
|
+
} else if (start < this.#selectedRow) {
|
|
675
|
+
used -= entryAt(start).length;
|
|
676
|
+
start++;
|
|
677
|
+
} else {
|
|
678
|
+
break;
|
|
448
679
|
}
|
|
449
680
|
}
|
|
681
|
+
const showTopOverflow = start > 0 && used < budget;
|
|
682
|
+
const showBottomOverflow = end < this.#rows.length && used + Number(showTopOverflow) < budget;
|
|
683
|
+
if (showTopOverflow) {
|
|
684
|
+
lines.push(theme.fg("dim", `… ${start} more`));
|
|
685
|
+
hitRows.push(undefined);
|
|
686
|
+
}
|
|
687
|
+
for (let i = start; i < end; i++) appendEntry(i);
|
|
688
|
+
if (showBottomOverflow) {
|
|
689
|
+
lines.push(theme.fg("dim", `… ${this.#rows.length - end} more`));
|
|
690
|
+
hitRows.push(undefined);
|
|
691
|
+
}
|
|
692
|
+
return { lines, hitRows };
|
|
693
|
+
}
|
|
450
694
|
|
|
451
|
-
|
|
452
|
-
|
|
695
|
+
#summaryLines(width: number): string[] {
|
|
696
|
+
const active = (label: string): string => theme.bg("selectedBg", theme.bold(theme.fg("accent", ` ${label} `)));
|
|
697
|
+
const inactive = (label: string): string => theme.fg("muted", ` ${label} `);
|
|
698
|
+
const projection =
|
|
699
|
+
this.#viewMode === "roster"
|
|
700
|
+
? `${active("Flat")}${theme.fg("dim", "/")}${inactive("By parent")}`
|
|
701
|
+
: `${inactive("Flat")}${theme.fg("dim", "/")}${active("By parent")}`;
|
|
702
|
+
const counts = this.#statusSummary();
|
|
703
|
+
const header = `${theme.bold("Roster")}${theme.fg("dim", theme.sep.dot)}${projection}${counts ? theme.fg("dim", theme.sep.dot) + counts : ""}`;
|
|
704
|
+
const lines = wrapTextWithAnsi(header, Math.max(1, width));
|
|
705
|
+
|
|
706
|
+
const metrics = this.#aggregate;
|
|
707
|
+
if (metrics.reportedAgents === 0) {
|
|
708
|
+
lines.push(
|
|
709
|
+
...wrapTextWithAnsi(
|
|
710
|
+
theme.fg("dim", `Usage —${theme.sep.dot}0/${this.#rows.length} measured`),
|
|
711
|
+
Math.max(1, width),
|
|
712
|
+
),
|
|
713
|
+
);
|
|
714
|
+
return lines;
|
|
453
715
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
716
|
+
const activeTime = formatMetricDuration(metrics);
|
|
717
|
+
const usage = [
|
|
718
|
+
theme.fg("statusLineCost", formatCost(metrics.cost)),
|
|
719
|
+
theme.fg("dim", activeTime ? `${activeTime} agent time` : "agent time —"),
|
|
720
|
+
theme.fg("dim", `${formatNumber(metrics.requests)} req`),
|
|
721
|
+
theme.fg("dim", `${formatNumber(metrics.tools)} tools`),
|
|
722
|
+
theme.fg("dim", `${formatNumber(metrics.tokens)} tok`),
|
|
723
|
+
theme.fg("dim", `${metrics.activeDurationAgents}/${metrics.reportedAgents} timed`),
|
|
724
|
+
theme.fg("dim", `${metrics.reportedAgents}/${this.#rows.length} measured`),
|
|
725
|
+
].join(theme.fg("dim", theme.sep.dot));
|
|
726
|
+
lines.push(...wrapTextWithAnsi(usage, Math.max(1, width)));
|
|
457
727
|
return lines;
|
|
458
728
|
}
|
|
459
729
|
|
|
460
730
|
#statusSummary(): string {
|
|
461
|
-
const counts: Record<AgentStatus, number> = { running: 0, idle: 0, parked: 0, aborted: 0 };
|
|
462
|
-
for (const ref of this.#rows) {
|
|
463
|
-
counts[ref.status]++;
|
|
464
|
-
}
|
|
465
731
|
const parts: string[] = [];
|
|
466
732
|
for (const status of ["running", "idle", "parked", "aborted"] as const) {
|
|
467
|
-
const count =
|
|
468
|
-
if (count > 0) parts.push(`${count} ${status}`);
|
|
733
|
+
const count = this.#statusCounts[status];
|
|
734
|
+
if (count > 0) parts.push(`${statusGlyph(status)} ${statusText(status, `${count} ${status}`)}`);
|
|
469
735
|
}
|
|
470
736
|
return parts.join(theme.sep.dot);
|
|
471
737
|
}
|
|
472
738
|
|
|
739
|
+
#refreshAggregate(refreshFallback = false): void {
|
|
740
|
+
const result = aggregateMetrics({
|
|
741
|
+
rows: this.#rows,
|
|
742
|
+
observedById: this.#observedById,
|
|
743
|
+
metricsFor: (ref, observed) => this.#metricsFor(ref, observed),
|
|
744
|
+
fallbackStatsSession: (ref, observed) => this.#fallbackStatsSession(ref, observed),
|
|
745
|
+
sessionMetrics: this.#sessionMetrics,
|
|
746
|
+
refreshFallback,
|
|
747
|
+
});
|
|
748
|
+
this.#aggregate = result.metrics;
|
|
749
|
+
this.#hasFallbackLiveSessions = result.hasFallbackLiveSessions;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
#observableFor(id: string): ObservableSession | undefined {
|
|
753
|
+
return this.#observedById.get(id) ?? this.#observers.getSession(id);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
#renderDetailPanel(
|
|
757
|
+
ref: AgentRef | undefined,
|
|
758
|
+
width: number,
|
|
759
|
+
rows: number,
|
|
760
|
+
_observedById: ReadonlyMap<string, ObservableSession>,
|
|
761
|
+
): string[] {
|
|
762
|
+
if (!ref) return [theme.fg("dim", "Select an agent to inspect"), ...Array.from({ length: rows - 1 }, () => "")];
|
|
763
|
+
const observed = this.#observableFor(ref.id);
|
|
764
|
+
const progress = observed?.progress;
|
|
765
|
+
const metrics = this.#metricsFor(ref, observed);
|
|
766
|
+
const children = this.#childrenByParent.get(ref.id) ?? [];
|
|
767
|
+
const lines: string[] = [];
|
|
768
|
+
const add = (line = ""): void => {
|
|
769
|
+
lines.push(truncateToWidth(line, width));
|
|
770
|
+
};
|
|
771
|
+
const addWrapped = (text: string, maxRows = 2): void => {
|
|
772
|
+
for (const wrapped of wrapTextWithAnsi(sanitizeLine(text), Math.max(1, width)).slice(0, maxRows)) add(wrapped);
|
|
773
|
+
};
|
|
774
|
+
const section = (label: string, contentRows = 0): void => {
|
|
775
|
+
if (lines.length > 0 && lines.length + 1 + contentRows < rows) add();
|
|
776
|
+
add(theme.bold(theme.fg("accent", label)));
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
add(`${statusGlyph(ref.status)} ${theme.bold(sanitizeDisplayText(ref.displayName || ref.id))}`);
|
|
780
|
+
if (ref.displayName && ref.displayName !== ref.id) add(theme.fg("dim", sanitizeDisplayText(ref.id)));
|
|
781
|
+
const lifecycleDetails = [
|
|
782
|
+
metrics ? formatMetricDuration(metrics) : undefined,
|
|
783
|
+
`active ${formatAge(Math.max(1, Math.round((Date.now() - ref.lastActivity) / 1000)))}`,
|
|
784
|
+
].filter(Boolean);
|
|
785
|
+
add(
|
|
786
|
+
`${statusText(ref.status, ref.status)}${theme.fg("dim", `${theme.sep.dot}${lifecycleDetails.join(theme.sep.dot)}`)}`,
|
|
787
|
+
);
|
|
788
|
+
const modelDetails: string[] = [];
|
|
789
|
+
const modelRole = progress?.modelRole ?? ref.history?.modelRole;
|
|
790
|
+
if (modelRole && this.#settings) modelDetails.push(formatRoleBadge(modelRole, this.#settings));
|
|
791
|
+
const badge = modelBadge(ref, observed);
|
|
792
|
+
if (badge) modelDetails.push(badge);
|
|
793
|
+
if (modelDetails.length > 0) add(modelDetails.join(theme.sep.dot));
|
|
794
|
+
|
|
795
|
+
const task = observed?.description ?? progress?.task ?? ref.activity;
|
|
796
|
+
if (task) {
|
|
797
|
+
section("Task");
|
|
798
|
+
addWrapped(task);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const current = progress?.currentTool
|
|
802
|
+
? `${progress.currentTool}${progress.currentToolArgs ? ` · ${progress.currentToolArgs}` : ""}`
|
|
803
|
+
: (progress?.lastIntent ?? ref.activity);
|
|
804
|
+
if (current) {
|
|
805
|
+
section("Current");
|
|
806
|
+
addWrapped(current);
|
|
807
|
+
if (progress?.retryState) {
|
|
808
|
+
add(theme.fg("warning", `retry ${progress.retryState.attempt}/${progress.retryState.maxAttempts}`));
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
section("Usage", 1);
|
|
813
|
+
if (metrics) {
|
|
814
|
+
addWrapped(formatMetrics(metrics), 3);
|
|
815
|
+
if (metrics.contextTokens !== undefined && metrics.contextWindow) {
|
|
816
|
+
add(contextGauge(metrics.contextTokens, metrics.contextWindow));
|
|
817
|
+
}
|
|
818
|
+
} else {
|
|
819
|
+
add(theme.fg("dim", "usage —"));
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
section("Lineage");
|
|
823
|
+
add(
|
|
824
|
+
`Spawned by ${sanitizeDisplayText(ref.parentId ?? MAIN_AGENT_ID)}${children.length > 0 ? ` · ${children.length} children` : ""}`,
|
|
825
|
+
);
|
|
826
|
+
if (children.length > 0) add(theme.fg("dim", formatChildIds(children, width)));
|
|
827
|
+
add(theme.fg("dim", `Registered ${new Date(ref.createdAt).toISOString().slice(0, 16).replace("T", " ")}Z`));
|
|
828
|
+
|
|
829
|
+
section("Changes");
|
|
830
|
+
add(
|
|
831
|
+
theme.fg(
|
|
832
|
+
"dim",
|
|
833
|
+
ref.kind === "advisor" || ref.history?.readOnly
|
|
834
|
+
? "Read-only · 0 LoC"
|
|
835
|
+
: "Shared workspace · per-agent LoC not attributable",
|
|
836
|
+
),
|
|
837
|
+
);
|
|
838
|
+
const artifacts = ref.history;
|
|
839
|
+
if (artifacts?.outputPath) addWrapped(`Output ${shortenPath(artifacts.outputPath)}`);
|
|
840
|
+
if (artifacts?.patchPath) addWrapped(`Patch ${shortenPath(artifacts.patchPath)}`);
|
|
841
|
+
if (artifacts?.branchName) addWrapped(`Worktree branch ${artifacts.branchName}`);
|
|
842
|
+
|
|
843
|
+
const maxScroll = Math.max(0, lines.length - rows);
|
|
844
|
+
this.#detailScrollOffset = Math.min(this.#detailScrollOffset, maxScroll);
|
|
845
|
+
const visible = lines.slice(this.#detailScrollOffset, this.#detailScrollOffset + rows);
|
|
846
|
+
while (visible.length < rows) visible.push("");
|
|
847
|
+
return visible;
|
|
848
|
+
}
|
|
849
|
+
|
|
473
850
|
/**
|
|
474
|
-
* One agent entry
|
|
475
|
-
*
|
|
476
|
-
*
|
|
477
|
-
* plus an indented dim task line when the agent's work is known.
|
|
851
|
+
* One agent entry keeps identity/model metadata on one line when it fits,
|
|
852
|
+
* then packs task and all five usage metrics together below. Narrow rows wrap
|
|
853
|
+
* only those dense secondary fields.
|
|
478
854
|
*/
|
|
479
|
-
#renderEntry(
|
|
480
|
-
|
|
855
|
+
#renderEntry(
|
|
856
|
+
ref: AgentRef,
|
|
857
|
+
selected: boolean,
|
|
858
|
+
width: number,
|
|
859
|
+
observed: ObservableSession | undefined,
|
|
860
|
+
hovered = false,
|
|
861
|
+
): string[] {
|
|
862
|
+
const max = Math.max(1, width);
|
|
481
863
|
const cursor = selected ? theme.fg("accent", theme.nav.cursor) : " ";
|
|
482
|
-
const
|
|
864
|
+
const depth = this.#viewMode === "tree" ? (this.#treeDepthById.get(ref.id) ?? 0) : 0;
|
|
865
|
+
const branch =
|
|
866
|
+
this.#viewMode === "tree"
|
|
867
|
+
? treeBranch(ref, max, this.#treeDepthById, this.#treeParentById, this.#treeLastSiblingById)
|
|
868
|
+
: "";
|
|
869
|
+
const id = sanitizeDisplayText(ref.id);
|
|
870
|
+
const styledId = selected ? theme.bold(theme.fg("accent", id)) : theme.bold(id);
|
|
871
|
+
const fields: string[] = [`${cursor} ${statusGlyph(ref.status)} ${branch}${styledId}`];
|
|
483
872
|
if (ref.displayName && ref.displayName !== ref.id) {
|
|
484
|
-
fields.push(theme.fg("dim",
|
|
873
|
+
fields.push(theme.fg("dim", sanitizeDisplayText(ref.displayName)));
|
|
485
874
|
}
|
|
486
|
-
if (ref.parentId && ref.parentId !== MAIN_AGENT_ID) {
|
|
487
|
-
fields.push(theme.fg("dim", `↳ ${
|
|
875
|
+
if (this.#viewMode === "roster" && ref.parentId && ref.parentId !== MAIN_AGENT_ID) {
|
|
876
|
+
fields.push(theme.fg("dim", `↳ ${sanitizeDisplayText(ref.parentId)}`));
|
|
488
877
|
}
|
|
489
878
|
if (ref.kind === "advisor") {
|
|
490
879
|
fields.push(theme.fg("warning", "read-only"));
|
|
@@ -493,10 +882,13 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
493
882
|
if (unread > 0) {
|
|
494
883
|
fields.push(theme.fg("warning", `⧉ ${unread}`));
|
|
495
884
|
}
|
|
496
|
-
const left =
|
|
885
|
+
const left = fields.join(" ");
|
|
497
886
|
|
|
498
|
-
const observed = this.#observableFor(ref.id);
|
|
499
887
|
const meta: string[] = [];
|
|
888
|
+
const modelRole = observed?.progress?.modelRole ?? ref.history?.modelRole;
|
|
889
|
+
if (modelRole && this.#settings) {
|
|
890
|
+
meta.push(formatRoleBadge(modelRole, this.#settings));
|
|
891
|
+
}
|
|
500
892
|
const badge = modelBadge(ref, observed);
|
|
501
893
|
if (badge) meta.push(badge);
|
|
502
894
|
meta.push(theme.fg("dim", formatAge(Math.max(1, Math.round((Date.now() - ref.lastActivity) / 1000)))));
|
|
@@ -504,25 +896,110 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
504
896
|
|
|
505
897
|
const leftWidth = visibleWidth(left);
|
|
506
898
|
const rightWidth = visibleWidth(right);
|
|
507
|
-
const
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
899
|
+
const entry: string[] = [];
|
|
900
|
+
const detailIndent = Math.min(max - 1, 4 + depth * 2);
|
|
901
|
+
if (leftWidth + 2 + rightWidth <= max) {
|
|
902
|
+
entry.push(left + padding(max - leftWidth - rightWidth) + right);
|
|
903
|
+
} else {
|
|
904
|
+
entry.push(truncateToWidth(left.replace(/[\r\n]+/g, " "), max));
|
|
905
|
+
entry.push(`${padding(Math.max(0, detailIndent))}${truncateToWidth(right, Math.max(1, max - detailIndent))}`);
|
|
906
|
+
}
|
|
512
907
|
|
|
908
|
+
const metrics = this.#metricsFor(ref, observed);
|
|
909
|
+
const usage = metrics ? theme.fg("dim", formatMetrics(metrics)) : theme.fg("dim", "usage —");
|
|
513
910
|
const task = observed?.description ?? observed?.progress?.task ?? ref.activity;
|
|
514
|
-
|
|
515
|
-
|
|
911
|
+
const detailWidth = Math.max(1, max - detailIndent);
|
|
912
|
+
const details = task
|
|
913
|
+
? `${theme.fg("muted", sanitizeLine(task, detailWidth))}${theme.fg("dim", theme.sep.dot)}${usage}`
|
|
914
|
+
: usage;
|
|
915
|
+
for (const wrapped of wrapTextWithAnsi(details, detailWidth)) {
|
|
916
|
+
entry.push(`${padding(Math.max(0, detailIndent))}${wrapped}`);
|
|
516
917
|
}
|
|
517
|
-
return entry;
|
|
918
|
+
if (!hovered) return entry;
|
|
919
|
+
return entry.map(lineRow => {
|
|
920
|
+
const rowWidth = visibleWidth(lineRow);
|
|
921
|
+
return theme.bg("selectedBg", rowWidth < max ? lineRow + padding(max - rowWidth) : lineRow);
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
#scrollDetails(direction: -1 | 1): void {
|
|
926
|
+
this.#detailScrollOffset = Math.max(0, this.#detailScrollOffset + direction * 5);
|
|
927
|
+
this.#requestRender();
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
#selectRow(index: number): void {
|
|
931
|
+
if (index !== this.#selectedRow) {
|
|
932
|
+
this.#detailScrollOffset = 0;
|
|
933
|
+
this.#detailAgentId = this.#rows[index]?.id;
|
|
934
|
+
}
|
|
935
|
+
this.#selectedRow = index;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
handleWheel(delta: -1 | 1): void {
|
|
939
|
+
this.#hoveredRow = null;
|
|
940
|
+
if (this.#rows.length > 0) {
|
|
941
|
+
this.#selectRow(Math.max(0, Math.min(this.#selectedRow + delta, this.#rows.length - 1)));
|
|
942
|
+
}
|
|
943
|
+
this.#requestRender();
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
hitTest(line: number): number | undefined {
|
|
947
|
+
return this.#hitRows[line];
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
setHoverIndex(index: number | null): void {
|
|
951
|
+
if (index === this.#hoveredRow) return;
|
|
952
|
+
this.#hoveredRow = index;
|
|
953
|
+
this.#requestRender();
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
clickItem(index: number): void {
|
|
957
|
+
const selected = this.#rows[index];
|
|
958
|
+
if (!selected) return;
|
|
959
|
+
this.#hoveredRow = index;
|
|
960
|
+
this.#selectRow(index);
|
|
961
|
+
this.#requestRender();
|
|
962
|
+
this.#activateAgent(selected);
|
|
518
963
|
}
|
|
519
964
|
|
|
520
965
|
#handleTableInput(keyData: string): void {
|
|
521
966
|
if (matchesKey(keyData, "escape")) {
|
|
522
|
-
this.#
|
|
967
|
+
if (this.#narrowDetailsOpen && !this.#lastRenderWasSplit) {
|
|
968
|
+
this.#narrowDetailsOpen = false;
|
|
969
|
+
this.#requestRender();
|
|
970
|
+
} else {
|
|
971
|
+
this.#onDone();
|
|
972
|
+
}
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
if ((matchesKey(keyData, "tab") || keyData === "\t") && !this.#lastRenderWasSplit) {
|
|
976
|
+
if (this.#rows.length > 0) this.#narrowDetailsOpen = !this.#narrowDetailsOpen;
|
|
977
|
+
this.#requestRender();
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
if (this.#lastRenderWasSplit || this.#narrowDetailsOpen) {
|
|
981
|
+
if (matchesKey(keyData, "pageUp")) {
|
|
982
|
+
this.#scrollDetails(-1);
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
if (matchesKey(keyData, "pageDown")) {
|
|
986
|
+
this.#scrollDetails(1);
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
if (keyData === "t") {
|
|
991
|
+
this.#hoveredRow = null;
|
|
992
|
+
this.#viewMode = this.#viewMode === "roster" ? "tree" : "roster";
|
|
993
|
+
this.#refreshRows();
|
|
994
|
+
this.#requestRender();
|
|
523
995
|
return;
|
|
524
996
|
}
|
|
525
997
|
if (matchesKey(keyData, "left")) {
|
|
998
|
+
if (this.#narrowDetailsOpen && !this.#lastRenderWasSplit) {
|
|
999
|
+
this.#narrowDetailsOpen = false;
|
|
1000
|
+
this.#requestRender();
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
526
1003
|
const now = Date.now();
|
|
527
1004
|
if (now - this.#lastLeftTap < LEFT_TAP_WINDOW_MS) {
|
|
528
1005
|
this.#lastLeftTap = 0;
|
|
@@ -532,16 +1009,17 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
532
1009
|
}
|
|
533
1010
|
return;
|
|
534
1011
|
}
|
|
1012
|
+
this.#hoveredRow = null;
|
|
535
1013
|
if (matchesKey(keyData, "j") || matchesSelectDown(keyData)) {
|
|
536
1014
|
if (this.#rows.length > 0) {
|
|
537
|
-
this.#
|
|
1015
|
+
this.#selectRow(Math.min(this.#selectedRow + 1, this.#rows.length - 1));
|
|
538
1016
|
}
|
|
539
1017
|
this.#requestRender();
|
|
540
1018
|
return;
|
|
541
1019
|
}
|
|
542
1020
|
if (matchesKey(keyData, "k") || matchesSelectUp(keyData)) {
|
|
543
1021
|
if (this.#rows.length > 0) {
|
|
544
|
-
this.#
|
|
1022
|
+
this.#selectRow(Math.max(this.#selectedRow - 1, 0));
|
|
545
1023
|
}
|
|
546
1024
|
this.#requestRender();
|
|
547
1025
|
return;
|