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