pi-ui-extend 0.1.32 → 0.1.34

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 (95) hide show
  1. package/README.md +1 -1
  2. package/dist/app/app.d.ts +2 -0
  3. package/dist/app/app.js +28 -0
  4. package/dist/app/commands/command-session-actions.js +29 -1
  5. package/dist/app/constants.d.ts +1 -1
  6. package/dist/app/constants.js +2 -2
  7. package/dist/app/icons.d.ts +4 -9
  8. package/dist/app/icons.js +12 -35
  9. package/dist/app/model/model-usage-status.d.ts +2 -1
  10. package/dist/app/model/model-usage-status.js +33 -25
  11. package/dist/app/rendering/conversation-entry-renderer.d.ts +1 -0
  12. package/dist/app/rendering/conversation-tool-renderer.d.ts +1 -0
  13. package/dist/app/rendering/conversation-tool-renderer.js +12 -18
  14. package/dist/app/rendering/conversation-viewport.d.ts +4 -0
  15. package/dist/app/rendering/conversation-viewport.js +144 -13
  16. package/dist/app/rendering/dcp-stats.js +42 -16
  17. package/dist/app/rendering/render-controller.js +4 -0
  18. package/dist/app/rendering/status-line-renderer.d.ts +8 -1
  19. package/dist/app/rendering/status-line-renderer.js +36 -1
  20. package/dist/app/rendering/tab-line-renderer.js +2 -2
  21. package/dist/app/rendering/tool-block-renderer.d.ts +1 -0
  22. package/dist/app/rendering/tool-block-renderer.js +37 -11
  23. package/dist/app/runtime.js +1 -1
  24. package/dist/app/screen/mouse-controller.d.ts +5 -1
  25. package/dist/app/screen/mouse-controller.js +16 -0
  26. package/dist/app/screen/scroll-controller.d.ts +20 -0
  27. package/dist/app/screen/scroll-controller.js +127 -10
  28. package/dist/app/session/lazy-session-manager.js +35 -5
  29. package/dist/app/session/pix-system-message.d.ts +1 -0
  30. package/dist/app/session/pix-system-message.js +14 -3
  31. package/dist/app/session/queued-message-controller.d.ts +11 -4
  32. package/dist/app/session/queued-message-controller.js +74 -59
  33. package/dist/app/session/queued-message-entries.d.ts +2 -1
  34. package/dist/app/session/queued-message-entries.js +12 -1
  35. package/dist/app/session/session-event-controller.d.ts +42 -1
  36. package/dist/app/session/session-event-controller.js +500 -31
  37. package/dist/app/session/session-history.js +23 -4
  38. package/dist/app/session/tabs-controller.d.ts +11 -1
  39. package/dist/app/session/tabs-controller.js +102 -21
  40. package/dist/app/types.d.ts +14 -1
  41. package/dist/bundled-extensions/question/contract.d.ts +25 -0
  42. package/dist/bundled-extensions/question/contract.js +94 -0
  43. package/dist/bundled-extensions/question/index.d.ts +7 -0
  44. package/dist/bundled-extensions/question/index.js +28 -0
  45. package/dist/bundled-extensions/question/render.d.ts +4 -0
  46. package/dist/bundled-extensions/question/render.js +27 -0
  47. package/dist/bundled-extensions/question/result.d.ts +6 -0
  48. package/dist/bundled-extensions/question/result.js +84 -0
  49. package/dist/bundled-extensions/question/tool-description.d.ts +7 -0
  50. package/dist/bundled-extensions/question/tool-description.js +11 -0
  51. package/dist/bundled-extensions/question/tui.d.ts +2 -0
  52. package/dist/bundled-extensions/question/tui.js +577 -0
  53. package/dist/bundled-extensions/question/types.d.ts +103 -0
  54. package/dist/bundled-extensions/question/types.js +1 -0
  55. package/dist/bundled-extensions/session-title/config.d.ts +17 -0
  56. package/dist/bundled-extensions/session-title/config.js +150 -0
  57. package/dist/bundled-extensions/session-title/index.d.ts +5 -0
  58. package/dist/bundled-extensions/session-title/index.js +384 -0
  59. package/dist/bundled-extensions/session-title/title-generation.d.ts +26 -0
  60. package/dist/bundled-extensions/session-title/title-generation.js +141 -0
  61. package/dist/bundled-extensions/terminal-bell/index.d.ts +14 -0
  62. package/dist/bundled-extensions/terminal-bell/index.js +491 -0
  63. package/dist/config.d.ts +1 -1
  64. package/dist/config.js +2 -1
  65. package/dist/default-pix-config.js +2 -1
  66. package/dist/icon-theme.d.ts +7 -0
  67. package/dist/icon-theme.js +36 -0
  68. package/dist/schemas/pi-tools-suite-schema.d.ts +4 -0
  69. package/dist/schemas/pi-tools-suite-schema.js +5 -0
  70. package/dist/schemas/pix-schema.d.ts +1 -0
  71. package/dist/schemas/pix-schema.js +1 -0
  72. package/external/pi-tools-suite/README.md +7 -7
  73. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +16 -16
  74. package/external/pi-tools-suite/src/async-subagents/core/state.ts +18 -4
  75. package/external/pi-tools-suite/src/async-subagents/core/types.ts +4 -0
  76. package/external/pi-tools-suite/src/async-subagents/tools/result.ts +14 -26
  77. package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +0 -1
  78. package/external/pi-tools-suite/src/dcp/config.ts +14 -14
  79. package/external/pi-tools-suite/src/dcp/index.ts +31 -43
  80. package/external/pi-tools-suite/src/dcp/state-persistence.ts +151 -0
  81. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +25 -18
  82. package/external/pi-tools-suite/src/tool-descriptions.ts +34 -54
  83. package/package.json +3 -2
  84. package/schemas/pi-tools-suite.json +14 -0
  85. package/schemas/pix.json +7 -0
  86. package/extensions/question/contract.ts +0 -100
  87. package/extensions/question/index.ts +0 -34
  88. package/extensions/question/render.ts +0 -28
  89. package/extensions/question/result.ts +0 -86
  90. package/extensions/question/tool-description.ts +0 -11
  91. package/extensions/question/tui.ts +0 -629
  92. package/extensions/question/types.ts +0 -123
  93. package/extensions/session-title/config.ts +0 -164
  94. package/extensions/session-title/index.ts +0 -502
  95. package/extensions/terminal-bell/index.ts +0 -345
