@pickle-pee/genesis-cli 0.0.1 → 0.0.2

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.
@@ -23,6 +23,7 @@ exports.movePermissionSelection = movePermissionSelection;
23
23
  exports.permissionDecisionFromSelection = permissionDecisionFromSelection;
24
24
  exports.formatInteractiveToolTitle = formatInteractiveToolTitle;
25
25
  exports.formatInteractiveToolResult = formatInteractiveToolResult;
26
+ exports.createDebouncedCallback = createDebouncedCallback;
26
27
  exports.computeSlashSuggestions = computeSlashSuggestions;
27
28
  exports.formatSlashSuggestionHint = formatSlashSuggestionHint;
28
29
  exports.acceptFirstSlashSuggestion = acceptFirstSlashSuggestion;
@@ -42,9 +43,11 @@ exports.formatTurnNotice = formatTurnNotice;
42
43
  exports.mergeStreamingText = mergeStreamingText;
43
44
  exports.wrapTranscriptContent = wrapTranscriptContent;
44
45
  exports.computeVisibleTranscriptLines = computeVisibleTranscriptLines;
46
+ exports.extractPlainTextSelection = extractPlainTextSelection;
45
47
  exports.computeTranscriptDisplayRows = computeTranscriptDisplayRows;
46
48
  exports.materializeAssistantTranscriptBlock = materializeAssistantTranscriptBlock;
47
49
  exports.appendAssistantTranscriptBlock = appendAssistantTranscriptBlock;
50
+ exports.appendTranscriptBlockWithSpacer = appendTranscriptBlockWithSpacer;
48
51
  exports.computeFooterStartRow = computeFooterStartRow;
49
52
  const node_child_process_1 = require("node:child_process");
50
53
  const promises_1 = require("node:fs/promises");
@@ -71,6 +74,8 @@ function createModeHandler(mode) {
71
74
  return new RpcModeHandler();
72
75
  }
73
76
  }
77
+ const TURN_NOTICE_ELAPSED_THRESHOLD_MS = 2_000;
78
+ const RESIZE_REDRAW_DEBOUNCE_MS = 120;
74
79
  // ---------------------------------------------------------------------------
75
80
  // Interactive mode
76
81
  // ---------------------------------------------------------------------------
