@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.
Files changed (281) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/dist/{CHANGELOG-8yy8h3bc.md → CHANGELOG-d9xenpn9.md} +83 -0
  3. package/dist/cli.js +10208 -12596
  4. package/dist/types/capability/mcp.d.ts +11 -0
  5. package/dist/types/capability/skill.d.ts +6 -0
  6. package/dist/types/cli/args.d.ts +1 -0
  7. package/dist/types/cli/command-help.d.ts +3 -0
  8. package/dist/types/commands/share.d.ts +25 -0
  9. package/dist/types/commit/agentic/index.d.ts +3 -1
  10. package/dist/types/commit/execute.d.ts +32 -0
  11. package/dist/types/commit/index.d.ts +2 -1
  12. package/dist/types/commit/pipeline.d.ts +7 -1
  13. package/dist/types/config/model-registry.d.ts +4 -0
  14. package/dist/types/config/model-resolver.d.ts +13 -0
  15. package/dist/types/config/settings-schema.d.ts +1 -34
  16. package/dist/types/discovery/agent-plugin-format.d.ts +143 -0
  17. package/dist/types/discovery/agent-plugins.d.ts +1 -0
  18. package/dist/types/discovery/contained-path.d.ts +34 -0
  19. package/dist/types/discovery/index.d.ts +1 -0
  20. package/dist/types/extensibility/custom-commands/types.d.ts +5 -5
  21. package/dist/types/extensibility/custom-tools/types.d.ts +3 -3
  22. package/dist/types/extensibility/extensions/loader.d.ts +9 -2
  23. package/dist/types/extensibility/extensions/types.d.ts +45 -8
  24. package/dist/types/extensibility/hooks/types.d.ts +5 -5
  25. package/dist/types/extensibility/shared-events.d.ts +3 -2
  26. package/dist/types/extensibility/skills.d.ts +5 -0
  27. package/dist/types/index.d.ts +2 -2
  28. package/dist/types/main.d.ts +1 -1
  29. package/dist/types/mcp/transports/header-policy.d.ts +45 -0
  30. package/dist/types/mcp/types.d.ts +15 -0
  31. package/dist/types/modes/acp/acp-agent.d.ts +1 -1
  32. package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -1
  33. package/dist/types/modes/acp/acp-event-mapper.d.ts +1 -1
  34. package/dist/types/modes/acp/acp-mode.d.ts +1 -1
  35. package/dist/types/modes/components/agent-hub-projection.d.ts +38 -0
  36. package/dist/types/modes/components/agent-hub-renderer.d.ts +41 -0
  37. package/dist/types/modes/components/agent-hub.d.ts +14 -5
  38. package/dist/types/modes/components/agent-transcript-viewer.d.ts +2 -0
  39. package/dist/types/modes/components/chat-transcript-builder.d.ts +2 -0
  40. package/dist/types/modes/components/custom-editor.d.ts +1 -2
  41. package/dist/types/modes/components/model-browser.d.ts +9 -1
  42. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  43. package/dist/types/modes/components/tool-execution.d.ts +2 -0
  44. package/dist/types/modes/interactive-mode.d.ts +8 -1
  45. package/dist/types/modes/utils/transcript-render-helpers.d.ts +3 -2
  46. package/dist/types/registry/agent-registry.d.ts +47 -0
  47. package/dist/types/registry/persisted-agents.d.ts +3 -1
  48. package/dist/types/secrets/index.d.ts +23 -1
  49. package/dist/types/session/agent-session-events.d.ts +2 -2
  50. package/dist/types/session/agent-session.d.ts +4 -2
  51. package/dist/types/session/messages.d.ts +1 -0
  52. package/dist/types/session/prewalk.d.ts +3 -1
  53. package/dist/types/session/session-entries.d.ts +10 -0
  54. package/dist/types/session/session-loader.d.ts +11 -1
  55. package/dist/types/session/session-manager.d.ts +9 -1
  56. package/dist/types/session/session-tools.d.ts +18 -1
  57. package/dist/types/session/turn-recovery.d.ts +7 -2
  58. package/dist/types/slash-commands/acp-builtins.d.ts +1 -1
  59. package/dist/types/slash-commands/available-commands.d.ts +1 -1
  60. package/dist/types/task/executor.d.ts +6 -0
  61. package/dist/types/task/index.d.ts +4 -3
  62. package/dist/types/task/read-only-policy.d.ts +3 -0
  63. package/dist/types/task/structured-subagent.d.ts +2 -0
  64. package/dist/types/task/types.d.ts +4 -0
  65. package/dist/types/tools/bash.d.ts +3 -3
  66. package/dist/types/tools/browser/launch.d.ts +2 -1
  67. package/dist/types/tools/browser/tab-worker.d.ts +2 -2
  68. package/dist/types/tools/path-utils.d.ts +1 -0
  69. package/dist/types/tools/read.d.ts +1 -5
  70. package/dist/types/tools/run-scope.d.ts +27 -1
  71. package/dist/types/tools/shell-tokenize.d.ts +16 -0
  72. package/dist/types/tools/terminal-output.d.ts +1 -1
  73. package/dist/types/utils/turndown.d.ts +1 -1
  74. package/dist/types/vibe/runtime.d.ts +2 -3
  75. package/dist/types/web/scrapers/readthedocs.d.ts +0 -3
  76. package/dist/types/web/search/providers/browser-headers.d.ts +2 -3
  77. package/examples/custom-tools/README.md +1 -1
  78. package/examples/extensions/README.md +1 -1
  79. package/examples/extensions/api-demo.ts +5 -6
  80. package/examples/extensions/chalk-logger.ts +1 -1
  81. package/examples/extensions/hello.ts +2 -2
  82. package/examples/extensions/reload-runtime.ts +1 -1
  83. package/examples/extensions/tools.ts +2 -2
  84. package/examples/extensions/with-deps/index.ts +1 -1
  85. package/examples/sdk/06-extensions.ts +1 -1
  86. package/package.json +15 -31
  87. package/scripts/bundle-dist.ts +1 -11
  88. package/scripts/legacy-pi-virtual-module.ts +4 -1
  89. package/src/advisor/runtime.ts +54 -0
  90. package/src/capability/mcp.ts +11 -0
  91. package/src/capability/skill.ts +6 -0
  92. package/src/cli/agents-cli.ts +1 -1
  93. package/src/cli/args.ts +24 -2
  94. package/src/cli/auth-broker-cli.ts +1 -1
  95. package/src/cli/auth-gateway-cli.ts +1 -1
  96. package/src/cli/bench-cli.ts +1 -1
  97. package/src/cli/claude-trace-cli.ts +1 -1
  98. package/src/cli/command-help.ts +4 -0
  99. package/src/cli/config-cli.ts +1 -1
  100. package/src/cli/dry-balance-cli.ts +1 -1
  101. package/src/cli/file-processor.ts +1 -1
  102. package/src/cli/flag-tables.ts +4 -0
  103. package/src/cli/gallery-cli.ts +1 -1
  104. package/src/cli/grep-cli.ts +1 -1
  105. package/src/cli/grievances-cli.ts +1 -1
  106. package/src/cli/help-extra.ts +1 -1
  107. package/src/cli/models-cli.ts +1 -1
  108. package/src/cli/plugin-cli.ts +1 -1
  109. package/src/cli/read-cli.ts +1 -1
  110. package/src/cli/setup-cli.ts +1 -1
  111. package/src/cli/shell-cli.ts +1 -1
  112. package/src/cli/ssh-cli.ts +1 -1
  113. package/src/cli/stats-cli.ts +1 -1
  114. package/src/cli/tiny-models-cli.ts +1 -1
  115. package/src/cli/ttsr-cli.ts +1 -1
  116. package/src/cli/update-cli.ts +1 -1
  117. package/src/cli/usage-cli.ts +1 -1
  118. package/src/cli/web-search-cli.ts +1 -1
  119. package/src/cli/worktree-cli.ts +1 -1
  120. package/src/cli-commands.ts +5 -0
  121. package/src/commands/commit.ts +16 -3
  122. package/src/commands/say.ts +1 -1
  123. package/src/commands/share.ts +71 -0
  124. package/src/commands/token.ts +1 -1
  125. package/src/commit/agentic/agent.ts +1 -1
  126. package/src/commit/agentic/index.ts +39 -21
  127. package/src/commit/cli.ts +1 -1
  128. package/src/commit/execute.ts +56 -0
  129. package/src/commit/index.ts +2 -1
  130. package/src/commit/pipeline.ts +20 -7
  131. package/src/config/model-discovery.ts +1 -1
  132. package/src/config/model-registry.ts +21 -1
  133. package/src/config/model-resolver.ts +54 -7
  134. package/src/config/settings-schema.ts +2 -33
  135. package/src/config/settings.ts +46 -0
  136. package/src/discovery/agent-plugin-format.ts +551 -0
  137. package/src/discovery/agent-plugins.ts +341 -0
  138. package/src/discovery/agents.ts +4 -2
  139. package/src/discovery/builtin-rules/ts-no-inline-cast-access.md +5 -4
  140. package/src/discovery/builtin-rules/ts-no-tiny-functions.md +1 -1
  141. package/src/discovery/claude-plugins.ts +21 -5
  142. package/src/discovery/contained-path.ts +78 -0
  143. package/src/discovery/helpers.ts +23 -9
  144. package/src/discovery/index.ts +1 -0
  145. package/src/discovery/omp-plugins.ts +20 -7
  146. package/src/eval/py/runner.py +38 -1
  147. package/src/exec/non-interactive-env.ts +1 -0
  148. package/src/extensibility/custom-commands/loader.ts +4 -4
  149. package/src/extensibility/custom-commands/types.ts +5 -5
  150. package/src/extensibility/custom-tools/loader.ts +4 -4
  151. package/src/extensibility/custom-tools/types.ts +3 -3
  152. package/src/extensibility/extensions/loader.ts +85 -20
  153. package/src/extensibility/extensions/runner.ts +6 -0
  154. package/src/extensibility/extensions/types.ts +51 -8
  155. package/src/extensibility/hooks/loader.ts +4 -5
  156. package/src/extensibility/hooks/types.ts +5 -5
  157. package/src/extensibility/plugins/legacy-pi-compat.ts +53 -0
  158. package/src/extensibility/plugins/marketplace/manager.ts +2 -1
  159. package/src/extensibility/shared-events.ts +3 -2
  160. package/src/extensibility/skills.ts +9 -0
  161. package/src/index.ts +2 -2
  162. package/src/internal-urls/memory-protocol.ts +5 -1
  163. package/src/internal-urls/skill-protocol.ts +14 -0
  164. package/src/internal-urls/vault-protocol.ts +2 -2
  165. package/src/launch/client.ts +11 -1
  166. package/src/launch/protocol.ts +7 -2
  167. package/src/launch/terminal-output.ts +1 -1
  168. package/src/main.ts +88 -23
  169. package/src/markit/converters/docx.ts +1 -1
  170. package/src/markit/converters/epub.ts +1 -1
  171. package/src/markit/converters/pptx.ts +1 -1
  172. package/src/markit/converters/xlsx.ts +1 -1
  173. package/src/mcp/config.ts +3 -0
  174. package/src/mcp/manager.ts +12 -9
  175. package/src/mcp/transports/header-policy.ts +95 -0
  176. package/src/mcp/transports/http.ts +35 -52
  177. package/src/mcp/transports/sse.ts +20 -27
  178. package/src/mcp/types.ts +15 -0
  179. package/src/modes/acp/acp-agent.ts +20 -9
  180. package/src/modes/acp/acp-client-bridge.ts +1 -1
  181. package/src/modes/acp/acp-event-mapper.ts +1 -1
  182. package/src/modes/acp/acp-mode.ts +1 -1
  183. package/src/modes/components/agent-dashboard.ts +2 -0
  184. package/src/modes/components/agent-hub-projection.ts +248 -0
  185. package/src/modes/components/agent-hub-renderer.ts +194 -0
  186. package/src/modes/components/agent-hub.ts +674 -207
  187. package/src/modes/components/agent-transcript-viewer.ts +3 -0
  188. package/src/modes/components/assistant-message.ts +1 -1
  189. package/src/modes/components/chat-transcript-builder.ts +3 -0
  190. package/src/modes/components/custom-editor.ts +0 -9
  191. package/src/modes/components/extensions/inspector-panel.ts +11 -6
  192. package/src/modes/components/footer.ts +1 -3
  193. package/src/modes/components/model-browser.ts +11 -3
  194. package/src/modes/components/model-hub.ts +4 -1
  195. package/src/modes/components/status-line/component.ts +1 -0
  196. package/src/modes/components/status-line/segments.ts +10 -7
  197. package/src/modes/components/status-line/types.ts +2 -0
  198. package/src/modes/components/tool-execution.ts +13 -16
  199. package/src/modes/components/tree-selector.ts +62 -3
  200. package/src/modes/controllers/command-controller.ts +4 -1
  201. package/src/modes/controllers/event-controller.ts +44 -20
  202. package/src/modes/controllers/extension-ui-controller.ts +2 -2
  203. package/src/modes/controllers/input-controller.ts +20 -2
  204. package/src/modes/controllers/mcp-command-controller.ts +81 -20
  205. package/src/modes/controllers/selector-controller.ts +41 -49
  206. package/src/modes/controllers/streaming-reveal.ts +1 -1
  207. package/src/modes/interactive-mode.ts +13 -2
  208. package/src/modes/rpc/rpc-mode.ts +2 -2
  209. package/src/modes/runtime-init.ts +1 -1
  210. package/src/modes/theme/theme.ts +2 -2
  211. package/src/modes/utils/transcript-render-helpers.ts +4 -2
  212. package/src/modes/utils/ui-helpers.ts +1 -0
  213. package/src/prompts/tools/computer.md +1 -1
  214. package/src/registry/agent-lifecycle.ts +14 -7
  215. package/src/registry/agent-registry.ts +65 -2
  216. package/src/registry/persisted-agents.ts +341 -16
  217. package/src/sdk.ts +6 -49
  218. package/src/secrets/index.ts +52 -1
  219. package/src/session/agent-session-events.ts +2 -2
  220. package/src/session/agent-session.ts +54 -16
  221. package/src/session/messages.ts +1 -0
  222. package/src/session/prewalk.ts +57 -17
  223. package/src/session/session-context.ts +3 -5
  224. package/src/session/session-entries.ts +10 -0
  225. package/src/session/session-handoff.ts +5 -1
  226. package/src/session/session-listing.ts +1 -1
  227. package/src/session/session-loader.ts +74 -8
  228. package/src/session/session-manager.ts +22 -2
  229. package/src/session/session-paths.ts +57 -9
  230. package/src/session/session-tools.ts +65 -4
  231. package/src/session/todo-tracker.ts +11 -1
  232. package/src/session/turn-recovery.ts +96 -74
  233. package/src/slash-commands/acp-builtins.ts +1 -1
  234. package/src/slash-commands/available-commands.ts +1 -1
  235. package/src/slash-commands/builtin-registry.ts +12 -8
  236. package/src/task/executor.ts +70 -20
  237. package/src/task/index.ts +30 -34
  238. package/src/task/isolation-runner.ts +24 -11
  239. package/src/task/persisted-revive.ts +12 -5
  240. package/src/task/read-only-policy.ts +27 -0
  241. package/src/task/structured-subagent.ts +14 -4
  242. package/src/task/types.ts +4 -0
  243. package/src/tools/auto-generated-guard.ts +1 -1
  244. package/src/tools/bash-interactive.ts +4 -4
  245. package/src/tools/bash-skill-urls.ts +15 -0
  246. package/src/tools/bash.ts +16 -17
  247. package/src/tools/browser/cmux/cmux-tab.ts +66 -18
  248. package/src/tools/browser/launch.ts +41 -12
  249. package/src/tools/browser/readable.ts +9 -9
  250. package/src/tools/browser/tab-supervisor.ts +12 -3
  251. package/src/tools/browser/tab-worker-entry.ts +1 -1
  252. package/src/tools/browser/tab-worker.ts +101 -12
  253. package/src/tools/debug.ts +1 -1
  254. package/src/tools/fetch.ts +1 -1
  255. package/src/tools/jtd-to-json-schema.ts +123 -21
  256. package/src/tools/path-utils.ts +9 -2
  257. package/src/tools/read.ts +21 -9
  258. package/src/tools/run-scope.ts +290 -4
  259. package/src/tools/shell-tokenize.ts +98 -0
  260. package/src/tools/terminal-output.ts +1 -1
  261. package/src/tools/todo.ts +1 -1
  262. package/src/utils/clipboard.ts +10 -2
  263. package/src/utils/external-editor.ts +4 -2
  264. package/src/utils/jj.ts +1 -1
  265. package/src/utils/turndown.ts +1 -2
  266. package/src/vibe/runtime.ts +5 -5
  267. package/src/web/scrapers/arxiv.ts +1 -1
  268. package/src/web/scrapers/go-pkg.ts +1 -1
  269. package/src/web/scrapers/iacr.ts +1 -1
  270. package/src/web/scrapers/readthedocs.ts +2 -1
  271. package/src/web/scrapers/twitter.ts +2 -2
  272. package/src/web/scrapers/types.ts +1 -1
  273. package/src/web/scrapers/wikipedia.ts +1 -1
  274. package/src/web/search/providers/browser-headers.ts +12 -39
  275. package/src/web/search/providers/codex.ts +3 -26
  276. package/src/web/search/providers/ecosia.ts +1 -1
  277. package/src/web/search/providers/exa.ts +1 -1
  278. package/src/web/search/providers/google.ts +1 -1
  279. package/src/web/search/providers/mojeek.ts +1 -1
  280. package/src/web/search/providers/startpage.ts +1 -1
  281. package/src/markit/converters/mammoth.d.ts +0 -24