@@ -2,7 +2,8 @@ import { isRecord } from "../guards.js";
2
2
  import { createId } from "../id.js";
3
3
  import { isOnlyHiddenMetadata } from "../../markdown-format.js";
4
4
  import { extractImageContents, renderContent, renderUserMessageContent, stringifyUnknown } from "../rendering/message-content.js";
5
- import { PIX_SESSION_ENTRY_ID_FIELD, PIX_SYSTEM_MESSAGE_CUSTOM_TYPE } from "./pix-system-message.js";
5
+ import { THINKING_TOOL_NAME } from "../constants.js";
6
+ import { PIX_SESSION_ENTRY_ID_FIELD, PIX_SYSTEM_MESSAGE_CUSTOM_TYPE, PIX_THINKING_LEVEL_FIELD } from "./pix-system-message.js";
6
7
  const HISTORICAL_SUBAGENTS_OBSERVATION = { showSnapshot: false };
7
8
  const DEFAULT_HISTORY_CHUNK_SIZE = 50;
8
9
  const DEFAULT_HISTORY_TAIL_MESSAGE_COUNT = 80;
@@ -178,6 +179,9 @@ function renderAssistantHistoryMessage(message, toolResults, options) {
178
179
  const content = message.content;
179
180
  if (!Array.isArray(content))
180
181
  return;
182
+ const messageThinkingLevel = typeof message[PIX_THINKING_LEVEL_FIELD] === "string"
183
+ ? message[PIX_THINKING_LEVEL_FIELD]
184
+ : undefined;
181
185
  let assistantText = "";
182
186
  let thinkingText = "";
183
187
  for (const block of content) {
@@ -186,7 +190,14 @@ function renderAssistantHistoryMessage(message, toolResults, options) {
186
190
  if (block.type === "toolCall") {
187
191
  // Flush accumulated text/thinking before tool call.
188
192
  if (thinkingText) {
189
- options.addEntry({ id: createId("thinking"), kind: "thinking", text: thinkingText, expanded: false, status: "done" });
193
+ options.addEntry({
194
+ id: createId("thinking"),
195
+ kind: "thinking",
196
+ text: thinkingText,
197
+ expanded: options.toolDefaultExpanded(THINKING_TOOL_NAME),
198
+ ...(messageThinkingLevel === undefined ? {} : { level: messageThinkingLevel }),
199
+ status: "done",
200
+ });
190
201
  thinkingText = "";
191
202
  }
192
203
  if (assistantText && !isOnlyHiddenMetadata(assistantText)) {
@@ -195,6 +206,7 @@ function renderAssistantHistoryMessage(message, toolResults, options) {
195
206
  assistantText = "";
196
207
  const toolCallId = String(block.id ?? createId("tool"));
197
208
  const result = toolResults.get(toolCallId);
209
+ const isPendingToolCall = result === undefined && message.stopReason !== "aborted" && message.stopReason !== "error";
198
210
  const toolName = result?.toolName ?? String(block.name ?? "unknown");
199
211
  const argsText = stringifyUnknown(block.arguments);
200
212
  const output = result ? renderContent(result.content) : "";
@@ -213,7 +225,7 @@ function renderAssistantHistoryMessage(message, toolResults, options) {
213
225
  ...(result?.details === undefined ? {} : { details: result.details }),
214
226
  expanded: options.toolDefaultExpanded(toolName),
215
227
  isError: result?.isError ?? false,
216
- status: "done",
228
+ status: isPendingToolCall ? "running" : "done",
217
229
  });
218
230
  options.setToolEntryId(toolCallId, entryId);
219
231
  }
@@ -226,7 +238,14 @@ function renderAssistantHistoryMessage(message, toolResults, options) {
226
238
  }
227
239
  // Flush remaining text.
228
240
  if (thinkingText) {
229
- options.addEntry({ id: createId("thinking"), kind: "thinking", text: thinkingText, expanded: false, status: "done" });
241
+ options.addEntry({
242
+ id: createId("thinking"),
243
+ kind: "thinking",
244
+ text: thinkingText,
245
+ expanded: options.toolDefaultExpanded(THINKING_TOOL_NAME),
246
+ ...(messageThinkingLevel === undefined ? {} : { level: messageThinkingLevel }),
247
+ status: "done",
248
+ });
230
249
  }
231
250
  if (assistantText && !isOnlyHiddenMetadata(assistantText)) {
232
251
  options.addEntry({ id: createId("assistant"), kind: "assistant", text: assistantText });
@@ -3,10 +3,12 @@ import type { BindCurrentSessionOptions } from "./session-lifecycle-controller.j
3
3
  import type { AppSessionEventControllerState } from "./session-event-controller.js";
4
4
  import type { InputEditorDraftState } from "../../input-editor.js";
5
5
  import type { AppBlinkController } from "../screen/blink-controller.js";
6
+ import type { AppScrollState } from "../screen/scroll-controller.js";
6
7
  import type { AppOptions, Entry, SessionActivity, SessionTab, SubmittedUserMessage } from "../types.js";
7
8
  type TabSessionView = {
8
9
  entries: Entry[];
9
10
  eventState: AppSessionEventControllerState;
11
+ scrollState: AppScrollState;
10
12
  };
11
13
  export type TabInputState = InputEditorDraftState;
12
14
  export type AppTabsControllerHost = {
@@ -31,10 +33,13 @@ export type AppTabsControllerHost = {
31
33
  }): Promise<boolean>;
32
34
  captureSessionView?(): TabSessionView;
33
35
  restoreSessionView?(view: TabSessionView): void;
36
+ restoreScrollState?(state: AppScrollState): void;
34
37
  syncUserSessionEntryMetadata(): void;
35
38
  captureInputState(): TabInputState;
36
39
  restoreInputState(state: TabInputState): void;
37
40
  closeMenusForTabSwitch?(): void;
41
+ captureAutoUserMessages?(): readonly SubmittedUserMessage[];
42
+ restoreAutoUserMessages?(messages: readonly SubmittedUserMessage[]): void;
38
43
  captureDeferredUserMessages?(): readonly SubmittedUserMessage[];
39
44
  restoreDeferredUserMessages?(messages: readonly SubmittedUserMessage[]): void;
40
45
  addEntry(entry: Entry): void;
@@ -50,8 +55,10 @@ export declare class AppTabsController {
50
55
  private readonly runtimeRefreshTimersByTabId;
51
56
  private readonly historyReloadTimersByTabId;
52
57
  private readonly inputStatesByTabId;
58
+ private readonly autoUserMessagesByTabId;
53
59
  private readonly deferredUserMessagesByTabId;
54
60
  private readonly sessionViewsByTabId;
61
+ private readonly scrollStatesByTabId;
55
62
  private readonly tabIdsNeedingHistoryReload;
56
63
  private activeTabId;
57
64
  private pendingActiveTabId;
@@ -92,6 +99,7 @@ export declare class AppTabsController {
92
99
  private clearStartupTabPlaceholders;
93
100
  private storeActiveRuntime;
94
101
  private storeActiveSessionView;
102
+ private restoreStoredScrollState;
95
103
  private setRuntimeForTab;
96
104
  private deleteRuntimeForTab;
97
105
  private clearRuntimeSubscriptions;
@@ -116,7 +124,8 @@ export declare class AppTabsController {
116
124
  private replaceTabs;
117
125
  private removeTab;
118
126
  private restorePersistedInputStates;
119
- private restorePersistedDeferredUserMessages;
127
+ private restorePersistedScrollStates;
128
+ private restorePersistedQueuedUserMessages;
120
129
  private ensureCurrentSessionTab;
121
130
  private tabFromSession;
122
131
  private updateTabFromSession;
@@ -136,6 +145,7 @@ export declare class AppTabsController {
136
145
  private refreshRestoredTabTitles;
137
146
  private loadTabs;
138
147
  private parsePersistedInputState;
148
+ private parsePersistedScrollState;
139
149
  private parsePersistedSubmittedUserMessages;
140
150
  private saveTabs;
141
151
  private filePath;
@@ -6,12 +6,12 @@ import { getAgentDir, } from "@earendil-works/pi-coding-agent";
6
6
  import { isRecord } from "../guards.js";
7
7
  import { createId } from "../id.js";
8
8
  import { tabPanelRows } from "../rendering/tab-line-renderer.js";
9
- const TAB_STATE_VERSION = 3;
9
+ const TAB_STATE_VERSION = 4;
10
10
  const MAX_RESTORED_TABS = 8;
11
11
  const BACKGROUND_PREWARM_TAB_LIMIT = 2;
12
12
  const TAB_ATTENTION_BLINK_KEY = "tab-attention";
13
13
  const LOADING_TAB_TITLE_PATTERN = /^loading(?:…|\.\.\.)?$/iu;
14
- const DEFAULT_SESSION_TITLE_PATTERN = /^session [0-9a-f]{8}$/iu;
14
+ const DEFAULT_SESSION_TITLE_PATTERN = /^(?:session )?[0-9a-f]{8}$/iu;
15
15
  const SESSION_TITLE_HEAD_SCAN_MAX_BYTES = 256 * 1024;
16
16
  const SESSION_TITLE_SCAN_MAX_BYTES = 2 * 1024 * 1024;
17
17
  export class AppTabsController {
@@ -23,8 +23,10 @@ export class AppTabsController {
23
23
  runtimeRefreshTimersByTabId = new Map();
24
24
  historyReloadTimersByTabId = new Map();
25
25
  inputStatesByTabId = new Map();
26
+ autoUserMessagesByTabId = new Map();
26
27
  deferredUserMessagesByTabId = new Map();
27
28
  sessionViewsByTabId = new Map();
29
+ scrollStatesByTabId = new Map();
28
30
  tabIdsNeedingHistoryReload = new Set();
29
31
  activeTabId;
30
32
  pendingActiveTabId;
@@ -195,7 +197,7 @@ export class AppTabsController {
195
197
  : restoredTabs[0]?.sessionPath;
196
198
  this.replaceTabs(restoredTabs, desiredPath);
197
199
  this.restorePersistedInputStates(saved);
198
- this.restorePersistedDeferredUserMessages(saved);
200
+ this.restorePersistedQueuedUserMessages(saved);
199
201
  const restoredSessionPaths = saved.tabs.map((tab) => tab.path);
200
202
  if (explicitSessionPath && currentPath)
201
203
  this.ensureCurrentSessionTab(runtime.session);
@@ -230,6 +232,7 @@ export class AppTabsController {
230
232
  this.clearStartupTabPlaceholders();
231
233
  if (this.activeTabId)
232
234
  this.restoreInputState(this.activeTabId);
235
+ this.restorePersistedScrollStates(saved);
233
236
  await this.saveTabs();
234
237
  this.scheduleProjectSessionRetention();
235
238
  this.scheduleTabPrewarm();
@@ -248,6 +251,7 @@ export class AppTabsController {
248
251
  return;
249
252
  this.cancelHistoryLoad();
250
253
  this.syncActiveTabFromRuntime();
254
+ this.storeActiveSessionView();
251
255
  this.storeActiveInputState();
252
256
  this.storeActiveDeferredUserMessages();
253
257
  const previousTabId = this.activeTabId;
@@ -347,6 +351,7 @@ export class AppTabsController {
347
351
  return true;
348
352
  }
349
353
  this.cancelHistoryLoad();
354
+ this.storeActiveSessionView();
350
355
  this.storeActiveInputState();
351
356
  this.storeActiveDeferredUserMessages();
352
357
  const previousTabId = this.activeTabId;
@@ -455,6 +460,7 @@ export class AppTabsController {
455
460
  }
456
461
  this.cancelHistoryLoad();
457
462
  this.syncActiveTabFromRuntime({ save: false });
463
+ this.storeActiveSessionView();
458
464
  this.storeActiveInputState();
459
465
  this.storeActiveDeferredUserMessages();
460
466
  const previousTabId = this.activeTabId;
@@ -670,6 +676,7 @@ export class AppTabsController {
670
676
  this.tabItems.splice(index, 1);
671
677
  this.deleteRuntimeForTab(tabId);
672
678
  this.inputStatesByTabId.delete(tabId);
679
+ this.autoUserMessagesByTabId.delete(tabId);
673
680
  this.deferredUserMessagesByTabId.delete(tabId);
674
681
  this.storeActiveInputState();
675
682
  this.storeActiveDeferredUserMessages();
@@ -697,6 +704,7 @@ export class AppTabsController {
697
704
  this.tabItems.splice(index, 1);
698
705
  this.deleteRuntimeForTab(tabId);
699
706
  this.inputStatesByTabId.delete(tabId);
707
+ this.autoUserMessagesByTabId.delete(tabId);
700
708
  this.deferredUserMessagesByTabId.delete(tabId);
701
709
  this.stopAttentionBlinkIfIdle();
702
710
  this.activeTabId = nextTab.id;
@@ -732,6 +740,7 @@ export class AppTabsController {
732
740
  this.updateTabFromSession(tab, runtime.session);
733
741
  this.setRuntimeForTab(tab.id, runtime);
734
742
  this.inputStatesByTabId.delete(tab.id);
743
+ this.autoUserMessagesByTabId.delete(tab.id);
735
744
  this.deferredUserMessagesByTabId.delete(tab.id);
736
745
  this.restoreInputState(tab.id);
737
746
  this.host.closeMenusForTabSwitch?.();
@@ -770,6 +779,7 @@ export class AppTabsController {
770
779
  });
771
780
  if (!completed || isCancelled())
772
781
  return;
782
+ this.restoreStoredScrollState(this.activeTabId);
773
783
  this.host.setSessionStatus(runtime.session);
774
784
  this.host.syncUserSessionEntryMetadata();
775
785
  this.host.setSessionActivity(this.sessionActivity(runtime.session));
@@ -824,7 +834,17 @@ export class AppTabsController {
824
834
  storeActiveSessionView() {
825
835
  if (!this.activeTabId || !this.host.captureSessionView)
826
836
  return;
827
- this.sessionViewsByTabId.set(this.activeTabId, this.host.captureSessionView());
837
+ const view = this.host.captureSessionView();
838
+ this.sessionViewsByTabId.set(this.activeTabId, view);
839
+ this.scrollStatesByTabId.set(this.activeTabId, view.scrollState);
840
+ }
841
+ restoreStoredScrollState(tabId) {
842
+ if (!tabId || !this.host.restoreScrollState)
843
+ return;
844
+ const scrollState = this.scrollStatesByTabId.get(tabId);
845
+ if (!scrollState)
846
+ return;
847
+ this.host.restoreScrollState(scrollState);
828
848
  }
829
849
  setRuntimeForTab(tabId, runtime) {
830
850
  this.runtimesByTabId.set(tabId, runtime);
@@ -834,6 +854,7 @@ export class AppTabsController {
834
854
  this.runtimesByTabId.delete(tabId);
835
855
  this.runtimeLoadsByTabId.delete(tabId);
836
856
  this.sessionViewsByTabId.delete(tabId);
857
+ this.scrollStatesByTabId.delete(tabId);
837
858
  this.clearRuntimeRefreshTimers(tabId);
838
859
  this.clearHistoryReloadTimers(tabId);
839
860
  this.tabIdsNeedingHistoryReload.delete(tabId);
@@ -977,11 +998,18 @@ export class AppTabsController {
977
998
  });
978
999
  }
979
1000
  storeActiveDeferredUserMessages() {
980
- if (!this.activeTabId || !this.host.captureDeferredUserMessages)
1001
+ if (!this.activeTabId)
981
1002
  return;
982
- const messages = this.host.captureDeferredUserMessages();
983
- if (messages.length > 0) {
984
- this.deferredUserMessagesByTabId.set(this.activeTabId, messages.map((message) => this.cloneSubmittedUserMessage(message)));
1003
+ const autoMessages = this.host.captureAutoUserMessages?.() ?? [];
1004
+ if (autoMessages.length > 0) {
1005
+ this.autoUserMessagesByTabId.set(this.activeTabId, autoMessages.map((message) => this.cloneSubmittedUserMessage(message)));
1006
+ }
1007
+ else {
1008
+ this.autoUserMessagesByTabId.delete(this.activeTabId);
1009
+ }
1010
+ const deferredMessages = this.host.captureDeferredUserMessages?.() ?? [];
1011
+ if (deferredMessages.length > 0) {
1012
+ this.deferredUserMessagesByTabId.set(this.activeTabId, deferredMessages.map((message) => this.cloneSubmittedUserMessage(message)));
985
1013
  }
986
1014
  else {
987
1015
  this.deferredUserMessagesByTabId.delete(this.activeTabId);
@@ -994,6 +1022,7 @@ export class AppTabsController {
994
1022
  return { text, cursor: text.length };
995
1023
  }
996
1024
  restoreDeferredUserMessages(tabId) {
1025
+ this.host.restoreAutoUserMessages?.(this.autoUserMessagesByTabId.get(tabId) ?? []);
997
1026
  this.host.restoreDeferredUserMessages?.(this.deferredUserMessagesByTabId.get(tabId) ?? []);
998
1027
  }
999
1028
  cloneSubmittedUserMessage(message) {
@@ -1050,8 +1079,10 @@ export class AppTabsController {
1050
1079
  this.runtimesByTabId.clear();
1051
1080
  this.clearRuntimeSubscriptions();
1052
1081
  this.inputStatesByTabId.clear();
1082
+ this.autoUserMessagesByTabId.clear();
1053
1083
  this.deferredUserMessagesByTabId.clear();
1054
1084
  this.sessionViewsByTabId.clear();
1085
+ this.scrollStatesByTabId.clear();
1055
1086
  const seen = new Set();
1056
1087
  for (const tab of tabs) {
1057
1088
  const sessionPath = tab.sessionPath ? resolve(tab.sessionPath) : undefined;
@@ -1081,6 +1112,7 @@ export class AppTabsController {
1081
1112
  this.tabItems.splice(index, 1);
1082
1113
  this.deleteRuntimeForTab(tabId);
1083
1114
  this.inputStatesByTabId.delete(tabId);
1115
+ this.autoUserMessagesByTabId.delete(tabId);
1084
1116
  this.deferredUserMessagesByTabId.delete(tabId);
1085
1117
  }
1086
1118
  restorePersistedInputStates(saved) {
@@ -1099,20 +1131,44 @@ export class AppTabsController {
1099
1131
  this.inputStatesByTabId.set(tab.id, input);
1100
1132
  }
1101
1133
  }
1102
- restorePersistedDeferredUserMessages(saved) {
1103
- const messagesByPath = new Map();
1134
+ restorePersistedScrollStates(saved) {
1135
+ const scrollStatesByPath = new Map();
1104
1136
  for (const tab of saved.tabs) {
1105
- if (!tab.deferredUserMessages || tab.deferredUserMessages.length === 0)
1137
+ if (!tab.scrollState)
1106
1138
  continue;
1107
- messagesByPath.set(resolve(tab.path), tab.deferredUserMessages.map((message) => this.cloneSubmittedUserMessage(message)));
1139
+ scrollStatesByPath.set(resolve(tab.path), tab.scrollState);
1108
1140
  }
1109
1141
  for (const tab of this.tabItems) {
1110
1142
  if (!tab.sessionPath)
1111
1143
  continue;
1112
- const messages = messagesByPath.get(resolve(tab.sessionPath));
1113
- if (!messages || messages.length === 0)
1144
+ const scrollState = scrollStatesByPath.get(resolve(tab.sessionPath));
1145
+ if (!scrollState)
1114
1146
  continue;
1115
- this.deferredUserMessagesByTabId.set(tab.id, messages);
1147
+ this.scrollStatesByTabId.set(tab.id, scrollState);
1148
+ }
1149
+ }
1150
+ restorePersistedQueuedUserMessages(saved) {
1151
+ const autoMessagesByPath = new Map();
1152
+ const deferredMessagesByPath = new Map();
1153
+ for (const tab of saved.tabs) {
1154
+ const sessionPath = resolve(tab.path);
1155
+ if (tab.autoUserMessages && tab.autoUserMessages.length > 0) {
1156
+ autoMessagesByPath.set(sessionPath, tab.autoUserMessages.map((message) => this.cloneSubmittedUserMessage(message)));
1157
+ }
1158
+ if (tab.deferredUserMessages && tab.deferredUserMessages.length > 0) {
1159
+ deferredMessagesByPath.set(sessionPath, tab.deferredUserMessages.map((message) => this.cloneSubmittedUserMessage(message)));
1160
+ }
1161
+ }
1162
+ for (const tab of this.tabItems) {
1163
+ if (!tab.sessionPath)
1164
+ continue;
1165
+ const sessionPath = resolve(tab.sessionPath);
1166
+ const autoMessages = autoMessagesByPath.get(sessionPath);
1167
+ if (autoMessages && autoMessages.length > 0)
1168
+ this.autoUserMessagesByTabId.set(tab.id, autoMessages);
1169
+ const deferredMessages = deferredMessagesByPath.get(sessionPath);
1170
+ if (deferredMessages && deferredMessages.length > 0)
1171
+ this.deferredUserMessagesByTabId.set(tab.id, deferredMessages);
1116
1172
  }
1117
1173
  }
1118
1174
  ensureCurrentSessionTab(session) {
@@ -1157,7 +1213,7 @@ export class AppTabsController {
1157
1213
  }
1158
1214
  sessionTitleFromParts(sessionId, sessionName) {
1159
1215
  const name = sessionName?.trim();
1160
- return name && !LOADING_TAB_TITLE_PATTERN.test(name) ? name : `session ${sessionId.slice(0, 8)}`;
1216
+ return name && !LOADING_TAB_TITLE_PATTERN.test(name) ? name : sessionId.slice(0, 8);
1161
1217
  }
1162
1218
  updatedSessionTitle(currentTitle, nextTitle, currentSessionPath, nextSessionPath, hasTitlePlaceholder) {
1163
1219
  if (!isDefaultSessionTitle(nextTitle))
@@ -1204,8 +1260,8 @@ export class AppTabsController {
1204
1260
  for (const tab of saved.tabs) {
1205
1261
  const sessionPath = resolve(tab.path);
1206
1262
  const hasDraftInput = (tab.input?.text.length ?? 0) > 0;
1207
- const hasDeferredQueue = (tab.deferredUserMessages?.length ?? 0) > 0;
1208
- if (seen.has(sessionPath) || (!existsSync(sessionPath) && !hasDraftInput && !hasDeferredQueue))
1263
+ const hasQueuedMessages = (tab.autoUserMessages?.length ?? 0) > 0 || (tab.deferredUserMessages?.length ?? 0) > 0;
1264
+ if (seen.has(sessionPath) || (!existsSync(sessionPath) && !hasDraftInput && !hasQueuedMessages))
1209
1265
  continue;
1210
1266
  seen.add(sessionPath);
1211
1267
  const savedTitle = tab.title?.trim();
@@ -1227,7 +1283,7 @@ export class AppTabsController {
1227
1283
  const fileName = basename(sessionPath, extname(sessionPath));
1228
1284
  const sessionId = /^[0-9a-f]{8}/iu.exec(fileName)?.[0]?.toLowerCase()
1229
1285
  ?? createHash("sha256").update(sessionPath).digest("hex").slice(0, 8);
1230
- return `session ${sessionId}`;
1286
+ return sessionId;
1231
1287
  }
1232
1288
  async loadSessionTitles(sessionPaths) {
1233
1289
  const uniquePaths = [...new Set(sessionPaths.map((sessionPath) => resolve(sessionPath)))].slice(0, MAX_RESTORED_TABS);
@@ -1267,23 +1323,27 @@ export class AppTabsController {
1267
1323
  try {
1268
1324
  const raw = await readFile(this.filePath(), "utf8");
1269
1325
  const parsed = JSON.parse(raw);
1270
- if (!isRecord(parsed) || (parsed.version !== 1 && parsed.version !== 2 && parsed.version !== TAB_STATE_VERSION) || !Array.isArray(parsed.tabs))
1326
+ if (!isRecord(parsed) || (parsed.version !== 1 && parsed.version !== 2 && parsed.version !== 3 && parsed.version !== TAB_STATE_VERSION) || !Array.isArray(parsed.tabs))
1271
1327
  return undefined;
1272
1328
  const tabs = [];
1273
1329
  for (const value of parsed.tabs) {
1274
1330
  if (!isRecord(value) || typeof value.path !== "string")
1275
1331
  continue;
1276
1332
  const input = this.parsePersistedInputState(value.input);
1333
+ const scrollState = this.parsePersistedScrollState(value.scrollState);
1334
+ const autoUserMessages = this.parsePersistedSubmittedUserMessages(value.autoUserMessages);
1277
1335
  const deferredUserMessages = this.parsePersistedSubmittedUserMessages(value.deferredUserMessages);
1278
1336
  tabs.push({
1279
1337
  path: value.path,
1280
1338
  ...(typeof value.title === "string" ? { title: value.title } : {}),
1281
1339
  ...(input ? { input } : {}),
1340
+ ...(scrollState ? { scrollState } : {}),
1341
+ ...(autoUserMessages.length > 0 ? { autoUserMessages } : {}),
1282
1342
  ...(deferredUserMessages.length > 0 ? { deferredUserMessages } : {}),
1283
1343
  });
1284
1344
  }
1285
1345
  return {
1286
- version: parsed.version === 1 ? 1 : parsed.version === 2 ? 2 : TAB_STATE_VERSION,
1346
+ version: parsed.version === 1 ? 1 : parsed.version === 2 ? 2 : parsed.version === 3 ? 3 : TAB_STATE_VERSION,
1287
1347
  cwd: typeof parsed.cwd === "string" ? parsed.cwd : this.host.options.cwd,
1288
1348
  tabs,
1289
1349
  ...(typeof parsed.activePath === "string" ? { activePath: parsed.activePath } : {}),
@@ -1306,6 +1366,18 @@ export class AppTabsController {
1306
1366
  : [];
1307
1367
  return { text: value.text, cursor, ...(attachments.length > 0 ? { attachments } : {}) };
1308
1368
  }
1369
+ parsePersistedScrollState(value) {
1370
+ if (!isRecord(value) || typeof value.scrollFromBottom !== "number" || !Number.isFinite(value.scrollFromBottom))
1371
+ return undefined;
1372
+ const scrollFromBottom = Math.max(0, Math.trunc(value.scrollFromBottom));
1373
+ const detachedScrollStart = typeof value.detachedScrollStart === "number" && Number.isFinite(value.detachedScrollStart)
1374
+ ? Math.max(0, Math.trunc(value.detachedScrollStart))
1375
+ : undefined;
1376
+ return {
1377
+ scrollFromBottom,
1378
+ ...(detachedScrollStart === undefined ? {} : { detachedScrollStart }),
1379
+ };
1380
+ }
1309
1381
  parsePersistedSubmittedUserMessages(value) {
1310
1382
  if (!Array.isArray(value))
1311
1383
  return [];
@@ -1341,6 +1413,11 @@ export class AppTabsController {
1341
1413
  continue;
1342
1414
  seen.add(sessionPath);
1343
1415
  const persistedTab = { path: sessionPath, title: tab.title };
1416
+ const scrollState = tab.id === this.activeTabId
1417
+ ? this.host.captureSessionView?.().scrollState
1418
+ : this.sessionViewsByTabId.get(tab.id)?.scrollState ?? this.scrollStatesByTabId.get(tab.id);
1419
+ if (scrollState)
1420
+ persistedTab.scrollState = scrollState;
1344
1421
  const input = this.inputStatesByTabId.get(tab.id);
1345
1422
  if (input && (input.text.length > 0 || (input.attachments?.length ?? 0) > 0)) {
1346
1423
  persistedTab.input = {
@@ -1349,6 +1426,10 @@ export class AppTabsController {
1349
1426
  ...(input.attachments && input.attachments.length > 0 ? { attachments: input.attachments.map(clonePersistedAttachment) } : {}),
1350
1427
  };
1351
1428
  }
1429
+ const autoUserMessages = this.autoUserMessagesByTabId.get(tab.id);
1430
+ if (autoUserMessages && autoUserMessages.length > 0) {
1431
+ persistedTab.autoUserMessages = autoUserMessages.map((message) => this.cloneSubmittedUserMessage(message));
1432
+ }
1352
1433
  const deferredUserMessages = this.deferredUserMessagesByTabId.get(tab.id);
1353
1434
  if (deferredUserMessages && deferredUserMessages.length > 0) {
1354
1435
  persistedTab.deferredUserMessages = deferredUserMessages.map((message) => this.cloneSubmittedUserMessage(message));
@@ -12,7 +12,7 @@ export type SessionActivity = "idle" | "running" | "thinking";
12
12
  export type SessionTabStatus = "active" | "waiting";
13
13
  export type SessionTabAttention = "terminal-bell";
14
14
  export type QueuedMessageMode = "steering" | "follow-up";
15
- export type QueuedMessageSource = "sdk-steering" | "sdk-follow-up" | "deferred";
15
+ export type QueuedMessageSource = "sdk-steering" | "sdk-follow-up" | "auto" | "deferred";
16
16
  export type SubmittedUserMessage = {
17
17
  id: string;
18
18
  promptText: string;
@@ -73,6 +73,7 @@ export type Entry = {
73
73
  text: string;
74
74
  expanded: boolean;
75
75
  status: "running" | "done";
76
+ level?: string;
76
77
  } | {
77
78
  id: string;
78
79
  kind: "tool";
@@ -265,6 +266,8 @@ export type StatusLineLayout = {
265
266
  inputBorderWidgetStartColumn?: number;
266
267
  modelUsageLabel?: string;
267
268
  contextBarLabel?: string;
269
+ quickScrollUpWidget?: StatusQuickScrollWidgetLayout;
270
+ quickScrollDownWidget?: StatusQuickScrollWidgetLayout;
268
271
  userJumpWidget?: StatusUserJumpWidgetLayout;
269
272
  draftQueueWidget?: StatusDraftQueueWidgetLayout;
270
273
  thinkingExpandWidget?: StatusThinkingExpandWidgetLayout;
@@ -273,6 +276,10 @@ export type StatusLineLayout = {
273
276
  promptEnhancerWidget?: StatusPromptEnhancerWidgetLayout;
274
277
  voiceWidget?: StatusVoiceWidgetLayout;
275
278
  };
279
+ export type StatusQuickScrollWidgetLayout = {
280
+ startColumn: number;
281
+ endColumn: number;
282
+ };
276
283
  export type StatusUserJumpWidgetLayout = {
277
284
  startColumn: number;
278
285
  endColumn: number;
@@ -587,6 +594,12 @@ export type StatusCompactToolsTarget = {
587
594
  startColumn: number;
588
595
  endColumn: number;
589
596
  };
597
+ export type StatusQuickScrollTarget = {
598
+ row: number;
599
+ startColumn: number;
600
+ endColumn: number;
601
+ direction: "up" | "down";
602
+ };
590
603
  export type ParsedSlashInput = {
591
604
  commandName: string;
592
605
  hasArguments: boolean;
@@ -0,0 +1,25 @@
1
+ import { Type } from "typebox";
2
+ import type { NormalizedQuestion, QuestionToolInput } from "./types.js";
3
+ export declare const CUSTOM_ANSWER_LABEL = "Something else\u2026";
4
+ export declare const CUSTOM_ANSWER_SENTINEL_VALUE = "__question_custom_answer__";
5
+ export declare const MIN_QUESTIONS = 1;
6
+ export declare const MAX_QUESTIONS = 5;
7
+ export declare const MIN_CHOICES = 2;
8
+ export declare const MAX_CHOICES = 5;
9
+ export declare const QUESTION_ID_PATTERN: RegExp;
10
+ export declare const questionParameters: Type.TObject<{
11
+ questions: Type.TArray<Type.TObject<{
12
+ id: Type.TString;
13
+ label: Type.TString;
14
+ prompt: Type.TString;
15
+ choices: Type.TArray<Type.TObject<{
16
+ value: Type.TString;
17
+ label: Type.TString;
18
+ description: Type.TOptional<Type.TString>;
19
+ }>>;
20
+ }>>;
21
+ }>;
22
+ export declare function normalizeQuestionInput(input: QuestionToolInput): NormalizedQuestion[];
23
+ export declare function trimString(value: string, field: string): string;
24
+ export declare function normalizeForUniqueness(value: string): string;
25
+ export declare function throwInvalid(problem: string, repair: string): never;
@@ -0,0 +1,94 @@
1
+ import { Type } from "typebox";
2
+ export const CUSTOM_ANSWER_LABEL = "Something else…";
3
+ export const CUSTOM_ANSWER_SENTINEL_VALUE = "__question_custom_answer__";
4
+ export const MIN_QUESTIONS = 1;
5
+ export const MAX_QUESTIONS = 5;
6
+ export const MIN_CHOICES = 2;
7
+ export const MAX_CHOICES = 5;
8
+ export const QUESTION_ID_PATTERN = /^[a-z][a-z0-9_-]*$/;
9
+ export const questionParameters = Type.Object({
10
+ questions: Type.Array(Type.Object({
11
+ id: Type.String({ description: "Unique stable question id." }),
12
+ label: Type.String({ description: "Short label for the question." }),
13
+ prompt: Type.String({ description: "Full question prompt to show the user." }),
14
+ choices: Type.Array(Type.Object({
15
+ value: Type.String({ description: "Value returned if this choice is selected." }),
16
+ label: Type.String({ description: "Display label for this choice." }),
17
+ description: Type.Optional(Type.String({ description: "Optional supporting text for this choice." })),
18
+ }), {
19
+ minItems: MIN_CHOICES,
20
+ maxItems: MAX_CHOICES,
21
+ description: "Two to five meaningful predefined choices.",
22
+ }),
23
+ }), {
24
+ minItems: MIN_QUESTIONS,
25
+ maxItems: MAX_QUESTIONS,
26
+ description: "One to five questions to ask the user.",
27
+ }),
28
+ });
29
+ export function normalizeQuestionInput(input) {
30
+ const questions = input.questions;
31
+ if (!Array.isArray(questions) || questions.length < MIN_QUESTIONS || questions.length > MAX_QUESTIONS) {
32
+ throwInvalid(`question requires ${MIN_QUESTIONS} to ${MAX_QUESTIONS} questions; received ${Array.isArray(questions) ? questions.length : "no"}.`, "Retry with a questions array containing one to five questions.");
33
+ }
34
+ const seenQuestionIds = new Set();
35
+ const seenQuestionLabels = new Set();
36
+ return questions.map((question, questionIndex) => {
37
+ const questionNumber = questionIndex + 1;
38
+ const id = trimString(question.id, `question ${questionNumber} id`);
39
+ const label = trimString(question.label, `question ${questionNumber} label`);
40
+ const prompt = trimString(question.prompt, `question ${questionNumber} prompt`);
41
+ if (!QUESTION_ID_PATTERN.test(id))
42
+ throwInvalid(`Question ${questionNumber} has invalid id "${id}". IDs must match ${QUESTION_ID_PATTERN.source}.`, "Retry with a stable id starting with a lowercase letter and containing only lowercase letters, numbers, underscores, or hyphens.");
43
+ if (seenQuestionIds.has(id))
44
+ throwInvalid(`Duplicate question id "${id}" makes question answers ambiguous.`, "Retry with a unique stable id for each question.");
45
+ seenQuestionIds.add(id);
46
+ const normalizedLabel = normalizeForUniqueness(label);
47
+ if (seenQuestionLabels.has(normalizedLabel))
48
+ throwInvalid(`Duplicate question label "${label}" makes question summaries ambiguous.`, "Retry with a unique short label for each question; labels are compared case-insensitively.");
49
+ seenQuestionLabels.add(normalizedLabel);
50
+ if (label.includes("\n") || label.includes("\r"))
51
+ throwInvalid(`Question ${questionNumber} label must be single-line.`, "Retry with a short single-line label and put longer text in the prompt.");
52
+ if (!Array.isArray(question.choices) || question.choices.length < MIN_CHOICES || question.choices.length > MAX_CHOICES) {
53
+ throwInvalid(`Question "${id}" requires ${MIN_CHOICES} to ${MAX_CHOICES} predefined Choices; received ${Array.isArray(question.choices) ? question.choices.length : "no"}.`, "Retry with two to five meaningful predefined Choices for each question.");
54
+ }
55
+ const seenChoiceValues = new Set();
56
+ const seenChoiceLabels = new Set();
57
+ return {
58
+ id,
59
+ label,
60
+ prompt,
61
+ choices: question.choices.map((choice, choiceIndex) => {
62
+ const choiceNumber = choiceIndex + 1;
63
+ const value = trimString(choice.value, `question "${id}" Choice ${choiceNumber} value`);
64
+ const choiceLabel = trimString(choice.label, `question "${id}" Choice ${choiceNumber} label`);
65
+ const description = choice.description === undefined ? undefined : choice.description.trim();
66
+ if (value === CUSTOM_ANSWER_SENTINEL_VALUE)
67
+ throwInvalid(`Question "${id}" Choice ${choiceNumber} uses the reserved Custom Answer sentinel value "${CUSTOM_ANSWER_SENTINEL_VALUE}".`, "Retry with a different Choice value; ordinary values like other or custom are allowed.");
68
+ const normalizedValue = normalizeForUniqueness(value);
69
+ if (seenChoiceValues.has(normalizedValue))
70
+ throwInvalid(`Question "${id}" has duplicate Choice value "${value}".`, "Retry with unique Choice values within each question; values are compared case-insensitively.");
71
+ seenChoiceValues.add(normalizedValue);
72
+ const normalizedChoiceLabel = normalizeForUniqueness(choiceLabel);
73
+ if (seenChoiceLabels.has(normalizedChoiceLabel))
74
+ throwInvalid(`Question "${id}" has duplicate Choice label "${choiceLabel}".`, "Retry with unique visible Choice labels within each question; labels are compared case-insensitively.");
75
+ seenChoiceLabels.add(normalizedChoiceLabel);
76
+ if (normalizedChoiceLabel === normalizeForUniqueness(CUSTOM_ANSWER_LABEL))
77
+ throwInvalid(`Question "${id}" Choice label "${choiceLabel}" collides with the implicit Custom Answer row label "${CUSTOM_ANSWER_LABEL}".`, "Retry with a different predefined Choice label; the Custom Answer row is added automatically.");
78
+ return description === undefined ? { value, label: choiceLabel } : { value, label: choiceLabel, description };
79
+ }),
80
+ };
81
+ });
82
+ }
83
+ export function trimString(value, field) {
84
+ const trimmed = value.trim();
85
+ if (!trimmed)
86
+ throwInvalid(`${field} must not be empty after trimming.`, "Retry with non-empty text for every required question field.");
87
+ return trimmed;
88
+ }
89
+ export function normalizeForUniqueness(value) {
90
+ return value.toLocaleLowerCase();
91
+ }
92
+ export function throwInvalid(problem, repair) {
93
+ throw new Error(`Invalid question input: ${problem} ${repair}`);
94
+ }
@@ -0,0 +1,7 @@
1
+ interface ExtensionApiLike {
2
+ registerTool(tool: unknown): void;
3
+ }
4
+ export default function questionExtension(pi: ExtensionApiLike): void;
5
+ export { questionParameters, normalizeQuestionInput } from "./contract.js";
6
+ export { createCanceledQuestionResult, createFallbackPrompt, createQuestionToolResult, createSuccessfulQuestionResult, summarizeQuestionResult } from "./result.js";
7
+ export type { QuestionResultDetails, QuestionToolInput } from "./types.js";
@@ -0,0 +1,28 @@
1
+ import { QUESTION_TOOL_DESCRIPTION } from "./tool-description.js";
2
+ import { questionParameters, normalizeQuestionInput } from "./contract.js";
3
+ import { renderQuestionCall, renderQuestionResult } from "./render.js";
4
+ import { createCanceledQuestionResult, createQuestionToolResult, createSuccessfulQuestionResult } from "./result.js";
5
+ import { runQuestionnaire } from "./tui.js";
6
+ export default function questionExtension(pi) {
7
+ pi.registerTool({
8
+ ...QUESTION_TOOL_DESCRIPTION,
9
+ parameters: questionParameters,
10
+ renderCall(args, theme) {
11
+ return renderQuestionCall(args, theme);
12
+ },
13
+ renderResult(result, _options, theme, context) {
14
+ return renderQuestionResult(result, theme, context.args);
15
+ },
16
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
17
+ const questions = normalizeQuestionInput(params);
18
+ if (!ctx.hasUI)
19
+ return createQuestionToolResult(createCanceledQuestionResult("ui_unavailable", questions), questions);
20
+ const selections = await runQuestionnaire(questions, ctx);
21
+ if (selections === null)
22
+ return createQuestionToolResult(createCanceledQuestionResult("user_canceled"), questions);
23
+ return createQuestionToolResult(createSuccessfulQuestionResult(questions, selections), questions);
24
+ },
25
+ });
26
+ }
27
+ export { questionParameters, normalizeQuestionInput } from "./contract.js";
28
+ export { createCanceledQuestionResult, createFallbackPrompt, createQuestionToolResult, createSuccessfulQuestionResult, summarizeQuestionResult } from "./result.js";