@@ -87,10 +92,17 @@ class InteractiveModeHandler {
87
92
  _changedPaths = new Set();
88
93
  _transcriptBlocks = [];
89
94
  _assistantBuffer = "";
90
- _streamingReservedRows = 0;
91
- _streamingDisplayRows = 0;
92
- _renderedStreamingStartRow = null;
93
95
  _turnNotice = null;
96
+ _turnNoticeAnimationFrame = 0;
97
+ _turnNoticeTimer = null;
98
+ _turnStartedAt = null;
99
+ _detailPanelExpanded = false;
100
+ _detailPanelScroll = 0;
101
+ _thinkingBuffer = "";
102
+ _activeTurnUsageTotals = emptyUsageSnapshot();
103
+ _currentMessageUsage = emptyUsageSnapshot();
104
+ _lastTurnUsage = null;
105
+ _sessionUsageTotals = emptyUsageSnapshot();
94
106
  _commandSuggestions = [];
95
107
  _toolCalls = new Map();
96
108
  _queuedInputs = [];
@@ -98,6 +110,9 @@ class InteractiveModeHandler {
98
110
  _renderedFooterUi = null;
99
111
  _renderedFooterStartRow = null;
100
112
  _welcomeLines = [];
113
+ _transcriptScrollOffset = 0;
114
+ _renderedTranscriptViewportLines = [];
115
+ _mouseSelection = null;
101
116
  async start(runtime) {
102
117
  const handler = this;
103
118
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
@@ -128,11 +143,14 @@ class InteractiveModeHandler {
128
143
  onResume: () => {
129
144
  this.rerenderInteractiveRegions();
130
145
  },
131
- useAlternateScreen: false,
132
- enableMouseTracking: false,
146
+ useAlternateScreen: true,
147
+ enableMouseTracking: true,
133
148
  });
134
- const onResize = () => {
149
+ const debouncedResizeRedraw = createDebouncedCallback(() => {
135
150
  this.rerenderInteractiveRegions();
151
+ }, RESIZE_REDRAW_DEBOUNCE_MS);
152
+ const onResize = () => {
153
+ debouncedResizeRedraw.schedule();
136
154
  };
137
155
  process.stdout.on("resize", onResize);
138
156
  const resolveAgentDir = () => {
@@ -150,16 +168,24 @@ class InteractiveModeHandler {
150
168
  this._changedPaths.clear();
151
169
  this._transcriptBlocks.length = 0;
152
170
  this._assistantBuffer = "";
153
- this._streamingReservedRows = 0;
154
- this._streamingDisplayRows = 0;
155
- this._renderedStreamingStartRow = null;
171
+ this.stopTurnNoticeAnimation();
156
172
  this._turnNotice = null;
173
+ this._turnNoticeAnimationFrame = 0;
174
+ this._turnStartedAt = null;
175
+ this._detailPanelExpanded = false;
176
+ this._detailPanelScroll = 0;
177
+ this._thinkingBuffer = "";
178
+ this._activeTurnUsageTotals = emptyUsageSnapshot();
179
+ this._currentMessageUsage = emptyUsageSnapshot();
180
+ this._lastTurnUsage = null;
181
+ this._sessionUsageTotals = emptyUsageSnapshot();
157
182
  this._commandSuggestions = [];
158
183
  this._toolCalls.clear();
159
184
  this._queuedInputs.length = 0;
160
185
  this._pendingPermissionSelection = 0;
161
186
  this._renderedFooterUi = null;
162
187
  this._renderedFooterStartRow = null;
188
+ this._transcriptScrollOffset = 0;
163
189
  sessionTitle = undefined;
164
190
  interactionState = (0, ui_1.initialInteractionState)();
165
191
  sessionRef.current.events.on("session_closed", (event) => {
@@ -772,6 +798,9 @@ class InteractiveModeHandler {
772
798
  this.rerenderInteractiveRegions();
773
799
  }
774
800
  },
801
+ onMouse: (event) => {
802
+ this.handleMouseEvent(event);
803
+ },
775
804
  });
776
805
  ttySession.enter();
777
806
  this.renderWelcome(sessionRef.current);
@@ -822,6 +851,8 @@ class InteractiveModeHandler {
822
851
  // Regular prompt
823
852
  if (this._activeTurn !== null) {
824
853
  this._queuedInputs.push(trimmed);
854
+ this.preserveThinkingNoticeForQueuedBacklog();
855
+ this.renderFooterRegion();
825
856
  line = await inputLoop.nextLine();
826
857
  continue;
827
858
  }
@@ -831,6 +862,7 @@ class InteractiveModeHandler {
831
862
  }
832
863
  finally {
833
864
  process.stdout.off("resize", onResize);
865
+ debouncedResizeRedraw.cancel();
834
866
  inputLoop.close();
835
867
  process.stdout.write(ansiResetScrollRegion());
836
868
  ttySession.restore();
@@ -850,7 +882,61 @@ class InteractiveModeHandler {
850
882
  renderPromptLine() {
851
883
  this.renderFooterRegion();
852
884
  }
885
+ handleMouseEvent(event) {
886
+ if (!this.isTranscriptMouseRow(event.row)) {
887
+ if (event.kind === "leftdown") {
888
+ this.clearMouseSelection();
889
+ }
890
+ return;
891
+ }
892
+ if (event.kind === "leftdown") {
893
+ this._mouseSelection = {
894
+ anchorRow: event.row,
895
+ anchorColumn: event.column,
896
+ focusRow: event.row,
897
+ focusColumn: event.column,
898
+ };
899
+ this.renderTranscriptViewport();
900
+ return;
901
+ }
902
+ if (this._mouseSelection === null) {
903
+ return;
904
+ }
905
+ this._mouseSelection = {
906
+ ...this._mouseSelection,
907
+ focusRow: event.row,
908
+ focusColumn: event.column,
909
+ };
910
+ this.renderTranscriptViewport();
911
+ if (event.kind === "leftup") {
912
+ const selectedText = this.currentTranscriptSelectionText();
913
+ if (selectedText.length > 0) {
914
+ copyTextToClipboard(selectedText);
915
+ }
916
+ }
917
+ }
853
918
  handleSpecialKey(key) {
919
+ if (key === "ctrlo") {
920
+ this.toggleDetailPanel();
921
+ return;
922
+ }
923
+ if (key === "esc" && this._detailPanelExpanded) {
924
+ this._detailPanelExpanded = false;
925
+ this.renderFooterRegion();
926
+ return;
927
+ }
928
+ if (this._detailPanelExpanded) {
929
+ const detailScrollDelta = detailPanelScrollDeltaForKey(key, this.currentDetailPanelViewport().viewportSize);
930
+ if (detailScrollDelta !== 0) {
931
+ this.scrollDetailPanel(detailScrollDelta);
932
+ return;
933
+ }
934
+ }
935
+ const transcriptScrollDelta = transcriptScrollDeltaForKey(key, this.currentTranscriptViewportRows());
936
+ if (transcriptScrollDelta !== 0) {
937
+ this.scrollTranscript(transcriptScrollDelta);
938
+ return;
939
+ }
854
940
  if (this._pendingPermissionCallId !== null) {
855
941
  if (key === "up" || key === "shifttab") {
856
942
  this._pendingPermissionSelection = movePermissionSelection(this._pendingPermissionSelection, -1);
@@ -894,11 +980,17 @@ class InteractiveModeHandler {
894
980
  }
895
981
  return;
896
982
  }
983
+ if (event.category === "usage" && event.type === "usage_updated") {
984
+ this.updateTurnUsage(event.usage, event.isFinal);
985
+ this.renderPromptLine();
986
+ return;
987
+ }
897
988
  if (!shouldRenderInteractiveTranscriptEvent(event)) {
898
989
  this.renderPromptLine();
899
990
  return;
900
991
  }
901
992
  if (event.category === "text" && event.type === "thinking_delta") {
993
+ this._thinkingBuffer += event.content;
902
994
  if (this._turnNotice === null) {
903
995
  this.startTurnFeedback();
904
996
  }
@@ -907,10 +999,14 @@ class InteractiveModeHandler {
907
999
  }
908
1000
  if (event.category === "text" && event.type === "text_delta") {
909
1001
  if (this._turnNotice !== "responding") {
1002
+ this._turnNoticeAnimationFrame = 0;
910
1003
  this._turnNotice = "responding";
1004
+ this.startTurnNoticeAnimation();
911
1005
  }
1006
+ const previousRows = this.currentTranscriptDisplayRows();
912
1007
  this._assistantBuffer = mergeStreamingText(this._assistantBuffer, event.content);
913
- this.renderStreamingAssistantBlock();
1008
+ this.adjustTranscriptScrollForGrowth(previousRows, this.currentTranscriptDisplayRows());
1009
+ this.fullRedrawInteractiveScreen();
914
1010
  return;
915
1011
  }
916
1012
  this.flushAssistantBuffer(false);
@@ -931,12 +1027,11 @@ class InteractiveModeHandler {
931
1027
  }
932
1028
  const assistantBlock = materializeAssistantTranscriptBlock(this._assistantBuffer);
933
1029
  if (assistantBlock !== null) {
1030
+ const previousRows = this.currentTranscriptDisplayRows();
934
1031
  this.rememberAssistantTranscriptBlock(assistantBlock);
1032
+ this.adjustTranscriptScrollForGrowth(previousRows, this.currentTranscriptDisplayRows());
935
1033
  }
936
1034
  this._assistantBuffer = "";
937
- this._streamingReservedRows = 0;
938
- this._streamingDisplayRows = 0;
939
- this._renderedStreamingStartRow = null;
940
1035
  if (redrawPrompt) {
941
1036
  this.renderPromptLine();
942
1037
  }
@@ -946,39 +1041,35 @@ class InteractiveModeHandler {
946
1041
  return;
947
1042
  }
948
1043
  this._turnNotice = "thinking";
1044
+ this._turnNoticeAnimationFrame = 0;
1045
+ this.startTurnNoticeAnimation();
949
1046
  this.renderPromptLine();
950
1047
  }
951
- renderStreamingAssistantBlock() {
952
- const rendered = formatTranscriptAssistantLine(this._assistantBuffer);
953
- const lines = wrapTranscriptContent(rendered, process.stdout.columns ?? 80);
954
- const renderedWidth = this.terminalWidth();
955
- const rows = countRenderedTerminalRows(lines, renderedWidth);
956
- const previousStartRow = this._renderedStreamingStartRow;
957
- const previousRows = this._streamingReservedRows;
958
- this._streamingDisplayRows = rows;
959
- this.renderFooterRegion();
960
- const footerStartRow = this._renderedFooterStartRow ??
961
- computeFooterStartRow(this._welcomeLines.length, this.terminalHeight(), this.currentFooterHeight(), this.currentTranscriptDisplayRows());
962
- if (isFooterBottomAnchored(footerStartRow, this.terminalHeight(), this.currentFooterHeight())) {
963
- this.reserveStreamingRows(rows);
1048
+ startTurnNoticeAnimation() {
1049
+ if (this._turnNoticeTimer !== null) {
1050
+ return;
964
1051
  }
965
- else {
966
- this._streamingReservedRows = rows;
967
- }
968
- const startRow = footerStartRow - rows;
969
- const transcriptBottomRow = footerStartRow - 1;
970
- const clearStartRow = previousStartRow === null ? startRow : Math.min(startRow, previousStartRow);
971
- const clearEndRow = previousStartRow === null
972
- ? transcriptBottomRow
973
- : Math.max(transcriptBottomRow, previousStartRow + previousRows - 1);
974
- this.clearTranscriptRows(clearStartRow, clearEndRow);
975
- this.writeLinesAtRow(startRow, lines, renderedWidth);
976
- this._renderedStreamingStartRow = startRow;
977
- this.renderFooterRegion();
1052
+ this._turnNoticeTimer = setInterval(() => {
1053
+ if (this._turnNotice === null) {
1054
+ return;
1055
+ }
1056
+ this._turnNoticeAnimationFrame = (this._turnNoticeAnimationFrame + 1) % 3;
1057
+ this.renderFooterRegion();
1058
+ }, 400);
1059
+ this._turnNoticeTimer.unref?.();
1060
+ }
1061
+ stopTurnNoticeAnimation() {
1062
+ if (this._turnNoticeTimer === null) {
1063
+ return;
1064
+ }
1065
+ clearInterval(this._turnNoticeTimer);
1066
+ this._turnNoticeTimer = null;
978
1067
  }
979
1068
  writeTranscriptText(text, newline, redrawPrompt = true) {
980
1069
  this.flushAssistantBuffer(false);
1070
+ const previousRows = this.currentTranscriptDisplayRows();
981
1071
  this.rememberTranscriptBlock(text, newline);
1072
+ this.adjustTranscriptScrollForGrowth(previousRows, this.currentTranscriptDisplayRows());
982
1073
  const logicalLines = text.split("\n");
983
1074
  const outputLines = newline ? logicalLines : logicalLines.slice(0, -1).concat(logicalLines.at(-1) ?? "");
984
1075
  if (outputLines.length > 0) {
@@ -1012,35 +1103,131 @@ class InteractiveModeHandler {
1012
1103
  this.renderPromptLine();
1013
1104
  }
1014
1105
  startPromptTurn(session, prompt, sink) {
1106
+ this.startUserTurn(session, prompt, sink, "prompt");
1107
+ }
1108
+ startQueuedContinueTurn(session, input, sink) {
1109
+ this.startUserTurn(session, input, sink, "continue");
1110
+ }
1111
+ startUserTurn(session, input, sink, mode) {
1015
1112
  this.flushAssistantBuffer(false);
1016
- this.writeTranscriptText(formatTranscriptUserLine(prompt), true, false);
1113
+ this.writeTranscriptText(formatTranscriptUserLine(input), true, false);
1114
+ this._turnStartedAt = Date.now();
1115
+ this._detailPanelExpanded = false;
1116
+ this._detailPanelScroll = 0;
1117
+ this._thinkingBuffer = "";
1118
+ this._activeTurnUsageTotals = emptyUsageSnapshot();
1119
+ this._currentMessageUsage = emptyUsageSnapshot();
1017
1120
  this.startTurnFeedback();
1018
- this.rememberHistory(prompt);
1019
- this._activeTurn = session
1020
- .prompt(prompt)
1121
+ this.rememberHistory(input);
1122
+ const sendTurn = mode === "continue" ? (value) => session.continue(value) : (value) => session.prompt(value);
1123
+ this._activeTurn = sendTurn(input)
1021
1124
  .catch((err) => {
1022
1125
  sink.writeError(`Error: ${err}`);
1023
1126
  })
1024
1127
  .finally(() => {
1128
+ this.stopTurnNoticeAnimation();
1129
+ const completedTurnUsage = this.currentTurnUsage();
1025
1130
  this._activeTurn = null;
1026
1131
  this.flushAssistantBuffer(false);
1027
1132
  this._turnNotice = null;
1028
- const nextQueued = this._queuedInputs.shift();
1029
- if (nextQueued) {
1030
- this.startPromptTurn(session, nextQueued, sink);
1133
+ this._turnNoticeAnimationFrame = 0;
1134
+ this._turnStartedAt = null;
1135
+ this._detailPanelExpanded = false;
1136
+ this._detailPanelScroll = 0;
1137
+ this._thinkingBuffer = "";
1138
+ if (hasUsageSnapshot(completedTurnUsage)) {
1139
+ this._lastTurnUsage = completedTurnUsage;
1140
+ this._sessionUsageTotals = addUsageSnapshots(this._sessionUsageTotals, completedTurnUsage);
1141
+ }
1142
+ this._activeTurnUsageTotals = emptyUsageSnapshot();
1143
+ this._currentMessageUsage = emptyUsageSnapshot();
1144
+ const queuedInputBatch = this.drainQueuedInputs();
1145
+ if (queuedInputBatch !== null) {
1146
+ this.startQueuedContinueTurn(session, queuedInputBatch, sink);
1031
1147
  return;
1032
1148
  }
1033
1149
  this.fullRedrawInteractiveScreen();
1034
1150
  });
1035
1151
  }
1152
+ drainQueuedInputs() {
1153
+ if (this._queuedInputs.length === 0) {
1154
+ return null;
1155
+ }
1156
+ const queued = [...this._queuedInputs];
1157
+ this._queuedInputs.length = 0;
1158
+ return queued.join("\n\n");
1159
+ }
1160
+ preserveThinkingNoticeForQueuedBacklog() {
1161
+ if (this._activeTurn === null) {
1162
+ return;
1163
+ }
1164
+ if (this._turnNotice === "responding") {
1165
+ this._turnNotice = "thinking";
1166
+ this.startTurnNoticeAnimation();
1167
+ return;
1168
+ }
1169
+ if (this._turnNotice === null) {
1170
+ this.startTurnFeedback();
1171
+ }
1172
+ }
1173
+ toggleDetailPanel() {
1174
+ if (this.currentDetailPanelContentLines().length === 0) {
1175
+ return;
1176
+ }
1177
+ this._detailPanelExpanded = !this._detailPanelExpanded;
1178
+ if (this._detailPanelExpanded) {
1179
+ this._detailPanelScroll = 0;
1180
+ }
1181
+ this.renderFooterRegion();
1182
+ }
1183
+ scrollDetailPanel(delta) {
1184
+ const viewport = this.currentDetailPanelViewport();
1185
+ if (viewport.totalLines <= viewport.viewportSize) {
1186
+ return;
1187
+ }
1188
+ const maxScroll = Math.max(0, viewport.totalLines - viewport.viewportSize);
1189
+ const next = Math.max(0, Math.min(maxScroll, this._detailPanelScroll + delta));
1190
+ if (next === this._detailPanelScroll) {
1191
+ return;
1192
+ }
1193
+ this._detailPanelScroll = next;
1194
+ this.renderFooterRegion();
1195
+ }
1196
+ scrollTranscript(delta) {
1197
+ const maxScroll = this.currentTranscriptMaxScroll();
1198
+ if (delta === 0 || maxScroll <= 0) {
1199
+ return;
1200
+ }
1201
+ const next = Math.max(0, Math.min(maxScroll, this._transcriptScrollOffset + delta));
1202
+ if (next === this._transcriptScrollOffset) {
1203
+ return;
1204
+ }
1205
+ this.clearMouseSelection(false);
1206
+ this._transcriptScrollOffset = next;
1207
+ this.renderTranscriptViewport();
1208
+ this.renderFooterRegion();
1209
+ }
1036
1210
  buildFooterUi() {
1211
+ const activeToolLabel = summarizeActiveToolNotice(this._toolCalls);
1212
+ const detailPanel = this.currentDetailPanelViewport();
1037
1213
  return formatInteractiveFooter({
1038
1214
  terminalWidth: process.stdout.columns ?? 80,
1039
1215
  prompt: this._prompt,
1040
1216
  buffer: this._inputState.buffer,
1041
1217
  cursor: this._inputState.cursor,
1042
1218
  suggestions: this._commandSuggestions,
1043
- turnNotice: this._turnNotice,
1219
+ turnNotice: activeToolLabel !== null ? "tool" : this._turnNotice,
1220
+ turnNoticeAnimationFrame: this._turnNoticeAnimationFrame,
1221
+ elapsedMs: this.currentTurnElapsedMs(),
1222
+ currentTurnUsage: this.currentTurnUsage(),
1223
+ lastTurnUsage: this._lastTurnUsage,
1224
+ sessionUsage: this._sessionUsageTotals,
1225
+ activeToolLabel,
1226
+ showPendingOutputIndicator: this.shouldShowPendingOutputIndicator(activeToolLabel),
1227
+ detailPanelExpanded: this._detailPanelExpanded,
1228
+ detailPanelLines: detailPanel.lines,
1229
+ detailPanelSummary: detailPanel.summary,
1230
+ queuedInputs: this._queuedInputs,
1044
1231
  permission: this._pendingPermissionCallId !== null && this._pendingPermissionDetails !== null
1045
1232
  ? {
1046
1233
  details: this._pendingPermissionDetails,
@@ -1049,15 +1236,132 @@ class InteractiveModeHandler {
1049
1236
  : null,
1050
1237
  });
1051
1238
  }
1239
+ currentDetailPanelContentLines() {
1240
+ if (this._thinkingBuffer.trim().length === 0) {
1241
+ return [];
1242
+ }
1243
+ return wrapTranscriptContent(this._thinkingBuffer.trim(), this.terminalWidth());
1244
+ }
1245
+ shouldShowPendingOutputIndicator(activeToolLabel) {
1246
+ if (this._assistantBuffer.length > 0) {
1247
+ return true;
1248
+ }
1249
+ if (activeToolLabel === null) {
1250
+ return false;
1251
+ }
1252
+ const lastNonEmptyIndex = findLastNonEmptyBlockIndex(this._transcriptBlocks);
1253
+ if (lastNonEmptyIndex === -1) {
1254
+ return false;
1255
+ }
1256
+ const lastBlock = this._transcriptBlocks[lastNonEmptyIndex] ?? "";
1257
+ return !isTranscriptUserBlock(lastBlock) && !lastBlock.startsWith("⏺ ") && !lastBlock.startsWith(" ⎿ ");
1258
+ }
1259
+ currentDetailPanelViewport() {
1260
+ const lines = this.currentDetailPanelContentLines();
1261
+ if (lines.length === 0) {
1262
+ return { lines: [], summary: null, viewportSize: 0, totalLines: 0 };
1263
+ }
1264
+ if (!this._detailPanelExpanded) {
1265
+ return {
1266
+ lines: [],
1267
+ summary: "ctrl+o to expand",
1268
+ viewportSize: 0,
1269
+ totalLines: lines.length,
1270
+ };
1271
+ }
1272
+ const viewportSize = Math.max(3, this.terminalHeight() - 8);
1273
+ const maxScroll = Math.max(0, lines.length - viewportSize);
1274
+ const start = Math.max(0, Math.min(this._detailPanelScroll, maxScroll));
1275
+ const end = Math.min(lines.length, start + viewportSize);
1276
+ const summary = lines.length <= viewportSize
1277
+ ? "esc to collapse · ↑↓"
1278
+ : `esc to collapse · ↑↓ · ${start + 1}-${end}/${lines.length}`;
1279
+ return {
1280
+ lines: lines.slice(start, end),
1281
+ summary,
1282
+ viewportSize,
1283
+ totalLines: lines.length,
1284
+ };
1285
+ }
1052
1286
  rerenderInteractiveRegions() {
1287
+ this.clearMouseSelection(false);
1288
+ this.clampTranscriptScrollOffset();
1053
1289
  this.fullRedrawInteractiveScreen();
1054
1290
  }
1291
+ adjustTranscriptScrollForGrowth(previousRows, nextRows) {
1292
+ this.clearMouseSelection(false);
1293
+ if (this._transcriptScrollOffset === 0) {
1294
+ return;
1295
+ }
1296
+ const delta = Math.max(0, nextRows - previousRows);
1297
+ if (delta === 0) {
1298
+ this.clampTranscriptScrollOffset();
1299
+ return;
1300
+ }
1301
+ this._transcriptScrollOffset += delta;
1302
+ this.clampTranscriptScrollOffset();
1303
+ }
1304
+ updateTurnUsage(usage, isFinal) {
1305
+ const normalized = normalizeUsageSnapshot(usage);
1306
+ if (isFinal) {
1307
+ this._activeTurnUsageTotals = addUsageSnapshots(this._activeTurnUsageTotals, normalized);
1308
+ this._currentMessageUsage = emptyUsageSnapshot();
1309
+ return;
1310
+ }
1311
+ this._currentMessageUsage = normalized;
1312
+ }
1313
+ currentTurnUsage() {
1314
+ const usage = addUsageSnapshots(this._activeTurnUsageTotals, this._currentMessageUsage);
1315
+ return hasUsageSnapshot(usage) ? usage : null;
1316
+ }
1317
+ currentTurnElapsedMs() {
1318
+ if (this._turnNotice === null || this._turnStartedAt === null) {
1319
+ return null;
1320
+ }
1321
+ return Math.max(0, Date.now() - this._turnStartedAt);
1322
+ }
1055
1323
  terminalWidth() {
1056
1324
  return Math.max(1, process.stdout.columns ?? 80);
1057
1325
  }
1058
1326
  terminalHeight() {
1059
1327
  return Math.max(6, process.stdout.rows ?? 24);
1060
1328
  }
1329
+ currentTranscriptViewportRows() {
1330
+ const footerUi = this.buildFooterUi();
1331
+ const transcriptTopRow = 1;
1332
+ const transcriptBottomRow = computeFooterStartRow(0, this.terminalHeight(), footerUi.lines.length, this.currentTranscriptDisplayRows()) - 1;
1333
+ return Math.max(0, transcriptBottomRow - transcriptTopRow + 1);
1334
+ }
1335
+ currentTranscriptMaxScroll() {
1336
+ return Math.max(0, this.currentTranscriptDisplayRows() - this.currentTranscriptViewportRows());
1337
+ }
1338
+ clampTranscriptScrollOffset() {
1339
+ this._transcriptScrollOffset = Math.max(0, Math.min(this._transcriptScrollOffset, this.currentTranscriptMaxScroll()));
1340
+ }
1341
+ isTranscriptMouseRow(row) {
1342
+ const footerStartRow = this._renderedFooterStartRow ?? computeFooterStartRow(0, this.terminalHeight(), this.currentFooterHeight(), this.currentTranscriptDisplayRows());
1343
+ return row >= 1 && row < footerStartRow;
1344
+ }
1345
+ clearMouseSelection(redraw = true) {
1346
+ if (this._mouseSelection === null) {
1347
+ return;
1348
+ }
1349
+ this._mouseSelection = null;
1350
+ if (redraw) {
1351
+ this.renderTranscriptViewport();
1352
+ }
1353
+ }
1354
+ currentTranscriptSelectionText() {
1355
+ if (this._mouseSelection === null) {
1356
+ return "";
1357
+ }
1358
+ return extractPlainTextSelection(this._renderedTranscriptViewportLines, {
1359
+ startRow: this._mouseSelection.anchorRow - 1,
1360
+ startColumn: this._mouseSelection.anchorColumn,
1361
+ endRow: this._mouseSelection.focusRow - 1,
1362
+ endColumn: this._mouseSelection.focusColumn,
1363
+ });
1364
+ }
1061
1365
  transcriptBottomRow(footerHeight = this.currentFooterHeight()) {
1062
1366
  return Math.max(1, this.terminalHeight() - footerHeight);
1063
1367
  }
@@ -1067,9 +1371,10 @@ class InteractiveModeHandler {
1067
1371
  renderFooterRegion() {
1068
1372
  const ui = this.buildFooterUi();
1069
1373
  const footerHeight = ui.lines.length;
1070
- const startRow = computeFooterStartRow(this._welcomeLines.length, this.terminalHeight(), footerHeight, this.currentTranscriptDisplayRows());
1374
+ const startRow = computeFooterStartRow(0, this.terminalHeight(), footerHeight, this.currentTranscriptDisplayRows());
1071
1375
  const oldStartRow = this._renderedFooterStartRow;
1072
1376
  const oldHeight = this._renderedFooterUi?.lines.length ?? 0;
1377
+ const footerMoved = oldStartRow !== null && oldStartRow !== startRow;
1073
1378
  if (oldStartRow !== null && oldStartRow !== startRow) {
1074
1379
  for (let index = 0; index < oldHeight; index += 1) {
1075
1380
  this.writeAbsoluteTerminalLine(oldStartRow + index, "");
@@ -1090,6 +1395,9 @@ class InteractiveModeHandler {
1090
1395
  process.stdout.write((0, ui_1.ansiShowCursor)());
1091
1396
  this._renderedFooterUi = { ...ui, renderedWidth: this.terminalWidth() };
1092
1397
  this._renderedFooterStartRow = startRow;
1398
+ if (footerMoved) {
1399
+ this.renderTranscriptViewport();
1400
+ }
1093
1401
  }
1094
1402
  applyTranscriptViewport(footerHeight) {
1095
1403
  process.stdout.write(ansiSetScrollRegion(1, this.transcriptBottomRow(footerHeight)));
@@ -1100,27 +1408,6 @@ class InteractiveModeHandler {
1100
1408
  }
1101
1409
  this.fullRedrawInteractiveScreen();
1102
1410
  }
1103
- reserveStreamingRows(rows) {
1104
- if (rows <= this._streamingReservedRows) {
1105
- return;
1106
- }
1107
- this.applyTranscriptViewport(this.currentFooterHeight());
1108
- for (let index = this._streamingReservedRows; index < rows; index += 1) {
1109
- process.stdout.write(ansiCursorTo(this.transcriptBottomRow(), 1));
1110
- process.stdout.write("\n");
1111
- }
1112
- this._streamingReservedRows = rows;
1113
- }
1114
- clearTranscriptRows(startRow, endRow) {
1115
- for (let row = startRow; row <= endRow; row += 1) {
1116
- this.writeAbsoluteTerminalLine(row, "");
1117
- }
1118
- }
1119
- writeLinesAtRow(startRow, lines, width) {
1120
- for (let index = 0; index < lines.length; index += 1) {
1121
- this.writeAbsoluteTerminalLine(startRow + index, fitTerminalLine(lines[index] ?? "", width));
1122
- }
1123
- }
1124
1411
  writeAbsoluteTerminalLine(row, line) {
1125
1412
  process.stdout.write(ansiDisableAutoWrap());
1126
1413
  process.stdout.write(ansiCursorTo(row, 1));
@@ -1128,12 +1415,19 @@ class InteractiveModeHandler {
1128
1415
  process.stdout.write(line);
1129
1416
  process.stdout.write(ansiEnableAutoWrap());
1130
1417
  }
1418
+ clearVisibleScreenRows() {
1419
+ for (let row = 1; row <= this.terminalHeight(); row += 1) {
1420
+ this.writeAbsoluteTerminalLine(row, "");
1421
+ }
1422
+ }
1131
1423
  rememberTranscriptBlock(text, newline) {
1132
1424
  const block = newline ? text : text.replace(/\n$/, "");
1133
1425
  if (block.length === 0) {
1134
1426
  return;
1135
1427
  }
1136
- this._transcriptBlocks.push(block);
1428
+ const nextBlocks = appendTranscriptBlockWithSpacer(this._transcriptBlocks, block);
1429
+ this._transcriptBlocks.length = 0;
1430
+ this._transcriptBlocks.push(...nextBlocks);
1137
1431
  }
1138
1432
  rememberAssistantTranscriptBlock(block) {
1139
1433
  const nextBlocks = appendAssistantTranscriptBlock(this._transcriptBlocks, block);
@@ -1141,42 +1435,94 @@ class InteractiveModeHandler {
1141
1435
  this._transcriptBlocks.push(...nextBlocks);
1142
1436
  }
1143
1437
  fullRedrawInteractiveScreen() {
1144
- if (this._welcomeLines.length === 0) {
1145
- return;
1146
- }
1438
+ this.clampTranscriptScrollOffset();
1147
1439
  process.stdout.write(ansiResetScrollRegion());
1148
1440
  process.stdout.write((0, ui_1.ansiCursorHome)());
1149
- process.stdout.write("\x1b[2J");
1150
- for (let index = 0; index < this._welcomeLines.length; index += 1) {
1151
- this.writeAbsoluteTerminalLine(index + 1, fitTerminalLine(this._welcomeLines[index] ?? "", this.terminalWidth()));
1152
- }
1441
+ this.clearVisibleScreenRows();
1153
1442
  this._renderedFooterUi = null;
1154
1443
  this._renderedFooterStartRow = null;
1155
- this._streamingReservedRows = 0;
1156
- this._renderedStreamingStartRow = null;
1157
- this._streamingDisplayRows =
1158
- this._assistantBuffer.length > 0
1159
- ? countRenderedTerminalRows(wrapTranscriptContent(formatTranscriptAssistantLine(this._assistantBuffer), this.terminalWidth()), this.terminalWidth())
1160
- : 0;
1161
1444
  this.renderTranscriptViewport();
1162
1445
  this.renderFooterRegion();
1163
- if (this._assistantBuffer.length > 0) {
1164
- this.renderStreamingAssistantBlock();
1165
- }
1166
1446
  }
1167
1447
  renderTranscriptViewport() {
1168
1448
  const footerUi = this.buildFooterUi();
1169
- const transcriptTopRow = this._welcomeLines.length + 1;
1170
- const transcriptBottomRow = computeFooterStartRow(this._welcomeLines.length, this.terminalHeight(), footerUi.lines.length, this.currentTranscriptDisplayRows()) - 1;
1449
+ const transcriptTopRow = 1;
1450
+ const transcriptBottomRow = computeFooterStartRow(0, this.terminalHeight(), footerUi.lines.length, this.currentTranscriptDisplayRows()) - 1;
1171
1451
  const availableRows = Math.max(0, transcriptBottomRow - transcriptTopRow + 1);
1172
- const visibleLines = computeVisibleTranscriptLines(this._transcriptBlocks, this.terminalWidth(), availableRows);
1452
+ const visibleLines = computeVisibleTranscriptLines(this.currentRenderedTranscriptBlocks(), this.terminalWidth(), availableRows, this._transcriptScrollOffset);
1453
+ this._renderedTranscriptViewportLines = visibleLines.map((line) => stripAnsiWelcome(line));
1173
1454
  for (let row = transcriptTopRow; row <= transcriptBottomRow; row += 1) {
1174
1455
  const index = row - transcriptTopRow;
1175
- this.writeAbsoluteTerminalLine(row, fitTerminalLine(visibleLines[index] ?? "", this.terminalWidth()));
1456
+ const visibleLine = visibleLines[index] ?? "";
1457
+ const plainLine = this._renderedTranscriptViewportLines[index] ?? "";
1458
+ const selectionColumns = this.selectionColumnsForRow(row);
1459
+ const renderedLine = selectionColumns === null
1460
+ ? fitTerminalLine(visibleLine, this.terminalWidth())
1461
+ : renderSelectedPlainTranscriptLine(plainLine, selectionColumns.startColumn, selectionColumns.endColumn, this.terminalWidth());
1462
+ this.writeAbsoluteTerminalLine(row, renderedLine);
1463
+ }
1464
+ }
1465
+ selectionColumnsForRow(row) {
1466
+ if (this._mouseSelection === null) {
1467
+ return null;
1468
+ }
1469
+ const start = compareTerminalSelectionPoints(this._mouseSelection.anchorRow, this._mouseSelection.anchorColumn, this._mouseSelection.focusRow, this._mouseSelection.focusColumn) <= 0
1470
+ ? {
1471
+ row: this._mouseSelection.anchorRow,
1472
+ column: this._mouseSelection.anchorColumn,
1473
+ }
1474
+ : {
1475
+ row: this._mouseSelection.focusRow,
1476
+ column: this._mouseSelection.focusColumn,
1477
+ };
1478
+ const end = start.row === this._mouseSelection.anchorRow && start.column === this._mouseSelection.anchorColumn
1479
+ ? {
1480
+ row: this._mouseSelection.focusRow,
1481
+ column: this._mouseSelection.focusColumn,
1482
+ }
1483
+ : {
1484
+ row: this._mouseSelection.anchorRow,
1485
+ column: this._mouseSelection.anchorColumn,
1486
+ };
1487
+ if (row < start.row || row > end.row) {
1488
+ return null;
1489
+ }
1490
+ if (start.row === end.row) {
1491
+ return {
1492
+ startColumn: Math.min(start.column, end.column),
1493
+ endColumn: Math.max(start.column, end.column),
1494
+ };
1176
1495
  }
1496
+ if (row === start.row) {
1497
+ return {
1498
+ startColumn: start.column,
1499
+ endColumn: this.terminalWidth() + 1,
1500
+ };
1501
+ }
1502
+ if (row === end.row) {
1503
+ return {
1504
+ startColumn: 1,
1505
+ endColumn: end.column,
1506
+ };
1507
+ }
1508
+ return {
1509
+ startColumn: 1,
1510
+ endColumn: this.terminalWidth() + 1,
1511
+ };
1177
1512
  }
1178
1513
  currentTranscriptDisplayRows() {
1179
- return computeTranscriptDisplayRows(this._transcriptBlocks, this.terminalWidth()) + this._streamingDisplayRows;
1514
+ return computeTranscriptDisplayRows(this.currentRenderedTranscriptBlocks(), this.terminalWidth());
1515
+ }
1516
+ currentRenderedTranscriptBlocks() {
1517
+ const welcomeBlocks = this._welcomeLines;
1518
+ if (this._assistantBuffer.length === 0) {
1519
+ return [...welcomeBlocks, ...this._transcriptBlocks];
1520
+ }
1521
+ const assistantBlock = materializeAssistantTranscriptBlock(this._assistantBuffer);
1522
+ if (assistantBlock === null) {
1523
+ return [...welcomeBlocks, ...this._transcriptBlocks];
1524
+ }
1525
+ return appendAssistantTranscriptBlock([...welcomeBlocks, ...this._transcriptBlocks], assistantBlock);
1180
1526
  }
1181
1527
  }
1182
1528
  // ---------------------------------------------------------------------------
@@ -1289,9 +1635,9 @@ function applyWelcomeBorderColor(text) {
1289
1635
  }
1290
1636
  function buildWelcomeHintLine(width) {
1291
1637
  if (width < 64) {
1292
- return "Start: Enter Help: /help Scroll: wheel/PageUp/PageDown";
1638
+ return "Start: Enter Help: /help Exit: /exit";
1293
1639
  }
1294
- return "Start: type a prompt and press Enter Help: /help Scroll: wheel/PageUp/PageDown";
1640
+ return "Start: type a prompt and press Enter Help: /help Exit: /exit";
1295
1641
  }
1296
1642
  exports.WELCOME_BIBLE_GREETINGS = [
1297
1643
  "Let there be light.",
@@ -1330,6 +1676,7 @@ function buildWelcomeLines(input) {
1330
1676
  center(`${CYAN}${input.model}${RESET} ${DIM}via${RESET} ${input.provider}`),
1331
1677
  formatWelcomeBottomBorder(width),
1332
1678
  buildWelcomeHintLine(width),
1679
+ "",
1333
1680
  ];
1334
1681
  }
1335
1682
  function formatWelcomeTopBorder(width, version) {
@@ -1394,7 +1741,31 @@ function formatInteractiveFooter(state) {
1394
1741
  const separator = formatInteractiveInputSeparator(computeInteractiveFooterSeparatorWidth(state.terminalWidth));
1395
1742
  const lines = [];
1396
1743
  if (state.turnNotice !== null) {
1397
- lines.push(formatTurnNotice(state.turnNotice));
1744
+ lines.push(formatTurnNotice(state.turnNotice, {
1745
+ animationFrame: state.turnNoticeAnimationFrame ?? 0,
1746
+ elapsedMs: state.elapsedMs ?? null,
1747
+ usage: state.currentTurnUsage ?? null,
1748
+ showPendingOutputIndicator: state.showPendingOutputIndicator ?? false,
1749
+ queuedCount: state.queuedInputs?.length ?? 0,
1750
+ toolLabel: state.activeToolLabel ?? null,
1751
+ }));
1752
+ }
1753
+ else {
1754
+ if (hasUsageSnapshot(state.lastTurnUsage ?? null)) {
1755
+ lines.push(formatUsageSummaryLine("Last turn", state.lastTurnUsage));
1756
+ }
1757
+ if (hasUsageSnapshot(state.sessionUsage ?? null)) {
1758
+ lines.push(formatUsageSummaryLine("Session", state.sessionUsage));
1759
+ }
1760
+ }
1761
+ if ((state.detailPanelSummary?.length ?? 0) > 0) {
1762
+ lines.push(`${theme_js_1.INTERACTIVE_THEME.muted}↳ ${state.detailPanelSummary}${theme_js_1.INTERACTIVE_THEME.reset}`);
1763
+ if (state.detailPanelExpanded) {
1764
+ lines.push(...(state.detailPanelLines ?? []));
1765
+ }
1766
+ }
1767
+ if ((state.queuedInputs?.length ?? 0) > 0) {
1768
+ lines.push(...formatQueuedPromptPreviewLines(state.queuedInputs ?? [], state.terminalWidth));
1398
1769
  }
1399
1770
  lines.push(separator);
1400
1771
  if (state.permission !== null) {
@@ -1523,6 +1894,78 @@ function summarizeToolParameters(toolName, parameters) {
1523
1894
  }
1524
1895
  return "";
1525
1896
  }
1897
+ function summarizeActiveToolNotice(toolCalls) {
1898
+ if (toolCalls.size === 0) {
1899
+ return null;
1900
+ }
1901
+ const [first] = toolCalls.values();
1902
+ if (!first) {
1903
+ return "Running tools";
1904
+ }
1905
+ const title = formatInteractiveToolTitle(first.toolName, first.parameters).replace(/^⏺\s+/, "");
1906
+ if (toolCalls.size === 1) {
1907
+ return `Running ${title}`;
1908
+ }
1909
+ return `Running ${toolCalls.size} tools`;
1910
+ }
1911
+ function detailPanelScrollDeltaForKey(key, viewportSize) {
1912
+ switch (key) {
1913
+ case "up":
1914
+ case "wheelup":
1915
+ return -1;
1916
+ case "down":
1917
+ case "wheeldown":
1918
+ return 1;
1919
+ case "pageup":
1920
+ return -Math.max(1, viewportSize - 1);
1921
+ case "pagedown":
1922
+ return Math.max(1, viewportSize - 1);
1923
+ default:
1924
+ return 0;
1925
+ }
1926
+ }
1927
+ function transcriptScrollDeltaForKey(key, viewportSize) {
1928
+ switch (key) {
1929
+ case "pageup":
1930
+ return Math.max(1, viewportSize - 1);
1931
+ case "pagedown":
1932
+ return -Math.max(1, viewportSize - 1);
1933
+ case "wheelup":
1934
+ return 3;
1935
+ case "wheeldown":
1936
+ return -3;
1937
+ default:
1938
+ return 0;
1939
+ }
1940
+ }
1941
+ function compareTerminalSelectionPoints(leftRow, leftColumn, rightRow, rightColumn) {
1942
+ if (leftRow !== rightRow) {
1943
+ return leftRow - rightRow;
1944
+ }
1945
+ return leftColumn - rightColumn;
1946
+ }
1947
+ function createDebouncedCallback(callback, delayMs) {
1948
+ let timer = null;
1949
+ return {
1950
+ schedule() {
1951
+ if (timer !== null) {
1952
+ clearTimeout(timer);
1953
+ }
1954
+ timer = setTimeout(() => {
1955
+ timer = null;
1956
+ callback();
1957
+ }, delayMs);
1958
+ timer.unref?.();
1959
+ },
1960
+ cancel() {
1961
+ if (timer === null) {
1962
+ return;
1963
+ }
1964
+ clearTimeout(timer);
1965
+ timer = null;
1966
+ },
1967
+ };
1968
+ }
1526
1969
  function normalizeToolResultLines(toolName, result, startedParameters) {
1527
1970
  if ((!result || result.trim().length === 0) && startedParameters) {
1528
1971
  if (toolName === "write" || toolName === "edit") {
@@ -1718,11 +2161,106 @@ function truncatePlainTerminalText(text, width) {
1718
2161
  }
1719
2162
  return output;
1720
2163
  }
1721
- function formatTurnNotice(kind) {
2164
+ function formatQueuedPromptPreviewLines(queuedInputs, terminalWidth) {
2165
+ const DIM = "\x1b[2m";
2166
+ const YELLOW = "\x1b[33m";
2167
+ const RESET = "\x1b[0m";
2168
+ const maxVisible = 2;
2169
+ const previewWidth = Math.max(12, terminalWidth - 18);
2170
+ const lines = queuedInputs.slice(0, maxVisible).map((input, index) => {
2171
+ const label = queuedInputs.length > 1 ? `↳ Queued ${index + 1}: ` : "↳ Queued: ";
2172
+ return `${DIM}${YELLOW}${label}${RESET}${truncatePlainTerminalText(input, previewWidth)}`;
2173
+ });
2174
+ if (queuedInputs.length > maxVisible) {
2175
+ lines.push(`${DIM}${YELLOW}↳ +${queuedInputs.length - maxVisible} more queued${RESET}`);
2176
+ }
2177
+ return lines;
2178
+ }
2179
+ function formatTurnNotice(kind, options = {}) {
2180
+ const DIM = "\x1b[2m";
2181
+ const CYAN = "\x1b[36m";
2182
+ const RESET = "\x1b[0m";
2183
+ const suffix = ".".repeat(((options.animationFrame ?? 0) % 3) + 1);
2184
+ const label = kind === "thinking"
2185
+ ? `Thinking${suffix}`
2186
+ : kind === "responding"
2187
+ ? `Responding${suffix}`
2188
+ : `${options.toolLabel ?? "Running tools"}${suffix}`;
2189
+ const meta = [];
2190
+ if ((options.elapsedMs ?? 0) >= TURN_NOTICE_ELAPSED_THRESHOLD_MS) {
2191
+ meta.push(formatElapsedLabel(options.elapsedMs ?? 0));
2192
+ }
2193
+ const usageLabel = formatUsageCompact(options.usage ?? null, options.showPendingOutputIndicator ?? false);
2194
+ if (usageLabel.length > 0) {
2195
+ meta.push(usageLabel);
2196
+ }
2197
+ if ((options.queuedCount ?? 0) > 0) {
2198
+ meta.push(`${options.queuedCount} queued`);
2199
+ }
2200
+ return `${DIM}${CYAN}· ${label}${meta.length > 0 ? ` (${meta.join(" · ")})` : ""}${RESET}`;
2201
+ }
2202
+ function formatUsageSummaryLine(label, usage) {
1722
2203
  const DIM = "\x1b[2m";
1723
2204
  const CYAN = "\x1b[36m";
1724
2205
  const RESET = "\x1b[0m";
1725
- return kind === "thinking" ? `${DIM}${CYAN}· Thinking…${RESET}` : `${DIM}${CYAN}· Responding…${RESET}`;
2206
+ return `${DIM}${CYAN}· ${label}: ${formatUsageCompact(usage)}${RESET}`;
2207
+ }
2208
+ function formatUsageCompact(usage, showPendingOutputIndicator = false) {
2209
+ if (usage && usage.output > 0) {
2210
+ return `↓ ${formatTokenCount(usage.output)} tokens`;
2211
+ }
2212
+ if (showPendingOutputIndicator) {
2213
+ return "↓";
2214
+ }
2215
+ return "";
2216
+ }
2217
+ function formatElapsedLabel(elapsedMs) {
2218
+ const totalSeconds = Math.max(0, Math.floor(elapsedMs / 1000));
2219
+ if (totalSeconds < 60) {
2220
+ return `${totalSeconds}s`;
2221
+ }
2222
+ const minutes = Math.floor(totalSeconds / 60);
2223
+ const seconds = totalSeconds % 60;
2224
+ if (minutes < 60) {
2225
+ return seconds === 0 ? `${minutes}m` : `${minutes}m ${seconds}s`;
2226
+ }
2227
+ const hours = Math.floor(minutes / 60);
2228
+ const remainingMinutes = minutes % 60;
2229
+ return remainingMinutes === 0 ? `${hours}h` : `${hours}h ${remainingMinutes}m`;
2230
+ }
2231
+ function formatTokenCount(count) {
2232
+ if (count < 1_000)
2233
+ return `${count}`;
2234
+ if (count < 10_000)
2235
+ return `${(count / 1_000).toFixed(1).replace(/\.0$/, "")}k`;
2236
+ return `${Math.round(count / 1_000)}k`;
2237
+ }
2238
+ function emptyUsageSnapshot() {
2239
+ return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0 };
2240
+ }
2241
+ function normalizeUsageSnapshot(usage) {
2242
+ return {
2243
+ input: Math.max(0, usage.input),
2244
+ output: Math.max(0, usage.output),
2245
+ cacheRead: Math.max(0, usage.cacheRead),
2246
+ cacheWrite: Math.max(0, usage.cacheWrite),
2247
+ totalTokens: Math.max(0, usage.totalTokens),
2248
+ };
2249
+ }
2250
+ function addUsageSnapshots(left, right) {
2251
+ return {
2252
+ input: left.input + right.input,
2253
+ output: left.output + right.output,
2254
+ cacheRead: left.cacheRead + right.cacheRead,
2255
+ cacheWrite: left.cacheWrite + right.cacheWrite,
2256
+ totalTokens: left.totalTokens + right.totalTokens,
2257
+ };
2258
+ }
2259
+ function hasUsageSnapshot(usage) {
2260
+ if (!usage) {
2261
+ return false;
2262
+ }
2263
+ return usage.input > 0 || usage.output > 0 || usage.cacheRead > 0 || usage.cacheWrite > 0 || usage.totalTokens > 0;
1726
2264
  }
1727
2265
  function mergeStreamingText(existing, incoming) {
1728
2266
  if (incoming.length === 0)
@@ -1781,7 +2319,7 @@ function wrapTranscriptContent(content, width) {
1781
2319
  lines.push(current);
1782
2320
  return lines;
1783
2321
  }
1784
- function computeVisibleTranscriptLines(blocks, width, maxRows) {
2322
+ function computeVisibleTranscriptLines(blocks, width, maxRows, offsetFromBottom = 0) {
1785
2323
  if (maxRows <= 0 || blocks.length === 0) {
1786
2324
  return [];
1787
2325
  }
@@ -1789,7 +2327,77 @@ function computeVisibleTranscriptLines(blocks, width, maxRows) {
1789
2327
  if (flattened.length <= maxRows) {
1790
2328
  return flattened;
1791
2329
  }
1792
- return flattened.slice(flattened.length - maxRows);
2330
+ const maxOffset = Math.max(0, flattened.length - maxRows);
2331
+ const clampedOffset = Math.max(0, Math.min(offsetFromBottom, maxOffset));
2332
+ const end = flattened.length - clampedOffset;
2333
+ const start = Math.max(0, end - maxRows);
2334
+ return flattened.slice(start, end);
2335
+ }
2336
+ function extractPlainTextSelection(lines, selection) {
2337
+ if (lines.length === 0) {
2338
+ return "";
2339
+ }
2340
+ const startFirst = selection.startRow < selection.endRow ||
2341
+ (selection.startRow === selection.endRow && selection.startColumn <= selection.endColumn);
2342
+ const start = startFirst
2343
+ ? { row: selection.startRow, column: selection.startColumn }
2344
+ : { row: selection.endRow, column: selection.endColumn };
2345
+ const end = startFirst
2346
+ ? { row: selection.endRow, column: selection.endColumn }
2347
+ : { row: selection.startRow, column: selection.startColumn };
2348
+ const selectedLines = [];
2349
+ for (let row = start.row; row <= end.row; row += 1) {
2350
+ const line = lines[row] ?? "";
2351
+ const fromColumn = row === start.row ? start.column : 1;
2352
+ const toColumn = row === end.row ? end.column : Number.MAX_SAFE_INTEGER;
2353
+ selectedLines.push(slicePlainTextByDisplayColumns(line, fromColumn - 1, toColumn - 1));
2354
+ }
2355
+ return selectedLines.join("\n").replace(/\s+$/g, "").replace(/\n[ \t]+$/gm, "");
2356
+ }
2357
+ function renderSelectedPlainTranscriptLine(line, startColumn, endColumn, width) {
2358
+ const safeStart = Math.max(1, Math.min(startColumn, endColumn));
2359
+ const safeEnd = Math.max(safeStart, Math.max(startColumn, endColumn));
2360
+ const before = slicePlainTextByDisplayColumns(line, 0, safeStart - 1);
2361
+ const selected = slicePlainTextByDisplayColumns(line, safeStart - 1, safeEnd - 1);
2362
+ const after = slicePlainTextByDisplayColumns(line, safeEnd - 1, Number.MAX_SAFE_INTEGER);
2363
+ const inverse = "\x1b[7m";
2364
+ const reset = "\x1b[0m";
2365
+ return fitTerminalLine(`${before}${inverse}${selected.length > 0 ? selected : " "}${reset}${after}`, width);
2366
+ }
2367
+ function slicePlainTextByDisplayColumns(text, startColumn, endColumnExclusive) {
2368
+ const start = Math.max(0, startColumn);
2369
+ const end = Math.max(start, endColumnExclusive);
2370
+ let output = "";
2371
+ let used = 0;
2372
+ for (const ch of text) {
2373
+ const charWidth = (0, terminal_display_width_js_1.measureTerminalDisplayWidth)(ch);
2374
+ const nextUsed = used + charWidth;
2375
+ if (nextUsed > start && used < end) {
2376
+ output += ch;
2377
+ }
2378
+ if (used >= end) {
2379
+ break;
2380
+ }
2381
+ used = nextUsed;
2382
+ }
2383
+ return output;
2384
+ }
2385
+ function copyTextToClipboard(text) {
2386
+ if (text.length === 0) {
2387
+ return;
2388
+ }
2389
+ if (process.platform === "darwin") {
2390
+ (0, node_child_process_1.spawnSync)("pbcopy", [], { input: text, stdio: ["pipe", "ignore", "ignore"] });
2391
+ return;
2392
+ }
2393
+ if (process.platform === "win32") {
2394
+ (0, node_child_process_1.spawnSync)("clip", [], { input: text, stdio: ["pipe", "ignore", "ignore"] });
2395
+ return;
2396
+ }
2397
+ (0, node_child_process_1.spawnSync)("sh", ["-c", "command -v wl-copy >/dev/null 2>&1 && wl-copy || xclip -selection clipboard"], {
2398
+ input: text,
2399
+ stdio: ["pipe", "ignore", "ignore"],
2400
+ });
1793
2401
  }
1794
2402
  function computeTranscriptDisplayRows(blocks, width) {
1795
2403
  return flattenTranscriptLines(blocks, width).length;
@@ -1801,20 +2409,32 @@ function materializeAssistantTranscriptBlock(buffer) {
1801
2409
  return formatTranscriptAssistantLine(buffer);
1802
2410
  }
1803
2411
  function appendAssistantTranscriptBlock(blocks, assistantBlock) {
1804
- const lastNonEmptyBlock = [...blocks].reverse().find((block) => block.length > 0);
1805
- if (lastNonEmptyBlock && isTranscriptUserBlock(lastNonEmptyBlock)) {
1806
- return [...blocks, "", assistantBlock];
2412
+ return appendTranscriptBlockWithSpacer(blocks, assistantBlock);
2413
+ }
2414
+ function appendTranscriptBlockWithSpacer(blocks, block) {
2415
+ if (block.length === 0) {
2416
+ return [...blocks];
2417
+ }
2418
+ const lastNonEmptyIndex = findLastNonEmptyBlockIndex(blocks);
2419
+ if (lastNonEmptyIndex === -1) {
2420
+ return [block];
2421
+ }
2422
+ const normalized = blocks.slice(0, lastNonEmptyIndex + 1);
2423
+ return [...normalized, "", block];
2424
+ }
2425
+ function findLastNonEmptyBlockIndex(blocks) {
2426
+ for (let index = blocks.length - 1; index >= 0; index -= 1) {
2427
+ if ((blocks[index] ?? "").length > 0) {
2428
+ return index;
2429
+ }
1807
2430
  }
1808
- return [...blocks, assistantBlock];
2431
+ return -1;
1809
2432
  }
1810
2433
  function computeFooterStartRow(welcomeLineCount, terminalHeight, footerHeight, transcriptRows) {
1811
2434
  const naturalStartRow = welcomeLineCount + 1 + Math.max(0, transcriptRows);
1812
2435
  const bottomAnchoredStartRow = Math.max(1, terminalHeight - footerHeight + 1);
1813
2436
  return Math.min(naturalStartRow, bottomAnchoredStartRow);
1814
2437
  }
1815
- function isFooterBottomAnchored(startRow, terminalHeight, footerHeight) {
1816
- return startRow === Math.max(1, terminalHeight - footerHeight + 1);
1817
- }
1818
2438
  function flattenTranscriptLines(blocks, width) {
1819
2439
  const flattened = [];
1820
2440
  for (const block of blocks) {