@esso0428/pi-subagents 0.15.5 → 0.15.7

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 (234) hide show
  1. package/CHANGELOG.md +10 -19
  2. package/README.md +5 -5
  3. package/dist/agent-history-list.d.ts +19 -0
  4. package/dist/agent-history-list.d.ts.map +1 -0
  5. package/dist/agent-history-list.js +69 -0
  6. package/dist/agent-history-list.js.map +1 -0
  7. package/dist/agent-history.d.ts +2 -1
  8. package/dist/agent-history.d.ts.map +1 -0
  9. package/dist/agent-history.js +32 -34
  10. package/dist/agent-history.js.map +1 -0
  11. package/dist/agent-manager.d.ts +24 -8
  12. package/dist/agent-manager.d.ts.map +1 -0
  13. package/dist/agent-manager.js +171 -93
  14. package/dist/agent-manager.js.map +1 -0
  15. package/dist/agent-runner.d.ts +1 -0
  16. package/dist/agent-runner.d.ts.map +1 -0
  17. package/dist/agent-runner.js +75 -86
  18. package/dist/agent-runner.js.map +1 -0
  19. package/dist/agent-types.d.ts +1 -0
  20. package/dist/agent-types.d.ts.map +1 -0
  21. package/dist/agent-types.js +28 -45
  22. package/dist/agent-types.js.map +1 -0
  23. package/dist/context.d.ts +1 -0
  24. package/dist/context.d.ts.map +1 -0
  25. package/dist/context.js +3 -6
  26. package/dist/context.js.map +1 -0
  27. package/dist/cross-extension-rpc.d.ts +1 -0
  28. package/dist/cross-extension-rpc.d.ts.map +1 -0
  29. package/dist/cross-extension-rpc.js +6 -9
  30. package/dist/cross-extension-rpc.js.map +1 -0
  31. package/dist/custom-agents.d.ts +1 -0
  32. package/dist/custom-agents.d.ts.map +1 -0
  33. package/dist/custom-agents.js +16 -18
  34. package/dist/custom-agents.js.map +1 -0
  35. package/dist/default-agents.d.ts +1 -0
  36. package/dist/default-agents.d.ts.map +1 -0
  37. package/dist/default-agents.js +2 -4
  38. package/dist/default-agents.js.map +1 -0
  39. package/dist/enabled-models.d.ts +1 -0
  40. package/dist/enabled-models.d.ts.map +1 -0
  41. package/dist/enabled-models.js +12 -16
  42. package/dist/enabled-models.js.map +1 -0
  43. package/dist/env.d.ts +1 -0
  44. package/dist/env.d.ts.map +1 -0
  45. package/dist/env.js +2 -4
  46. package/dist/env.js.map +1 -0
  47. package/dist/group-join.d.ts +1 -0
  48. package/dist/group-join.d.ts.map +1 -0
  49. package/dist/group-join.js +2 -5
  50. package/dist/group-join.js.map +1 -0
  51. package/dist/index.d.ts +1 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +306 -292
  54. package/dist/index.js.map +1 -0
  55. package/dist/invocation-config.d.ts +1 -0
  56. package/dist/invocation-config.d.ts.map +1 -0
  57. package/dist/invocation-config.js +3 -6
  58. package/dist/invocation-config.js.map +1 -0
  59. package/dist/memory.d.ts +1 -0
  60. package/dist/memory.d.ts.map +1 -0
  61. package/dist/memory.js +25 -34
  62. package/dist/memory.js.map +1 -0
  63. package/dist/model-resolver.d.ts +1 -0
  64. package/dist/model-resolver.d.ts.map +1 -0
  65. package/dist/model-resolver.js +2 -4
  66. package/dist/model-resolver.js.map +1 -0
  67. package/dist/nico-overrides.d.ts +1 -0
  68. package/dist/nico-overrides.d.ts.map +1 -0
  69. package/dist/nico-overrides.js +15 -20
  70. package/dist/nico-overrides.js.map +1 -0
  71. package/dist/output-file.d.ts +1 -0
  72. package/dist/output-file.d.ts.map +1 -0
  73. package/dist/output-file.js +17 -22
  74. package/dist/output-file.js.map +1 -0
  75. package/dist/prompts.d.ts +1 -0
  76. package/dist/prompts.d.ts.map +1 -0
  77. package/dist/prompts.js +2 -4
  78. package/dist/prompts.js.map +1 -0
  79. package/dist/schedule-store.d.ts +1 -0
  80. package/dist/schedule-store.d.ts.map +1 -0
  81. package/dist/schedule-store.js +17 -21
  82. package/dist/schedule-store.js.map +1 -0
  83. package/dist/schedule.d.ts +1 -0
  84. package/dist/schedule.d.ts.map +1 -0
  85. package/dist/schedule.js +9 -12
  86. package/dist/schedule.js.map +1 -0
  87. package/dist/settings.d.ts +1 -0
  88. package/dist/settings.d.ts.map +1 -0
  89. package/dist/settings.js +16 -23
  90. package/dist/settings.js.map +1 -0
  91. package/dist/skill-loader.d.ts +1 -0
  92. package/dist/skill-loader.d.ts.map +1 -0
  93. package/dist/skill-loader.js +21 -23
  94. package/dist/skill-loader.js.map +1 -0
  95. package/dist/status-note.d.ts +1 -0
  96. package/dist/status-note.d.ts.map +1 -0
  97. package/dist/status-note.js +2 -4
  98. package/dist/status-note.js.map +1 -0
  99. package/dist/types.d.ts +5 -1
  100. package/dist/types.d.ts.map +1 -0
  101. package/dist/types.js +2 -4
  102. package/dist/types.js.map +1 -0
  103. package/dist/ui/agent-widget.d.ts +2 -5
  104. package/dist/ui/agent-widget.d.ts.map +1 -0
  105. package/dist/ui/agent-widget.js +29 -51
  106. package/dist/ui/agent-widget.js.map +1 -0
  107. package/dist/ui/ccstyle/diff/ansi-utils.d.ts +11 -0
  108. package/dist/ui/ccstyle/diff/ansi-utils.d.ts.map +1 -0
  109. package/dist/ui/ccstyle/diff/ansi-utils.js +145 -0
  110. package/dist/ui/ccstyle/diff/ansi-utils.js.map +1 -0
  111. package/dist/ui/ccstyle/diff/diff-presentation.d.ts +12 -0
  112. package/dist/ui/ccstyle/diff/diff-presentation.d.ts.map +1 -0
  113. package/dist/ui/ccstyle/diff/diff-presentation.js +49 -0
  114. package/dist/ui/ccstyle/diff/diff-presentation.js.map +1 -0
  115. package/dist/ui/ccstyle/diff/diff-renderer.d.ts +46 -0
  116. package/dist/ui/ccstyle/diff/diff-renderer.d.ts.map +1 -0
  117. package/dist/ui/ccstyle/diff/diff-renderer.js +2044 -0
  118. package/dist/ui/ccstyle/diff/diff-renderer.js.map +1 -0
  119. package/dist/ui/ccstyle/diff/line-width-safety.d.ts +12 -0
  120. package/dist/ui/ccstyle/diff/line-width-safety.d.ts.map +1 -0
  121. package/dist/ui/ccstyle/diff/line-width-safety.js +59 -0
  122. package/dist/ui/ccstyle/diff/line-width-safety.js.map +1 -0
  123. package/dist/ui/ccstyle/diff/render-utils.d.ts +6 -0
  124. package/dist/ui/ccstyle/diff/render-utils.d.ts.map +1 -0
  125. package/dist/ui/ccstyle/diff/render-utils.js +25 -0
  126. package/dist/ui/ccstyle/diff/render-utils.js.map +1 -0
  127. package/dist/ui/ccstyle/diff/types.d.ts +22 -0
  128. package/dist/ui/ccstyle/diff/types.d.ts.map +1 -0
  129. package/dist/ui/ccstyle/diff/types.js +10 -0
  130. package/dist/ui/ccstyle/diff/types.js.map +1 -0
  131. package/dist/ui/ccstyle/diff/write-display-utils.d.ts +2 -0
  132. package/dist/ui/ccstyle/diff/write-display-utils.d.ts.map +1 -0
  133. package/dist/ui/ccstyle/diff/write-display-utils.js +12 -0
  134. package/dist/ui/ccstyle/diff/write-display-utils.js.map +1 -0
  135. package/dist/ui/ccstyle/tool-renderer.d.ts +17 -0
  136. package/dist/ui/ccstyle/tool-renderer.d.ts.map +1 -0
  137. package/dist/ui/ccstyle/tool-renderer.js +58 -0
  138. package/dist/ui/ccstyle/tool-renderer.js.map +1 -0
  139. package/dist/ui/ccstyle/tool-result.d.ts +24 -0
  140. package/dist/ui/ccstyle/tool-result.d.ts.map +1 -0
  141. package/dist/ui/ccstyle/tool-result.js +193 -0
  142. package/dist/ui/ccstyle/tool-result.js.map +1 -0
  143. package/dist/ui/conversation-blocks.d.ts +46 -0
  144. package/dist/ui/conversation-blocks.d.ts.map +1 -0
  145. package/dist/ui/conversation-blocks.js +306 -0
  146. package/dist/ui/conversation-blocks.js.map +1 -0
  147. package/dist/ui/conversation-nvim.d.ts +8 -0
  148. package/dist/ui/conversation-nvim.d.ts.map +1 -0
  149. package/dist/ui/conversation-nvim.js +117 -0
  150. package/dist/ui/conversation-nvim.js.map +1 -0
  151. package/dist/ui/conversation-role.d.ts +13 -0
  152. package/dist/ui/conversation-role.d.ts.map +1 -0
  153. package/dist/ui/conversation-role.js +39 -0
  154. package/dist/ui/conversation-role.js.map +1 -0
  155. package/dist/ui/conversation-search.d.ts +39 -0
  156. package/dist/ui/conversation-search.d.ts.map +1 -0
  157. package/dist/ui/conversation-search.js +124 -0
  158. package/dist/ui/conversation-search.js.map +1 -0
  159. package/dist/ui/conversation-timeline.d.ts +62 -0
  160. package/dist/ui/conversation-timeline.d.ts.map +1 -0
  161. package/dist/ui/conversation-timeline.js +263 -0
  162. package/dist/ui/conversation-timeline.js.map +1 -0
  163. package/dist/ui/conversation-viewer.d.ts +62 -20
  164. package/dist/ui/conversation-viewer.d.ts.map +1 -0
  165. package/dist/ui/conversation-viewer.js +449 -224
  166. package/dist/ui/conversation-viewer.js.map +1 -0
  167. package/dist/ui/fleet-list.d.ts +24 -11
  168. package/dist/ui/fleet-list.d.ts.map +1 -0
  169. package/dist/ui/fleet-list.js +86 -78
  170. package/dist/ui/fleet-list.js.map +1 -0
  171. package/dist/ui/schedule-menu.d.ts +1 -0
  172. package/dist/ui/schedule-menu.d.ts.map +1 -0
  173. package/dist/ui/schedule-menu.js +2 -4
  174. package/dist/ui/schedule-menu.js.map +1 -0
  175. package/dist/ui/viewer-keys.d.ts +1 -0
  176. package/dist/ui/viewer-keys.d.ts.map +1 -0
  177. package/dist/ui/viewer-keys.js +8 -10
  178. package/dist/ui/viewer-keys.js.map +1 -0
  179. package/dist/usage.d.ts +1 -0
  180. package/dist/usage.d.ts.map +1 -0
  181. package/dist/usage.js +5 -10
  182. package/dist/usage.js.map +1 -0
  183. package/dist/worktree.d.ts +1 -0
  184. package/dist/worktree.d.ts.map +1 -0
  185. package/dist/worktree.js +27 -31
  186. package/dist/worktree.js.map +1 -0
  187. package/dist/write-execution.d.ts +42 -0
  188. package/dist/write-execution.d.ts.map +1 -0
  189. package/dist/write-execution.js +138 -0
  190. package/dist/write-execution.js.map +1 -0
  191. package/package.json +1 -1
  192. package/src/agent-history-list.ts +91 -0
  193. package/src/agent-history.ts +12 -17
  194. package/src/agent-manager.ts +169 -81
  195. package/src/agent-runner.ts +12 -13
  196. package/src/index.ts +117 -98
  197. package/src/output-file.ts +1 -1
  198. package/src/types.ts +4 -1
  199. package/src/ui/agent-widget.ts +3 -9
  200. package/src/ui/ccstyle/ATTRIBUTION.md +37 -0
  201. package/src/ui/ccstyle/diff/ansi-utils.ts +168 -0
  202. package/src/ui/ccstyle/diff/diff-presentation.ts +70 -0
  203. package/src/ui/ccstyle/diff/diff-renderer.ts +3148 -0
  204. package/src/ui/ccstyle/diff/line-width-safety.ts +92 -0
  205. package/src/ui/ccstyle/diff/render-utils.ts +32 -0
  206. package/src/ui/ccstyle/diff/types.ts +33 -0
  207. package/src/ui/ccstyle/diff/write-display-utils.ts +12 -0
  208. package/src/ui/ccstyle/tool-renderer.ts +71 -0
  209. package/src/ui/ccstyle/tool-result.ts +238 -0
  210. package/src/ui/conversation-blocks.ts +404 -0
  211. package/src/ui/conversation-nvim.ts +127 -0
  212. package/src/ui/conversation-role.ts +45 -0
  213. package/src/ui/conversation-search.ts +161 -0
  214. package/src/ui/conversation-timeline.ts +315 -0
  215. package/src/ui/conversation-viewer.ts +418 -210
  216. package/src/ui/fleet-list.ts +73 -52
  217. package/src/write-execution.ts +180 -0
  218. package/test/agent-history-list.test.ts +121 -0
  219. package/test/agent-manager-history.test.ts +59 -58
  220. package/test/agent-menu-history-navigation.test.ts +86 -0
  221. package/test/output-file-history.test.ts +50 -0
  222. package/test/ui/conversation-blocks.test.ts +103 -0
  223. package/test/ui/conversation-diff.test.ts +91 -0
  224. package/test/ui/conversation-search.test.ts +55 -0
  225. package/test/ui/conversation-viewer.test.ts +182 -0
  226. package/test/ui/fleet-list.test.ts +131 -0
  227. package/test/write-execution.test.ts +83 -0
  228. package/tsconfig.json +16 -0
  229. package/vitest.temp.config.mts +6 -0
  230. package/dist/ui/markdown-result.d.ts +0 -3
  231. package/dist/ui/markdown-result.js +0 -53
  232. package/src/ui/markdown-result.ts +0 -56
  233. package/test/agent-history.test.ts +0 -63
  234. package/test/ui/markdown-result.test.ts +0 -45
