@ai-agent-forge/agent-forge 0.86.4 → 0.86.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/dist/bundle/.build-manifest.json +470 -404
  3. package/dist/bundle/chunks/anthropic-messages-ALDZBUKI.js +6 -0
  4. package/dist/bundle/chunks/anthropic.js +2 -2
  5. package/dist/bundle/chunks/{azure-openai-responses-WQCZWFAH.js → azure-openai-responses-A2WJWZFB.js} +1 -1
  6. package/dist/bundle/chunks/bedrock-converse-stream.js +1 -1
  7. package/dist/bundle/chunks/chunk-A7GUOX2H.js +141 -0
  8. package/dist/bundle/chunks/{chunk-WGLBM2BP.js → chunk-CIX3FSBK.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-ZV3W36HK.js → chunk-EALJ5DDX.js} +99 -92
  10. package/dist/bundle/chunks/chunk-OO6DJ3CL.js +31 -0
  11. package/dist/bundle/chunks/{chunk-PE3XUHPT.js → chunk-SQFG22WN.js} +1 -1
  12. package/dist/bundle/chunks/{chunk-V46XUTK3.js → chunk-Y7HXTASS.js} +1 -1
  13. package/dist/bundle/chunks/{chunk-3WE3W52L.js → chunk-YJCQVXW5.js} +1 -1
  14. package/dist/bundle/chunks/github-copilot.js +1 -1
  15. package/dist/bundle/chunks/{google-generative-ai-LQ375GYC.js → google-generative-ai-NIB4WONV.js} +1 -1
  16. package/dist/bundle/chunks/{google-vertex-HXSRAKEZ.js → google-vertex-DCPYUEUO.js} +1 -1
  17. package/dist/bundle/chunks/{mistral-conversations-CSNMFR4J.js → mistral-conversations-OC6LA5XL.js} +1 -1
  18. package/dist/bundle/chunks/{openai-codex-responses-YDJSZOIH.js → openai-codex-responses-IO2X4Y7L.js} +1 -1
  19. package/dist/bundle/chunks/openai-codex.js +2 -2
  20. package/dist/bundle/chunks/{openai-completions-UPRWTQPT.js → openai-completions-XYQQHETU.js} +1 -1
  21. package/dist/bundle/chunks/{openai-responses-O63W6HQ3.js → openai-responses-DLIRN3CI.js} +1 -1
  22. package/dist/bundle/chunks/{openrouter-images-G3GT5BVT.js → openrouter-images-G4GQRUSG.js} +1 -1
  23. package/dist/bundle/chunks/openrouter.js +2 -2
  24. package/dist/bundle/chunks/{pi-messages-SLF355GF.js → pi-messages-E6RRYBO7.js} +1 -1
  25. package/dist/bundle/chunks/radius.js +2 -2
  26. package/dist/bundle/chunks/{virtual-modules-O72ICTMQ.js → virtual-modules-3IZ4MYSQ.js} +1 -1
  27. package/dist/bundle/cli.js +1 -1
  28. package/dist/bundle/index.js +1 -1
  29. package/dist/bundle/rpc-entry.js +1 -1
  30. package/dist/capabilities/builtin.d.ts +4 -2
  31. package/dist/capabilities/builtin.d.ts.map +1 -1
  32. package/dist/capabilities/builtin.js +5 -1
  33. package/dist/capabilities/builtin.js.map +1 -1
  34. package/dist/cli/plugin-command.d.ts.map +1 -1
  35. package/dist/cli/plugin-command.js +51 -5
  36. package/dist/cli/plugin-command.js.map +1 -1
  37. package/dist/core/agent-session-compaction.d.ts.map +1 -1
  38. package/dist/core/agent-session-compaction.js +13 -5
  39. package/dist/core/agent-session-compaction.js.map +1 -1
  40. package/dist/core/agent-session-internal.d.ts +2 -4
  41. package/dist/core/agent-session-internal.d.ts.map +1 -1
  42. package/dist/core/agent-session-internal.js.map +1 -1
  43. package/dist/core/agent-session-paths.d.ts +2 -0
  44. package/dist/core/agent-session-paths.d.ts.map +1 -1
  45. package/dist/core/agent-session-paths.js +2 -1
  46. package/dist/core/agent-session-paths.js.map +1 -1
  47. package/dist/core/agent-session-projection.d.ts +10 -4
  48. package/dist/core/agent-session-projection.d.ts.map +1 -1
  49. package/dist/core/agent-session-projection.js +17 -4
  50. package/dist/core/agent-session-projection.js.map +1 -1
  51. package/dist/core/agent-session.d.ts +19 -5
  52. package/dist/core/agent-session.d.ts.map +1 -1
  53. package/dist/core/agent-session.js +30 -4
  54. package/dist/core/agent-session.js.map +1 -1
  55. package/dist/core/extensions/bootstrap-lifecycle.d.ts +1 -1
  56. package/dist/core/extensions/bootstrap-lifecycle.d.ts.map +1 -1
  57. package/dist/core/extensions/bootstrap-lifecycle.js +5 -2
  58. package/dist/core/extensions/bootstrap-lifecycle.js.map +1 -1
  59. package/dist/core/extensions/capability-runtime-store.d.ts.map +1 -1
  60. package/dist/core/extensions/capability-runtime-store.js +1 -0
  61. package/dist/core/extensions/capability-runtime-store.js.map +1 -1
  62. package/dist/core/extensions/capability-runtime-types.d.ts +16 -1
  63. package/dist/core/extensions/capability-runtime-types.d.ts.map +1 -1
  64. package/dist/core/extensions/capability-runtime-types.js.map +1 -1
  65. package/dist/core/extensions/capability-runtime.d.ts.map +1 -1
  66. package/dist/core/extensions/capability-runtime.js +38 -2
  67. package/dist/core/extensions/capability-runtime.js.map +1 -1
  68. package/dist/core/extensions/index.d.ts +1 -1
  69. package/dist/core/extensions/index.d.ts.map +1 -1
  70. package/dist/core/extensions/index.js.map +1 -1
  71. package/dist/core/extensions/lifecycle-catalog.d.ts +46 -0
  72. package/dist/core/extensions/lifecycle-catalog.d.ts.map +1 -1
  73. package/dist/core/extensions/lifecycle-catalog.js +101 -0
  74. package/dist/core/extensions/lifecycle-catalog.js.map +1 -1
  75. package/dist/core/extensions/provider-failover.d.ts +106 -0
  76. package/dist/core/extensions/provider-failover.d.ts.map +1 -0
  77. package/dist/core/extensions/provider-failover.js +692 -0
  78. package/dist/core/extensions/provider-failover.js.map +1 -0
  79. package/dist/core/facade/coding-agent-facade.d.ts +13 -6
  80. package/dist/core/facade/coding-agent-facade.d.ts.map +1 -1
  81. package/dist/core/facade/coding-agent-facade.js +3 -1
  82. package/dist/core/facade/coding-agent-facade.js.map +1 -1
  83. package/dist/core/keybindings.d.ts +6 -6
  84. package/dist/core/keybindings.d.ts.map +1 -1
  85. package/dist/core/keybindings.js +23 -6
  86. package/dist/core/keybindings.js.map +1 -1
  87. package/dist/core/model-config.d.ts +102 -0
  88. package/dist/core/model-config.d.ts.map +1 -1
  89. package/dist/core/model-config.js +46 -14
  90. package/dist/core/model-config.js.map +1 -1
  91. package/dist/core/model-resolver.d.ts.map +1 -1
  92. package/dist/core/model-resolver.js +1 -1
  93. package/dist/core/model-resolver.js.map +1 -1
  94. package/dist/core/model-runtime.d.ts +15 -0
  95. package/dist/core/model-runtime.d.ts.map +1 -1
  96. package/dist/core/model-runtime.js +30 -18
  97. package/dist/core/model-runtime.js.map +1 -1
  98. package/dist/core/remote-catalog-provider.d.ts.map +1 -1
  99. package/dist/core/remote-catalog-provider.js +4 -9
  100. package/dist/core/remote-catalog-provider.js.map +1 -1
  101. package/dist/core/sdk.d.ts.map +1 -1
  102. package/dist/core/sdk.js +107 -24
  103. package/dist/core/sdk.js.map +1 -1
  104. package/dist/core/session-manager.d.ts +2 -0
  105. package/dist/core/session-manager.d.ts.map +1 -1
  106. package/dist/core/session-manager.js +10 -5
  107. package/dist/core/session-manager.js.map +1 -1
  108. package/dist/core/settings-manager.d.ts +71 -5
  109. package/dist/core/settings-manager.d.ts.map +1 -1
  110. package/dist/core/settings-manager.js +138 -9
  111. package/dist/core/settings-manager.js.map +1 -1
  112. package/dist/core/slash-commands.d.ts.map +1 -1
  113. package/dist/core/slash-commands.js +17 -2
  114. package/dist/core/slash-commands.js.map +1 -1
  115. package/dist/core/tools/render-utils.d.ts +6 -0
  116. package/dist/core/tools/render-utils.d.ts.map +1 -1
  117. package/dist/core/tools/render-utils.js +26 -0
  118. package/dist/core/tools/render-utils.js.map +1 -1
  119. package/dist/core/tools/tool-definition.d.ts +21 -2
  120. package/dist/core/tools/tool-definition.d.ts.map +1 -1
  121. package/dist/core/tools/tool-definition.js.map +1 -1
  122. package/dist/main.d.ts.map +1 -1
  123. package/dist/main.js +3 -6
  124. package/dist/main.js.map +1 -1
  125. package/dist/modes/interactive/components/agents-view.d.ts +57 -0
  126. package/dist/modes/interactive/components/agents-view.d.ts.map +1 -0
  127. package/dist/modes/interactive/components/agents-view.js +260 -0
  128. package/dist/modes/interactive/components/agents-view.js.map +1 -0
  129. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  130. package/dist/modes/interactive/components/custom-editor.js +2 -2
  131. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  132. package/dist/modes/interactive/components/footer.d.ts +7 -0
  133. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  134. package/dist/modes/interactive/components/footer.js +51 -6
  135. package/dist/modes/interactive/components/footer.js.map +1 -1
  136. package/dist/modes/interactive/components/scoped-models-selector.d.ts +1 -1
  137. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  138. package/dist/modes/interactive/components/scoped-models-selector.js +1 -1
  139. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  140. package/dist/modes/interactive/components/settings-selector.d.ts +3 -1
  141. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  142. package/dist/modes/interactive/components/settings-selector.js +12 -2
  143. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  144. package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  145. package/dist/modes/interactive/components/thinking-selector.js +1 -1
  146. package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  147. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  148. package/dist/modes/interactive/components/tool-execution.js +89 -10
  149. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  150. package/dist/modes/interactive/components/user-message-selector.d.ts +3 -0
  151. package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
  152. package/dist/modes/interactive/components/user-message-selector.js +9 -3
  153. package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
  154. package/dist/modes/interactive/components/workflow-view.d.ts +119 -0
  155. package/dist/modes/interactive/components/workflow-view.d.ts.map +1 -0
  156. package/dist/modes/interactive/components/workflow-view.js +638 -0
  157. package/dist/modes/interactive/components/workflow-view.js.map +1 -0
  158. package/dist/modes/interactive/interactive-commands.d.ts +22 -0
  159. package/dist/modes/interactive/interactive-commands.d.ts.map +1 -1
  160. package/dist/modes/interactive/interactive-commands.js +56 -0
  161. package/dist/modes/interactive/interactive-commands.js.map +1 -1
  162. package/dist/modes/interactive/interactive-mode.d.ts +51 -1
  163. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  164. package/dist/modes/interactive/interactive-mode.js +218 -37
  165. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  166. package/dist/modes/interactive/interactive-selectors.d.ts +2 -4
  167. package/dist/modes/interactive/interactive-selectors.d.ts.map +1 -1
  168. package/dist/modes/interactive/interactive-selectors.js +10 -1
  169. package/dist/modes/interactive/interactive-selectors.js.map +1 -1
  170. package/dist/modes/print-mode.d.ts.map +1 -1
  171. package/dist/modes/print-mode.js +5 -0
  172. package/dist/modes/print-mode.js.map +1 -1
  173. package/dist/modes/rpc/rpc-client.d.ts +3 -1
  174. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  175. package/dist/modes/rpc/rpc-client.js +4 -2
  176. package/dist/modes/rpc/rpc-client.js.map +1 -1
  177. package/dist/modes/rpc/rpc-types.d.ts +1 -0
  178. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  179. package/dist/modes/rpc/rpc-types.js.map +1 -1
  180. package/dist/plugins/ecosystem/bootstrap-factory-manifest.d.ts.map +1 -1
  181. package/dist/plugins/ecosystem/bootstrap-factory-manifest.js +7 -0
  182. package/dist/plugins/ecosystem/bootstrap-factory-manifest.js.map +1 -1
  183. package/dist/plugins/ecosystem/cli.d.ts.map +1 -1
  184. package/dist/plugins/ecosystem/cli.js +1 -1
  185. package/dist/plugins/ecosystem/cli.js.map +1 -1
  186. package/dist/plugins/ecosystem/install-store-paths.d.ts +15 -0
  187. package/dist/plugins/ecosystem/install-store-paths.d.ts.map +1 -0
  188. package/dist/plugins/ecosystem/install-store-paths.js +58 -0
  189. package/dist/plugins/ecosystem/install-store-paths.js.map +1 -0
  190. package/dist/plugins/ecosystem/node-disk-registry-source.d.ts.map +1 -1
  191. package/dist/plugins/ecosystem/node-disk-registry-source.js +3 -8
  192. package/dist/plugins/ecosystem/node-disk-registry-source.js.map +1 -1
  193. package/dist/plugins/ecosystem/node-package-install-host.d.ts.map +1 -1
  194. package/dist/plugins/ecosystem/node-package-install-host.js +9 -1
  195. package/dist/plugins/ecosystem/node-package-install-host.js.map +1 -1
  196. package/dist/plugins/ecosystem/node-package-install-persistence.d.ts.map +1 -1
  197. package/dist/plugins/ecosystem/node-package-install-persistence.js +3 -8
  198. package/dist/plugins/ecosystem/node-package-install-persistence.js.map +1 -1
  199. package/dist/profile/plugin-profiles.d.ts +2 -0
  200. package/dist/profile/plugin-profiles.d.ts.map +1 -1
  201. package/dist/profile/plugin-profiles.js +4 -0
  202. package/dist/profile/plugin-profiles.js.map +1 -1
  203. package/dist/profiles/builtin-suites.d.ts +19 -2
  204. package/dist/profiles/builtin-suites.d.ts.map +1 -1
  205. package/dist/profiles/builtin-suites.js +20 -2
  206. package/dist/profiles/builtin-suites.js.map +1 -1
  207. package/dist/profiles/suite-loader.d.ts.map +1 -1
  208. package/dist/profiles/suite-loader.js +4 -1
  209. package/dist/profiles/suite-loader.js.map +1 -1
  210. package/dist/utils/clipboard.d.ts.map +1 -1
  211. package/dist/utils/clipboard.js +10 -10
  212. package/dist/utils/clipboard.js.map +1 -1
  213. package/docs/keybindings.md +8 -8
  214. package/docs/models.md +1 -1
  215. package/docs/rpc.md +3 -1
  216. package/docs/settings.md +2 -1
  217. package/package.json +4 -4
  218. package/dist/bundle/chunks/anthropic-messages-EVFQ6MLR.js +0 -6
  219. package/dist/bundle/chunks/chunk-F6SBZUJG.js +0 -141
  220. package/dist/bundle/chunks/chunk-NUHFSC37.js +0 -25
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Read-only subagent session browser (D-060 §3 /agents overlay).
3
+ *
4
+ * Lists this session's own subagent transcripts from the on-disk store
5
+ * (`sessions/_agents/`, same summary source as the subagent_list model tool)
6
+ * and shows one session's bounded timeline on demand. Everything here is
7
+ * read-only: no steer, no cancel, no resume — operations stay with the
8
+ * conversation and the subagent tools; this module never imports plugin code.
9
+ *
10
+ * Entries load lazily (one `readAgentSession` per session on entering detail);
11
+ * a failed load renders as a detail header state instead of throwing.
12
+ */
13
+ import { Container } from "@agent-forge/tui";
14
+ import type { AgentSessionReadV1, AgentSessionSummaryV1 } from "../../../core/agent-session-store.ts";
15
+ /**
16
+ * One line per entry: message entries render as `[role] text`; anything else
17
+ * renders as `[<type>] <json>` (or bare `[<type>]` when it has no JSON form),
18
+ * each line capped at ~160 characters.
19
+ */
20
+ export declare function formatAgentSessionEntries(entries: readonly unknown[]): readonly string[];
21
+ export interface AgentsViewConfig {
22
+ /** Summaries of this session's subagent sessions (newest first, parent-scoped). */
23
+ readonly sessions: readonly AgentSessionSummaryV1[];
24
+ /** Bounded transcript loader (lazy: called once per session when entering detail). */
25
+ readonly loadEntries: (sessionId: string) => AgentSessionReadV1 | undefined;
26
+ /** Maximum visible rows before the list scrolls. */
27
+ readonly maxRows: number;
28
+ /** Request a TUI re-render. */
29
+ requestRender?: () => void;
30
+ }
31
+ export declare class AgentsViewComponent extends Container {
32
+ onClose?: () => void;
33
+ private config;
34
+ private mode;
35
+ private rows;
36
+ private selectedIndex;
37
+ private offset;
38
+ /** Detail state: the loaded view plus the list row to restore on return. */
39
+ private detail;
40
+ private titleText;
41
+ private modeHintText;
42
+ private helpText;
43
+ constructor(config: AgentsViewConfig);
44
+ private updateModeTexts;
45
+ private buildHelpLine;
46
+ private formatSummaryRow;
47
+ private buildDetailHeader;
48
+ private buildRows;
49
+ private clampOffset;
50
+ private syncRows;
51
+ renderRows(width: number): string[];
52
+ private move;
53
+ private enterDetail;
54
+ private exitDetail;
55
+ handleInput(keyData: string): void;
56
+ }
57
+ //# sourceMappingURL=agents-view.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agents-view.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/agents-view.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAkB,SAAS,EAAyC,MAAM,kBAAkB,CAAC;AACpG,OAAO,KAAK,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,sCAAsC,CAAC;AAkCtG;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,SAAS,OAAO,EAAE,GAAG,SAAS,MAAM,EAAE,CAcxF;AAqBD,MAAM,WAAW,gBAAgB;IAChC,mFAAmF;IACnF,QAAQ,CAAC,QAAQ,EAAE,SAAS,qBAAqB,EAAE,CAAC;IACpD,sFAAsF;IACtF,QAAQ,CAAC,WAAW,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,kBAAkB,GAAG,SAAS,CAAC;IAC5E,oDAAoD;IACpD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,+BAA+B;IAC/B,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;CAC3B;AAED,qBAAa,mBAAoB,SAAQ,SAAS;IAC1C,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IAE5B,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,IAAI,CAA6B;IACzC,OAAO,CAAC,IAAI,CAAgB;IAC5B,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,MAAM,CAAK;IACnB,4EAA4E;IAC5E,OAAO,CAAC,MAAM,CAA0E;IACxF,OAAO,CAAC,SAAS,CAAO;IACxB,OAAO,CAAC,YAAY,CAAO;IAC3B,OAAO,CAAC,QAAQ,CAAO;IAEvB,YAAY,MAAM,EAAE,gBAAgB,EAenC;IAED,OAAO,CAAC,eAAe;IAMvB,OAAO,CAAC,aAAa;IAWrB,OAAO,CAAC,gBAAgB;IAOxB,OAAO,CAAC,iBAAiB;IAUzB,OAAO,CAAC,SAAS;IAkBjB,OAAO,CAAC,WAAW;IAOnB,OAAO,CAAC,QAAQ;IAMhB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CA2BlC;IAED,OAAO,CAAC,IAAI;IAMZ,OAAO,CAAC,WAAW;IAWnB,OAAO,CAAC,UAAU;IAUlB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAqBjC;CACD","sourcesContent":["/**\n * Read-only subagent session browser (D-060 §3 /agents overlay).\n *\n * Lists this session's own subagent transcripts from the on-disk store\n * (`sessions/_agents/`, same summary source as the subagent_list model tool)\n * and shows one session's bounded timeline on demand. Everything here is\n * read-only: no steer, no cancel, no resume — operations stay with the\n * conversation and the subagent tools; this module never imports plugin code.\n *\n * Entries load lazily (one `readAgentSession` per session on entering detail);\n * a failed load renders as a detail header state instead of throwing.\n */\n\nimport { type Component, Container, getKeybindings, Text, truncateToWidth } from \"@agent-forge/tui\";\nimport type { AgentSessionReadV1, AgentSessionSummaryV1 } from \"../../../core/agent-session-store.ts\";\nimport { theme } from \"../theme/theme.ts\";\nimport { DynamicBorder } from \"./dynamic-border.ts\";\nimport { keyHint, rawKeyHint } from \"./keybinding-hints.ts\";\n\n// ---------------------------------------------------------------------------\n// Entry timeline formatting (pure; exported for tests)\n// ---------------------------------------------------------------------------\n\nconst ENTRY_LINE_MAX_CHARS = 160;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * Message text extraction mirrors SessionManager's session-file scan\n * (`extractTextContent` in core/session-manager.ts): string content verbatim,\n * block content joined over its `type: \"text\"` blocks.\n */\nfunction messageText(message: Record<string, unknown>): string | undefined {\n\tconst content = message.content;\n\tif (typeof content === \"string\") return content;\n\tif (!Array.isArray(content)) return undefined;\n\treturn content\n\t\t.filter((block): block is Record<string, unknown> => isRecord(block) && block.type === \"text\")\n\t\t.map((block) => (typeof block.text === \"string\" ? block.text : \"\"))\n\t\t.join(\" \");\n}\n\nfunction truncateLine(text: string): string {\n\treturn text.length > ENTRY_LINE_MAX_CHARS ? `${text.slice(0, ENTRY_LINE_MAX_CHARS - 1)}…` : text;\n}\n\n/**\n * One line per entry: message entries render as `[role] text`; anything else\n * renders as `[<type>] <json>` (or bare `[<type>]` when it has no JSON form),\n * each line capped at ~160 characters.\n */\nexport function formatAgentSessionEntries(entries: readonly unknown[]): readonly string[] {\n\tconst lines: string[] = [];\n\tfor (const entry of entries) {\n\t\tif (isRecord(entry) && entry.type === \"message\" && isRecord(entry.message)) {\n\t\t\tconst role = typeof entry.message.role === \"string\" ? entry.message.role : \"message\";\n\t\t\tconst text = messageText(entry.message);\n\t\t\tlines.push(truncateLine(text === undefined || text.length === 0 ? `[${role}]` : `[${role}] ${text}`));\n\t\t\tcontinue;\n\t\t}\n\t\tconst type = isRecord(entry) && typeof entry.type === \"string\" ? entry.type : \"entry\";\n\t\tconst json = JSON.stringify(entry);\n\t\tlines.push(truncateLine(json === undefined ? `[${type}]` : `[${type}] ${json}`));\n\t}\n\treturn Object.freeze(lines);\n}\n\n// ---------------------------------------------------------------------------\n// Component (list + detail)\n// ---------------------------------------------------------------------------\n\n/** Renders the visible row window; reads live state from the owning component. */\nclass AgentsRowsView implements Component {\n\tprivate view: AgentsViewComponent;\n\n\tconstructor(view: AgentsViewComponent) {\n\t\tthis.view = view;\n\t}\n\n\tinvalidate(): void {}\n\n\trender(width: number): string[] {\n\t\treturn this.view.renderRows(width);\n\t}\n}\n\nexport interface AgentsViewConfig {\n\t/** Summaries of this session's subagent sessions (newest first, parent-scoped). */\n\treadonly sessions: readonly AgentSessionSummaryV1[];\n\t/** Bounded transcript loader (lazy: called once per session when entering detail). */\n\treadonly loadEntries: (sessionId: string) => AgentSessionReadV1 | undefined;\n\t/** Maximum visible rows before the list scrolls. */\n\treadonly maxRows: number;\n\t/** Request a TUI re-render. */\n\trequestRender?: () => void;\n}\n\nexport class AgentsViewComponent extends Container {\n\tpublic onClose?: () => void;\n\n\tprivate config: AgentsViewConfig;\n\tprivate mode: \"list\" | \"detail\" = \"list\";\n\tprivate rows: string[] = [];\n\tprivate selectedIndex = 0;\n\tprivate offset = 0;\n\t/** Detail state: the loaded view plus the list row to restore on return. */\n\tprivate detail: { view: AgentSessionReadV1 | undefined; listIndex: number } | undefined;\n\tprivate titleText: Text;\n\tprivate modeHintText: Text;\n\tprivate helpText: Text;\n\n\tconstructor(config: AgentsViewConfig) {\n\t\tsuper();\n\n\t\tthis.config = config;\n\t\tthis.titleText = new Text(theme.fg(\"accent\", theme.bold(\" Subagent Sessions\")), 1, 0);\n\t\tthis.modeHintText = new Text(\"\", 1, 0);\n\t\tthis.helpText = new Text(\"\", 1, 0);\n\t\tthis.updateModeTexts();\n\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(this.titleText);\n\t\tthis.addChild(this.modeHintText);\n\t\tthis.addChild(new AgentsRowsView(this));\n\t\tthis.addChild(this.helpText);\n\t\tthis.addChild(new DynamicBorder());\n\t}\n\n\tprivate updateModeTexts(): void {\n\t\tconst label = this.mode === \"list\" ? \"this session's subagents (newest first)\" : \"transcript (read-only)\";\n\t\tthis.modeHintText.setText(theme.fg(\"dim\", ` ${label}`));\n\t\tthis.helpText.setText(this.buildHelpLine());\n\t}\n\n\tprivate buildHelpLine(): string {\n\t\tconst parts = [rawKeyHint(\"↑↓\", \"move\")];\n\t\tif (this.mode === \"list\") {\n\t\t\tparts.push(rawKeyHint(\"tab/l\", \"detail\"), keyHint(\"tui.select.confirm\", \"open\"));\n\t\t} else {\n\t\t\tparts.push(rawKeyHint(\"tab/l\", \"list\"));\n\t\t}\n\t\tparts.push(keyHint(\"tui.select.cancel\", this.mode === \"list\" ? \"close\" : \"back\"));\n\t\treturn ` ${parts.join(\" \")}`;\n\t}\n\n\tprivate formatSummaryRow(summary: AgentSessionSummaryV1): string {\n\t\tconst role = summary.agentRole ?? \"-\";\n\t\tconst id = summary.sessionId.slice(0, 8);\n\t\tconst modified = new Date(summary.modifiedAt).toLocaleString();\n\t\treturn ` ${role} ${id} ${modified} ${summary.bytes}B`;\n\t}\n\n\tprivate buildDetailHeader(view: AgentSessionReadV1): string[] {\n\t\tconst identity = [`session ${view.sessionId}`];\n\t\tif (view.agentRole !== undefined) identity.push(`role ${view.agentRole}`);\n\t\tconst facts: string[] = [];\n\t\tif (view.createdAt !== undefined) facts.push(`created ${view.createdAt}`);\n\t\tfacts.push(`${view.bytes}B`, `${view.entryCount} entries`);\n\t\tif (view.truncated) facts.push(theme.fg(\"warning\", \"truncated\"));\n\t\treturn [` ${identity.join(\" · \")}`, ` ${facts.join(\" · \")}`];\n\t}\n\n\tprivate buildRows(): string[] {\n\t\tif (this.mode === \"detail\") {\n\t\t\tif (this.detail === undefined) return [\" no session selected\"];\n\t\t\tconst view = this.detail.view;\n\t\t\tif (view === undefined) {\n\t\t\t\treturn [\n\t\t\t\t\t` session ${this.config.sessions[this.detail.listIndex]?.sessionId ?? \"?\"} — ${theme.fg(\n\t\t\t\t\t\t\"error\",\n\t\t\t\t\t\t\"transcript unavailable (missing or unreadable)\",\n\t\t\t\t\t)}`,\n\t\t\t\t];\n\t\t\t}\n\t\t\treturn [...this.buildDetailHeader(view), ...formatAgentSessionEntries(view.entries)];\n\t\t}\n\t\tif (this.config.sessions.length === 0) return [\" no subagent sessions in this session\"];\n\t\treturn this.config.sessions.map((summary) => this.formatSummaryRow(summary));\n\t}\n\n\tprivate clampOffset(): void {\n\t\tconst maxRows = Math.max(1, this.config.maxRows);\n\t\tif (this.selectedIndex < this.offset) this.offset = this.selectedIndex;\n\t\tif (this.selectedIndex >= this.offset + maxRows) this.offset = this.selectedIndex - maxRows + 1;\n\t\tif (this.offset < 0) this.offset = 0;\n\t}\n\n\tprivate syncRows(): void {\n\t\tthis.rows = this.buildRows();\n\t\tif (this.selectedIndex >= this.rows.length) this.selectedIndex = Math.max(0, this.rows.length - 1);\n\t\tthis.clampOffset();\n\t}\n\n\trenderRows(width: number): string[] {\n\t\tthis.syncRows();\n\t\tconst maxRows = Math.max(1, this.config.maxRows);\n\t\tconst lines: string[] = [];\n\t\tfor (let index = this.offset; index < Math.min(this.rows.length, this.offset + maxRows); index++) {\n\t\t\tconst row = this.rows[index];\n\t\t\tif (row === undefined) continue;\n\t\t\tconst selected = index === this.selectedIndex;\n\t\t\tconst prefix = selected ? theme.fg(\"accent\", \"→ \") : \" \";\n\t\t\tlines.push(truncateToWidth(`${prefix}${row}`, width));\n\t\t}\n\t\tconst hiddenAbove = this.offset;\n\t\tconst hiddenBelow = this.rows.length - this.offset - maxRows;\n\t\tconst hidden: string[] = [];\n\t\tif (hiddenAbove > 0) hidden.push(`↑${hiddenAbove}`);\n\t\tif (hiddenBelow > 0) hidden.push(`↓${hiddenBelow}`);\n\t\t// Fixed output height (exactly maxRows content rows plus this last\n\t\t// row): the overlay's grid footprint must not shrink with mode\n\t\t// switches or row counts. A height change moves the chat/editor\n\t\t// layout boundary between frames, and every boundary shift now\n\t\t// forces the renderer's geometry guard into a full redraw (see\n\t\t// tui-alt-screen.ts doRender); constant-height output keeps pure\n\t\t// mode switches on the cheap incremental diff path. Padding rows\n\t\t// carry a real space so the indicator row position stays stable.\n\t\twhile (lines.length < maxRows) lines.push(truncateToWidth(\" \", width));\n\t\tlines.push(truncateToWidth(hidden.length > 0 ? theme.fg(\"dim\", ` ${hidden.join(\" \")}`) : \" \", width));\n\t\treturn lines;\n\t}\n\n\tprivate move(delta: number): void {\n\t\tif (this.rows.length === 0) return;\n\t\tthis.selectedIndex = Math.min(this.rows.length - 1, Math.max(0, this.selectedIndex + delta));\n\t\tthis.clampOffset();\n\t}\n\n\tprivate enterDetail(): void {\n\t\tconst summary = this.config.sessions[this.selectedIndex];\n\t\tif (summary === undefined) return;\n\t\tthis.detail = { view: this.config.loadEntries(summary.sessionId), listIndex: this.selectedIndex };\n\t\tthis.mode = \"detail\";\n\t\tthis.selectedIndex = 0;\n\t\tthis.offset = 0;\n\t\tthis.updateModeTexts();\n\t\tthis.config.requestRender?.();\n\t}\n\n\tprivate exitDetail(): void {\n\t\tconst restore = this.detail?.listIndex ?? 0;\n\t\tthis.detail = undefined;\n\t\tthis.mode = \"list\";\n\t\tthis.selectedIndex = restore;\n\t\tthis.clampOffset();\n\t\tthis.updateModeTexts();\n\t\tthis.config.requestRender?.();\n\t}\n\n\thandleInput(keyData: string): void {\n\t\tconst kb = getKeybindings();\n\t\tif (kb.matches(keyData, \"tui.select.cancel\")) {\n\t\t\t// Layered cancel (tree-list style): detail escapes to the list first;\n\t\t\t// only a list-mode cancel closes the overlay.\n\t\t\tif (this.mode === \"detail\") this.exitDetail();\n\t\t\telse this.onClose?.();\n\t\t} else if (kb.matches(keyData, \"tui.input.tab\") || keyData === \"l\") {\n\t\t\tif (this.mode === \"list\") this.enterDetail();\n\t\t\telse this.exitDetail();\n\t\t} else if (kb.matches(keyData, \"tui.select.confirm\")) {\n\t\t\tif (this.mode === \"list\") this.enterDetail();\n\t\t} else if (kb.matches(keyData, \"tui.select.up\") || keyData === \"k\") {\n\t\t\tthis.move(-1);\n\t\t} else if (kb.matches(keyData, \"tui.select.down\") || keyData === \"j\") {\n\t\t\tthis.move(1);\n\t\t} else if (kb.matches(keyData, \"tui.select.pageUp\")) {\n\t\t\tthis.move(-Math.max(1, this.config.maxRows));\n\t\t} else if (kb.matches(keyData, \"tui.select.pageDown\")) {\n\t\t\tthis.move(Math.max(1, this.config.maxRows));\n\t\t}\n\t}\n}\n"]}
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Read-only subagent session browser (D-060 §3 /agents overlay).
3
+ *
4
+ * Lists this session's own subagent transcripts from the on-disk store
5
+ * (`sessions/_agents/`, same summary source as the subagent_list model tool)
6
+ * and shows one session's bounded timeline on demand. Everything here is
7
+ * read-only: no steer, no cancel, no resume — operations stay with the
8
+ * conversation and the subagent tools; this module never imports plugin code.
9
+ *
10
+ * Entries load lazily (one `readAgentSession` per session on entering detail);
11
+ * a failed load renders as a detail header state instead of throwing.
12
+ */
13
+ import { Container, getKeybindings, Text, truncateToWidth } from "@agent-forge/tui";
14
+ import { theme } from "../theme/theme.js";
15
+ import { DynamicBorder } from "./dynamic-border.js";
16
+ import { keyHint, rawKeyHint } from "./keybinding-hints.js";
17
+ // ---------------------------------------------------------------------------
18
+ // Entry timeline formatting (pure; exported for tests)
19
+ // ---------------------------------------------------------------------------
20
+ const ENTRY_LINE_MAX_CHARS = 160;
21
+ function isRecord(value) {
22
+ return typeof value === "object" && value !== null && !Array.isArray(value);
23
+ }
24
+ /**
25
+ * Message text extraction mirrors SessionManager's session-file scan
26
+ * (`extractTextContent` in core/session-manager.ts): string content verbatim,
27
+ * block content joined over its `type: "text"` blocks.
28
+ */
29
+ function messageText(message) {
30
+ const content = message.content;
31
+ if (typeof content === "string")
32
+ return content;
33
+ if (!Array.isArray(content))
34
+ return undefined;
35
+ return content
36
+ .filter((block) => isRecord(block) && block.type === "text")
37
+ .map((block) => (typeof block.text === "string" ? block.text : ""))
38
+ .join(" ");
39
+ }
40
+ function truncateLine(text) {
41
+ return text.length > ENTRY_LINE_MAX_CHARS ? `${text.slice(0, ENTRY_LINE_MAX_CHARS - 1)}…` : text;
42
+ }
43
+ /**
44
+ * One line per entry: message entries render as `[role] text`; anything else
45
+ * renders as `[<type>] <json>` (or bare `[<type>]` when it has no JSON form),
46
+ * each line capped at ~160 characters.
47
+ */
48
+ export function formatAgentSessionEntries(entries) {
49
+ const lines = [];
50
+ for (const entry of entries) {
51
+ if (isRecord(entry) && entry.type === "message" && isRecord(entry.message)) {
52
+ const role = typeof entry.message.role === "string" ? entry.message.role : "message";
53
+ const text = messageText(entry.message);
54
+ lines.push(truncateLine(text === undefined || text.length === 0 ? `[${role}]` : `[${role}] ${text}`));
55
+ continue;
56
+ }
57
+ const type = isRecord(entry) && typeof entry.type === "string" ? entry.type : "entry";
58
+ const json = JSON.stringify(entry);
59
+ lines.push(truncateLine(json === undefined ? `[${type}]` : `[${type}] ${json}`));
60
+ }
61
+ return Object.freeze(lines);
62
+ }
63
+ // ---------------------------------------------------------------------------
64
+ // Component (list + detail)
65
+ // ---------------------------------------------------------------------------
66
+ /** Renders the visible row window; reads live state from the owning component. */
67
+ class AgentsRowsView {
68
+ view;
69
+ constructor(view) {
70
+ this.view = view;
71
+ }
72
+ invalidate() { }
73
+ render(width) {
74
+ return this.view.renderRows(width);
75
+ }
76
+ }
77
+ export class AgentsViewComponent extends Container {
78
+ onClose;
79
+ config;
80
+ mode = "list";
81
+ rows = [];
82
+ selectedIndex = 0;
83
+ offset = 0;
84
+ /** Detail state: the loaded view plus the list row to restore on return. */
85
+ detail;
86
+ titleText;
87
+ modeHintText;
88
+ helpText;
89
+ constructor(config) {
90
+ super();
91
+ this.config = config;
92
+ this.titleText = new Text(theme.fg("accent", theme.bold(" Subagent Sessions")), 1, 0);
93
+ this.modeHintText = new Text("", 1, 0);
94
+ this.helpText = new Text("", 1, 0);
95
+ this.updateModeTexts();
96
+ this.addChild(new DynamicBorder());
97
+ this.addChild(this.titleText);
98
+ this.addChild(this.modeHintText);
99
+ this.addChild(new AgentsRowsView(this));
100
+ this.addChild(this.helpText);
101
+ this.addChild(new DynamicBorder());
102
+ }
103
+ updateModeTexts() {
104
+ const label = this.mode === "list" ? "this session's subagents (newest first)" : "transcript (read-only)";
105
+ this.modeHintText.setText(theme.fg("dim", ` ${label}`));
106
+ this.helpText.setText(this.buildHelpLine());
107
+ }
108
+ buildHelpLine() {
109
+ const parts = [rawKeyHint("↑↓", "move")];
110
+ if (this.mode === "list") {
111
+ parts.push(rawKeyHint("tab/l", "detail"), keyHint("tui.select.confirm", "open"));
112
+ }
113
+ else {
114
+ parts.push(rawKeyHint("tab/l", "list"));
115
+ }
116
+ parts.push(keyHint("tui.select.cancel", this.mode === "list" ? "close" : "back"));
117
+ return ` ${parts.join(" ")}`;
118
+ }
119
+ formatSummaryRow(summary) {
120
+ const role = summary.agentRole ?? "-";
121
+ const id = summary.sessionId.slice(0, 8);
122
+ const modified = new Date(summary.modifiedAt).toLocaleString();
123
+ return ` ${role} ${id} ${modified} ${summary.bytes}B`;
124
+ }
125
+ buildDetailHeader(view) {
126
+ const identity = [`session ${view.sessionId}`];
127
+ if (view.agentRole !== undefined)
128
+ identity.push(`role ${view.agentRole}`);
129
+ const facts = [];
130
+ if (view.createdAt !== undefined)
131
+ facts.push(`created ${view.createdAt}`);
132
+ facts.push(`${view.bytes}B`, `${view.entryCount} entries`);
133
+ if (view.truncated)
134
+ facts.push(theme.fg("warning", "truncated"));
135
+ return [` ${identity.join(" · ")}`, ` ${facts.join(" · ")}`];
136
+ }
137
+ buildRows() {
138
+ if (this.mode === "detail") {
139
+ if (this.detail === undefined)
140
+ return [" no session selected"];
141
+ const view = this.detail.view;
142
+ if (view === undefined) {
143
+ return [
144
+ ` session ${this.config.sessions[this.detail.listIndex]?.sessionId ?? "?"} — ${theme.fg("error", "transcript unavailable (missing or unreadable)")}`,
145
+ ];
146
+ }
147
+ return [...this.buildDetailHeader(view), ...formatAgentSessionEntries(view.entries)];
148
+ }
149
+ if (this.config.sessions.length === 0)
150
+ return [" no subagent sessions in this session"];
151
+ return this.config.sessions.map((summary) => this.formatSummaryRow(summary));
152
+ }
153
+ clampOffset() {
154
+ const maxRows = Math.max(1, this.config.maxRows);
155
+ if (this.selectedIndex < this.offset)
156
+ this.offset = this.selectedIndex;
157
+ if (this.selectedIndex >= this.offset + maxRows)
158
+ this.offset = this.selectedIndex - maxRows + 1;
159
+ if (this.offset < 0)
160
+ this.offset = 0;
161
+ }
162
+ syncRows() {
163
+ this.rows = this.buildRows();
164
+ if (this.selectedIndex >= this.rows.length)
165
+ this.selectedIndex = Math.max(0, this.rows.length - 1);
166
+ this.clampOffset();
167
+ }
168
+ renderRows(width) {
169
+ this.syncRows();
170
+ const maxRows = Math.max(1, this.config.maxRows);
171
+ const lines = [];
172
+ for (let index = this.offset; index < Math.min(this.rows.length, this.offset + maxRows); index++) {
173
+ const row = this.rows[index];
174
+ if (row === undefined)
175
+ continue;
176
+ const selected = index === this.selectedIndex;
177
+ const prefix = selected ? theme.fg("accent", "→ ") : " ";
178
+ lines.push(truncateToWidth(`${prefix}${row}`, width));
179
+ }
180
+ const hiddenAbove = this.offset;
181
+ const hiddenBelow = this.rows.length - this.offset - maxRows;
182
+ const hidden = [];
183
+ if (hiddenAbove > 0)
184
+ hidden.push(`↑${hiddenAbove}`);
185
+ if (hiddenBelow > 0)
186
+ hidden.push(`↓${hiddenBelow}`);
187
+ // Fixed output height (exactly maxRows content rows plus this last
188
+ // row): the overlay's grid footprint must not shrink with mode
189
+ // switches or row counts. A height change moves the chat/editor
190
+ // layout boundary between frames, and every boundary shift now
191
+ // forces the renderer's geometry guard into a full redraw (see
192
+ // tui-alt-screen.ts doRender); constant-height output keeps pure
193
+ // mode switches on the cheap incremental diff path. Padding rows
194
+ // carry a real space so the indicator row position stays stable.
195
+ while (lines.length < maxRows)
196
+ lines.push(truncateToWidth(" ", width));
197
+ lines.push(truncateToWidth(hidden.length > 0 ? theme.fg("dim", ` ${hidden.join(" ")}`) : " ", width));
198
+ return lines;
199
+ }
200
+ move(delta) {
201
+ if (this.rows.length === 0)
202
+ return;
203
+ this.selectedIndex = Math.min(this.rows.length - 1, Math.max(0, this.selectedIndex + delta));
204
+ this.clampOffset();
205
+ }
206
+ enterDetail() {
207
+ const summary = this.config.sessions[this.selectedIndex];
208
+ if (summary === undefined)
209
+ return;
210
+ this.detail = { view: this.config.loadEntries(summary.sessionId), listIndex: this.selectedIndex };
211
+ this.mode = "detail";
212
+ this.selectedIndex = 0;
213
+ this.offset = 0;
214
+ this.updateModeTexts();
215
+ this.config.requestRender?.();
216
+ }
217
+ exitDetail() {
218
+ const restore = this.detail?.listIndex ?? 0;
219
+ this.detail = undefined;
220
+ this.mode = "list";
221
+ this.selectedIndex = restore;
222
+ this.clampOffset();
223
+ this.updateModeTexts();
224
+ this.config.requestRender?.();
225
+ }
226
+ handleInput(keyData) {
227
+ const kb = getKeybindings();
228
+ if (kb.matches(keyData, "tui.select.cancel")) {
229
+ // Layered cancel (tree-list style): detail escapes to the list first;
230
+ // only a list-mode cancel closes the overlay.
231
+ if (this.mode === "detail")
232
+ this.exitDetail();
233
+ else
234
+ this.onClose?.();
235
+ }
236
+ else if (kb.matches(keyData, "tui.input.tab") || keyData === "l") {
237
+ if (this.mode === "list")
238
+ this.enterDetail();
239
+ else
240
+ this.exitDetail();
241
+ }
242
+ else if (kb.matches(keyData, "tui.select.confirm")) {
243
+ if (this.mode === "list")
244
+ this.enterDetail();
245
+ }
246
+ else if (kb.matches(keyData, "tui.select.up") || keyData === "k") {
247
+ this.move(-1);
248
+ }
249
+ else if (kb.matches(keyData, "tui.select.down") || keyData === "j") {
250
+ this.move(1);
251
+ }
252
+ else if (kb.matches(keyData, "tui.select.pageUp")) {
253
+ this.move(-Math.max(1, this.config.maxRows));
254
+ }
255
+ else if (kb.matches(keyData, "tui.select.pageDown")) {
256
+ this.move(Math.max(1, this.config.maxRows));
257
+ }
258
+ }
259
+ }
260
+ //# sourceMappingURL=agents-view.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agents-view.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/agents-view.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Read-only subagent session browser (D-060 §3 /agents overlay).\n *\n * Lists this session's own subagent transcripts from the on-disk store\n * (`sessions/_agents/`, same summary source as the subagent_list model tool)\n * and shows one session's bounded timeline on demand. Everything here is\n * read-only: no steer, no cancel, no resume — operations stay with the\n * conversation and the subagent tools; this module never imports plugin code.\n *\n * Entries load lazily (one `readAgentSession` per session on entering detail);\n * a failed load renders as a detail header state instead of throwing.\n */\n\nimport { type Component, Container, getKeybindings, Text, truncateToWidth } from \"@agent-forge/tui\";\nimport type { AgentSessionReadV1, AgentSessionSummaryV1 } from \"../../../core/agent-session-store.ts\";\nimport { theme } from \"../theme/theme.ts\";\nimport { DynamicBorder } from \"./dynamic-border.ts\";\nimport { keyHint, rawKeyHint } from \"./keybinding-hints.ts\";\n\n// ---------------------------------------------------------------------------\n// Entry timeline formatting (pure; exported for tests)\n// ---------------------------------------------------------------------------\n\nconst ENTRY_LINE_MAX_CHARS = 160;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * Message text extraction mirrors SessionManager's session-file scan\n * (`extractTextContent` in core/session-manager.ts): string content verbatim,\n * block content joined over its `type: \"text\"` blocks.\n */\nfunction messageText(message: Record<string, unknown>): string | undefined {\n\tconst content = message.content;\n\tif (typeof content === \"string\") return content;\n\tif (!Array.isArray(content)) return undefined;\n\treturn content\n\t\t.filter((block): block is Record<string, unknown> => isRecord(block) && block.type === \"text\")\n\t\t.map((block) => (typeof block.text === \"string\" ? block.text : \"\"))\n\t\t.join(\" \");\n}\n\nfunction truncateLine(text: string): string {\n\treturn text.length > ENTRY_LINE_MAX_CHARS ? `${text.slice(0, ENTRY_LINE_MAX_CHARS - 1)}…` : text;\n}\n\n/**\n * One line per entry: message entries render as `[role] text`; anything else\n * renders as `[<type>] <json>` (or bare `[<type>]` when it has no JSON form),\n * each line capped at ~160 characters.\n */\nexport function formatAgentSessionEntries(entries: readonly unknown[]): readonly string[] {\n\tconst lines: string[] = [];\n\tfor (const entry of entries) {\n\t\tif (isRecord(entry) && entry.type === \"message\" && isRecord(entry.message)) {\n\t\t\tconst role = typeof entry.message.role === \"string\" ? entry.message.role : \"message\";\n\t\t\tconst text = messageText(entry.message);\n\t\t\tlines.push(truncateLine(text === undefined || text.length === 0 ? `[${role}]` : `[${role}] ${text}`));\n\t\t\tcontinue;\n\t\t}\n\t\tconst type = isRecord(entry) && typeof entry.type === \"string\" ? entry.type : \"entry\";\n\t\tconst json = JSON.stringify(entry);\n\t\tlines.push(truncateLine(json === undefined ? `[${type}]` : `[${type}] ${json}`));\n\t}\n\treturn Object.freeze(lines);\n}\n\n// ---------------------------------------------------------------------------\n// Component (list + detail)\n// ---------------------------------------------------------------------------\n\n/** Renders the visible row window; reads live state from the owning component. */\nclass AgentsRowsView implements Component {\n\tprivate view: AgentsViewComponent;\n\n\tconstructor(view: AgentsViewComponent) {\n\t\tthis.view = view;\n\t}\n\n\tinvalidate(): void {}\n\n\trender(width: number): string[] {\n\t\treturn this.view.renderRows(width);\n\t}\n}\n\nexport interface AgentsViewConfig {\n\t/** Summaries of this session's subagent sessions (newest first, parent-scoped). */\n\treadonly sessions: readonly AgentSessionSummaryV1[];\n\t/** Bounded transcript loader (lazy: called once per session when entering detail). */\n\treadonly loadEntries: (sessionId: string) => AgentSessionReadV1 | undefined;\n\t/** Maximum visible rows before the list scrolls. */\n\treadonly maxRows: number;\n\t/** Request a TUI re-render. */\n\trequestRender?: () => void;\n}\n\nexport class AgentsViewComponent extends Container {\n\tpublic onClose?: () => void;\n\n\tprivate config: AgentsViewConfig;\n\tprivate mode: \"list\" | \"detail\" = \"list\";\n\tprivate rows: string[] = [];\n\tprivate selectedIndex = 0;\n\tprivate offset = 0;\n\t/** Detail state: the loaded view plus the list row to restore on return. */\n\tprivate detail: { view: AgentSessionReadV1 | undefined; listIndex: number } | undefined;\n\tprivate titleText: Text;\n\tprivate modeHintText: Text;\n\tprivate helpText: Text;\n\n\tconstructor(config: AgentsViewConfig) {\n\t\tsuper();\n\n\t\tthis.config = config;\n\t\tthis.titleText = new Text(theme.fg(\"accent\", theme.bold(\" Subagent Sessions\")), 1, 0);\n\t\tthis.modeHintText = new Text(\"\", 1, 0);\n\t\tthis.helpText = new Text(\"\", 1, 0);\n\t\tthis.updateModeTexts();\n\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(this.titleText);\n\t\tthis.addChild(this.modeHintText);\n\t\tthis.addChild(new AgentsRowsView(this));\n\t\tthis.addChild(this.helpText);\n\t\tthis.addChild(new DynamicBorder());\n\t}\n\n\tprivate updateModeTexts(): void {\n\t\tconst label = this.mode === \"list\" ? \"this session's subagents (newest first)\" : \"transcript (read-only)\";\n\t\tthis.modeHintText.setText(theme.fg(\"dim\", ` ${label}`));\n\t\tthis.helpText.setText(this.buildHelpLine());\n\t}\n\n\tprivate buildHelpLine(): string {\n\t\tconst parts = [rawKeyHint(\"↑↓\", \"move\")];\n\t\tif (this.mode === \"list\") {\n\t\t\tparts.push(rawKeyHint(\"tab/l\", \"detail\"), keyHint(\"tui.select.confirm\", \"open\"));\n\t\t} else {\n\t\t\tparts.push(rawKeyHint(\"tab/l\", \"list\"));\n\t\t}\n\t\tparts.push(keyHint(\"tui.select.cancel\", this.mode === \"list\" ? \"close\" : \"back\"));\n\t\treturn ` ${parts.join(\" \")}`;\n\t}\n\n\tprivate formatSummaryRow(summary: AgentSessionSummaryV1): string {\n\t\tconst role = summary.agentRole ?? \"-\";\n\t\tconst id = summary.sessionId.slice(0, 8);\n\t\tconst modified = new Date(summary.modifiedAt).toLocaleString();\n\t\treturn ` ${role} ${id} ${modified} ${summary.bytes}B`;\n\t}\n\n\tprivate buildDetailHeader(view: AgentSessionReadV1): string[] {\n\t\tconst identity = [`session ${view.sessionId}`];\n\t\tif (view.agentRole !== undefined) identity.push(`role ${view.agentRole}`);\n\t\tconst facts: string[] = [];\n\t\tif (view.createdAt !== undefined) facts.push(`created ${view.createdAt}`);\n\t\tfacts.push(`${view.bytes}B`, `${view.entryCount} entries`);\n\t\tif (view.truncated) facts.push(theme.fg(\"warning\", \"truncated\"));\n\t\treturn [` ${identity.join(\" · \")}`, ` ${facts.join(\" · \")}`];\n\t}\n\n\tprivate buildRows(): string[] {\n\t\tif (this.mode === \"detail\") {\n\t\t\tif (this.detail === undefined) return [\" no session selected\"];\n\t\t\tconst view = this.detail.view;\n\t\t\tif (view === undefined) {\n\t\t\t\treturn [\n\t\t\t\t\t` session ${this.config.sessions[this.detail.listIndex]?.sessionId ?? \"?\"} — ${theme.fg(\n\t\t\t\t\t\t\"error\",\n\t\t\t\t\t\t\"transcript unavailable (missing or unreadable)\",\n\t\t\t\t\t)}`,\n\t\t\t\t];\n\t\t\t}\n\t\t\treturn [...this.buildDetailHeader(view), ...formatAgentSessionEntries(view.entries)];\n\t\t}\n\t\tif (this.config.sessions.length === 0) return [\" no subagent sessions in this session\"];\n\t\treturn this.config.sessions.map((summary) => this.formatSummaryRow(summary));\n\t}\n\n\tprivate clampOffset(): void {\n\t\tconst maxRows = Math.max(1, this.config.maxRows);\n\t\tif (this.selectedIndex < this.offset) this.offset = this.selectedIndex;\n\t\tif (this.selectedIndex >= this.offset + maxRows) this.offset = this.selectedIndex - maxRows + 1;\n\t\tif (this.offset < 0) this.offset = 0;\n\t}\n\n\tprivate syncRows(): void {\n\t\tthis.rows = this.buildRows();\n\t\tif (this.selectedIndex >= this.rows.length) this.selectedIndex = Math.max(0, this.rows.length - 1);\n\t\tthis.clampOffset();\n\t}\n\n\trenderRows(width: number): string[] {\n\t\tthis.syncRows();\n\t\tconst maxRows = Math.max(1, this.config.maxRows);\n\t\tconst lines: string[] = [];\n\t\tfor (let index = this.offset; index < Math.min(this.rows.length, this.offset + maxRows); index++) {\n\t\t\tconst row = this.rows[index];\n\t\t\tif (row === undefined) continue;\n\t\t\tconst selected = index === this.selectedIndex;\n\t\t\tconst prefix = selected ? theme.fg(\"accent\", \"→ \") : \" \";\n\t\t\tlines.push(truncateToWidth(`${prefix}${row}`, width));\n\t\t}\n\t\tconst hiddenAbove = this.offset;\n\t\tconst hiddenBelow = this.rows.length - this.offset - maxRows;\n\t\tconst hidden: string[] = [];\n\t\tif (hiddenAbove > 0) hidden.push(`↑${hiddenAbove}`);\n\t\tif (hiddenBelow > 0) hidden.push(`↓${hiddenBelow}`);\n\t\t// Fixed output height (exactly maxRows content rows plus this last\n\t\t// row): the overlay's grid footprint must not shrink with mode\n\t\t// switches or row counts. A height change moves the chat/editor\n\t\t// layout boundary between frames, and every boundary shift now\n\t\t// forces the renderer's geometry guard into a full redraw (see\n\t\t// tui-alt-screen.ts doRender); constant-height output keeps pure\n\t\t// mode switches on the cheap incremental diff path. Padding rows\n\t\t// carry a real space so the indicator row position stays stable.\n\t\twhile (lines.length < maxRows) lines.push(truncateToWidth(\" \", width));\n\t\tlines.push(truncateToWidth(hidden.length > 0 ? theme.fg(\"dim\", ` ${hidden.join(\" \")}`) : \" \", width));\n\t\treturn lines;\n\t}\n\n\tprivate move(delta: number): void {\n\t\tif (this.rows.length === 0) return;\n\t\tthis.selectedIndex = Math.min(this.rows.length - 1, Math.max(0, this.selectedIndex + delta));\n\t\tthis.clampOffset();\n\t}\n\n\tprivate enterDetail(): void {\n\t\tconst summary = this.config.sessions[this.selectedIndex];\n\t\tif (summary === undefined) return;\n\t\tthis.detail = { view: this.config.loadEntries(summary.sessionId), listIndex: this.selectedIndex };\n\t\tthis.mode = \"detail\";\n\t\tthis.selectedIndex = 0;\n\t\tthis.offset = 0;\n\t\tthis.updateModeTexts();\n\t\tthis.config.requestRender?.();\n\t}\n\n\tprivate exitDetail(): void {\n\t\tconst restore = this.detail?.listIndex ?? 0;\n\t\tthis.detail = undefined;\n\t\tthis.mode = \"list\";\n\t\tthis.selectedIndex = restore;\n\t\tthis.clampOffset();\n\t\tthis.updateModeTexts();\n\t\tthis.config.requestRender?.();\n\t}\n\n\thandleInput(keyData: string): void {\n\t\tconst kb = getKeybindings();\n\t\tif (kb.matches(keyData, \"tui.select.cancel\")) {\n\t\t\t// Layered cancel (tree-list style): detail escapes to the list first;\n\t\t\t// only a list-mode cancel closes the overlay.\n\t\t\tif (this.mode === \"detail\") this.exitDetail();\n\t\t\telse this.onClose?.();\n\t\t} else if (kb.matches(keyData, \"tui.input.tab\") || keyData === \"l\") {\n\t\t\tif (this.mode === \"list\") this.enterDetail();\n\t\t\telse this.exitDetail();\n\t\t} else if (kb.matches(keyData, \"tui.select.confirm\")) {\n\t\t\tif (this.mode === \"list\") this.enterDetail();\n\t\t} else if (kb.matches(keyData, \"tui.select.up\") || keyData === \"k\") {\n\t\t\tthis.move(-1);\n\t\t} else if (kb.matches(keyData, \"tui.select.down\") || keyData === \"j\") {\n\t\t\tthis.move(1);\n\t\t} else if (kb.matches(keyData, \"tui.select.pageUp\")) {\n\t\t\tthis.move(-Math.max(1, this.config.maxRows));\n\t\t} else if (kb.matches(keyData, \"tui.select.pageDown\")) {\n\t\t\tthis.move(Math.max(1, this.config.maxRows));\n\t\t}\n\t}\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"custom-editor.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/custom-editor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,aAAa,EAAE,KAAK,WAAW,EAAE,KAAK,GAAG,EAAE,MAAM,kBAAkB,CAAC;AAC1F,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAEtF;;GAEG;AACH,qBAAa,YAAa,SAAQ,MAAM;IACvC,OAAO,CAAC,WAAW,CAAqB;IACjC,cAAc,EAAE,GAAG,CAAC,aAAa,EAAE,MAAM,IAAI,CAAC,CAAa;IAG3D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IACjC,YAAY,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,kBAAkB,EAAE,OAAO,CAAC,EAAE,aAAa,EAGjG;IAED;;OAEG;IACH,QAAQ,CAAC,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI,CAEzD;IAED,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAsD9B;CACD","sourcesContent":["import { Editor, type EditorOptions, type EditorTheme, type TUI } from \"@agent-forge/tui\";\nimport type { AppKeybinding, KeybindingsManager } from \"../../../core/keybindings.ts\";\n\n/**\n * Custom editor that handles app-level keybindings for coding-agent.\n */\nexport class CustomEditor extends Editor {\n\tprivate keybindings: KeybindingsManager;\n\tpublic actionHandlers: Map<AppKeybinding, () => void> = new Map();\n\n\t// Special handlers that can be dynamically replaced\n\tpublic onEscape?: () => void;\n\tpublic onCtrlD?: () => void;\n\tpublic onPasteImage?: () => void;\n\tconstructor(tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager, options?: EditorOptions) {\n\t\tsuper(tui, theme, options);\n\t\tthis.keybindings = keybindings;\n\t}\n\n\t/**\n\t * Register a handler for an app action.\n\t */\n\tonAction(action: AppKeybinding, handler: () => void): void {\n\t\tthis.actionHandlers.set(action, handler);\n\t}\n\n\thandleInput(data: string): void {\n\t\t// Check for clipboard paste keybinding\n\t\tif (this.keybindings.matches(data, \"app.clipboard.pasteImage\")) {\n\t\t\tthis.onPasteImage?.();\n\t\t\treturn;\n\t\t}\n\n\t\t// Check app keybindings first\n\n\t\t// Escape/interrupt - only if autocomplete is NOT active\n\t\tif (this.keybindings.matches(data, \"app.interrupt\")) {\n\t\t\tif (!this.isShowingAutocomplete()) {\n\t\t\t\t// Use dynamic onEscape if set, otherwise registered handler\n\t\t\t\tconst handler = this.onEscape ?? this.actionHandlers.get(\"app.interrupt\");\n\t\t\t\tif (handler) {\n\t\t\t\t\thandler();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Let parent handle escape for autocomplete cancellation\n\t\t\tsuper.handleInput(data);\n\t\t\treturn;\n\t\t}\n\n\t\t// Exit (Ctrl+D) - only when editor is empty\n\t\tif (this.keybindings.matches(data, \"app.exit\")) {\n\t\t\tif (this.getText().length === 0) {\n\t\t\t\tconst handler = this.onCtrlD ?? this.actionHandlers.get(\"app.exit\");\n\t\t\t\tif (handler) handler();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Fall through to editor handling for delete-char-forward when not empty\n\t\t}\n\n\t\t// Explicit history bindings take precedence over app actions while the editor is focused.\n\t\t// This lets users bind Ctrl+P even though it cycles models by default.\n\t\tif (\n\t\t\tthis.keybindings.matches(data, \"tui.editor.historyPrevious\") ||\n\t\t\tthis.keybindings.matches(data, \"tui.editor.historyNext\")\n\t\t) {\n\t\t\tsuper.handleInput(data);\n\t\t\treturn;\n\t\t}\n\n\t\t// Check all other app actions\n\t\tfor (const [action, handler] of this.actionHandlers) {\n\t\t\tif (action !== \"app.interrupt\" && action !== \"app.exit\" && this.keybindings.matches(data, action)) {\n\t\t\t\thandler();\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\t// Pass to parent for editor handling\n\t\tsuper.handleInput(data);\n\t}\n}\n"]}
1
+ {"version":3,"file":"custom-editor.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/custom-editor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,aAAa,EAAE,KAAK,WAAW,EAAE,KAAK,GAAG,EAAE,MAAM,kBAAkB,CAAC;AAC1F,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAEtF;;GAEG;AACH,qBAAa,YAAa,SAAQ,MAAM;IACvC,OAAO,CAAC,WAAW,CAAqB;IACjC,cAAc,EAAE,GAAG,CAAC,aAAa,EAAE,MAAM,IAAI,CAAC,CAAa;IAG3D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IACjC,YAAY,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,kBAAkB,EAAE,OAAO,CAAC,EAAE,aAAa,EAGjG;IAED;;OAEG;IACH,QAAQ,CAAC,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI,CAEzD;IAED,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAsD9B;CACD","sourcesContent":["import { Editor, type EditorOptions, type EditorTheme, type TUI } from \"@agent-forge/tui\";\nimport type { AppKeybinding, KeybindingsManager } from \"../../../core/keybindings.ts\";\n\n/**\n * Custom editor that handles app-level keybindings for coding-agent.\n */\nexport class CustomEditor extends Editor {\n\tprivate keybindings: KeybindingsManager;\n\tpublic actionHandlers: Map<AppKeybinding, () => void> = new Map();\n\n\t// Special handlers that can be dynamically replaced\n\tpublic onEscape?: () => void;\n\tpublic onCtrlD?: () => void;\n\tpublic onPasteImage?: () => void;\n\tconstructor(tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager, options?: EditorOptions) {\n\t\tsuper(tui, theme, options);\n\t\tthis.keybindings = keybindings;\n\t}\n\n\t/**\n\t * Register a handler for an app action.\n\t */\n\tonAction(action: AppKeybinding, handler: () => void): void {\n\t\tthis.actionHandlers.set(action, handler);\n\t}\n\n\thandleInput(data: string): void {\n\t\t// Check for clipboard paste keybinding\n\t\tif (this.keybindings.matches(data, \"app.clipboard.pasteImage\")) {\n\t\t\tthis.onPasteImage?.();\n\t\t\treturn;\n\t\t}\n\n\t\t// Check app keybindings first\n\n\t\t// Escape/interrupt - only if autocomplete is NOT active\n\t\tif (this.keybindings.matches(data, \"app.interrupt\")) {\n\t\t\tif (!this.isShowingAutocomplete()) {\n\t\t\t\t// Use dynamic onEscape if set, otherwise registered handler\n\t\t\t\tconst handler = this.onEscape ?? this.actionHandlers.get(\"app.interrupt\");\n\t\t\t\tif (handler) {\n\t\t\t\t\thandler();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Let parent handle escape for autocomplete cancellation\n\t\t\tsuper.handleInput(data);\n\t\t\treturn;\n\t\t}\n\n\t\t// Exit (Ctrl+D) - only when editor is empty\n\t\tif (this.keybindings.matches(data, \"app.exit\")) {\n\t\t\tif (this.getText().length === 0) {\n\t\t\t\tconst handler = this.onCtrlD ?? this.actionHandlers.get(\"app.exit\");\n\t\t\t\tif (handler) handler();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Fall through to editor handling for delete-char-forward when not empty\n\t\t}\n\n\t\t// Explicit history bindings take precedence over app actions while the editor is focused\n\t\t// (e.g. a user-configured model-cycle key must not shadow editor history keys).\n\t\tif (\n\t\t\tthis.keybindings.matches(data, \"tui.editor.historyPrevious\") ||\n\t\t\tthis.keybindings.matches(data, \"tui.editor.historyNext\")\n\t\t) {\n\t\t\tsuper.handleInput(data);\n\t\t\treturn;\n\t\t}\n\n\t\t// Check all other app actions\n\t\tfor (const [action, handler] of this.actionHandlers) {\n\t\t\tif (action !== \"app.interrupt\" && action !== \"app.exit\" && this.keybindings.matches(data, action)) {\n\t\t\t\thandler();\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\t// Pass to parent for editor handling\n\t\tsuper.handleInput(data);\n\t}\n}\n"]}
@@ -50,8 +50,8 @@ export class CustomEditor extends Editor {
50
50
  }