@@ -0,0 +1,248 @@
1
+ import type { AgentMetricsSummary, AgentRef, AgentStatus } from "../../registry/agent-registry";
2
+ import { MAIN_AGENT_ID } from "../../registry/agent-registry";
3
+ import type { ObservableSession } from "../session-observer-registry";
4
+
5
+ export type AgentMetrics = AgentMetricsSummary;
6
+
7
+ export interface AggregateMetrics extends AgentMetrics {
8
+ reportedAgents: number;
9
+ /** Rows whose duration is an observer-measured active runtime. */
10
+ activeDurationAgents: number;
11
+ }
12
+
13
+ export interface AgentTreeProjection {
14
+ rows: AgentRef[];
15
+ depthById: Map<string, number>;
16
+ parentById: Map<string, string>;
17
+ lastSiblingById: Map<string, boolean>;
18
+ }
19
+
20
+ export const STATUS_ORDER: Record<AgentStatus, number> = { running: 0, idle: 1, parked: 2, aborted: 3 };
21
+
22
+ function finiteMetric(value: number | undefined): number {
23
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
24
+ }
25
+
26
+ /** Exact observer usage for one roster entry. */
27
+ export function progressMetrics(observed: ObservableSession | undefined): AgentMetrics | undefined {
28
+ const progress = observed?.progress;
29
+ if (!progress) return undefined;
30
+ const { tokens, requests, toolCount: tools, cost, durationMs } = progress;
31
+ if (
32
+ typeof tokens !== "number" ||
33
+ !Number.isFinite(tokens) ||
34
+ typeof requests !== "number" ||
35
+ !Number.isFinite(requests) ||
36
+ typeof tools !== "number" ||
37
+ !Number.isFinite(tools) ||
38
+ typeof cost !== "number" ||
39
+ !Number.isFinite(cost) ||
40
+ typeof durationMs !== "number" ||
41
+ !Number.isFinite(durationMs)
42
+ ) {
43
+ return undefined;
44
+ }
45
+ return {
46
+ tokens,
47
+ requests,
48
+ tools,
49
+ cost,
50
+ durationMs,
51
+ durationKind: "active",
52
+ contextTokens:
53
+ typeof progress.contextTokens === "number" && Number.isFinite(progress.contextTokens)
54
+ ? progress.contextTokens
55
+ : undefined,
56
+ contextWindow:
57
+ typeof progress.contextWindow === "number" && Number.isFinite(progress.contextWindow)
58
+ ? progress.contextWindow
59
+ : undefined,
60
+ };
61
+ }
62
+
63
+ /**
64
+ * Read direct assistant usage from a live session. SessionStats also includes
65
+ * usage embedded in completed `task` tool results, so using it for a parent
66
+ * row would double-count child rows in the aggregate.
67
+ */
68
+ export function readSessionMetrics(session: NonNullable<AgentRef["session"]>): AgentMetrics | undefined {
69
+ try {
70
+ const stats = session.getSessionStats();
71
+ const messages = session.agent?.state?.messages;
72
+ if (!Array.isArray(messages)) {
73
+ return {
74
+ tokens: stats.tokens.input + stats.tokens.output + stats.tokens.cacheWrite,
75
+ requests: stats.assistantMessages,
76
+ tools: stats.toolCalls,
77
+ cost: stats.cost,
78
+ durationMs: 0,
79
+ durationKind: "unknown",
80
+ contextTokens: stats.contextUsage?.tokens,
81
+ contextWindow: stats.contextUsage?.contextWindow,
82
+ };
83
+ }
84
+
85
+ let tokens = 0;
86
+ let requests = 0;
87
+ let tools = 0;
88
+ let cost = 0;
89
+ for (const message of messages) {
90
+ if (message.role !== "assistant") continue;
91
+ requests++;
92
+ tokens += message.usage.input + message.usage.output + message.usage.cacheWrite;
93
+ tools += message.content.filter(content => content.type === "toolCall").length;
94
+ cost += message.usage.cost.total;
95
+ }
96
+ return {
97
+ tokens,
98
+ requests,
99
+ tools,
100
+ cost,
101
+ durationMs: 0,
102
+ durationKind: "unknown",
103
+ contextTokens: stats.contextUsage?.tokens,
104
+ contextWindow: stats.contextUsage?.contextWindow,
105
+ };
106
+ } catch {
107
+ // Render-only doubles and sessions being torn down may not expose a
108
+ // complete statistics host. Missing metrics are preferable to a broken hub.
109
+ return undefined;
110
+ }
111
+ }
112
+
113
+ export function aggregateMetrics(args: {
114
+ rows: readonly AgentRef[];
115
+ observedById: ReadonlyMap<string, ObservableSession>;
116
+ metricsFor: (ref: AgentRef, observed: ObservableSession | undefined) => AgentMetrics | undefined;
117
+ fallbackStatsSession: (
118
+ ref: AgentRef,
119
+ observed: ObservableSession | undefined,
120
+ ) => NonNullable<AgentRef["session"]> | undefined;
121
+ sessionMetrics: WeakMap<object, { metrics: AgentMetrics | undefined }>;
122
+ refreshFallback: boolean;
123
+ }): { metrics: AggregateMetrics; hasFallbackLiveSessions: boolean } {
124
+ const total: AggregateMetrics = {
125
+ tokens: 0,
126
+ requests: 0,
127
+ tools: 0,
128
+ cost: 0,
129
+ durationMs: 0,
130
+ durationKind: "active",
131
+ reportedAgents: 0,
132
+ activeDurationAgents: 0,
133
+ };
134
+ let hasFallbackLiveSessions = false;
135
+ const countedFallbackSessions = new Set<NonNullable<AgentRef["session"]>>();
136
+ for (const ref of args.rows) {
137
+ const observed = args.observedById.get(ref.id);
138
+ const fallbackSession = args.fallbackStatsSession(ref, observed);
139
+ if (fallbackSession) {
140
+ hasFallbackLiveSessions = true;
141
+ if (args.refreshFallback || !args.sessionMetrics.has(fallbackSession)) {
142
+ args.sessionMetrics.set(fallbackSession, { metrics: readSessionMetrics(fallbackSession) });
143
+ }
144
+ }
145
+ const metrics = args.metricsFor(ref, observed);
146
+ if (!metrics || (fallbackSession && countedFallbackSessions.has(fallbackSession))) continue;
147
+ if (fallbackSession) countedFallbackSessions.add(fallbackSession);
148
+ total.reportedAgents++;
149
+ total.tokens += finiteMetric(metrics.tokens);
150
+ total.requests += finiteMetric(metrics.requests);
151
+ total.tools += finiteMetric(metrics.tools);
152
+ total.cost += finiteMetric(metrics.cost);
153
+ if (metrics.durationKind === "active") {
154
+ total.durationMs += finiteMetric(metrics.durationMs);
155
+ total.activeDurationAgents++;
156
+ }
157
+ }
158
+ return { metrics: total, hasFallbackLiveSessions };
159
+ }
160
+
161
+ /** Parent-before-child projection preserving the roster's stable sibling order. */
162
+ export function projectAgentTree(refs: readonly AgentRef[]): AgentTreeProjection {
163
+ const ids = new Set<string>();
164
+ const operationalIndex = new Map<string, number>();
165
+ for (let i = 0; i < refs.length; i++) {
166
+ ids.add(refs[i].id);
167
+ operationalIndex.set(refs[i].id, i);
168
+ }
169
+
170
+ const parentById = new Map<string, string>();
171
+ const children = new Map<string, AgentRef[]>();
172
+ for (const ref of refs) {
173
+ const parent =
174
+ ref.parentId && ref.parentId !== MAIN_AGENT_ID && ids.has(ref.parentId) ? ref.parentId : MAIN_AGENT_ID;
175
+ parentById.set(ref.id, parent);
176
+ const siblings = children.get(parent);
177
+ if (siblings) siblings.push(ref);
178
+ else children.set(parent, [ref]);
179
+ }
180
+
181
+ // A tree group occupies the position of its earliest operational row.
182
+ // Compute subtree minima iteratively so pathological lineage depth remains stack-safe.
183
+ const subtreeOrder = new Map<string, number>();
184
+ const visiting = new Set<string>();
185
+ const ranked = new Set<string>();
186
+ for (const start of refs) {
187
+ if (ranked.has(start.id)) continue;
188
+ const stack: Array<{ ref: AgentRef; expanded: boolean }> = [{ ref: start, expanded: false }];
189
+ while (stack.length > 0) {
190
+ const current = stack.pop();
191
+ if (!current) continue;
192
+ if (current.expanded) {
193
+ let order = operationalIndex.get(current.ref.id) ?? Number.MAX_SAFE_INTEGER;
194
+ for (const child of children.get(current.ref.id) ?? []) {
195
+ order = Math.min(order, subtreeOrder.get(child.id) ?? Number.MAX_SAFE_INTEGER);
196
+ }
197
+ subtreeOrder.set(current.ref.id, order);
198
+ visiting.delete(current.ref.id);
199
+ ranked.add(current.ref.id);
200
+ continue;
201
+ }
202
+ if (ranked.has(current.ref.id) || visiting.has(current.ref.id)) continue;
203
+ visiting.add(current.ref.id);
204
+ stack.push({ ref: current.ref, expanded: true });
205
+ const descendants = children.get(current.ref.id);
206
+ if (!descendants) continue;
207
+ for (let i = descendants.length - 1; i >= 0; i--) {
208
+ const child = descendants[i];
209
+ if (!ranked.has(child.id) && !visiting.has(child.id)) stack.push({ ref: child, expanded: false });
210
+ }
211
+ }
212
+ }
213
+ for (const siblings of children.values()) {
214
+ siblings.sort(
215
+ (a, b) =>
216
+ (subtreeOrder.get(a.id) ?? Number.MAX_SAFE_INTEGER) - (subtreeOrder.get(b.id) ?? Number.MAX_SAFE_INTEGER) ||
217
+ (operationalIndex.get(a.id) ?? Number.MAX_SAFE_INTEGER) -
218
+ (operationalIndex.get(b.id) ?? Number.MAX_SAFE_INTEGER),
219
+ );
220
+ }
221
+
222
+ const lastSiblingById = new Map<string, boolean>();
223
+ for (const siblings of children.values()) {
224
+ for (let i = 0; i < siblings.length; i++) lastSiblingById.set(siblings[i].id, i === siblings.length - 1);
225
+ }
226
+
227
+ const rows: AgentRef[] = [];
228
+ const visited = new Set<string>();
229
+ const depthById = new Map<string, number>();
230
+ const visit = (root: AgentRef, rootDepth: number): void => {
231
+ const stack: Array<{ ref: AgentRef; depth: number }> = [{ ref: root, depth: rootDepth }];
232
+ while (stack.length > 0) {
233
+ const current = stack.pop();
234
+ if (!current || visited.has(current.ref.id)) continue;
235
+ visited.add(current.ref.id);
236
+ depthById.set(current.ref.id, current.depth);
237
+ rows.push(current.ref);
238
+ const descendants = children.get(current.ref.id);
239
+ if (!descendants) continue;
240
+ for (let i = descendants.length - 1; i >= 0; i--)
241
+ stack.push({ ref: descendants[i], depth: current.depth + 1 });
242
+ }
243
+ };
244
+ for (const root of children.get(MAIN_AGENT_ID) ?? []) visit(root, 0);
245
+ // Corrupt persisted parent cycles remain visible as roots instead of disappearing.
246
+ for (const ref of refs) visit(ref, 0);
247
+ return { rows, depthById, parentById, lastSiblingById };
248
+ }
@@ -0,0 +1,194 @@
1
+ import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
+ import { Ellipsis, visibleWidth } from "@oh-my-pi/pi-tui";
3
+ import { formatDuration, formatNumber, sanitizeText } from "@oh-my-pi/pi-utils";
4
+ import { getRoleInfo } from "../../config/model-roles";
5
+ import type { Settings } from "../../config/settings";
6
+ import { type AgentRef, MAIN_AGENT_ID } from "../../registry/agent-registry";
7
+ import { parseThinkingLevel } from "../../thinking";
8
+ import { replaceTabs, TRUNCATE_LENGTHS, truncateToWidth } from "../../tools/render-utils";
9
+ import type { ObservableSession } from "../session-observer-registry";
10
+ import { theme } from "../theme/theme";
11
+ import type { AgentMetrics } from "./agent-hub-projection";
12
+
13
+ export interface RosterRender {
14
+ lines: string[];
15
+ hitRows: Array<number | undefined>;
16
+ }
17
+
18
+ /** Legacy progress snapshots may omit counters; snapshot absence remains distinct. */
19
+ export function metricNumber(value: number | undefined): number {
20
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
21
+ }
22
+
23
+ /** Compute the max content width for the current terminal, accounting for chrome. */
24
+ export function contentWidth(): number {
25
+ return Math.max(TRUNCATE_LENGTHS.SHORT, (process.stdout.columns || 80) - 6);
26
+ }
27
+
28
+ /** Remove terminal controls and normalize a value before it reaches the TUI. */
29
+ export function sanitizeDisplayText(text: string): string {
30
+ return replaceTabs(sanitizeText(text)).replace(/[\r\n]+/g, " ");
31
+ }
32
+
33
+ /** Sanitize a line for TUI display and truncate it to the viewport width. */
34
+ export function sanitizeLine(text: string, maxWidth?: number): string {
35
+ return truncateToWidth(sanitizeDisplayText(text), maxWidth ?? contentWidth());
36
+ }
37
+
38
+ export function clampHubLine(line: string, width: number): string {
39
+ return truncateToWidth(line.replace(/[\r\n]+/g, " "), Math.max(1, width), Ellipsis.Omit);
40
+ }
41
+
42
+ /** Status glyph, colored per theme status conventions. The title-line counts spell out the words. */
43
+ export function statusGlyph(status: AgentRef["status"]): string {
44
+ switch (status) {
45
+ case "running":
46
+ return theme.fg("accent", theme.status.running);
47
+ case "idle":
48
+ return theme.fg("success", theme.status.enabled);
49
+ case "parked":
50
+ return theme.fg("muted", theme.status.shadowed);
51
+ case "aborted":
52
+ return theme.fg("error", theme.status.aborted);
53
+ }
54
+ }
55
+
56
+ export function statusText(status: AgentRef["status"], text: string): string {
57
+ switch (status) {
58
+ case "running":
59
+ return theme.fg("accent", text);
60
+ case "idle":
61
+ return theme.fg("success", text);
62
+ case "parked":
63
+ return theme.fg("muted", text);
64
+ case "aborted":
65
+ return theme.fg("error", text);
66
+ }
67
+ }
68
+
69
+ /** Model id + thinking level (`sonnet-4-6 ◒ high`), level colored per theme. */
70
+ export function formatModelBadge(modelId: string, level: ThinkingLevel | undefined): string {
71
+ const model = theme.fg("muted", sanitizeDisplayText(modelId));
72
+ if (!level || level === ThinkingLevel.Off || level === ThinkingLevel.Inherit) return model;
73
+ const display = theme.thinking[level as keyof typeof theme.thinking] ?? level;
74
+ return `${model} ${theme.getThinkingBorderColor(level)(display)}`;
75
+ }
76
+
77
+ /** Textual model-role tag; color reinforces (but never replaces) the label. */
78
+ export function formatRoleBadge(role: string, settings: Settings): string {
79
+ const info = getRoleInfo(role, settings);
80
+ return theme.fg(info.color ?? "muted", sanitizeDisplayText(info.tag ?? info.name ?? role));
81
+ }
82
+
83
+ /** Format a resolved selector, preserving provider identity when requested. */
84
+ export function formatResolvedModelBadge(
85
+ resolved: string,
86
+ preserveProvider = false,
87
+ fallbackLevel?: ThinkingLevel,
88
+ ): string {
89
+ const cleanResolved = sanitizeDisplayText(resolved);
90
+ // Model ids may themselves contain colons (`qwen3:14b`), so only treat the
91
+ // suffix as a thinking level when it parses as one.
92
+ const colon = cleanResolved.lastIndexOf(":");
93
+ const explicitLevel = colon >= 0 ? parseThinkingLevel(cleanResolved.slice(colon + 1)) : undefined;
94
+ const selector = explicitLevel !== undefined ? cleanResolved.slice(0, colon) : cleanResolved;
95
+ const label = preserveProvider ? selector : selector.slice(selector.indexOf("/") + 1);
96
+ return formatModelBadge(label, explicitLevel ?? fallbackLevel);
97
+ }
98
+
99
+ /**
100
+ * Resolved model + reasoning level for a hub row. Exact executor progress is
101
+ * authoritative (and survives completion); direct live sessions are the
102
+ * fallback for agents without an observer snapshot.
103
+ */
104
+ export function modelBadge(ref: AgentRef, observed: ObservableSession | undefined): string | undefined {
105
+ const progress = observed?.progress;
106
+ const liveThinkingLevel = ref.session?.thinkingLevel;
107
+ const fallbackSelector =
108
+ ref.session?.retryFallbackModel ??
109
+ (progress?.resolvedModelIsFallback ? progress.resolvedModel : undefined) ??
110
+ (ref.history?.resolvedModelIsFallback ? ref.history.resolvedModel : undefined);
111
+ if (fallbackSelector) {
112
+ return `${theme.fg("warning", "fallback →")} ${formatResolvedModelBadge(fallbackSelector, true, liveThinkingLevel)}`;
113
+ }
114
+ const resolvedModel = progress?.resolvedModel ?? ref.history?.resolvedModel;
115
+ if (resolvedModel) return formatResolvedModelBadge(resolvedModel, false, liveThinkingLevel);
116
+ const model = ref.session?.model;
117
+ if (!model) return undefined;
118
+ const level = model.thinking ? liveThinkingLevel : undefined;
119
+ return formatModelBadge(model.id, level);
120
+ }
121
+
122
+ export function formatMetricDuration(metrics: AgentMetrics): string | undefined {
123
+ const durationMs = metricNumber(metrics.durationMs);
124
+ if (durationMs <= 0) return undefined;
125
+ const label = metrics.durationKind === "active" ? "active" : metrics.durationKind === "span" ? "span" : "duration";
126
+ return `${formatDuration(durationMs)} ${label}`;
127
+ }
128
+
129
+ export function formatCost(cost: number): string {
130
+ const amount = metricNumber(cost);
131
+ if (amount < 0.01) return `$${amount.toFixed(4)}`;
132
+ if (amount < 1) return `$${amount.toFixed(3)}`;
133
+ return `$${amount.toFixed(2)}`;
134
+ }
135
+
136
+ export function formatMetrics(metrics: AgentMetrics): string {
137
+ return [
138
+ formatCost(metrics.cost),
139
+ formatMetricDuration(metrics) ?? "time —",
140
+ `${formatNumber(metrics.requests)} req`,
141
+ `${formatNumber(metrics.tools)} tools`,
142
+ `${formatNumber(metrics.tokens)} tok`,
143
+ ].join(theme.sep.dot);
144
+ }
145
+
146
+ export function contextGauge(tokens: number, window: number): string {
147
+ const ratio = Math.max(0, Math.min(1, tokens / window));
148
+ const filled = Math.round(ratio * 10);
149
+ return `${theme.fg("accent", "━".repeat(filled))}${theme.fg("dim", "─".repeat(10 - filled))} ${formatNumber(tokens)}/${formatNumber(window)} ${Math.round(ratio * 100)}%`;
150
+ }
151
+
152
+ /** Fit a child-id preview without joining an arbitrarily large child set. */
153
+ export function formatChildIds(children: readonly AgentRef[], width: number): string {
154
+ const max = Math.max(1, width);
155
+ let shown = 0;
156
+ let text = "";
157
+ while (shown < children.length) {
158
+ const id = sanitizeLine(children[shown].id, max);
159
+ const candidate = text ? `${text}, ${id}` : id;
160
+ const remaining = children.length - shown - 1;
161
+ const suffix = remaining > 0 ? `, … +${remaining}` : "";
162
+ if (visibleWidth(candidate + suffix) > max) {
163
+ const includesCurrent = text.length === 0;
164
+ const omitted = children.length - shown - Number(includesCurrent);
165
+ return truncateToWidth(`${includesCurrent ? id : text}${omitted > 0 ? `, … +${omitted}` : ""}`, max);
166
+ }
167
+ text = candidate;
168
+ shown++;
169
+ }
170
+ return text;
171
+ }
172
+
173
+ /** Bash `tree`-style ancestry prefix, clipped from the left on pathological depth. */
174
+ export function treeBranch(
175
+ ref: AgentRef,
176
+ maxWidth: number,
177
+ depthById: ReadonlyMap<string, number>,
178
+ parentById: ReadonlyMap<string, string>,
179
+ lastSiblingById: ReadonlyMap<string, boolean>,
180
+ ): string {
181
+ if ((depthById.get(ref.id) ?? 0) === 0) return "";
182
+ const segments: string[] = [lastSiblingById.get(ref.id) ? "└── " : "├── "];
183
+ const ancestry = new Set<string>();
184
+ let parent = parentById.get(ref.id);
185
+ while (parent && parentById.get(parent) !== MAIN_AGENT_ID && !ancestry.has(parent)) {
186
+ ancestry.add(parent);
187
+ segments.push(lastSiblingById.get(parent) ? " " : "│ ");
188
+ parent = parentById.get(parent);
189
+ }
190
+ const maxSegments = Math.max(1, Math.floor(Math.max(4, maxWidth - 2) / 4));
191
+ const omitted = Math.max(0, segments.length - maxSegments);
192
+ const prefix = segments.slice(0, maxSegments).reverse().join("");
193
+ return theme.fg("dim", `${omitted > 0 ? "… " : ""}${prefix}`);
194
+ }