@@ -1,27 +1,31 @@
1
- "use strict";
2
1
  /**
3
- * conversation-viewer.ts — Live conversation overlay for viewing agent sessions.
2
+ * conversation-viewer.ts — Live and historical conversation overlay.
4
3
  *
5
- * Displays a scrollable, live-updating view of an agent's conversation.
6
- * Subscribes to session events for real-time streaming updates.
4
+ * The viewer deliberately keeps rendering state separate from the transcript:
5
+ * blocks are formatted data, while Markdown/ANSI lines are a bounded cache.
7
6
  */
8
- Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.ConversationViewer = exports.VIEWPORT_HEIGHT_PCT = void 0;
10
- exports.createStaticConversationSource = createStaticConversationSource;
11
- const pi_tui_1 = require("@earendil-works/pi-tui");
12
- const context_js_1 = require("../context.js");
13
- const usage_js_1 = require("../usage.js");
14
- const agent_widget_js_1 = require("./agent-widget.js");
15
- const viewer_keys_js_1 = require("./viewer-keys.js");
16
- /** Base lines consumed by chrome: top border + header + header sep + footer sep + footer + bottom border. */
7
+ import { copyToClipboard } from "@earendil-works/pi-coding-agent";
8
+ import { Input, Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
9
+ import { getLifetimeTotal, getSessionContextPercent } from "../usage.js";
10
+ import { buildInvocationTags, describeActivity, fgPreservingNestedStyles, formatDuration, formatSessionTokens, getDisplayName, getPromptModeLabel } from "./agent-widget.js";
11
+ import { formatConversationMessages } from "./conversation-blocks.js";
12
+ import { ConversationTimeline } from "./conversation-timeline.js";
13
+ import { renderConversationRoleHeader } from "./conversation-role.js";
14
+ import { editLiveAssistantBlockInNvim, viewConversationBlockInNvim } from "./conversation-nvim.js";
15
+ import { findConversationMatches, highlightConversationLine, nearestConversationMatchAfter, nearestConversationMatchBefore, stripAnsi } from "./conversation-search.js";
16
+ import { createViewerKeys } from "./viewer-keys.js";
17
+ /** Base lines consumed by the overlay: borders, header, separator and footer. */
17
18
  const CHROME_LINES_BASE = 6;
18
19
  const MIN_VIEWPORT = 3;
19
- /** Height ceiling shared by the overlay's `maxHeight` and the viewer's internal viewport cap. */
20
- exports.VIEWPORT_HEIGHT_PCT = 70;
21
- function createStaticConversationSource(messages) {
20
+ export const VIEWPORT_HEIGHT_PCT = 70;
21
+ const MAX_COPY_CHARS = 500_000;
22
+ const MATCH_BG = "\x1b[48;5;238m";
23
+ const MATCH_BG_CURRENT = "\x1b[48;5;220m\x1b[30m";
24
+ const BG_RESET = "\x1b[49m\x1b[39m";
25
+ export function createStaticConversationSource(messages) {
22
26
  return { messages, subscribe: () => () => { } };
23
27
  }
24
- class ConversationViewer {
28
+ export class ConversationViewer {
25
29
  tui;
26
30
  session;
27
31
  record;
@@ -30,23 +34,38 @@ class ConversationViewer {
30
34
  done;
31
35
  onStop;
32
36
  onSteer;
37
+ operations;
38
+ blocks = [];
39
+ showTools = true;
40
+ selectedMessageIdx = 0;
41
+ cachedWidth = 0;
42
+ cachedLines = [];
43
+ cachedMessageStarts = [];
44
+ cachedMessageBlocks = [];
45
+ cacheVersion = 0;
46
+ cachedVersion = -1;
33
47
  scrollOffset = 0;
34
48
  autoScroll = true;
35
49
  unsubscribe;
36
50
  lastInnerW = 0;
37
51
  closed = false;
38
- /** Two-press confirm guard for the stop key, so a stray key can't kill the agent. */
39
52
  stopArmed = false;
40
53
  keys;
41
- /** Steering composer — present while the user is typing a message to the agent. */
42
54
  composer;
55
+ searchMode = false;
56
+ searchQuery = "";
57
+ searchMatches = [];
58
+ currentMatchIdx = -1;
59
+ timeline;
43
60
  constructor(tui, session, record, activity, theme, done,
44
- /** Abort the agent shown here. Omitted → no stop affordance (e.g. read-only history). */
61
+ /** Abort the agent shown here. Omitted means read-only history. */
45
62
  onStop,
46
- /** User keybindings from `ctx.ui.custom()`. Omitted → hardcoded defaults. */
63
+ /** User keybindings from ctx.ui.custom(). */
47
64
  keybindings,
48
- /** Send a steering message to the agent. Omitted → no compose affordance. */
49
- onSteer) {
65
+ /** Send a steering message to the agent. */
66
+ onSteer,
67
+ /** Optional clipboard/nvim operation context, appended for compatibility. */
68
+ operations) {
50
69
  this.tui = tui;
51
70
  this.session = session;
52
71
  this.record = record;
@@ -55,37 +74,57 @@ class ConversationViewer {
55
74
  this.done = done;
56
75
  this.onStop = onStop;
57
76
  this.onSteer = onSteer;
58
- this.keys = (0, viewer_keys_js_1.createViewerKeys)(keybindings);
77
+ this.operations = operations;
78
+ this.keys = createViewerKeys(keybindings);
79
+ this.blocks = formatConversationMessages(session.messages);
80
+ this.timeline = new ConversationTimeline(tui, theme, {
81
+ cwd: operations?.ctx.cwd,
82
+ record,
83
+ onChange: () => {
84
+ this.cacheVersion++;
85
+ this.invalidate();
86
+ this.tui.requestRender();
87
+ },
88
+ });
89
+ this.timeline.setBlocks(this.blocks);
59
90
  this.unsubscribe = session.subscribe(() => {
60
91
  if (this.closed)
61
92
  return;
93
+ this.blocks = formatConversationMessages(this.session.messages);
94
+ this.timeline.setBlocks(this.blocks);
95
+ this.cacheVersion++;
96
+ this.invalidate();
62
97
  this.tui.requestRender();
63
98
  });
64
99
  }
65
100
  handleInput(data) {
66
- // While composing a steer message, the input owns all keys (Enter sends,
67
- // Esc cancels — both wired in openComposer()). Editing keys flow through.
68
101
  if (this.composer) {
69
102
  this.composer.handleInput(data);
70
103
  this.tui.requestRender();
71
104
  return;
72
105
  }
73
- if ((0, pi_tui_1.matchesKey)(data, "escape") || (0, pi_tui_1.matchesKey)(data, "q")) {
106
+ if (this.searchMode) {
107
+ this.handleSearchInput(data);
108
+ return;
109
+ }
110
+ if (matchesKey(data, "escape") || matchesKey(data, "q")) {
74
111
  this.closed = true;
75
112
  this.done(undefined);
76
113
  return;
77
114
  }
78
- // Enter opens the steering composer (only while the agent can still be
79
- // steered) — then type + Enter sends, Esc or an empty submit returns. When
80
- // not steerable, fall through so the key still disarms a pending stop.
81
- if ((0, pi_tui_1.matchesKey)(data, "enter") && this.canSteer()) {
82
- this.stopArmed = false;
83
- this.openComposer();
84
- return;
115
+ if (matchesKey(data, "enter")) {
116
+ this.buildContentLines(this.lastInnerW || 80);
117
+ if (this.timeline.isToolFocused()) {
118
+ this.timeline.toggleFocusedTool();
119
+ return;
120
+ }
121
+ if (this.canSteer()) {
122
+ this.stopArmed = false;
123
+ this.openComposer();
124
+ return;
125
+ }
85
126
  }
86
- // Stop/abort the agent (only while it can still be stopped). Two-press:
87
- // first "x" arms, second confirms — any other key disarms.
88
- if ((0, pi_tui_1.matchesKey)(data, "x")) {
127
+ if (matchesKey(data, "x")) {
89
128
  if (this.isStoppable()) {
90
129
  if (this.stopArmed) {
91
130
  this.stopArmed = false;
@@ -100,137 +139,435 @@ class ConversationViewer {
100
139
  }
101
140
  if (this.stopArmed)
102
141
  this.stopArmed = false;
103
- const totalLines = this.buildContentLines(this.lastInnerW).length;
142
+ // Input can arrive before the first render in tests and in a freshly-opened overlay.
143
+ this.buildContentLines(this.lastInnerW || 80);
104
144
  const viewportHeight = this.viewportHeight();
105
- const maxScroll = Math.max(0, totalLines - viewportHeight);
106
- if (this.keys.scrollUp(data)) {
107
- this.scrollOffset = Math.max(0, this.scrollOffset - 1);
108
- this.autoScroll = this.scrollOffset >= maxScroll;
145
+ const maxScroll = Math.max(0, this.cachedLines.length - viewportHeight);
146
+ if (data === "t") {
147
+ this.showTools = !this.showTools;
148
+ this.timeline.setShowTools(this.showTools);
149
+ this.invalidate();
150
+ this.tui.requestRender();
151
+ }
152
+ else if (data === "\t") {
153
+ this.timeline.moveFocus(1);
154
+ this.invalidate();
155
+ this.tui.requestRender();
156
+ }
157
+ else if (matchesKey(data, Key.shift("tab"))) {
158
+ this.timeline.moveFocus(-1);
159
+ this.invalidate();
160
+ this.tui.requestRender();
161
+ }
162
+ else if (data === "J" || matchesKey(data, Key.shift("j"))) {
163
+ this.jumpToMessage(1);
164
+ }
165
+ else if (data === "K" || matchesKey(data, Key.shift("k"))) {
166
+ this.jumpToMessage(-1);
167
+ }
168
+ else if (data === "g") {
169
+ this.scrollToTop();
170
+ }
171
+ else if (data === "G" || matchesKey(data, Key.shift("g"))) {
172
+ this.scrollToBottom();
173
+ }
174
+ else if (data === "/") {
175
+ this.enterSearch();
176
+ }
177
+ else if (data === "n") {
178
+ this.gotoMatch(1);
179
+ }
180
+ else if (data === "N") {
181
+ this.gotoMatch(-1);
182
+ }
183
+ else if (data === "M") {
184
+ void this.copyCurrentMessage();
185
+ }
186
+ else if (data === "o") {
187
+ void this.viewCurrentInNvim();
188
+ }
189
+ else if (data === "O" && this.isLive()) {
190
+ void this.editCurrentInNvim();
191
+ }
192
+ else if (this.keys.scrollUp(data)) {
193
+ this.scrollBy(-1);
109
194
  }
110
195
  else if (this.keys.scrollDown(data)) {
111
- this.scrollOffset = Math.min(maxScroll, this.scrollOffset + 1);
112
- this.autoScroll = this.scrollOffset >= maxScroll;
196
+ this.scrollBy(1);
113
197
  }
114
198
  else if (this.keys.pageUp(data)) {
115
- this.scrollOffset = Math.max(0, this.scrollOffset - viewportHeight);
116
- this.autoScroll = false;
199
+ this.scrollBy(-viewportHeight);
117
200
  }
118
201
  else if (this.keys.pageDown(data)) {
119
- this.scrollOffset = Math.min(maxScroll, this.scrollOffset + viewportHeight);
120
- this.autoScroll = this.scrollOffset >= maxScroll;
202
+ this.scrollBy(viewportHeight);
121
203
  }
122
- else if ((0, pi_tui_1.matchesKey)(data, "home")) {
204
+ else if (matchesKey(data, "home")) {
123
205
  this.scrollOffset = 0;
206
+ this.selectedMessageIdx = 0;
124
207
  this.autoScroll = false;
208
+ this.tui.requestRender();
125
209
  }
126
- else if ((0, pi_tui_1.matchesKey)(data, "end")) {
127
- this.scrollOffset = maxScroll;
128
- this.autoScroll = true;
210
+ else if (matchesKey(data, "end")) {
211
+ this.scrollToBottom();
212
+ }
213
+ // Keep the computed max alive for the key path even when the cache is empty.
214
+ void maxScroll;
215
+ }
216
+ handleMouse(event) {
217
+ if (this.composer || this.searchMode)
218
+ return undefined;
219
+ const innerW = Math.max(1, this.lastInnerW || event.width - 4);
220
+ const contentLines = this.buildContentLines(innerW);
221
+ const viewportHeight = this.viewportHeight();
222
+ const maxScroll = Math.max(0, contentLines.length - viewportHeight);
223
+ const visibleStart = Math.min(this.scrollOffset, maxScroll);
224
+ const contentOrigin = 3 + (this.invocationLine() ? 1 : 0);
225
+ const contentY = event.y - contentOrigin + visibleStart;
226
+ if (contentY < 0 || contentY >= contentLines.length)
227
+ return undefined;
228
+ const result = this.timeline.handleMouse({ ...event, y: contentY });
229
+ if (result?.handled) {
230
+ this.cacheVersion++;
231
+ this.invalidate();
232
+ this.tui.requestRender();
129
233
  }
234
+ return result;
130
235
  }
131
236
  render(width) {
132
237
  if (width < 6)
133
- return []; // too narrow for any meaningful rendering
238
+ return [];
134
239
  const th = this.theme;
135
- const innerW = width - 4; // border + padding
240
+ const innerW = Math.max(1, width - 4);
136
241
  this.lastInnerW = innerW;
137
242
  const lines = [];
138
- const pad = (s, len) => {
139
- const vis = (0, pi_tui_1.visibleWidth)(s);
140
- return s + " ".repeat(Math.max(0, len - vis));
141
- };
142
- const row = (content) => th.fg("border", "│") + " " + (0, pi_tui_1.truncateToWidth)(pad(content, innerW), innerW, "...", true) + " " + th.fg("border", "│");
143
- const hrTop = th.fg("border", `╭${"─".repeat(width - 2)}╮`);
144
- const hrBot = th.fg("border", `╰${"─".repeat(width - 2)}╯`);
243
+ const pad = (s, len) => s + " ".repeat(Math.max(0, len - visibleWidth(s)));
244
+ const row = (content) => th.fg("border", "│") + " " + truncateToWidth(pad(content, innerW), innerW, "...", true) + " " + th.fg("border", "│");
245
+ const hrTop = th.fg("border", `╭${"─".repeat(Math.max(0, width - 2))}╮`);
246
+ const hrBot = th.fg("border", `╰${"─".repeat(Math.max(0, width - 2))}╯`);
145
247
  const hrMid = row(th.fg("dim", "─".repeat(innerW)));
146
- // Header
147
248
  lines.push(hrTop);
148
- const name = (0, agent_widget_js_1.getDisplayName)(this.record.type);
149
- const modeLabel = (0, agent_widget_js_1.getPromptModeLabel)(this.record.type);
249
+ const name = getDisplayName(this.record.type);
250
+ const modeLabel = getPromptModeLabel(this.record.type);
150
251
  const modeTag = modeLabel ? ` ${th.fg("dim", `(${modeLabel})`)}` : "";
151
- const statusIcon = this.record.status === "running"
152
- ? th.fg("accent", "●")
153
- : this.record.status === "completed"
154
- ? th.fg("success", "✓")
155
- : this.record.status === "error"
156
- ? th.fg("error", "✗")
157
- : th.fg("dim", "○");
158
- const duration = (0, agent_widget_js_1.formatDuration)(this.record.startedAt, this.record.completedAt);
252
+ const statusIcon = this.record.status === "running" ? th.fg("accent", "●") : this.record.status === "completed" ? th.fg("success", "✓") : this.record.status === "error" ? th.fg("error", "✗") : th.fg("dim", "○");
253
+ const duration = formatDuration(this.record.startedAt, this.record.completedAt);
159
254
  const headerParts = [duration];
160
255
  const toolUses = this.activity?.toolUses ?? this.record.toolUses;
161
256
  if (toolUses > 0)
162
257
  headerParts.unshift(`${toolUses} tool${toolUses === 1 ? "" : "s"}`);
163
- const tokens = (0, usage_js_1.getLifetimeTotal)(this.activity?.lifetimeUsage);
258
+ const tokens = getLifetimeTotal(this.activity?.lifetimeUsage);
164
259
  if (tokens > 0) {
165
- const percent = (0, usage_js_1.getSessionContextPercent)(this.activity?.session);
166
- headerParts.push((0, agent_widget_js_1.formatSessionTokens)(tokens, percent, th, this.record.compactionCount));
260
+ const percent = getSessionContextPercent(this.activity?.session);
261
+ headerParts.push(formatSessionTokens(tokens, percent, th, this.record.compactionCount));
167
262
  }
168
- lines.push(row(`${statusIcon} ${th.bold(name)}${modeTag} ${th.fg("muted", this.record.description)} ${th.fg("dim", "·")} ${(0, agent_widget_js_1.fgPreservingNestedStyles)(th, "dim", headerParts.join(" · "))}`));
263
+ lines.push(row(`${statusIcon} ${th.bold(name)}${modeTag} ${th.fg("muted", this.record.description)} ${th.fg("dim", "·")} ${fgPreservingNestedStyles(th, "dim", headerParts.join(" · "))}`));
169
264
  const invocationLine = this.invocationLine();
170
265
  if (invocationLine)
171
266
  lines.push(row(invocationLine));
172
267
  lines.push(hrMid);
173
- // Content area — rebuild every render (live data, no cache needed)
174
268
  const contentLines = this.buildContentLines(innerW);
175
269
  const viewportHeight = this.viewportHeight();
176
270
  const maxScroll = Math.max(0, contentLines.length - viewportHeight);
177
- if (this.autoScroll) {
271
+ if (this.autoScroll)
178
272
  this.scrollOffset = maxScroll;
179
- }
180
273
  const visibleStart = Math.min(this.scrollOffset, maxScroll);
181
274
  const visible = contentLines.slice(visibleStart, visibleStart + viewportHeight);
182
- for (let i = 0; i < viewportHeight; i++) {
183
- lines.push(row(visible[i] ?? ""));
275
+ const currentBlockIdx = this.currentBlockGlobalIndex();
276
+ const headerLine = this.activeHeaderLine();
277
+ const showSticky = headerLine >= 0 && headerLine < visibleStart && visible.length > 0;
278
+ const displayed = visible.slice();
279
+ if (showSticky) {
280
+ const block = this.cachedMessageBlocks[this.currentMessageIndex()];
281
+ if (block) {
282
+ const header = renderConversationRoleHeader(block, this.theme);
283
+ displayed[0] = { text: ` ${header}`, plain: header, blockIndex: currentBlockIdx, railable: true };
284
+ }
285
+ }
286
+ for (let i = 0; i < displayed.length; i++) {
287
+ const line = displayed[i];
288
+ let text = line.text;
289
+ if (this.searchMatches.length > 0 && !(showSticky && i === 0)) {
290
+ const hits = this.searchMatches.filter((match) => match.lineIdx === visibleStart + i).map((match) => ({ lineIdx: match.lineIdx, col: match.col, len: match.len, current: this.searchMatches.indexOf(match) === this.currentMatchIdx }));
291
+ if (hits.length > 0)
292
+ text = highlightConversationLine(text, hits, MATCH_BG, MATCH_BG_CURRENT, BG_RESET);
293
+ }
294
+ if (line.railable && line.blockIndex === currentBlockIdx) {
295
+ text = text.startsWith(" ") ? th.fg("accent", "▎") + text.slice(1) : th.fg("accent", "▎") + text;
296
+ }
297
+ lines.push(row(text));
184
298
  }
185
- // Footer
299
+ for (let i = displayed.length; i < viewportHeight; i++)
300
+ lines.push(row(""));
186
301
  lines.push(hrMid);
187
302
  if (this.composer) {
188
- // Composer row: the Input renders its own `> ` prompt and cursor.
189
303
  lines.push(row(this.composer.render(innerW)[0] ?? ""));
190
- const composeHint = th.fg("dim", "Enter send · Esc cancel");
191
- const composeLeft = th.fg("accent", "✎ steer");
192
- const composeGap = Math.max(1, innerW - (0, pi_tui_1.visibleWidth)(composeLeft) - (0, pi_tui_1.visibleWidth)(composeHint));
193
- lines.push(row(composeLeft + " ".repeat(composeGap) + composeHint));
304
+ const hint = th.fg("dim", "Enter send · Esc cancel");
305
+ const label = th.fg("accent", "✎ steer");
306
+ lines.push(row(label + " ".repeat(Math.max(1, innerW - visibleWidth(label) - visibleWidth(hint))) + hint));
307
+ }
308
+ else if (this.searchMode) {
309
+ const query = `/${this.searchQuery}`;
310
+ const matchCount = this.searchMatches.length > 0
311
+ ? ` · ${this.searchMatches.length} match${this.searchMatches.length === 1 ? "" : "es"}`
312
+ : "";
313
+ const hints = th.fg("dim", " · Enter apply · Esc cancel");
314
+ lines.push(row(truncateToWidth(th.fg("accent", query) + th.fg("dim", matchCount) + hints, innerW, "...", true)));
194
315
  }
195
316
  else {
196
- // Actions on the left, navigation on the right. The scroll hint keeps its
197
- // full key list so the less-obvious bindings stay discoverable; it leads
198
- // the right group so "Esc close" is the only part that truncates first.
199
- const sep = th.fg("dim", " · ");
200
317
  const actions = [];
201
318
  if (this.canSteer())
202
319
  actions.push(th.fg("dim", "Enter steer"));
203
- if (this.isStoppable()) {
320
+ if (this.isStoppable())
204
321
  actions.push(this.stopArmed ? th.fg("error", "x again to STOP") : th.fg("dim", "x stop"));
205
- }
206
- const footerRight = th.fg("dim", "↑↓ scroll · PgUp/PgDn or Shift+↑↓ · Esc close");
207
- // Prepend the line-count/scroll-% readout only when there's spare width —
208
- // it's the first thing dropped so it never crowds out the hints.
209
- const scrollPct = contentLines.length <= viewportHeight
210
- ? "100%"
211
- : `${Math.round(((visibleStart + viewportHeight) / contentLines.length) * 100)}%`;
322
+ const editHint = this.isLive() ? " O" : "";
323
+ const shortcutHints = [
324
+ `j/k · J/K · g/G t M o${editHint} /n N q`,
325
+ `M o${editHint} /n N q`,
326
+ `M o${editHint} /n N q`,
327
+ ];
328
+ const scrollPct = contentLines.length <= viewportHeight ? "100%" : `${Math.round(((visibleStart + viewportHeight) / contentLines.length) * 100)}%`;
212
329
  const count = th.fg("dim", `${contentLines.length} lines · ${scrollPct}`);
213
- const withCount = [count, ...actions].join(sep);
214
- const footerLeft = (0, pi_tui_1.visibleWidth)(withCount) + (0, pi_tui_1.visibleWidth)(footerRight) + 1 <= innerW
215
- ? withCount
216
- : actions.join(sep);
217
- const footerGap = Math.max(1, innerW - (0, pi_tui_1.visibleWidth)(footerLeft) - (0, pi_tui_1.visibleWidth)(footerRight));
218
- lines.push(row(footerLeft + " ".repeat(footerGap) + footerRight));
330
+ const countWidth = visibleWidth(count);
331
+ let status = "";
332
+ for (const hint of shortcutHints) {
333
+ const styledHint = th.fg("dim", hint);
334
+ const withActions = [styledHint, ...actions].join(" ");
335
+ if (visibleWidth(withActions) <= innerW) {
336
+ status = withActions;
337
+ break;
338
+ }
339
+ if (visibleWidth(styledHint) <= innerW)
340
+ status ||= styledHint;
341
+ }
342
+ if (!status)
343
+ status = th.fg("dim", shortcutHints[shortcutHints.length - 1]);
344
+ const statusWidth = visibleWidth(status);
345
+ const footer = statusWidth + countWidth + 1 <= innerW
346
+ ? status + " ".repeat(Math.max(1, innerW - statusWidth - countWidth)) + count
347
+ : status;
348
+ lines.push(row(truncateToWidth(footer, innerW, "...", true)));
219
349
  }
220
350
  lines.push(hrBot);
221
351
  return lines;
222
352
  }
223
- /** Stoppable only when a stop handler exists and the agent is still active. */
353
+ invalidate() {
354
+ this.cachedWidth = 0;
355
+ this.cachedLines = [];
356
+ this.cachedMessageStarts = [];
357
+ this.cachedMessageBlocks = [];
358
+ this.cachedVersion = -1;
359
+ }
360
+ dispose() {
361
+ this.closed = true;
362
+ this.unsubscribe?.();
363
+ this.unsubscribe = undefined;
364
+ }
365
+ viewportHeight() {
366
+ const maxRows = Math.floor((this.tui.terminal.rows * VIEWPORT_HEIGHT_PCT) / 100);
367
+ return Math.max(MIN_VIEWPORT, maxRows - this.chromeLines());
368
+ }
369
+ chromeLines() {
370
+ return CHROME_LINES_BASE + (this.invocationLine() ? 1 : 0) + (this.composer ? 1 : 0);
371
+ }
372
+ invocationLine() {
373
+ const liveModel = this.isLive() ? this.record.session?.model : undefined;
374
+ const invocation = this.record.invocation
375
+ ? {
376
+ ...this.record.invocation,
377
+ ...(liveModel && { effectiveModelName: liveModel.name ?? liveModel.id }),
378
+ ...(this.isLive() && { effectiveThinking: this.record.session?.thinkingLevel }),
379
+ }
380
+ : liveModel || (this.isLive() && this.record.session?.thinkingLevel)
381
+ ? {
382
+ effectiveModelName: liveModel?.name ?? liveModel?.id,
383
+ effectiveThinking: this.record.session?.thinkingLevel,
384
+ }
385
+ : undefined;
386
+ if (!invocation)
387
+ return this.theme.fg("dim", " ↳ model: unknown · thinking: unknown");
388
+ const { modelName, tags } = buildInvocationTags(invocation);
389
+ const effectiveModelName = invocation.effectiveModelName ?? modelName;
390
+ const parts = effectiveModelName ? [`model: ${effectiveModelName}`, ...tags] : ["model: inherit", ...tags];
391
+ return this.theme.fg("dim", ` ↳ ${parts.join(" · ")}`);
392
+ }
393
+ buildContentLines(width) {
394
+ if (width <= 0)
395
+ return [];
396
+ if (this.cachedVersion === this.cacheVersion && this.cachedWidth === width)
397
+ return this.cachedLines;
398
+ this.timeline.setShowTools(this.showTools);
399
+ this.timeline.render(width);
400
+ const timelineSnapshot = this.timeline.getSnapshot();
401
+ const lines = timelineSnapshot.lines.map((line) => ({ ...line }));
402
+ this.cachedMessageStarts = [...timelineSnapshot.messageStarts];
403
+ this.cachedMessageBlocks = [...timelineSnapshot.messageBlocks];
404
+ if (this.record.status === "running" && this.activity) {
405
+ const act = describeActivity(this.activity.activeTools, this.activity.responseText);
406
+ const text = `▍ ${act}`;
407
+ lines.push({ text: truncateToWidth(this.theme.fg("accent", "▍ ") + this.theme.fg("dim", act), width), plain: text, blockIndex: -1 });
408
+ }
409
+ this.cachedWidth = width;
410
+ this.cachedVersion = this.cacheVersion;
411
+ this.cachedLines = lines;
412
+ if (this.searchMode || this.searchQuery)
413
+ this.recomputeMatches();
414
+ return lines;
415
+ }
416
+ currentMessageIndex() {
417
+ return this.cachedMessageStarts.length === 0 ? 0 : Math.max(0, Math.min(this.selectedMessageIdx, this.cachedMessageStarts.length - 1));
418
+ }
419
+ currentBlockGlobalIndex() {
420
+ const block = this.cachedMessageBlocks[this.currentMessageIndex()];
421
+ return block ? this.blocks.indexOf(block) : -1;
422
+ }
423
+ activeHeaderLine() {
424
+ return this.cachedMessageStarts[this.currentMessageIndex()] ?? -1;
425
+ }
426
+ currentBlock() {
427
+ return this.cachedMessageBlocks[this.currentMessageIndex()];
428
+ }
429
+ messageIndexForOffset(offset) {
430
+ let result = 0;
431
+ for (let i = 0; i < this.cachedMessageStarts.length; i++) {
432
+ if (this.cachedMessageStarts[i] <= offset)
433
+ result = i;
434
+ else
435
+ break;
436
+ }
437
+ return result;
438
+ }
439
+ jumpToMessage(dir) {
440
+ if (this.cachedMessageStarts.length === 0)
441
+ return;
442
+ const target = this.currentMessageIndex() + dir;
443
+ if (target < 0 || target >= this.cachedMessageStarts.length)
444
+ return;
445
+ this.selectedMessageIdx = target;
446
+ this.scrollOffset = Math.min(this.cachedMessageStarts[target], Math.max(0, this.cachedLines.length - this.viewportHeight()));
447
+ this.autoScroll = false;
448
+ this.tui.requestRender();
449
+ }
450
+ scrollBy(delta) {
451
+ const max = Math.max(0, this.cachedLines.length - this.viewportHeight());
452
+ this.scrollOffset = Math.max(0, Math.min(max, this.scrollOffset + delta));
453
+ this.selectedMessageIdx = this.messageIndexForOffset(this.scrollOffset);
454
+ this.autoScroll = this.scrollOffset >= max;
455
+ this.tui.requestRender();
456
+ }
457
+ scrollToTop() {
458
+ this.scrollOffset = 0;
459
+ this.selectedMessageIdx = 0;
460
+ this.autoScroll = false;
461
+ this.tui.requestRender();
462
+ }
463
+ scrollToBottom() {
464
+ this.scrollOffset = Math.max(0, this.cachedLines.length - this.viewportHeight());
465
+ this.selectedMessageIdx = Math.max(0, this.cachedMessageStarts.length - 1);
466
+ this.autoScroll = true;
467
+ this.tui.requestRender();
468
+ }
469
+ enterSearch() {
470
+ this.searchMode = true;
471
+ this.searchQuery = "";
472
+ this.searchMatches = [];
473
+ this.currentMatchIdx = -1;
474
+ this.tui.requestRender();
475
+ }
476
+ handleSearchInput(data) {
477
+ if (matchesKey(data, "escape") || matchesKey(data, Key.ctrl("c"))) {
478
+ this.searchMode = false;
479
+ this.searchQuery = "";
480
+ this.searchMatches = [];
481
+ this.currentMatchIdx = -1;
482
+ }
483
+ else if (matchesKey(data, "enter") || data === "\r" || data === "\n") {
484
+ this.searchMode = false;
485
+ if (this.searchMatches.length > 0)
486
+ this.scrollToMatch(this.currentMatchIdx < 0 ? 0 : this.currentMatchIdx);
487
+ }
488
+ else if (matchesKey(data, "backspace") || data === "\x7f" || data === "\b") {
489
+ this.searchQuery = this.searchQuery.slice(0, -1);
490
+ this.recomputeMatches();
491
+ }
492
+ else if (data.length === 1 && data >= " " && data !== "\x7f") {
493
+ this.searchQuery += data;
494
+ this.recomputeMatches();
495
+ }
496
+ this.tui.requestRender();
497
+ }
498
+ recomputeMatches() {
499
+ this.searchMatches = findConversationMatches(this.cachedLines, this.searchQuery);
500
+ this.currentMatchIdx = this.searchMatches.length > 0 ? Math.min(Math.max(this.currentMatchIdx, 0), this.searchMatches.length - 1) : -1;
501
+ }
502
+ gotoMatch(dir) {
503
+ if (this.searchMatches.length === 0)
504
+ return;
505
+ this.currentMatchIdx = this.currentMatchIdx < 0
506
+ ? (dir === 1 ? nearestConversationMatchAfter(this.searchMatches, this.scrollOffset) : nearestConversationMatchBefore(this.searchMatches, this.scrollOffset))
507
+ : (this.currentMatchIdx + dir + this.searchMatches.length) % this.searchMatches.length;
508
+ this.scrollToMatch(this.currentMatchIdx);
509
+ this.tui.requestRender();
510
+ }
511
+ scrollToMatch(index) {
512
+ const match = this.searchMatches[index];
513
+ if (!match)
514
+ return;
515
+ const max = Math.max(0, this.cachedLines.length - this.viewportHeight());
516
+ this.scrollOffset = Math.max(0, Math.min(max, match.lineIdx - Math.floor(this.viewportHeight() / 2)));
517
+ this.selectedMessageIdx = this.messageIndexForOffset(this.scrollOffset);
518
+ this.autoScroll = false;
519
+ }
520
+ async copyCurrentMessage() {
521
+ const block = this.currentBlock();
522
+ if (!block || !this.operations) {
523
+ if (!this.operations)
524
+ return;
525
+ return;
526
+ }
527
+ const source = `${stripAnsi(block.header)}\n${block.copyText}`.trimEnd();
528
+ const clipped = source.length > MAX_COPY_CHARS ? `${source.slice(0, MAX_COPY_CHARS)}\n… [truncated by subagent copy]` : source;
529
+ try {
530
+ await copyToClipboard(clipped);
531
+ this.operations.ctx.ui.notify(`Copied current message (${clipped.length} chars)`, "info");
532
+ }
533
+ catch (error) {
534
+ this.operations.ctx.ui.notify(`Copy failed: ${error instanceof Error ? error.message : String(error)}`, "error");
535
+ }
536
+ }
537
+ async viewCurrentInNvim() {
538
+ const block = this.currentBlock();
539
+ if (!block || !this.operations)
540
+ return;
541
+ await viewConversationBlockInNvim(this.tui, this.operations.ctx, block);
542
+ this.invalidate();
543
+ this.tui.requestRender(true);
544
+ }
545
+ async editCurrentInNvim() {
546
+ const block = this.currentBlock();
547
+ if (!block || !this.operations || !this.isLive() || block.role !== "assistant")
548
+ return;
549
+ const sent = await editLiveAssistantBlockInNvim(this.operations.pi, this.tui, this.operations.ctx, block);
550
+ if (sent)
551
+ this.done(undefined);
552
+ else {
553
+ this.invalidate();
554
+ this.tui.requestRender(true);
555
+ }
556
+ }
557
+ isReadOnly() {
558
+ return this.operations?.readOnly === true;
559
+ }
560
+ isLive() {
561
+ return !this.isReadOnly() && this.record.session !== undefined;
562
+ }
224
563
  isStoppable() {
225
- return !!this.onStop && (this.record.status === "running" || this.record.status === "queued");
564
+ return !this.isReadOnly() && !!this.onStop && (this.record.status === "running" || this.record.status === "queued");
226
565
  }
227
- /** Steerable only when a steer handler exists and the agent is still active. */
228
566
  canSteer() {
229
- return !!this.onSteer && (this.record.status === "running" || this.record.status === "queued");
567
+ return !this.isReadOnly() && !!this.onSteer && (this.record.status === "running" || this.record.status === "queued");
230
568
  }
231
- /** Open the inline steering composer and route subsequent input to it. */
232
569
  openComposer() {
233
- const input = new pi_tui_1.Input();
570
+ const input = new Input();
234
571
  input.focused = true;
235
572
  input.onSubmit = (value) => {
236
573
  const message = value.trim();
@@ -246,117 +583,5 @@ class ConversationViewer {
246
583
  this.composer = input;
247
584
  this.tui.requestRender();
248
585
  }
249
- invalidate() { }
250
- dispose() {
251
- this.closed = true;
252
- if (this.unsubscribe) {
253
- this.unsubscribe();
254
- this.unsubscribe = undefined;
255
- }
256
- }
257
- // ---- Private ----
258
- viewportHeight() {
259
- // Cap mirrors the overlay's maxHeight — otherwise the viewer would render
260
- // more lines than the overlay shows and clip the footer.
261
- const maxRows = Math.floor((this.tui.terminal.rows * exports.VIEWPORT_HEIGHT_PCT) / 100);
262
- return Math.max(MIN_VIEWPORT, maxRows - this.chromeLines());
263
- }
264
- chromeLines() {
265
- // The composer adds one row above the footer hint while it's open.
266
- return CHROME_LINES_BASE + (this.invocationLine() ? 1 : 0) + (this.composer ? 1 : 0);
267
- }
268
- invocationLine() {
269
- const { modelName, tags } = (0, agent_widget_js_1.buildInvocationTags)(this.record.invocation);
270
- const parts = modelName ? [modelName, ...tags] : tags;
271
- if (parts.length === 0)
272
- return undefined;
273
- return this.theme.fg("dim", ` ↳ ${parts.join(" · ")}`);
274
- }
275
- buildContentLines(width) {
276
- if (width <= 0)
277
- return [];
278
- const th = this.theme;
279
- const messages = this.session.messages;
280
- const lines = [];
281
- if (messages.length === 0) {
282
- lines.push(th.fg("dim", "(waiting for first message...)"));
283
- return lines;
284
- }
285
- let needsSeparator = false;
286
- for (const msg of messages) {
287
- if (msg.role === "user") {
288
- const text = typeof msg.content === "string"
289
- ? msg.content
290
- : (0, context_js_1.extractText)(msg.content);
291
- if (!text.trim())
292
- continue;
293
- if (needsSeparator)
294
- lines.push(th.fg("dim", "───"));
295
- lines.push(th.fg("accent", "[User]"));
296
- for (const line of (0, pi_tui_1.wrapTextWithAnsi)(text.trim(), width)) {
297
- lines.push(line);
298
- }
299
- }
300
- else if (msg.role === "assistant") {
301
- const textParts = [];
302
- const toolCalls = [];
303
- for (const c of msg.content) {
304
- if (c.type === "text" && c.text)
305
- textParts.push(c.text);
306
- else if (c.type === "toolCall") {
307
- toolCalls.push(c.name ?? c.toolName ?? "unknown");
308
- }
309
- }
310
- if (needsSeparator)
311
- lines.push(th.fg("dim", "───"));
312
- lines.push(th.bold("[Assistant]"));
313
- if (textParts.length > 0) {
314
- for (const line of (0, pi_tui_1.wrapTextWithAnsi)(textParts.join("\n").trim(), width)) {
315
- lines.push(line);
316
- }
317
- }
318
- for (const name of toolCalls) {
319
- lines.push((0, pi_tui_1.truncateToWidth)(th.fg("muted", ` [Tool: ${name}]`), width));
320
- }
321
- }
322
- else if (msg.role === "toolResult") {
323
- const text = (0, context_js_1.extractText)(msg.content);
324
- const truncated = text.length > 500 ? text.slice(0, 500) + "... (truncated)" : text;
325
- if (!truncated.trim())
326
- continue;
327
- if (needsSeparator)
328
- lines.push(th.fg("dim", "───"));
329
- lines.push(th.fg("dim", "[Result]"));
330
- for (const line of (0, pi_tui_1.wrapTextWithAnsi)(truncated.trim(), width)) {
331
- lines.push(th.fg("dim", line));
332
- }
333
- }
334
- else if (msg.role === "bashExecution") {
335
- const bash = msg;
336
- if (needsSeparator)
337
- lines.push(th.fg("dim", "───"));
338
- lines.push((0, pi_tui_1.truncateToWidth)(th.fg("muted", ` $ ${bash.command}`), width));
339
- if (bash.output?.trim()) {
340
- const out = bash.output.length > 500
341
- ? bash.output.slice(0, 500) + "... (truncated)"
342
- : bash.output;
343
- for (const line of (0, pi_tui_1.wrapTextWithAnsi)(out.trim(), width)) {
344
- lines.push(th.fg("dim", line));
345
- }
346
- }
347
- }
348
- else {
349
- continue;
350
- }
351
- needsSeparator = true;
352
- }
353
- // Streaming indicator for running agents
354
- if (this.record.status === "running" && this.activity) {
355
- const act = (0, agent_widget_js_1.describeActivity)(this.activity.activeTools, this.activity.responseText);
356
- lines.push("");
357
- lines.push((0, pi_tui_1.truncateToWidth)(th.fg("accent", "▍ ") + th.fg("dim", act), width));
358
- }
359
- return lines.map(l => (0, pi_tui_1.truncateToWidth)(l, width));
360
- }
361
586
  }
362
- exports.ConversationViewer = ConversationViewer;
587
+ //# sourceMappingURL=conversation-viewer.js.map