51
51
  // Fall through to editor handling for delete-char-forward when not empty
52
52
  }
53
- // Explicit history bindings take precedence over app actions while the editor is focused.
54
- // This lets users bind Ctrl+P even though it cycles models by default.
53
+ // Explicit history bindings take precedence over app actions while the editor is focused
54
+ // (e.g. a user-configured model-cycle key must not shadow editor history keys).
55
55
  if (this.keybindings.matches(data, "tui.editor.historyPrevious") ||
56
56
  this.keybindings.matches(data, "tui.editor.historyNext")) {
57
57
  super.handleInput(data);
@@ -1 +1 @@
1
- {"version":3,"file":"custom-editor.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/custom-editor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAkD,MAAM,kBAAkB,CAAC;AAG1F;;GAEG;AACH,MAAM,OAAO,YAAa,SAAQ,MAAM;IAC/B,WAAW,CAAqB;IACjC,cAAc,GAAmC,IAAI,GAAG,EAAE,CAAC;IAElE,oDAAoD;IAC7C,QAAQ,CAAc;IACtB,OAAO,CAAc;IACrB,YAAY,CAAc;IACjC,YAAY,GAAQ,EAAE,KAAkB,EAAE,WAA+B,EAAE,OAAuB,EAAE;QACnG,KAAK,CAAC,GAAG,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;QAC3B,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;IAAA,CAC/B;IAED;;OAEG;IACH,QAAQ,CAAC,MAAqB,EAAE,OAAmB,EAAQ;QAC1D,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAA,CACzC;IAED,WAAW,CAAC,IAAY,EAAQ;QAC/B,uCAAuC;QACvC,IAAI,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,0BAA0B,CAAC,EAAE,CAAC;YAChE,IAAI,CAAC,YAAY,EAAE,EAAE,CAAC;YACtB,OAAO;QACR,CAAC;QAED,8BAA8B;QAE9B,wDAAwD;QACxD,IAAI,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,eAAe,CAAC,EAAE,CAAC;YACrD,IAAI,CAAC,IAAI,CAAC,qBAAqB,EAAE,EAAE,CAAC;gBACnC,4DAA4D;gBAC5D,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;gBAC1E,IAAI,OAAO,EAAE,CAAC;oBACb,OAAO,EAAE,CAAC;oBACV,OAAO;gBACR,CAAC;YACF,CAAC;YACD,yDAAyD;YACzD,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;YACxB,OAAO;QACR,CAAC;QAED,4CAA4C;QAC5C,IAAI,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,UAAU,CAAC,EAAE,CAAC;YAChD,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACjC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;gBACpE,IAAI,OAAO;oBAAE,OAAO,EAAE,CAAC;gBACvB,OAAO;YACR,CAAC;YACD,yEAAyE;QAC1E,CAAC;QAED,0FAA0F;QAC1F,uEAAuE;QACvE,IACC,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,4BAA4B,CAAC;YAC5D,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,wBAAwB,CAAC,EACvD,CAAC;YACF,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;YACxB,OAAO;QACR,CAAC;QAED,8BAA8B;QAC9B,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACrD,IAAI,MAAM,KAAK,eAAe,IAAI,MAAM,KAAK,UAAU,IAAI,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC;gBACnG,OAAO,EAAE,CAAC;gBACV,OAAO;YACR,CAAC;QACF,CAAC;QAED,qCAAqC;QACrC,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;IAAA,CACxB;CACD","sourcesContent":["import { Editor, type EditorOptions, type EditorTheme, type TUI } from \"@agent-forge/tui\";\nimport type { AppKeybinding, KeybindingsManager } from \"../../../core/keybindings.ts\";\n\n/**\n * Custom editor that handles app-level keybindings for coding-agent.\n */\nexport class CustomEditor extends Editor {\n\tprivate keybindings: KeybindingsManager;\n\tpublic actionHandlers: Map<AppKeybinding, () => void> = new Map();\n\n\t// Special handlers that can be dynamically replaced\n\tpublic onEscape?: () => void;\n\tpublic onCtrlD?: () => void;\n\tpublic onPasteImage?: () => void;\n\tconstructor(tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager, options?: EditorOptions) {\n\t\tsuper(tui, theme, options);\n\t\tthis.keybindings = keybindings;\n\t}\n\n\t/**\n\t * Register a handler for an app action.\n\t */\n\tonAction(action: AppKeybinding, handler: () => void): void {\n\t\tthis.actionHandlers.set(action, handler);\n\t}\n\n\thandleInput(data: string): void {\n\t\t// Check for clipboard paste keybinding\n\t\tif (this.keybindings.matches(data, \"app.clipboard.pasteImage\")) {\n\t\t\tthis.onPasteImage?.();\n\t\t\treturn;\n\t\t}\n\n\t\t// Check app keybindings first\n\n\t\t// Escape/interrupt - only if autocomplete is NOT active\n\t\tif (this.keybindings.matches(data, \"app.interrupt\")) {\n\t\t\tif (!this.isShowingAutocomplete()) {\n\t\t\t\t// Use dynamic onEscape if set, otherwise registered handler\n\t\t\t\tconst handler = this.onEscape ?? this.actionHandlers.get(\"app.interrupt\");\n\t\t\t\tif (handler) {\n\t\t\t\t\thandler();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Let parent handle escape for autocomplete cancellation\n\t\t\tsuper.handleInput(data);\n\t\t\treturn;\n\t\t}\n\n\t\t// Exit (Ctrl+D) - only when editor is empty\n\t\tif (this.keybindings.matches(data, \"app.exit\")) {\n\t\t\tif (this.getText().length === 0) {\n\t\t\t\tconst handler = this.onCtrlD ?? this.actionHandlers.get(\"app.exit\");\n\t\t\t\tif (handler) handler();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Fall through to editor handling for delete-char-forward when not empty\n\t\t}\n\n\t\t// Explicit history bindings take precedence over app actions while the editor is focused.\n\t\t// This lets users bind Ctrl+P even though it cycles models by default.\n\t\tif (\n\t\t\tthis.keybindings.matches(data, \"tui.editor.historyPrevious\") ||\n\t\t\tthis.keybindings.matches(data, \"tui.editor.historyNext\")\n\t\t) {\n\t\t\tsuper.handleInput(data);\n\t\t\treturn;\n\t\t}\n\n\t\t// Check all other app actions\n\t\tfor (const [action, handler] of this.actionHandlers) {\n\t\t\tif (action !== \"app.interrupt\" && action !== \"app.exit\" && this.keybindings.matches(data, action)) {\n\t\t\t\thandler();\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\t// Pass to parent for editor handling\n\t\tsuper.handleInput(data);\n\t}\n}\n"]}
1
+ {"version":3,"file":"custom-editor.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/custom-editor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAkD,MAAM,kBAAkB,CAAC;AAG1F;;GAEG;AACH,MAAM,OAAO,YAAa,SAAQ,MAAM;IAC/B,WAAW,CAAqB;IACjC,cAAc,GAAmC,IAAI,GAAG,EAAE,CAAC;IAElE,oDAAoD;IAC7C,QAAQ,CAAc;IACtB,OAAO,CAAc;IACrB,YAAY,CAAc;IACjC,YAAY,GAAQ,EAAE,KAAkB,EAAE,WAA+B,EAAE,OAAuB,EAAE;QACnG,KAAK,CAAC,GAAG,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;QAC3B,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;IAAA,CAC/B;IAED;;OAEG;IACH,QAAQ,CAAC,MAAqB,EAAE,OAAmB,EAAQ;QAC1D,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAA,CACzC;IAED,WAAW,CAAC,IAAY,EAAQ;QAC/B,uCAAuC;QACvC,IAAI,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,0BAA0B,CAAC,EAAE,CAAC;YAChE,IAAI,CAAC,YAAY,EAAE,EAAE,CAAC;YACtB,OAAO;QACR,CAAC;QAED,8BAA8B;QAE9B,wDAAwD;QACxD,IAAI,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,eAAe,CAAC,EAAE,CAAC;YACrD,IAAI,CAAC,IAAI,CAAC,qBAAqB,EAAE,EAAE,CAAC;gBACnC,4DAA4D;gBAC5D,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;gBAC1E,IAAI,OAAO,EAAE,CAAC;oBACb,OAAO,EAAE,CAAC;oBACV,OAAO;gBACR,CAAC;YACF,CAAC;YACD,yDAAyD;YACzD,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;YACxB,OAAO;QACR,CAAC;QAED,4CAA4C;QAC5C,IAAI,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,UAAU,CAAC,EAAE,CAAC;YAChD,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACjC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;gBACpE,IAAI,OAAO;oBAAE,OAAO,EAAE,CAAC;gBACvB,OAAO;YACR,CAAC;YACD,yEAAyE;QAC1E,CAAC;QAED,yFAAyF;QACzF,gFAAgF;QAChF,IACC,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,4BAA4B,CAAC;YAC5D,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,wBAAwB,CAAC,EACvD,CAAC;YACF,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;YACxB,OAAO;QACR,CAAC;QAED,8BAA8B;QAC9B,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACrD,IAAI,MAAM,KAAK,eAAe,IAAI,MAAM,KAAK,UAAU,IAAI,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC;gBACnG,OAAO,EAAE,CAAC;gBACV,OAAO;YACR,CAAC;QACF,CAAC;QAED,qCAAqC;QACrC,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;IAAA,CACxB;CACD","sourcesContent":["import { Editor, type EditorOptions, type EditorTheme, type TUI } from \"@agent-forge/tui\";\nimport type { AppKeybinding, KeybindingsManager } from \"../../../core/keybindings.ts\";\n\n/**\n * Custom editor that handles app-level keybindings for coding-agent.\n */\nexport class CustomEditor extends Editor {\n\tprivate keybindings: KeybindingsManager;\n\tpublic actionHandlers: Map<AppKeybinding, () => void> = new Map();\n\n\t// Special handlers that can be dynamically replaced\n\tpublic onEscape?: () => void;\n\tpublic onCtrlD?: () => void;\n\tpublic onPasteImage?: () => void;\n\tconstructor(tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager, options?: EditorOptions) {\n\t\tsuper(tui, theme, options);\n\t\tthis.keybindings = keybindings;\n\t}\n\n\t/**\n\t * Register a handler for an app action.\n\t */\n\tonAction(action: AppKeybinding, handler: () => void): void {\n\t\tthis.actionHandlers.set(action, handler);\n\t}\n\n\thandleInput(data: string): void {\n\t\t// Check for clipboard paste keybinding\n\t\tif (this.keybindings.matches(data, \"app.clipboard.pasteImage\")) {\n\t\t\tthis.onPasteImage?.();\n\t\t\treturn;\n\t\t}\n\n\t\t// Check app keybindings first\n\n\t\t// Escape/interrupt - only if autocomplete is NOT active\n\t\tif (this.keybindings.matches(data, \"app.interrupt\")) {\n\t\t\tif (!this.isShowingAutocomplete()) {\n\t\t\t\t// Use dynamic onEscape if set, otherwise registered handler\n\t\t\t\tconst handler = this.onEscape ?? this.actionHandlers.get(\"app.interrupt\");\n\t\t\t\tif (handler) {\n\t\t\t\t\thandler();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Let parent handle escape for autocomplete cancellation\n\t\t\tsuper.handleInput(data);\n\t\t\treturn;\n\t\t}\n\n\t\t// Exit (Ctrl+D) - only when editor is empty\n\t\tif (this.keybindings.matches(data, \"app.exit\")) {\n\t\t\tif (this.getText().length === 0) {\n\t\t\t\tconst handler = this.onCtrlD ?? this.actionHandlers.get(\"app.exit\");\n\t\t\t\tif (handler) handler();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Fall through to editor handling for delete-char-forward when not empty\n\t\t}\n\n\t\t// Explicit history bindings take precedence over app actions while the editor is focused\n\t\t// (e.g. a user-configured model-cycle key must not shadow editor history keys).\n\t\tif (\n\t\t\tthis.keybindings.matches(data, \"tui.editor.historyPrevious\") ||\n\t\t\tthis.keybindings.matches(data, \"tui.editor.historyNext\")\n\t\t) {\n\t\t\tsuper.handleInput(data);\n\t\t\treturn;\n\t\t}\n\n\t\t// Check all other app actions\n\t\tfor (const [action, handler] of this.actionHandlers) {\n\t\t\tif (action !== \"app.interrupt\" && action !== \"app.exit\" && this.keybindings.matches(data, action)) {\n\t\t\t\thandler();\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\t// Pass to parent for editor handling\n\t\tsuper.handleInput(data);\n\t}\n}\n"]}
@@ -36,6 +36,13 @@ export declare class FooterComponent implements Component {
36
36
  * applies; until then the previous frame's values are reused.
37
37
  */
38
38
  private getSessionStats;
39
+ /**
40
+ * Whether the latest `provider.failover.state` snapshot reports an open
41
+ * breaker. Drives the interactive-mode per-second re-render ticker (设计
42
+ * 供应商能力槽位与主备切换 §4.7 Phase 3); cheap because it reuses the
43
+ * cached session stats.
44
+ */
45
+ isProviderFailoverOpen(): boolean;
39
46
  render(width: number): string[];
40
47
  }
41
48
  //# sourceMappingURL=footer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"footer.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,SAAS,EAAiC,MAAM,kBAAkB,CAAC;AACjF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAInE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,uCAAuC,CAAC;AAgBxF;;GAEG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMlD;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAYhF;AAkED;;;GAGG;AACH,qBAAa,eAAgB,YAAW,SAAS;IAChD,OAAO,CAAC,kBAAkB,CAAQ;IAClC,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,UAAU,CAA6B;IAC/C,OAAO,CAAC,YAAY,CAAC,CAAqB;IAE1C,YAAY,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,0BAA0B,EAGxE;IAED,UAAU,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAEtC;IAKD,OAAO,KAAK,MAAM,GAEjB;IAED,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE5C;IAED;;;OAGG;IACH,UAAU,IAAI,IAAI,CAEjB;IAED;;;OAGG;IACH,OAAO,IAAI,IAAI,CAEd;IAED;;;;;OAKG;IACH,OAAO,CAAC,eAAe;IA4EvB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAiJ9B;CACD","sourcesContent":["import { isAbsolute, relative, resolve, sep } from \"node:path\";\nimport { type Component, truncateToWidth, visibleWidth } from \"@agent-forge/tui\";\nimport type { AgentSession } from \"../../../core/agent-session.ts\";\nimport { areExperimentalFeaturesEnabled } from \"../../../core/experimental.ts\";\nimport type { ContextUsage } from \"../../../core/extensions/public-api.ts\";\nimport { type CodingAgentFacadeV1, createCodingAgentFacade } from \"../../../core/facade/index.ts\";\nimport type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.ts\";\nimport { addUsageToTotals, createUsageTotals, type UsageTotals } from \"../../../core/usage-totals.ts\";\nimport { theme } from \"../theme/theme.ts\";\n\n/**\n * Sanitize text for display in a single-line status.\n * Removes newlines, tabs, carriage returns, and other control characters.\n */\nfunction sanitizeStatusText(text: string): string {\n\t// Replace newlines, tabs, carriage returns with space, then collapse multiple spaces\n\treturn text\n\t\t.replace(/[\\r\\n\\t]/g, \" \")\n\t\t.replace(/ +/g, \" \")\n\t\t.trim();\n}\n\n/**\n * Format token counts for compact footer display.\n */\nexport function formatTokens(count: number): string {\n\tif (count < 1000) return count.toString();\n\tif (count < 10000) return `${(count / 1000).toFixed(1)}k`;\n\tif (count < 1000000) return `${Math.round(count / 1000)}k`;\n\tif (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;\n\treturn `${Math.round(count / 1000000)}M`;\n}\n\nexport function formatCwdForFooter(cwd: string, home: string | undefined): string {\n\tif (!home) return cwd;\n\n\tconst resolvedCwd = resolve(cwd);\n\tconst resolvedHome = resolve(home);\n\tconst relativeToHome = relative(resolvedHome, resolvedCwd);\n\tconst isInsideHome =\n\t\trelativeToHome === \"\" ||\n\t\t(relativeToHome !== \"..\" && !relativeToHome.startsWith(`..${sep}`) && !isAbsolute(relativeToHome));\n\n\tif (!isInsideHome) return cwd;\n\treturn relativeToHome === \"\" ? \"~\" : `~${sep}${relativeToHome}`;\n}\n\n/** Shape of the `goal.state` custom entry data written by the builtin goal capability. */\ninterface FooterGoalState {\n\tgoals?: Array<{ title?: unknown }>;\n}\n\n/** Shape of the `todo.state` custom entry data written by the builtin todo capability. */\ninterface FooterTodoState {\n\ttodos?: Array<{ status?: unknown }>;\n}\n\n/** Shape of the `workflow.state` custom entry data written by the workflow plugin. */\ninterface FooterWorkflowState {\n\tworkflows?: Array<{\n\t\ttitle?: unknown;\n\t\tstatus?: unknown;\n\t\truntime?: Array<{ status?: unknown }>;\n\t\tdiagnostics?: unknown[];\n\t}>;\n}\n\n/**\n * Build the one-line workflow summary from the latest `workflow.state` snapshot:\n * `wf: <title> <done>/<total>` plus status markers, in order — `▶n` (n>=2\n * parallel active stages), `⏸` (a stage awaiting review), `⚠` (non-empty\n * diagnostics), `✗` (failed workflow); done counts completed+skipped stages.\n * Picks the newest non-terminal workflow, falling back to the newest one when\n * all are terminal; returns undefined when there is no workflow to show.\n */\nfunction summarizeWorkflowState(state: FooterWorkflowState | undefined): string | undefined {\n\tconst workflows = state?.workflows;\n\tconst records = Array.isArray(workflows) ? workflows : [];\n\tconst record =\n\t\t[...records].reverse().find((item) => item?.status === \"draft\" || item?.status === \"running\") ?? records.at(-1);\n\tif (record === undefined || typeof record.title !== \"string\") {\n\t\treturn undefined;\n\t}\n\n\tconst stages = Array.isArray(record.runtime) ? record.runtime : [];\n\tconst done = stages.filter((stage) => stage?.status === \"completed\" || stage?.status === \"skipped\").length;\n\tconst activeCount = stages.filter((stage) => stage?.status === \"active\").length;\n\n\tconst parts = [`wf: ${record.title} ${done}/${stages.length}`];\n\tif (activeCount >= 2) parts.push(`▶${activeCount}`);\n\tif (stages.some((stage) => stage?.status === \"awaiting_review\")) parts.push(\"⏸\");\n\tif (Array.isArray(record.diagnostics) && record.diagnostics.length > 0) parts.push(\"⚠\");\n\tif (record.status === \"failed\") parts.push(\"✗\");\n\treturn parts.join(\" \");\n}\n\n/** Session-derived stats cached across frames; all fields come from one entries scan. */\ninterface CachedSessionStats {\n\tsession: AgentSession;\n\tsessionId: string;\n\tleafId: string | null;\n\tmodel: unknown;\n\tusageTotals: UsageTotals;\n\tlatestCacheHitRate: number | undefined;\n\tgoalTitle: string | undefined;\n\ttodoDone: number;\n\ttodoTotal: number;\n\tworkflowState: FooterWorkflowState | undefined;\n\tcontextUsage: ContextUsage | undefined;\n}\n\n/**\n * Footer component that shows pwd, token stats, and context usage.\n * Computes token/context stats from session, gets git branch and extension statuses from provider.\n */\nexport class FooterComponent implements Component {\n\tprivate autoCompactEnabled = true;\n\tprivate session: AgentSession;\n\tprivate footerData: ReadonlyFooterDataProvider;\n\tprivate sessionStats?: CachedSessionStats;\n\n\tconstructor(session: AgentSession, footerData: ReadonlyFooterDataProvider) {\n\t\tthis.session = session;\n\t\tthis.footerData = footerData;\n\t}\n\n\tsetSession(session: AgentSession): void {\n\t\tthis.session = session;\n\t}\n\n\t// Stable facade (阶段 1 slice 1): derived on each access; the facade and\n\t// `this.session` always resolve to the same AgentSession, so setSession()\n\t// rebind stays the only rebind path.\n\tprivate get facade(): CodingAgentFacadeV1 {\n\t\treturn createCodingAgentFacade(this.session);\n\t}\n\n\tsetAutoCompactEnabled(enabled: boolean): void {\n\t\tthis.autoCompactEnabled = enabled;\n\t}\n\n\t/**\n\t * No-op: git branch caching now handled by provider.\n\t * Kept for compatibility with existing call sites in interactive-mode.\n\t */\n\tinvalidate(): void {\n\t\t// No-op: git branch is cached/invalidated by provider\n\t}\n\n\t/**\n\t * Clean up resources.\n\t * Git watcher cleanup now handled by provider.\n\t */\n\tdispose(): void {\n\t\t// Git watcher cleanup handled by provider\n\t}\n\n\t/**\n\t * Usage totals, goal/todo/workflow state, and context usage scan the whole session, and the\n\t * footer renders on every frame. The session is append-only and every append moves the leaf,\n\t * so the scan results only change with the session, leaf, or the model whose context window\n\t * applies; until then the previous frame's values are reused.\n\t */\n\tprivate getSessionStats(): CachedSessionStats {\n\t\tconst facade = this.facade;\n\t\tconst sessionId = facade.getSessionId();\n\t\tconst leafId = facade.getLeafId();\n\t\tconst model = facade.getModel();\n\t\tconst cached = this.sessionStats;\n\t\tif (\n\t\t\tcached &&\n\t\t\tcached.session === this.session &&\n\t\t\tcached.sessionId === sessionId &&\n\t\t\tcached.leafId === leafId &&\n\t\t\tcached.model === model\n\t\t) {\n\t\t\treturn cached;\n\t\t}\n\n\t\t// Calculate cumulative usage from ALL session entries (not just post-compaction messages)\n\t\tconst usageTotals = createUsageTotals();\n\t\tlet latestCacheHitRate: number | undefined;\n\n\t\t// Latest goal/todo capability state: the builtin goal/todo capabilities announce\n\t\t// goal.changed / todo.changed and persist the same state as goal.state / todo.state\n\t\t// custom entries; the last entry per type wins.\n\t\tlet goalTitle: string | undefined;\n\t\tlet todoDone = 0;\n\t\tlet todoTotal = 0;\n\t\tlet workflowState: FooterWorkflowState | undefined;\n\n\t\tfor (const entry of facade.getEntries()) {\n\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\taddUsageToTotals(usageTotals, entry.message.usage);\n\n\t\t\t\tconst latestPromptTokens =\n\t\t\t\t\tentry.message.usage.input + entry.message.usage.cacheRead + entry.message.usage.cacheWrite;\n\t\t\t\tlatestCacheHitRate =\n\t\t\t\t\tlatestPromptTokens > 0 ? (entry.message.usage.cacheRead / latestPromptTokens) * 100 : undefined;\n\t\t\t} else if (entry.type === \"message\" && entry.message.role === \"toolResult\" && entry.message.usage) {\n\t\t\t\taddUsageToTotals(usageTotals, entry.message.usage);\n\t\t\t} else if ((entry.type === \"branch_summary\" || entry.type === \"compaction\") && entry.usage) {\n\t\t\t\taddUsageToTotals(usageTotals, entry.usage);\n\t\t\t} else if (entry.type === \"custom\" && entry.customType === \"goal.state\") {\n\t\t\t\tconst goals = (entry.data as FooterGoalState | undefined)?.goals;\n\t\t\t\tconst current = Array.isArray(goals) ? goals.at(-1) : undefined;\n\t\t\t\tif (current && typeof current.title === \"string\") {\n\t\t\t\t\tgoalTitle = current.title;\n\t\t\t\t}\n\t\t\t} else if (entry.type === \"custom\" && entry.customType === \"todo.state\") {\n\t\t\t\tconst todos = (entry.data as FooterTodoState | undefined)?.todos;\n\t\t\t\tif (Array.isArray(todos)) {\n\t\t\t\t\ttodoDone = todos.filter((item) => item?.status === \"done\").length;\n\t\t\t\t\ttodoTotal = todos.length;\n\t\t\t\t}\n\t\t\t} else if (entry.type === \"custom\" && entry.customType === \"workflow.state\") {\n\t\t\t\tworkflowState = entry.data as FooterWorkflowState | undefined;\n\t\t\t}\n\t\t}\n\n\t\t// Calculate context usage from session (handles compaction correctly).\n\t\t// After compaction, tokens are unknown until the next LLM response.\n\t\tconst contextUsage = facade.getContextUsage();\n\t\tthis.sessionStats = {\n\t\t\tsession: this.session,\n\t\t\tsessionId,\n\t\t\tleafId,\n\t\t\tmodel,\n\t\t\tusageTotals,\n\t\t\tlatestCacheHitRate,\n\t\t\tgoalTitle,\n\t\t\ttodoDone,\n\t\t\ttodoTotal,\n\t\t\tworkflowState,\n\t\t\tcontextUsage,\n\t\t};\n\t\treturn this.sessionStats;\n\t}\n\n\trender(width: number): string[] {\n\t\tconst state = this.facade.getState();\n\t\tconst { usageTotals, latestCacheHitRate, contextUsage, goalTitle, todoDone, todoTotal, workflowState } =\n\t\t\tthis.getSessionStats();\n\t\tconst contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;\n\t\tconst contextPercentValue = contextUsage?.percent ?? 0;\n\t\tconst contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : \"?\";\n\n\t\t// Replace home directory with ~\n\t\tlet pwd = formatCwdForFooter(this.facade.getCwd(), process.env.HOME || process.env.USERPROFILE);\n\n\t\t// Add git branch if available\n\t\tconst branch = this.footerData.getGitBranch();\n\t\tif (branch) {\n\t\t\tpwd = `${pwd} (${branch})`;\n\t\t}\n\n\t\t// Add session name if set\n\t\tconst sessionName = this.facade.getSessionName();\n\t\tif (sessionName) {\n\t\t\tpwd = `${pwd} • ${sessionName}`;\n\t\t}\n\n\t\t// Build stats line\n\t\tconst statsParts = [];\n\t\tif (usageTotals.input) statsParts.push(`↑${formatTokens(usageTotals.input)}`);\n\t\tif (usageTotals.output) statsParts.push(`↓${formatTokens(usageTotals.output)}`);\n\t\tif (usageTotals.cacheRead) statsParts.push(`R${formatTokens(usageTotals.cacheRead)}`);\n\t\tif (usageTotals.cacheWrite) statsParts.push(`W${formatTokens(usageTotals.cacheWrite)}`);\n\t\tif ((usageTotals.cacheRead > 0 || usageTotals.cacheWrite > 0) && latestCacheHitRate !== undefined) {\n\t\t\tstatsParts.push(`CH${latestCacheHitRate.toFixed(1)}%`);\n\t\t}\n\n\t\t// Kimi Coding is subscription-backed despite using API-key authentication.\n\t\tconst usingSubscription = state.model\n\t\t\t? state.model.provider === \"kimi-coding\" || this.facade.isUsingSubscription(state.model.provider)\n\t\t\t: false;\n\t\tif (usageTotals.cost || usingSubscription) {\n\t\t\tconst costStr = `$${usageTotals.cost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`;\n\t\t\tstatsParts.push(costStr);\n\t\t}\n\n\t\t// Colorize context percentage based on usage\n\t\tlet contextPercentStr: string;\n\t\tconst autoIndicator = this.autoCompactEnabled ? \" (auto)\" : \"\";\n\t\tconst contextPercentDisplay =\n\t\t\tcontextPercent === \"?\"\n\t\t\t\t? `?/${formatTokens(contextWindow)}${autoIndicator}`\n\t\t\t\t: `${contextPercent}%/${formatTokens(contextWindow)}${autoIndicator}`;\n\t\tif (contextPercentValue > 90) {\n\t\t\tcontextPercentStr = theme.fg(\"error\", contextPercentDisplay);\n\t\t} else if (contextPercentValue > 70) {\n\t\t\tcontextPercentStr = theme.fg(\"warning\", contextPercentDisplay);\n\t\t} else {\n\t\t\tcontextPercentStr = contextPercentDisplay;\n\t\t}\n\t\tstatsParts.push(contextPercentStr);\n\t\tif (areExperimentalFeaturesEnabled()) {\n\t\t\tstatsParts.push(`${theme.fg(\"dim\", \"•\")} ${theme.bold(theme.fg(\"warning\", \"xp\"))}`);\n\t\t}\n\n\t\tlet statsLeft = statsParts.join(\" \");\n\n\t\t// Add model name on the right side, plus thinking level if model supports it\n\t\tconst modelName = state.model?.id || \"no-model\";\n\n\t\tlet statsLeftWidth = visibleWidth(statsLeft);\n\n\t\t// If statsLeft is too wide, truncate it\n\t\tif (statsLeftWidth > width) {\n\t\t\tstatsLeft = truncateToWidth(statsLeft, width, \"...\");\n\t\t\tstatsLeftWidth = visibleWidth(statsLeft);\n\t\t}\n\n\t\t// Calculate available space for padding (minimum 2 spaces between stats and model)\n\t\tconst minPadding = 2;\n\n\t\t// Add thinking level indicator if model supports reasoning\n\t\tlet rightSideWithoutProvider = modelName;\n\t\tif (state.model?.reasoning) {\n\t\t\tconst thinkingLevel = state.thinkingLevel || \"off\";\n\t\t\trightSideWithoutProvider =\n\t\t\t\tthinkingLevel === \"off\" ? `${modelName} • thinking off` : `${modelName} • ${thinkingLevel}`;\n\t\t}\n\n\t\t// Prepend the provider in parentheses if there are multiple providers and there's enough room\n\t\tlet rightSide = rightSideWithoutProvider;\n\t\tif (this.footerData.getAvailableProviderCount() > 1 && state.model) {\n\t\t\trightSide = `(${state.model!.provider}) ${rightSideWithoutProvider}`;\n\t\t\tif (statsLeftWidth + minPadding + visibleWidth(rightSide) > width) {\n\t\t\t\t// Too wide, fall back\n\t\t\t\trightSide = rightSideWithoutProvider;\n\t\t\t}\n\t\t}\n\n\t\tconst rightSideWidth = visibleWidth(rightSide);\n\t\tconst totalNeeded = statsLeftWidth + minPadding + rightSideWidth;\n\n\t\tlet statsLine: string;\n\t\tif (totalNeeded <= width) {\n\t\t\t// Both fit - add padding to right-align model\n\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - rightSideWidth);\n\t\t\tstatsLine = statsLeft + padding + rightSide;\n\t\t} else {\n\t\t\t// Need to truncate right side\n\t\t\tconst availableForRight = width - statsLeftWidth - minPadding;\n\t\t\tif (availableForRight > 0) {\n\t\t\t\tconst truncatedRight = truncateToWidth(rightSide, availableForRight, \"\");\n\t\t\t\tconst truncatedRightWidth = visibleWidth(truncatedRight);\n\t\t\t\tconst padding = \" \".repeat(Math.max(0, width - statsLeftWidth - truncatedRightWidth));\n\t\t\t\tstatsLine = statsLeft + padding + truncatedRight;\n\t\t\t} else {\n\t\t\t\t// Not enough space for right side at all\n\t\t\t\tstatsLine = statsLeft;\n\t\t\t}\n\t\t}\n\n\t\t// Apply dim to each part separately. statsLeft may contain color codes (for context %)\n\t\t// that end with a reset, which would clear an outer dim wrapper. So we dim the parts\n\t\t// before and after the colored section independently.\n\t\tconst dimStatsLeft = theme.fg(\"dim\", statsLeft);\n\t\tconst remainder = statsLine.slice(statsLeft.length); // padding + rightSide\n\t\tconst dimRemainder = theme.fg(\"dim\", remainder);\n\n\t\tconst pwdLine = truncateToWidth(theme.fg(\"dim\", pwd), width, theme.fg(\"dim\", \"...\"));\n\t\tconst lines = [pwdLine, dimStatsLeft + dimRemainder];\n\n\t\t// Add goal/todo status, only when a goal exists; an empty todo set shows the goal alone.\n\t\tif (goalTitle !== undefined) {\n\t\t\tconst goalParts = [`goal: ${goalTitle}`];\n\t\t\tif (todoTotal > 0) {\n\t\t\t\tgoalParts.push(`todo ${todoDone}/${todoTotal}`);\n\t\t\t}\n\t\t\tconst goalLine = theme.fg(\"dim\", sanitizeStatusText(goalParts.join(\" • \")));\n\t\t\tlines.push(truncateToWidth(goalLine, width, theme.fg(\"dim\", \"...\")));\n\t\t}\n\n\t\t// Add workflow status, only when a workflow snapshot reports one.\n\t\tconst workflowSummary = summarizeWorkflowState(workflowState);\n\t\tif (workflowSummary !== undefined) {\n\t\t\tconst workflowLine = theme.fg(\"dim\", sanitizeStatusText(workflowSummary));\n\t\t\tlines.push(truncateToWidth(workflowLine, width, theme.fg(\"dim\", \"...\")));\n\t\t}\n\n\t\treturn lines;\n\t}\n}\n"]}
1
+ {"version":3,"file":"footer.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,SAAS,EAAiC,MAAM,kBAAkB,CAAC;AACjF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAInE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,uCAAuC,CAAC;AAgBxF;;GAEG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMlD;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAYhF;AAmGD;;;GAGG;AACH,qBAAa,eAAgB,YAAW,SAAS;IAChD,OAAO,CAAC,kBAAkB,CAAQ;IAClC,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,UAAU,CAA6B;IAC/C,OAAO,CAAC,YAAY,CAAC,CAAqB;IAE1C,YAAY,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,0BAA0B,EAGxE;IAED,UAAU,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAEtC;IAKD,OAAO,KAAK,MAAM,GAEjB;IAED,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE5C;IAED;;;OAGG;IACH,UAAU,IAAI,IAAI,CAEjB;IAED;;;OAGG;IACH,OAAO,IAAI,IAAI,CAEd;IAED;;;;;OAKG;IACH,OAAO,CAAC,eAAe;IAgFvB;;;;;OAKG;IACH,sBAAsB,IAAI,OAAO,CAEhC;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAkK9B;CACD","sourcesContent":["import { isAbsolute, relative, resolve, sep } from \"node:path\";\nimport { type Component, truncateToWidth, visibleWidth } from \"@agent-forge/tui\";\nimport type { AgentSession } from \"../../../core/agent-session.ts\";\nimport { areExperimentalFeaturesEnabled } from \"../../../core/experimental.ts\";\nimport type { ContextUsage } from \"../../../core/extensions/public-api.ts\";\nimport { type CodingAgentFacadeV1, createCodingAgentFacade } from \"../../../core/facade/index.ts\";\nimport type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.ts\";\nimport { addUsageToTotals, createUsageTotals, type UsageTotals } from \"../../../core/usage-totals.ts\";\nimport { theme } from \"../theme/theme.ts\";\n\n/**\n * Sanitize text for display in a single-line status.\n * Removes newlines, tabs, carriage returns, and other control characters.\n */\nfunction sanitizeStatusText(text: string): string {\n\t// Replace newlines, tabs, carriage returns with space, then collapse multiple spaces\n\treturn text\n\t\t.replace(/[\\r\\n\\t]/g, \" \")\n\t\t.replace(/ +/g, \" \")\n\t\t.trim();\n}\n\n/**\n * Format token counts for compact footer display.\n */\nexport function formatTokens(count: number): string {\n\tif (count < 1000) return count.toString();\n\tif (count < 10000) return `${(count / 1000).toFixed(1)}k`;\n\tif (count < 1000000) return `${Math.round(count / 1000)}k`;\n\tif (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;\n\treturn `${Math.round(count / 1000000)}M`;\n}\n\nexport function formatCwdForFooter(cwd: string, home: string | undefined): string {\n\tif (!home) return cwd;\n\n\tconst resolvedCwd = resolve(cwd);\n\tconst resolvedHome = resolve(home);\n\tconst relativeToHome = relative(resolvedHome, resolvedCwd);\n\tconst isInsideHome =\n\t\trelativeToHome === \"\" ||\n\t\t(relativeToHome !== \"..\" && !relativeToHome.startsWith(`..${sep}`) && !isAbsolute(relativeToHome));\n\n\tif (!isInsideHome) return cwd;\n\treturn relativeToHome === \"\" ? \"~\" : `~${sep}${relativeToHome}`;\n}\n\n/** Shape of the `goal.state` custom entry data written by the builtin goal capability. */\ninterface FooterGoalState {\n\tgoals?: Array<{ title?: unknown }>;\n}\n\n/** Shape of the `todo.state` custom entry data written by the builtin todo capability. */\ninterface FooterTodoState {\n\ttodos?: Array<{ status?: unknown }>;\n}\n\n/** Shape of the `workflow.state` custom entry data written by the workflow plugin. */\ninterface FooterWorkflowState {\n\tworkflows?: Array<{\n\t\ttitle?: unknown;\n\t\tstatus?: unknown;\n\t\truntime?: Array<{ status?: unknown }>;\n\t\tdiagnostics?: unknown[];\n\t}>;\n}\n\n/**\n * Shape of the `provider.failover.state` snapshot written by the provider-failover\n * plugin (设计 供应商能力槽位与主备切换 §4.7). Only the last entry per session wins;\n * `status: \"open\"` is the only rendered state (half-open is transient — entered at\n * request time and immediately resolved to an open/closed snapshot).\n */\ninterface FooterFailoverState {\n\tchainId?: unknown;\n\tstatus?: unknown;\n\tbackupProvider?: unknown;\n\tprobeAtMs?: unknown;\n}\n\n/**\n * Build the one-line failover summary from the latest `provider.failover.state`\n * snapshot: `failover: 主 <primary> 不可用 • 经 <backup> • 下次试探 <Ns>`. The\n * countdown is computed against `nowMs`; at or past zero it reads `等待试探`\n * (cooldown over, next real request is the probe). Returns undefined when there is\n * nothing to show (no snapshot, not open, or an unusable chainId).\n */\nfunction summarizeFailoverState(state: FooterFailoverState | undefined, nowMs: number): string | undefined {\n\tif (state?.status !== \"open\" || typeof state.chainId !== \"string\" || state.chainId === \"\") return undefined;\n\tconst parts = [`failover: 主 ${state.chainId} 不可用`];\n\tif (typeof state.backupProvider === \"string\" && state.backupProvider !== \"\") {\n\t\tparts.push(`经 ${state.backupProvider}`);\n\t}\n\tconst probeAtMs = typeof state.probeAtMs === \"number\" ? state.probeAtMs : 0;\n\tif (probeAtMs - nowMs <= 0) parts.push(\"等待试探\");\n\telse parts.push(`下次试探 ${Math.ceil((probeAtMs - nowMs) / 1000)}s`);\n\treturn parts.join(\" • \");\n}\n\n/**\n * Build the one-line workflow summary from the latest `workflow.state` snapshot:\n * `wf: <title> <done>/<total>` plus status markers, in order — `▶n` (n>=2\n * parallel active stages), `⏸` (a stage awaiting review), `⚠` (non-empty\n * diagnostics), `✗` (failed workflow); done counts completed+skipped stages.\n * Picks the newest non-terminal workflow, falling back to the newest one when\n * all are terminal; returns undefined when there is no workflow to show.\n */\nfunction summarizeWorkflowState(state: FooterWorkflowState | undefined): string | undefined {\n\tconst workflows = state?.workflows;\n\tconst records = Array.isArray(workflows) ? workflows : [];\n\tconst record =\n\t\t[...records].reverse().find((item) => item?.status === \"draft\" || item?.status === \"running\") ?? records.at(-1);\n\tif (record === undefined || typeof record.title !== \"string\") {\n\t\treturn undefined;\n\t}\n\n\tconst stages = Array.isArray(record.runtime) ? record.runtime : [];\n\tconst done = stages.filter((stage) => stage?.status === \"completed\" || stage?.status === \"skipped\").length;\n\tconst activeCount = stages.filter((stage) => stage?.status === \"active\").length;\n\n\tconst parts = [`wf: ${record.title} ${done}/${stages.length}`];\n\tif (activeCount >= 2) parts.push(`▶${activeCount}`);\n\tif (stages.some((stage) => stage?.status === \"awaiting_review\")) parts.push(\"⏸\");\n\tif (Array.isArray(record.diagnostics) && record.diagnostics.length > 0) parts.push(\"⚠\");\n\tif (record.status === \"failed\") parts.push(\"✗\");\n\treturn parts.join(\" \");\n}\n\n/** Session-derived stats cached across frames; all fields come from one entries scan. */\ninterface CachedSessionStats {\n\tsession: AgentSession;\n\tsessionId: string;\n\tleafId: string | null;\n\tmodel: unknown;\n\tusageTotals: UsageTotals;\n\tlatestCacheHitRate: number | undefined;\n\tgoalTitle: string | undefined;\n\ttodoDone: number;\n\ttodoTotal: number;\n\tworkflowState: FooterWorkflowState | undefined;\n\tfailoverState: FooterFailoverState | undefined;\n\tcontextUsage: ContextUsage | undefined;\n}\n\n/**\n * Footer component that shows pwd, token stats, and context usage.\n * Computes token/context stats from session, gets git branch and extension statuses from provider.\n */\nexport class FooterComponent implements Component {\n\tprivate autoCompactEnabled = true;\n\tprivate session: AgentSession;\n\tprivate footerData: ReadonlyFooterDataProvider;\n\tprivate sessionStats?: CachedSessionStats;\n\n\tconstructor(session: AgentSession, footerData: ReadonlyFooterDataProvider) {\n\t\tthis.session = session;\n\t\tthis.footerData = footerData;\n\t}\n\n\tsetSession(session: AgentSession): void {\n\t\tthis.session = session;\n\t}\n\n\t// Stable facade (阶段 1 slice 1): derived on each access; the facade and\n\t// `this.session` always resolve to the same AgentSession, so setSession()\n\t// rebind stays the only rebind path.\n\tprivate get facade(): CodingAgentFacadeV1 {\n\t\treturn createCodingAgentFacade(this.session);\n\t}\n\n\tsetAutoCompactEnabled(enabled: boolean): void {\n\t\tthis.autoCompactEnabled = enabled;\n\t}\n\n\t/**\n\t * No-op: git branch caching now handled by provider.\n\t * Kept for compatibility with existing call sites in interactive-mode.\n\t */\n\tinvalidate(): void {\n\t\t// No-op: git branch is cached/invalidated by provider\n\t}\n\n\t/**\n\t * Clean up resources.\n\t * Git watcher cleanup now handled by provider.\n\t */\n\tdispose(): void {\n\t\t// Git watcher cleanup handled by provider\n\t}\n\n\t/**\n\t * Usage totals, goal/todo/workflow state, and context usage scan the whole session, and the\n\t * footer renders on every frame. The session is append-only and every append moves the leaf,\n\t * so the scan results only change with the session, leaf, or the model whose context window\n\t * applies; until then the previous frame's values are reused.\n\t */\n\tprivate getSessionStats(): CachedSessionStats {\n\t\tconst facade = this.facade;\n\t\tconst sessionId = facade.getSessionId();\n\t\tconst leafId = facade.getLeafId();\n\t\tconst model = facade.getModel();\n\t\tconst cached = this.sessionStats;\n\t\tif (\n\t\t\tcached &&\n\t\t\tcached.session === this.session &&\n\t\t\tcached.sessionId === sessionId &&\n\t\t\tcached.leafId === leafId &&\n\t\t\tcached.model === model\n\t\t) {\n\t\t\treturn cached;\n\t\t}\n\n\t\t// Calculate cumulative usage from ALL session entries (not just post-compaction messages)\n\t\tconst usageTotals = createUsageTotals();\n\t\tlet latestCacheHitRate: number | undefined;\n\n\t\t// Latest goal/todo capability state: the builtin goal/todo capabilities announce\n\t\t// goal.changed / todo.changed and persist the same state as goal.state / todo.state\n\t\t// custom entries; the last entry per type wins.\n\t\tlet goalTitle: string | undefined;\n\t\tlet todoDone = 0;\n\t\tlet todoTotal = 0;\n\t\tlet workflowState: FooterWorkflowState | undefined;\n\t\tlet failoverState: FooterFailoverState | undefined;\n\n\t\tfor (const entry of facade.getEntries()) {\n\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\taddUsageToTotals(usageTotals, entry.message.usage);\n\n\t\t\t\tconst latestPromptTokens =\n\t\t\t\t\tentry.message.usage.input + entry.message.usage.cacheRead + entry.message.usage.cacheWrite;\n\t\t\t\tlatestCacheHitRate =\n\t\t\t\t\tlatestPromptTokens > 0 ? (entry.message.usage.cacheRead / latestPromptTokens) * 100 : undefined;\n\t\t\t} else if (entry.type === \"message\" && entry.message.role === \"toolResult\" && entry.message.usage) {\n\t\t\t\taddUsageToTotals(usageTotals, entry.message.usage);\n\t\t\t} else if ((entry.type === \"branch_summary\" || entry.type === \"compaction\") && entry.usage) {\n\t\t\t\taddUsageToTotals(usageTotals, entry.usage);\n\t\t\t} else if (entry.type === \"custom\" && entry.customType === \"goal.state\") {\n\t\t\t\tconst goals = (entry.data as FooterGoalState | undefined)?.goals;\n\t\t\t\tconst current = Array.isArray(goals) ? goals.at(-1) : undefined;\n\t\t\t\tif (current && typeof current.title === \"string\") {\n\t\t\t\t\tgoalTitle = current.title;\n\t\t\t\t}\n\t\t\t} else if (entry.type === \"custom\" && entry.customType === \"todo.state\") {\n\t\t\t\tconst todos = (entry.data as FooterTodoState | undefined)?.todos;\n\t\t\t\tif (Array.isArray(todos)) {\n\t\t\t\t\ttodoDone = todos.filter((item) => item?.status === \"done\").length;\n\t\t\t\t\ttodoTotal = todos.length;\n\t\t\t\t}\n\t\t\t} else if (entry.type === \"custom\" && entry.customType === \"workflow.state\") {\n\t\t\t\tworkflowState = entry.data as FooterWorkflowState | undefined;\n\t\t\t} else if (entry.type === \"custom\" && entry.customType === \"provider.failover.state\") {\n\t\t\t\tfailoverState = entry.data as FooterFailoverState | undefined;\n\t\t\t}\n\t\t}\n\n\t\t// Calculate context usage from session (handles compaction correctly).\n\t\t// After compaction, tokens are unknown until the next LLM response.\n\t\tconst contextUsage = facade.getContextUsage();\n\t\tthis.sessionStats = {\n\t\t\tsession: this.session,\n\t\t\tsessionId,\n\t\t\tleafId,\n\t\t\tmodel,\n\t\t\tusageTotals,\n\t\t\tlatestCacheHitRate,\n\t\t\tgoalTitle,\n\t\t\ttodoDone,\n\t\t\ttodoTotal,\n\t\t\tworkflowState,\n\t\t\tfailoverState,\n\t\t\tcontextUsage,\n\t\t};\n\t\treturn this.sessionStats;\n\t}\n\n\t/**\n\t * Whether the latest `provider.failover.state` snapshot reports an open\n\t * breaker. Drives the interactive-mode per-second re-render ticker (设计\n\t * 供应商能力槽位与主备切换 §4.7 Phase 3); cheap because it reuses the\n\t * cached session stats.\n\t */\n\tisProviderFailoverOpen(): boolean {\n\t\treturn this.getSessionStats().failoverState?.status === \"open\";\n\t}\n\n\trender(width: number): string[] {\n\t\tconst state = this.facade.getState();\n\t\tconst {\n\t\t\tusageTotals,\n\t\t\tlatestCacheHitRate,\n\t\t\tcontextUsage,\n\t\t\tgoalTitle,\n\t\t\ttodoDone,\n\t\t\ttodoTotal,\n\t\t\tworkflowState,\n\t\t\tfailoverState,\n\t\t} = this.getSessionStats();\n\t\tconst contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;\n\t\tconst contextPercentValue = contextUsage?.percent ?? 0;\n\t\tconst contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : \"?\";\n\n\t\t// Replace home directory with ~\n\t\tlet pwd = formatCwdForFooter(this.facade.getCwd(), process.env.HOME || process.env.USERPROFILE);\n\n\t\t// Add git branch if available\n\t\tconst branch = this.footerData.getGitBranch();\n\t\tif (branch) {\n\t\t\tpwd = `${pwd} (${branch})`;\n\t\t}\n\n\t\t// Add session name if set\n\t\tconst sessionName = this.facade.getSessionName();\n\t\tif (sessionName) {\n\t\t\tpwd = `${pwd} • ${sessionName}`;\n\t\t}\n\n\t\t// Build stats line\n\t\tconst statsParts = [];\n\t\tif (usageTotals.input) statsParts.push(`↑${formatTokens(usageTotals.input)}`);\n\t\tif (usageTotals.output) statsParts.push(`↓${formatTokens(usageTotals.output)}`);\n\t\tif (usageTotals.cacheRead) statsParts.push(`R${formatTokens(usageTotals.cacheRead)}`);\n\t\tif (usageTotals.cacheWrite) statsParts.push(`W${formatTokens(usageTotals.cacheWrite)}`);\n\t\tif ((usageTotals.cacheRead > 0 || usageTotals.cacheWrite > 0) && latestCacheHitRate !== undefined) {\n\t\t\tstatsParts.push(`CH${latestCacheHitRate.toFixed(1)}%`);\n\t\t}\n\n\t\t// Kimi Coding is subscription-backed despite using API-key authentication.\n\t\tconst usingSubscription = state.model\n\t\t\t? state.model.provider === \"kimi-coding\" || this.facade.isUsingSubscription(state.model.provider)\n\t\t\t: false;\n\t\tif (usageTotals.cost || usingSubscription) {\n\t\t\tconst costStr = `$${usageTotals.cost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`;\n\t\t\tstatsParts.push(costStr);\n\t\t}\n\n\t\t// Colorize context percentage based on usage\n\t\tlet contextPercentStr: string;\n\t\tconst autoIndicator = this.autoCompactEnabled ? \" (auto)\" : \"\";\n\t\tconst contextPercentDisplay =\n\t\t\tcontextPercent === \"?\"\n\t\t\t\t? `?/${formatTokens(contextWindow)}${autoIndicator}`\n\t\t\t\t: `${contextPercent}%/${formatTokens(contextWindow)}${autoIndicator}`;\n\t\tif (contextPercentValue > 90) {\n\t\t\tcontextPercentStr = theme.fg(\"error\", contextPercentDisplay);\n\t\t} else if (contextPercentValue > 70) {\n\t\t\tcontextPercentStr = theme.fg(\"warning\", contextPercentDisplay);\n\t\t} else {\n\t\t\tcontextPercentStr = contextPercentDisplay;\n\t\t}\n\t\tstatsParts.push(contextPercentStr);\n\t\tif (areExperimentalFeaturesEnabled()) {\n\t\t\tstatsParts.push(`${theme.fg(\"dim\", \"•\")} ${theme.bold(theme.fg(\"warning\", \"xp\"))}`);\n\t\t}\n\n\t\tlet statsLeft = statsParts.join(\" \");\n\n\t\t// Add model name on the right side, plus thinking level if model supports it\n\t\tconst modelName = state.model?.id || \"no-model\";\n\n\t\tlet statsLeftWidth = visibleWidth(statsLeft);\n\n\t\t// If statsLeft is too wide, truncate it\n\t\tif (statsLeftWidth > width) {\n\t\t\tstatsLeft = truncateToWidth(statsLeft, width, \"...\");\n\t\t\tstatsLeftWidth = visibleWidth(statsLeft);\n\t\t}\n\n\t\t// Calculate available space for padding (minimum 2 spaces between stats and model)\n\t\tconst minPadding = 2;\n\n\t\t// Add thinking level indicator if model supports reasoning\n\t\tlet rightSideWithoutProvider = modelName;\n\t\tif (state.model?.reasoning) {\n\t\t\tconst thinkingLevel = state.thinkingLevel || \"off\";\n\t\t\trightSideWithoutProvider =\n\t\t\t\tthinkingLevel === \"off\" ? `${modelName} • thinking off` : `${modelName} • ${thinkingLevel}`;\n\t\t}\n\n\t\t// Prepend the provider in parentheses if there are multiple providers and there's enough room\n\t\tlet rightSide = rightSideWithoutProvider;\n\t\tif (this.footerData.getAvailableProviderCount() > 1 && state.model) {\n\t\t\trightSide = `(${state.model!.provider}) ${rightSideWithoutProvider}`;\n\t\t\tif (statsLeftWidth + minPadding + visibleWidth(rightSide) > width) {\n\t\t\t\t// Too wide, fall back\n\t\t\t\trightSide = rightSideWithoutProvider;\n\t\t\t}\n\t\t}\n\n\t\tconst rightSideWidth = visibleWidth(rightSide);\n\t\tconst totalNeeded = statsLeftWidth + minPadding + rightSideWidth;\n\n\t\tlet statsLine: string;\n\t\tif (totalNeeded <= width) {\n\t\t\t// Both fit - add padding to right-align model\n\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - rightSideWidth);\n\t\t\tstatsLine = statsLeft + padding + rightSide;\n\t\t} else {\n\t\t\t// Need to truncate right side\n\t\t\tconst availableForRight = width - statsLeftWidth - minPadding;\n\t\t\tif (availableForRight > 0) {\n\t\t\t\tconst truncatedRight = truncateToWidth(rightSide, availableForRight, \"\");\n\t\t\t\tconst truncatedRightWidth = visibleWidth(truncatedRight);\n\t\t\t\tconst padding = \" \".repeat(Math.max(0, width - statsLeftWidth - truncatedRightWidth));\n\t\t\t\tstatsLine = statsLeft + padding + truncatedRight;\n\t\t\t} else {\n\t\t\t\t// Not enough space for right side at all\n\t\t\t\tstatsLine = statsLeft;\n\t\t\t}\n\t\t}\n\n\t\t// Apply dim to each part separately. statsLeft may contain color codes (for context %)\n\t\t// that end with a reset, which would clear an outer dim wrapper. So we dim the parts\n\t\t// before and after the colored section independently.\n\t\tconst dimStatsLeft = theme.fg(\"dim\", statsLeft);\n\t\tconst remainder = statsLine.slice(statsLeft.length); // padding + rightSide\n\t\tconst dimRemainder = theme.fg(\"dim\", remainder);\n\n\t\tconst pwdLine = truncateToWidth(theme.fg(\"dim\", pwd), width, theme.fg(\"dim\", \"...\"));\n\t\tconst lines = [pwdLine, dimStatsLeft + dimRemainder];\n\n\t\t// Add goal/todo status when either exists; a todo set without a goal\n\t\t// still surfaces its progress line alone.\n\t\tif (goalTitle !== undefined || todoTotal > 0) {\n\t\t\tconst goalParts: string[] = [];\n\t\t\tif (goalTitle !== undefined) goalParts.push(`goal: ${goalTitle}`);\n\t\t\tif (todoTotal > 0) goalParts.push(`todo ${todoDone}/${todoTotal}`);\n\t\t\tconst goalLine = theme.fg(\"dim\", sanitizeStatusText(goalParts.join(\" • \")));\n\t\t\tlines.push(truncateToWidth(goalLine, width, theme.fg(\"dim\", \"...\")));\n\t\t}\n\n\t\t// Add workflow status, only when a workflow snapshot reports one.\n\t\tconst workflowSummary = summarizeWorkflowState(workflowState);\n\t\tif (workflowSummary !== undefined) {\n\t\t\tconst workflowLine = theme.fg(\"dim\", sanitizeStatusText(workflowSummary));\n\t\t\tlines.push(truncateToWidth(workflowLine, width, theme.fg(\"dim\", \"...\")));\n\t\t}\n\n\t\t// Add provider failover status, only while the latest snapshot reports an\n\t\t// open breaker. Warning color: an active degradation, not passive info\n\t\t// (context% 变色先例). The countdown is recomputed per frame from Date.now().\n\t\tconst failoverSummary = summarizeFailoverState(failoverState, Date.now());\n\t\tif (failoverSummary !== undefined) {\n\t\t\tconst failoverLine = theme.fg(\"warning\", sanitizeStatusText(failoverSummary));\n\t\t\tlines.push(truncateToWidth(failoverLine, width, theme.fg(\"warning\", \"...\")));\n\t\t}\n\n\t\treturn lines;\n\t}\n}\n"]}