@bastani/atomic 0.9.13-alpha.3 → 0.9.13

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 (99) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/dist/builtin/i-have-adhd/CHANGELOG.md +13 -0
  3. package/dist/builtin/i-have-adhd/package.json +1 -1
  4. package/dist/builtin/intercom/CHANGELOG.md +26 -0
  5. package/dist/builtin/intercom/package.json +1 -1
  6. package/dist/builtin/mcp/CHANGELOG.md +15 -0
  7. package/dist/builtin/mcp/package.json +1 -1
  8. package/dist/builtin/subagents/CHANGELOG.md +41 -0
  9. package/dist/builtin/subagents/package.json +1 -1
  10. package/dist/builtin/web-access/CHANGELOG.md +14 -0
  11. package/dist/builtin/web-access/package.json +1 -1
  12. package/dist/builtin/workflows/CHANGELOG.md +57 -0
  13. package/dist/builtin/workflows/package.json +1 -1
  14. package/dist/builtin/workflows/src/extension/index.bundle.mjs +70 -16
  15. package/dist/builtin/workflows/src/extension/ui-surface.ts +2 -0
  16. package/dist/builtin/workflows/src/tui/keybindings-adapter.ts +1 -0
  17. package/dist/builtin/workflows/src/tui/overlay-adapter.ts +1 -0
  18. package/dist/builtin/workflows/src/tui/stage-chat-view-archive-history.ts +27 -8
  19. package/dist/builtin/workflows/src/tui/stage-chat-view-input.ts +17 -1
  20. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +20 -3
  21. package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +10 -1
  22. package/dist/core/agent-session-events.d.ts.map +1 -1
  23. package/dist/core/agent-session-events.js +6 -0
  24. package/dist/core/agent-session-events.js.map +1 -1
  25. package/dist/core/agent-session-methods.d.ts +2 -0
  26. package/dist/core/agent-session-methods.d.ts.map +1 -1
  27. package/dist/core/agent-session-methods.js.map +1 -1
  28. package/dist/core/agent-session-prompt.d.ts.map +1 -1
  29. package/dist/core/agent-session-prompt.js +63 -0
  30. package/dist/core/agent-session-prompt.js.map +1 -1
  31. package/dist/core/agent-session-queue-pause.d.ts.map +1 -1
  32. package/dist/core/agent-session-queue-pause.js +6 -0
  33. package/dist/core/agent-session-queue-pause.js.map +1 -1
  34. package/dist/core/agent-session.d.ts +14 -0
  35. package/dist/core/agent-session.d.ts.map +1 -1
  36. package/dist/core/agent-session.js +14 -0
  37. package/dist/core/agent-session.js.map +1 -1
  38. package/dist/core/extensions/ui-types.d.ts +2 -0
  39. package/dist/core/extensions/ui-types.d.ts.map +1 -1
  40. package/dist/core/extensions/ui-types.js.map +1 -1
  41. package/dist/core/keybindings.d.ts +0 -5
  42. package/dist/core/keybindings.d.ts.map +1 -1
  43. package/dist/core/keybindings.js +0 -2
  44. package/dist/core/keybindings.js.map +1 -1
  45. package/dist/index.d.ts +1 -1
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +1 -1
  48. package/dist/index.js.map +1 -1
  49. package/dist/modes/interactive/components/index.d.ts +1 -0
  50. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  51. package/dist/modes/interactive/components/index.js +1 -0
  52. package/dist/modes/interactive/components/index.js.map +1 -1
  53. package/dist/modes/interactive/components/transcript-follow-indicator.d.ts +14 -0
  54. package/dist/modes/interactive/components/transcript-follow-indicator.d.ts.map +1 -0
  55. package/dist/modes/interactive/components/transcript-follow-indicator.js +36 -0
  56. package/dist/modes/interactive/components/transcript-follow-indicator.js.map +1 -0
  57. package/dist/modes/interactive/interactive-extension-custom-ui.js +3 -0
  58. package/dist/modes/interactive/interactive-extension-custom-ui.js.map +1 -1
  59. package/dist/modes/interactive/interactive-input-handling.js +0 -3
  60. package/dist/modes/interactive/interactive-input-handling.js.map +1 -1
  61. package/dist/modes/interactive/interactive-mode-base.d.ts +1 -0
  62. package/dist/modes/interactive/interactive-mode-base.d.ts.map +1 -1
  63. package/dist/modes/interactive/interactive-mode-base.js +7 -1
  64. package/dist/modes/interactive/interactive-mode-base.js.map +1 -1
  65. package/dist/modes/interactive/interactive-mode-surface.d.ts +3 -3
  66. package/dist/modes/interactive/interactive-mode-surface.d.ts.map +1 -1
  67. package/dist/modes/interactive/interactive-mode-surface.js.map +1 -1
  68. package/dist/modes/interactive/interactive-mode.d.ts +1 -0
  69. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  70. package/dist/modes/interactive/interactive-mode.js +1 -0
  71. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  72. package/dist/modes/interactive/interactive-slash-commands.js +2 -5
  73. package/dist/modes/interactive/interactive-slash-commands.js.map +1 -1
  74. package/dist/modes/interactive/interactive-startup.d.ts.map +1 -1
  75. package/dist/modes/interactive/interactive-startup.js +6 -0
  76. package/dist/modes/interactive/interactive-startup.js.map +1 -1
  77. package/dist/modes/interactive/interactive-transcript-follow.d.ts +2 -0
  78. package/dist/modes/interactive/interactive-transcript-follow.d.ts.map +1 -0
  79. package/dist/modes/interactive/interactive-transcript-follow.js +6 -0
  80. package/dist/modes/interactive/interactive-transcript-follow.js.map +1 -0
  81. package/dist/modes/interactive/interactive-tui.d.ts +13 -1
  82. package/dist/modes/interactive/interactive-tui.d.ts.map +1 -1
  83. package/dist/modes/interactive/interactive-tui.js +59 -1
  84. package/dist/modes/interactive/interactive-tui.js.map +1 -1
  85. package/dist/modes/interactive-engine/engine-custom-ui.d.ts +2 -0
  86. package/dist/modes/interactive-engine/engine-custom-ui.d.ts.map +1 -1
  87. package/dist/modes/interactive-engine/engine-custom-ui.js +1 -0
  88. package/dist/modes/interactive-engine/engine-custom-ui.js.map +1 -1
  89. package/dist/modes/interactive-engine/protocol.d.ts +1 -0
  90. package/dist/modes/interactive-engine/protocol.d.ts.map +1 -1
  91. package/dist/modes/interactive-engine/protocol.js +1 -0
  92. package/dist/modes/interactive-engine/protocol.js.map +1 -1
  93. package/dist/modes/interactive-engine/remote-component.d.ts.map +1 -1
  94. package/dist/modes/interactive-engine/remote-component.js +21 -9
  95. package/dist/modes/interactive-engine/remote-component.js.map +1 -1
  96. package/docs/keybindings.md +2 -5
  97. package/docs/usage.md +2 -0
  98. package/npm-shrinkwrap.json +29 -29
  99. package/package.json +2 -2
@@ -38951,6 +38951,7 @@ var APP_ACTION = {
38951
38951
  thinkingToggle: "app.thinking.toggle"
38952
38952
  };
38953
38953
  var TUI_ACTION = {
38954
+ altScreenBottom: "tui.altScreen.bottom",
38954
38955
  editorCursorUp: "tui.editor.cursorUp",
38955
38956
  editorCursorDown: "tui.editor.cursorDown",
38956
38957
  editorCursorLeft: "tui.editor.cursorLeft",
@@ -40606,6 +40607,11 @@ function remoteTerminalControlFrom(tui) {
40606
40607
  return { setAutowrap: terminal.setAutowrap.bind(terminal) };
40607
40608
  }
40608
40609
 
40610
+ // dist/builtin/workflows/src/tui/workflow-attach-pane.ts
40611
+ import {
40612
+ TRANSCRIPT_JUMP_TO_END_URL as TRANSCRIPT_JUMP_TO_END_URL2
40613
+ } from "@bastani/atomic";
40614
+
40609
40615
  // dist/builtin/workflows/src/runs/foreground/stage-queued-user-messages.ts
40610
40616
  var EMPTY_STAGE_QUEUED_USER_MESSAGES = Object.freeze({
40611
40617
  steering: Object.freeze([]),
@@ -43796,6 +43802,9 @@ class StageChatComposerDrafts {
43796
43802
  }
43797
43803
  }
43798
43804
 
43805
+ // dist/builtin/workflows/src/tui/stage-chat-view.ts
43806
+ import { keyText as keyText2, TranscriptFollowIndicator } from "@bastani/atomic";
43807
+
43799
43808
  // dist/builtin/workflows/src/tui/stage-chat-layout.ts
43800
43809
  function resolveStageChatViewportRows(reported, fallbackRows) {
43801
43810
  if (typeof reported !== "number" || !Number.isFinite(reported)) {
@@ -44130,7 +44139,7 @@ function postMortemUnavailableMessage(reason) {
44130
44139
  return;
44131
44140
  }
44132
44141
  }
44133
- function renderReadOnlyArchiveBody(ctx, width, budget, stage) {
44142
+ function renderReadOnlyArchiveBody(ctx, width, budget, stage, indicatorLines = [], renderIndicator = () => indicatorLines) {
44134
44143
  if (stage?.promptFootprint) {
44135
44144
  return renderReadOnlyPromptArchiveBody(ctx, width, budget, stage);
44136
44145
  }
@@ -44141,8 +44150,13 @@ function renderReadOnlyArchiveBody(ctx, width, budget, stage) {
44141
44150
  callout.push(...bannerLines(ctx, width, unavailableMessage === undefined ? "info" : "warning", unavailableMessage === undefined ? "◌" : "!", unavailableMessage === undefined ? "READ-ONLY SESSION" : "SESSION UNAVAILABLE", unavailableMessage === undefined ? stage?.sessionFile ? "archived transcript" : "no live chat session" : "post-mortem chat cannot be reopened"));
44142
44151
  callout.push(...new Text2(paint2(unavailableMessage ?? "This node is no longer attached to a live chat session.", t.textMuted), 2, 0).render(width));
44143
44152
  const transcriptBudget = Math.max(0, budget - callout.length);
44144
- const lines = transcriptBudget > 0 ? ctx.chatHost.renderBody(width, transcriptBudget) : [];
44145
- lines.push(...callout);
44153
+ const reservedIndicatorRows = indicatorLines.length > 0 && transcriptBudget > indicatorLines.length ? indicatorLines.length : 0;
44154
+ const transcriptRows = transcriptBudget - reservedIndicatorRows;
44155
+ const lines = transcriptRows > 0 ? ctx.chatHost.renderBody(width, transcriptRows) : [];
44156
+ const visibleIndicatorLines = reservedIndicatorRows > 0 ? [...renderIndicator().slice(0, reservedIndicatorRows)] : [];
44157
+ while (visibleIndicatorLines.length < reservedIndicatorRows)
44158
+ visibleIndicatorLines.push(blankLine(width));
44159
+ lines.push(...visibleIndicatorLines, ...callout);
44146
44160
  while (lines.length < budget)
44147
44161
  lines.push(blankLine(width));
44148
44162
  if (lines.length > budget)
@@ -44218,16 +44232,21 @@ function formatReadOnlyPromptAnswer(value2, kind) {
44218
44232
  return String(value2);
44219
44233
  }
44220
44234
  }
44221
- function renderPausedBody(ctx, width, budget) {
44235
+ function renderPausedBody(ctx, width, budget, indicatorLines = [], renderIndicator = () => indicatorLines) {
44222
44236
  const t = ctx.theme;
44223
44237
  const callout = [];
44224
44238
  callout.push(blankLine(width));
44225
44239
  callout.push(...bannerLines(ctx, width, "warning", "❚❚", "PAUSED", "enter resumes · ctrl+x return to graph"));
44226
44240
  callout.push(...new Text2(paint2("This workflow stage is paused. Type a message below and press Enter to resume.", t.textMuted), 2, 0).render(width));
44227
- const calloutRows = Math.min(callout.length, Math.max(0, budget - 1));
44228
- const transcriptBudget = Math.max(1, budget - calloutRows);
44229
- const lines = ctx.chatHost.renderBody(width, transcriptBudget);
44230
- lines.push(...callout.slice(0, calloutRows));
44241
+ const calloutRows = Math.min(callout.length, Math.max(0, budget));
44242
+ const transcriptBudget = Math.max(0, budget - calloutRows);
44243
+ const reservedIndicatorRows = indicatorLines.length > 0 && transcriptBudget > indicatorLines.length ? indicatorLines.length : 0;
44244
+ const transcriptRows = transcriptBudget - reservedIndicatorRows;
44245
+ const lines = transcriptRows > 0 ? ctx.chatHost.renderBody(width, transcriptRows) : [];
44246
+ const visibleIndicatorLines = reservedIndicatorRows > 0 ? [...renderIndicator().slice(0, reservedIndicatorRows)] : [];
44247
+ while (visibleIndicatorLines.length < reservedIndicatorRows)
44248
+ visibleIndicatorLines.push(blankLine(width));
44249
+ lines.push(...visibleIndicatorLines, ...callout.slice(0, calloutRows));
44231
44250
  while (lines.length < budget)
44232
44251
  lines.push(blankLine(width));
44233
44252
  if (lines.length > budget)
@@ -44670,6 +44689,9 @@ function canSubmitPrompt(ctx, promptId) {
44670
44689
  return ctx.canSubmitPrompt?.(ctx.runId, ctx.stageId, promptId) ?? true;
44671
44690
  }
44672
44691
 
44692
+ // dist/builtin/workflows/src/tui/stage-chat-view-input.ts
44693
+ import { TRANSCRIPT_JUMP_TO_END_URL } from "@bastani/atomic";
44694
+
44673
44695
  // dist/builtin/workflows/src/tui/stage-chat-view-state.ts
44674
44696
  import { ChatSessionHost } from "@bastani/atomic";
44675
44697
  import { Editor } from "@earendil-works/pi-tui";
@@ -45644,6 +45666,8 @@ function promptPageSize(ctx) {
45644
45666
 
45645
45667
  // dist/builtin/workflows/src/tui/stage-chat-view-input.ts
45646
45668
  function handleStageChatInput(ctx, data) {
45669
+ if (data === TRANSCRIPT_JUMP_TO_END_URL)
45670
+ return handleStageChatJumpToBottom(ctx, data);
45647
45671
  const keybindings = isKeybindingsLike(ctx.piKeybindings) ? ctx.piKeybindings : undefined;
45648
45672
  if (matchesKey(data, Key.ctrl("t")) && matchesAction(keybindings, data, APP_ACTION.thinkingToggle)) {
45649
45673
  return false;
@@ -45677,6 +45701,8 @@ function handleStageChatInput(ctx, data) {
45677
45701
  if (readOnlyPromptArchive && handlePromptScrollInput(ctx, data, true)) {
45678
45702
  return true;
45679
45703
  }
45704
+ if (handleStageChatJumpToBottom(ctx, data))
45705
+ return true;
45680
45706
  if (ctx.chatHost.handleScrollInput(data))
45681
45707
  return true;
45682
45708
  if (matchesKey(data, Key.escape)) {
@@ -45707,6 +45733,13 @@ function handleStageChatInput(ctx, data) {
45707
45733
  return true;
45708
45734
  return ctx.chatHost.handleInput(data);
45709
45735
  }
45736
+ function handleStageChatJumpToBottom(ctx, data) {
45737
+ const keybindings = isKeybindingsLike(ctx.piKeybindings) ? ctx.piKeybindings : undefined;
45738
+ if (data !== TRANSCRIPT_JUMP_TO_END_URL && !matchesAction(keybindings, data, TUI_ACTION.altScreenBottom))
45739
+ return false;
45740
+ ctx.chatHost.scrollToBottom();
45741
+ return true;
45742
+ }
45710
45743
  function handleToolsExpandInput(ctx, data) {
45711
45744
  const keybindings = isKeybindingsLike(ctx.piKeybindings) ? ctx.piKeybindings : undefined;
45712
45745
  if (!matchesAction(keybindings, data, APP_ACTION.toolsExpand))
@@ -45730,6 +45763,10 @@ function handleMountedCustomUiInput(ctx, data) {
45730
45763
  ctx.onClose();
45731
45764
  return true;
45732
45765
  }
45766
+ if (handleStageChatJumpToBottom(ctx, data)) {
45767
+ ctx.requestRender?.();
45768
+ return true;
45769
+ }
45733
45770
  if (ctx.chatHost.handleScrollInput(data)) {
45734
45771
  ctx.requestRender?.();
45735
45772
  return true;
@@ -45904,6 +45941,12 @@ class StageChatView {
45904
45941
  if (blocked)
45905
45942
  this.chatHost.scrollToBottom();
45906
45943
  let bodyLines;
45944
+ let transcriptBodyActive = false;
45945
+ let reservedIndicatorLines = [];
45946
+ const indicator = new TranscriptFollowIndicator({
45947
+ isFollowing: () => this.chatHost.bodyScrollFromBottom() === 0,
45948
+ keyLabel: () => keyText2("tui.altScreen.bottom")
45949
+ });
45907
45950
  if (bodyBudget <= 0) {
45908
45951
  bodyLines = [];
45909
45952
  } else if (promptActive) {
@@ -45911,16 +45954,24 @@ class StageChatView {
45911
45954
  } else if (blocked) {
45912
45955
  bodyLines = renderBlockedBody(ctx, w, bodyBudget, stage);
45913
45956
  } else if (!readOnlyArchive && isPaused(ctx, stage)) {
45914
- bodyLines = renderPausedBody(ctx, w, bodyBudget);
45957
+ reservedIndicatorLines = bodyBudget > 1 ? indicator.render(w) : [];
45958
+ bodyLines = renderPausedBody(ctx, w, bodyBudget, reservedIndicatorLines, () => indicator.render(w));
45915
45959
  } else if (readOnlyArchive) {
45916
- bodyLines = renderReadOnlyArchiveBody(ctx, w, bodyBudget, stage);
45960
+ reservedIndicatorLines = bodyBudget > 1 ? indicator.render(w) : [];
45961
+ bodyLines = renderReadOnlyArchiveBody(ctx, w, bodyBudget, stage, reservedIndicatorLines, () => indicator.render(w));
45917
45962
  } else {
45963
+ transcriptBodyActive = true;
45918
45964
  bodyLines = this.chatHost.renderBody(w, bodyBudget);
45919
45965
  }
45966
+ const indicatorLines = transcriptBodyActive && bodyBudget > 1 ? indicator.render(w) : [];
45967
+ const indicatorVisible = indicatorLines.length > 0;
45968
+ const dropBodyRow = transcriptBodyActive && indicatorVisible && bodyLines.length >= bodyBudget;
45969
+ const visibleBodyLines = bodyLines.slice(dropBodyRow ? 1 : 0, bodyBudget);
45920
45970
  const lines = [
45921
45971
  ...headerLines,
45922
45972
  ...sepLines,
45923
- ...bodyLines,
45973
+ ...visibleBodyLines,
45974
+ ...indicatorLines,
45924
45975
  ...visiblePendingLines,
45925
45976
  ...visibleWorkingLines,
45926
45977
  ...visibleUsageLines,
@@ -46288,6 +46339,8 @@ class WorkflowAttachPane {
46288
46339
  return true;
46289
46340
  return this.chatView.handleInput(data);
46290
46341
  }
46342
+ if (data === TRANSCRIPT_JUMP_TO_END_URL2)
46343
+ return false;
46291
46344
  if (this._shouldQuarantineGraphEnter(data))
46292
46345
  return true;
46293
46346
  return this.graphView.handleInput(data);
@@ -46655,6 +46708,7 @@ function buildGraphOverlayAdapter(pi, store2, buildOpts = {}) {
46655
46708
  overlay: true,
46656
46709
  deferInlineCustomUiFocus: true,
46657
46710
  handlesCtrlC: true,
46711
+ handlesInternalUiAction: true,
46658
46712
  overlayOptions: FULLSCREEN_OVERLAY_OPTIONS,
46659
46713
  onHandle: (handle) => {
46660
46714
  currentHandle = handle;
@@ -73435,7 +73489,7 @@ function openInputsPicker(ui, opts) {
73435
73489
  }
73436
73490
 
73437
73491
  // dist/builtin/workflows/src/tui/session-picker.ts
73438
- import { keyText as keyText2 } from "@bastani/atomic";
73492
+ import { keyText as keyText3 } from "@bastani/atomic";
73439
73493
  var ESCAPE_CODE = 27;
73440
73494
  function isQuitRun3(run2) {
73441
73495
  return run2.endedAt === undefined && run2.status === "paused" && run2.exitReason === "quit";
@@ -73528,12 +73582,12 @@ function renderHintsRow(width, theme, state2) {
73528
73582
  const muted = hexToAnsi(theme.textMuted);
73529
73583
  const sep7 = `${dim} · ${RESET}`;
73530
73584
  const hint = (key2, label) => `${text}${key2}${RESET} ${muted}${label}${RESET}`;
73531
- const parts = state2.filterFocused ? [hint(keyText2("tui.select.confirm"), "Submit"), hint(keyText2("tui.select.cancel"), "Exit Filter")] : [
73532
- hint(`${keyText2("tui.select.up")}/${keyText2("tui.select.down")}`, "Navigate"),
73533
- hint(keyText2("tui.select.confirm"), "Connect"),
73585
+ const parts = state2.filterFocused ? [hint(keyText3("tui.select.confirm"), "Submit"), hint(keyText3("tui.select.cancel"), "Exit Filter")] : [
73586
+ hint(`${keyText3("tui.select.up")}/${keyText3("tui.select.down")}`, "Navigate"),
73587
+ hint(keyText3("tui.select.confirm"), "Connect"),
73534
73588
  hint("a", state2.includeAll ? "Active Only" : "All"),
73535
73589
  hint("/", "Filter"),
73536
- hint(keyText2("tui.select.cancel"), "Close")
73590
+ hint(keyText3("tui.select.cancel"), "Close")
73537
73591
  ];
73538
73592
  const line = ` ${parts.join(sep7)}`;
73539
73593
  const clipped = truncateToWidth(line, width, "…");
@@ -120,6 +120,8 @@ export interface PiCustomOverlayOptions {
120
120
  * surface whose hint row offers `ctrl+c Skip`, `ctrl+c Close`, or cancel.
121
121
  */
122
122
  handlesCtrlC?: boolean;
123
+ /** Declare that this component claims internal UI actions such as jump-to-bottom. */
124
+ handlesInternalUiAction?: boolean;
123
125
  /**
124
126
  * Geometry / anchoring intended for pi-tui's `resolveOverlayLayout`.
125
127
  * NOT forwarded by current pi interactive `custom()` — see
@@ -44,6 +44,7 @@ export const APP_ACTION = {
44
44
  } as const;
45
45
 
46
46
  export const TUI_ACTION = {
47
+ altScreenBottom: "tui.altScreen.bottom",
47
48
  editorCursorUp: "tui.editor.cursorUp",
48
49
  editorCursorDown: "tui.editor.cursorDown",
49
50
  editorCursorLeft: "tui.editor.cursorLeft",
@@ -412,6 +412,7 @@ export function buildGraphOverlayAdapter(
412
412
  // Ctrl+C (Skip / Close / cancel), so the host must forward it here
413
413
  // instead of closing this overlay on the first press.
414
414
  handlesCtrlC: true,
415
+ handlesInternalUiAction: true,
415
416
  overlayOptions: FULLSCREEN_OVERLAY_OPTIONS,
416
417
  onHandle: (handle) => {
417
418
  currentHandle = handle;
@@ -38,6 +38,8 @@ export function renderReadOnlyArchiveBody(
38
38
  width: number,
39
39
  budget: number,
40
40
  stage: StageSnapshot | undefined,
41
+ indicatorLines: readonly string[] = [],
42
+ renderIndicator: () => readonly string[] = () => indicatorLines,
41
43
  ): string[] {
42
44
  if (stage?.promptFootprint) {
43
45
  return renderReadOnlyPromptArchiveBody(ctx, width, budget, stage);
@@ -69,8 +71,14 @@ export function renderReadOnlyArchiveBody(
69
71
  ).render(width),
70
72
  );
71
73
  const transcriptBudget = Math.max(0, budget - callout.length);
72
- const lines = transcriptBudget > 0 ? ctx.chatHost.renderBody(width, transcriptBudget) : [];
73
- lines.push(...callout);
74
+ const reservedIndicatorRows =
75
+ indicatorLines.length > 0 && transcriptBudget > indicatorLines.length ? indicatorLines.length : 0;
76
+ const transcriptRows = transcriptBudget - reservedIndicatorRows;
77
+ const lines = transcriptRows > 0 ? ctx.chatHost.renderBody(width, transcriptRows) : [];
78
+ const visibleIndicatorLines =
79
+ reservedIndicatorRows > 0 ? [...renderIndicator().slice(0, reservedIndicatorRows)] : [];
80
+ while (visibleIndicatorLines.length < reservedIndicatorRows) visibleIndicatorLines.push(blankLine(width));
81
+ lines.push(...visibleIndicatorLines, ...callout);
74
82
  while (lines.length < budget) lines.push(blankLine(width));
75
83
  if (lines.length > budget) lines.length = budget;
76
84
  return lines;
@@ -163,7 +171,13 @@ function formatReadOnlyPromptAnswer(value: unknown, kind: PendingPrompt["kind"])
163
171
  }
164
172
  }
165
173
 
166
- export function renderPausedBody(ctx: StageChatViewContext, width: number, budget: number): string[] {
174
+ export function renderPausedBody(
175
+ ctx: StageChatViewContext,
176
+ width: number,
177
+ budget: number,
178
+ indicatorLines: readonly string[] = [],
179
+ renderIndicator: () => readonly string[] = () => indicatorLines,
180
+ ): string[] {
167
181
  const t = ctx.theme;
168
182
  const callout: string[] = [];
169
183
  callout.push(blankLine(width));
@@ -175,11 +189,16 @@ export function renderPausedBody(ctx: StageChatViewContext, width: number, budge
175
189
  0,
176
190
  ).render(width),
177
191
  );
178
-
179
- const calloutRows = Math.min(callout.length, Math.max(0, budget - 1));
180
- const transcriptBudget = Math.max(1, budget - calloutRows);
181
- const lines = ctx.chatHost.renderBody(width, transcriptBudget);
182
- lines.push(...callout.slice(0, calloutRows));
192
+ const calloutRows = Math.min(callout.length, Math.max(0, budget));
193
+ const transcriptBudget = Math.max(0, budget - calloutRows);
194
+ const reservedIndicatorRows =
195
+ indicatorLines.length > 0 && transcriptBudget > indicatorLines.length ? indicatorLines.length : 0;
196
+ const transcriptRows = transcriptBudget - reservedIndicatorRows;
197
+ const lines = transcriptRows > 0 ? ctx.chatHost.renderBody(width, transcriptRows) : [];
198
+ const visibleIndicatorLines =
199
+ reservedIndicatorRows > 0 ? [...renderIndicator().slice(0, reservedIndicatorRows)] : [];
200
+ while (visibleIndicatorLines.length < reservedIndicatorRows) visibleIndicatorLines.push(blankLine(width));
201
+ lines.push(...visibleIndicatorLines, ...callout.slice(0, calloutRows));
183
202
  while (lines.length < budget) lines.push(blankLine(width));
184
203
  if (lines.length > budget) lines.length = budget;
185
204
  return lines;
@@ -1,4 +1,6 @@
1
- import { APP_ACTION, isKeybindingsLike, matchesAction } from "./keybindings-adapter.js";
1
+ import { TRANSCRIPT_JUMP_TO_END_URL } from "@bastani/atomic";
2
+
3
+ import { APP_ACTION, isKeybindingsLike, matchesAction, TUI_ACTION } from "./keybindings-adapter.js";
2
4
  import { parseTerminalMouseInput, terminalMouseWheelDirection } from "./mouse-input.js";
3
5
  import { defaultResponseFor, handlePromptCardInput, isPromptEscapeInput } from "./prompt-card.js";
4
6
  import { releaseMountedCustomUi } from "./stage-chat-view-custom-ui.js";
@@ -18,6 +20,7 @@ import { PROMPT_SCROLL_STEP_ROWS, type StageChatViewContext } from "./stage-chat
18
20
  import { Key, matchesKey } from "./text-helpers.js";
19
21
 
20
22
  export function handleStageChatInput(ctx: StageChatViewContext, data: string): boolean {
23
+ if (data === TRANSCRIPT_JUMP_TO_END_URL) return handleStageChatJumpToBottom(ctx, data);
21
24
  const keybindings = isKeybindingsLike(ctx.piKeybindings) ? ctx.piKeybindings : undefined;
22
25
  // Only the default physical Ctrl+T belongs to the host thinking action. A
23
26
  // user remap may reuse an editor key, so let that key reach the composer.
@@ -51,6 +54,7 @@ export function handleStageChatInput(ctx: StageChatViewContext, data: string): b
51
54
  if (readOnlyPromptArchive && handlePromptScrollInput(ctx, data, true)) {
52
55
  return true;
53
56
  }
57
+ if (handleStageChatJumpToBottom(ctx, data)) return true;
54
58
  if (ctx.chatHost.handleScrollInput(data)) return true;
55
59
  if (matchesKey(data, Key.escape)) {
56
60
  if (ctx.chatHost.isCompacting() || ctx.chatHost.isBashRunning() || ctx.chatHost.isEditingBashCommand()) {
@@ -79,6 +83,14 @@ export function handleStageChatInput(ctx: StageChatViewContext, data: string): b
79
83
  if (blocked) return true;
80
84
  return ctx.chatHost.handleInput(data);
81
85
  }
86
+
87
+ function handleStageChatJumpToBottom(ctx: StageChatViewContext, data: string): boolean {
88
+ const keybindings = isKeybindingsLike(ctx.piKeybindings) ? ctx.piKeybindings : undefined;
89
+ if (data !== TRANSCRIPT_JUMP_TO_END_URL && !matchesAction(keybindings, data, TUI_ACTION.altScreenBottom))
90
+ return false;
91
+ ctx.chatHost.scrollToBottom();
92
+ return true;
93
+ }
82
94
  function handleToolsExpandInput(ctx: StageChatViewContext, data: string): boolean {
83
95
  const keybindings = isKeybindingsLike(ctx.piKeybindings) ? ctx.piKeybindings : undefined;
84
96
  if (!matchesAction(keybindings, data, APP_ACTION.toolsExpand)) return false;
@@ -106,6 +118,10 @@ function handleMountedCustomUiInput(ctx: StageChatViewContext, data: string): bo
106
118
  }
107
119
  // Let scroll input reach the transcript so history stays scrollable while the
108
120
  // question is shown, matching the standalone ask_user_question tool.
121
+ if (handleStageChatJumpToBottom(ctx, data)) {
122
+ ctx.requestRender?.();
123
+ return true;
124
+ }
109
125
  if (ctx.chatHost.handleScrollInput(data)) {
110
126
  ctx.requestRender?.();
111
127
  return true;
@@ -23,6 +23,7 @@
23
23
  * - https://pi.dev/docs/latest/tui (canonical Pi-tui component contract)
24
24
  */
25
25
 
26
+ import { keyText, TranscriptFollowIndicator } from "@bastani/atomic";
26
27
  import type { Component, Focusable } from "@earendil-works/pi-tui";
27
28
  import { fitStageChatFrame, planStageChatFrame } from "./stage-chat-layout.js";
28
29
  import {
@@ -157,6 +158,12 @@ export class StageChatView implements Component, Focusable {
157
158
  if (blocked) this.chatHost.scrollToBottom();
158
159
 
159
160
  let bodyLines: string[];
161
+ let transcriptBodyActive = false;
162
+ let reservedIndicatorLines: readonly string[] = [];
163
+ const indicator = new TranscriptFollowIndicator({
164
+ isFollowing: () => this.chatHost.bodyScrollFromBottom() === 0,
165
+ keyLabel: () => keyText("tui.altScreen.bottom"),
166
+ });
160
167
  if (bodyBudget <= 0) {
161
168
  bodyLines = [];
162
169
  } else if (promptActive) {
@@ -164,17 +171,27 @@ export class StageChatView implements Component, Focusable {
164
171
  } else if (blocked) {
165
172
  bodyLines = renderBlockedBody(ctx, w, bodyBudget, stage);
166
173
  } else if (!readOnlyArchive && isPaused(ctx, stage)) {
167
- bodyLines = renderPausedBody(ctx, w, bodyBudget);
174
+ reservedIndicatorLines = bodyBudget > 1 ? indicator.render(w) : [];
175
+ bodyLines = renderPausedBody(ctx, w, bodyBudget, reservedIndicatorLines, () => indicator.render(w));
168
176
  } else if (readOnlyArchive) {
169
- bodyLines = renderReadOnlyArchiveBody(ctx, w, bodyBudget, stage);
177
+ reservedIndicatorLines = bodyBudget > 1 ? indicator.render(w) : [];
178
+ bodyLines = renderReadOnlyArchiveBody(ctx, w, bodyBudget, stage, reservedIndicatorLines, () =>
179
+ indicator.render(w),
180
+ );
170
181
  } else {
182
+ transcriptBodyActive = true;
171
183
  bodyLines = this.chatHost.renderBody(w, bodyBudget);
172
184
  }
185
+ const indicatorLines = transcriptBodyActive && bodyBudget > 1 ? indicator.render(w) : [];
186
+ const indicatorVisible = indicatorLines.length > 0;
187
+ const dropBodyRow = transcriptBodyActive && indicatorVisible && bodyLines.length >= bodyBudget;
188
+ const visibleBodyLines = bodyLines.slice(dropBodyRow ? 1 : 0, bodyBudget);
173
189
 
174
190
  const lines = [
175
191
  ...headerLines,
176
192
  ...sepLines,
177
- ...bodyLines,
193
+ ...visibleBodyLines,
194
+ ...indicatorLines,
178
195
  ...visiblePendingLines,
179
196
  ...visibleWorkingLines,
180
197
  ...visibleUsageLines,
@@ -8,7 +8,11 @@
8
8
  * `stage-chat-view.ts`, and `stage-control-registry.ts`.
9
9
  */
10
10
 
11
- import type { ChatMessageRenderOptions, ReadonlyFooterDataProvider } from "@bastani/atomic";
11
+ import {
12
+ type ChatMessageRenderOptions,
13
+ type ReadonlyFooterDataProvider,
14
+ TRANSCRIPT_JUMP_TO_END_URL,
15
+ } from "@bastani/atomic";
12
16
  import type { Component, EditorComponent, EditorTheme, TUI } from "@earendil-works/pi-tui";
13
17
  import type { StageControlRegistry } from "../runs/foreground/stage-control-registry.js";
14
18
  import { stageQueuedUserMessageCount } from "../runs/foreground/stage-queued-user-messages.js";
@@ -340,6 +344,11 @@ export class WorkflowAttachPane implements Component {
340
344
  if (this._shouldQuarantineStagePromptEnter(data)) return true;
341
345
  return this.chatView.handleInput(data);
342
346
  }
347
+ // The overlay opts into internal UI actions for the whole adapter, so the
348
+ // graph view is offered the jump URL too. It draws no follow indicator and
349
+ // has nothing to jump to: decline so the host transcript stays the fallback
350
+ // instead of swallowing the activation.
351
+ if (data === TRANSCRIPT_JUMP_TO_END_URL) return false;
343
352
  if (this._shouldQuarantineGraphEnter(data)) return true;
344
353
  return this.graphView.handleInput(data);
345
354
  }
@@ -1 +1 @@
1
- {"version":3,"file":"agent-session-events.d.ts","sourceRoot":"","sources":["../../src/core/agent-session-events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAC9E,OAAO,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAe,MAAM,8BAA8B,CAAC;AAE3F,OAAO,KAAK,EAAE,2BAA2B,IAAI,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAS9F,OAAO,EACN,KAAK,iBAAiB,EACtB,KAAK,yBAAyB,EAI9B,MAAM,0BAA0B,CAAC;AAgBlC,wBAAgB,KAAK,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAIxE;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAMzD;AAED,4EAA4E;AAE5E,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CA2B7F;AAED,wBAAgB,8BAA8B,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CA6B1F;AAED,wBAAgB,4BAA4B,CAC3C,IAAI,EAAE,YAAY,EAClB,QAAQ,EAAE,YAAY,EAAE,GACtB,gBAAgB,GAAG,SAAS,CAQ9B;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAgM7F;AAED,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CA0BvF;AAED,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CAQvF;AAED,wCAAwC;AAExC,wBAAgB,aAAa,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAMtD;AAED,0CAA0C;AAE1C,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAMhF;AAED,8EAA8E;AAE9E,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,YAAY,GAAG,gBAAgB,GAAG,SAAS,CAS1F;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,GAAG,IAAI,CAchH;AAED,kDAAkD;AAElD,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAsE9F;AAED;;;;GAIG;AAEH,wBAAgB,SAAS,CAAC,IAAI,EAAE,YAAY,EAAE,QAAQ,EAAE,yBAAyB,GAAG,MAAM,IAAI,CAU7F;AAED,oDAAoD;AACpD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAK7D;AAED;;;GAGG;AAEH,wBAAgB,OAAO,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAmBhD;AAMD,uBAAuB;AAEvB,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;CAiBrC,CAAC"}
1
+ {"version":3,"file":"agent-session-events.d.ts","sourceRoot":"","sources":["../../src/core/agent-session-events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAC9E,OAAO,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAe,MAAM,8BAA8B,CAAC;AAE3F,OAAO,KAAK,EAAE,2BAA2B,IAAI,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAS9F,OAAO,EACN,KAAK,iBAAiB,EACtB,KAAK,yBAAyB,EAI9B,MAAM,0BAA0B,CAAC;AAgBlC,wBAAgB,KAAK,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAIxE;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAMzD;AAED,4EAA4E;AAE5E,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CA2B7F;AAED,wBAAgB,8BAA8B,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CA6B1F;AAED,wBAAgB,4BAA4B,CAC3C,IAAI,EAAE,YAAY,EAClB,QAAQ,EAAE,YAAY,EAAE,GACtB,gBAAgB,GAAG,SAAS,CAQ9B;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAsM7F;AAED,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CA0BvF;AAED,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CAQvF;AAED,wCAAwC;AAExC,wBAAgB,aAAa,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAMtD;AAED,0CAA0C;AAE1C,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAMhF;AAED,8EAA8E;AAE9E,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,YAAY,GAAG,gBAAgB,GAAG,SAAS,CAS1F;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,GAAG,IAAI,CAchH;AAED,kDAAkD;AAElD,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAsE9F;AAED;;;;GAIG;AAEH,wBAAgB,SAAS,CAAC,IAAI,EAAE,YAAY,EAAE,QAAQ,EAAE,yBAAyB,GAAG,MAAM,IAAI,CAU7F;AAED,oDAAoD;AACpD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAK7D;AAED;;;GAGG;AAEH,wBAAgB,OAAO,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAmBhD;AAMD,uBAAuB;AAEvB,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;CAiBrC,CAAC"}
@@ -89,6 +89,11 @@ export async function _processAgentEvent(event) {
89
89
  const steeringIndex = this._steeringMessages.indexOf(messageText);
90
90
  if (steeringIndex !== -1) {
91
91
  this._steeringMessages.splice(steeringIndex, 1);
92
+ // The loop already polled this message out of the agent queue, so the
93
+ // pause hold can no longer reach it and Escape can no longer restore it
94
+ // to the editor. Record it so an interrupt that kills its reply before
95
+ // any output can still schedule that reply (issue #2362).
96
+ this._admittedQueuedMessageAwaitingReply = messageText;
92
97
  this._emitQueueUpdate();
93
98
  }
94
99
  else {
@@ -96,6 +101,7 @@ export async function _processAgentEvent(event) {
96
101
  const followUpIndex = this._followUpMessages.indexOf(messageText);
97
102
  if (followUpIndex !== -1) {
98
103
  this._followUpMessages.splice(followUpIndex, 1);
104
+ this._admittedQueuedMessageAwaitingReply = messageText;
99
105
  this._emitQueueUpdate();
100
106
  }
101
107
  }