pi-ui-extend 0.1.13 → 0.1.17

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 (111) hide show
  1. package/README.md +1 -1
  2. package/dist/app/app.d.ts +7 -0
  3. package/dist/app/app.js +102 -17
  4. package/dist/app/commands/command-controller.js +2 -0
  5. package/dist/app/commands/command-host.d.ts +5 -0
  6. package/dist/app/commands/command-model-actions.d.ts +2 -0
  7. package/dist/app/commands/command-model-actions.js +40 -4
  8. package/dist/app/commands/command-navigation-actions.d.ts +9 -0
  9. package/dist/app/commands/command-navigation-actions.js +62 -0
  10. package/dist/app/commands/command-registry.d.ts +2 -0
  11. package/dist/app/commands/command-registry.js +16 -0
  12. package/dist/app/constants.d.ts +0 -1
  13. package/dist/app/constants.js +0 -1
  14. package/dist/app/extensions/extension-ui-controller.d.ts +16 -5
  15. package/dist/app/extensions/extension-ui-controller.js +99 -61
  16. package/dist/app/icons.d.ts +1 -0
  17. package/dist/app/icons.js +2 -0
  18. package/dist/app/input/input-action-controller.d.ts +2 -0
  19. package/dist/app/input/input-action-controller.js +8 -1
  20. package/dist/app/logger.d.ts +25 -0
  21. package/dist/app/logger.js +90 -0
  22. package/dist/app/model/model-usage-status.js +30 -15
  23. package/dist/app/popup/menu-items-controller.d.ts +4 -0
  24. package/dist/app/popup/menu-items-controller.js +68 -6
  25. package/dist/app/popup/popup-action-controller.d.ts +2 -1
  26. package/dist/app/popup/popup-action-controller.js +7 -4
  27. package/dist/app/popup/popup-menu-controller.d.ts +36 -23
  28. package/dist/app/popup/popup-menu-controller.js +97 -326
  29. package/dist/app/rendering/conversation-entry-renderer.js +3 -3
  30. package/dist/app/rendering/conversation-viewport.d.ts +10 -2
  31. package/dist/app/rendering/conversation-viewport.js +157 -16
  32. package/dist/app/rendering/editor-panels.js +22 -9
  33. package/dist/app/rendering/popup-menu-renderer.d.ts +62 -0
  34. package/dist/app/rendering/popup-menu-renderer.js +405 -0
  35. package/dist/app/rendering/render-controller.js +30 -28
  36. package/dist/app/rendering/render-text.js +5 -2
  37. package/dist/app/rendering/status-line-renderer.d.ts +8 -1
  38. package/dist/app/rendering/status-line-renderer.js +217 -117
  39. package/dist/app/rendering/toast-controller.d.ts +12 -3
  40. package/dist/app/rendering/toast-controller.js +70 -12
  41. package/dist/app/runtime.d.ts +2 -1
  42. package/dist/app/runtime.js +20 -10
  43. package/dist/app/screen/mouse-controller.d.ts +2 -2
  44. package/dist/app/screen/mouse-controller.js +27 -48
  45. package/dist/app/screen/screen-styler.d.ts +1 -1
  46. package/dist/app/screen/screen-styler.js +9 -7
  47. package/dist/app/screen/scroll-controller.d.ts +12 -9
  48. package/dist/app/screen/scroll-controller.js +56 -45
  49. package/dist/app/screen/status-controller.js +2 -1
  50. package/dist/app/session/lazy-session-manager.d.ts +11 -0
  51. package/dist/app/session/lazy-session-manager.js +539 -0
  52. package/dist/app/session/pix-system-message.d.ts +16 -0
  53. package/dist/app/session/pix-system-message.js +64 -0
  54. package/dist/app/session/request-history.d.ts +4 -0
  55. package/dist/app/session/request-history.js +11 -0
  56. package/dist/app/session/session-event-controller.d.ts +11 -0
  57. package/dist/app/session/session-event-controller.js +58 -2
  58. package/dist/app/session/session-history.d.ts +18 -0
  59. package/dist/app/session/session-history.js +72 -3
  60. package/dist/app/session/session-lifecycle-controller.d.ts +6 -2
  61. package/dist/app/session/session-lifecycle-controller.js +7 -2
  62. package/dist/app/session/session-search.js +10 -0
  63. package/dist/app/session/tabs-controller.d.ts +17 -5
  64. package/dist/app/session/tabs-controller.js +308 -29
  65. package/dist/app/todo/todo-model.d.ts +4 -2
  66. package/dist/app/todo/todo-model.js +23 -13
  67. package/dist/app/types.d.ts +17 -6
  68. package/dist/app/workspace/workspace-actions-controller.d.ts +2 -0
  69. package/dist/app/workspace/workspace-actions-controller.js +12 -0
  70. package/dist/config.d.ts +6 -1
  71. package/dist/config.js +82 -25
  72. package/dist/default-pix-config.js +4 -0
  73. package/dist/fuzzy.d.ts +2 -0
  74. package/dist/fuzzy.js +27 -7
  75. package/dist/input-editor.d.ts +9 -0
  76. package/dist/input-editor.js +52 -0
  77. package/dist/schemas/pi-tools-suite-schema.d.ts +1 -0
  78. package/dist/schemas/pi-tools-suite-schema.js +1 -0
  79. package/dist/schemas/pix-schema.d.ts +3 -1
  80. package/dist/schemas/pix-schema.js +6 -4
  81. package/dist/terminal-width.d.ts +2 -0
  82. package/dist/terminal-width.js +64 -3
  83. package/dist/theme.js +6 -6
  84. package/dist/ui.d.ts +8 -0
  85. package/external/pi-tools-suite/README.md +3 -2
  86. package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +52 -8
  87. package/external/pi-tools-suite/src/antigravity-auth/commands.ts +3 -41
  88. package/external/pi-tools-suite/src/antigravity-auth/constants.ts +0 -2
  89. package/external/pi-tools-suite/src/antigravity-auth/index.ts +11 -18
  90. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +129 -61
  91. package/external/pi-tools-suite/src/antigravity-auth/status.ts +82 -3
  92. package/external/pi-tools-suite/src/antigravity-auth/stream.ts +20 -7
  93. package/external/pi-tools-suite/src/antigravity-auth/types.ts +21 -0
  94. package/external/pi-tools-suite/src/config.ts +8 -0
  95. package/external/pi-tools-suite/src/dcp/index.ts +16 -1
  96. package/external/pi-tools-suite/src/dcp/state.ts +35 -0
  97. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +3 -0
  98. package/external/pi-tools-suite/src/todo/index.ts +123 -14
  99. package/external/pi-tools-suite/src/todo/state/persistence.ts +0 -1
  100. package/external/pi-tools-suite/src/todo/state/state-reducer.ts +26 -43
  101. package/external/pi-tools-suite/src/todo/todo.ts +12 -23
  102. package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +34 -16
  103. package/external/pi-tools-suite/src/todo/tool/types.ts +7 -28
  104. package/external/pi-tools-suite/src/todo/view/format.ts +2 -3
  105. package/external/pi-tools-suite/src/tool-descriptions.ts +6 -4
  106. package/external/pi-tools-suite/src/usage/index.ts +5 -2
  107. package/external/pi-tools-suite/src/usage/lib/google.ts +53 -40
  108. package/external/pi-tools-suite/src/usage/lib/types.ts +12 -2
  109. package/package.json +1 -1
  110. package/schemas/pi-tools-suite.json +4 -0
  111. package/schemas/pix.json +11 -2
package/README.md CHANGED
@@ -215,7 +215,7 @@ Type `/` in the prompt to open the command picker. Commands that accept argument
215
215
  | `/settings` | — | Show current session, model, theme, and key settings. |
216
216
  | `/model` | `[provider/model[:thinking]]` | Select the active model. Without arguments, opens the model picker. With a reference like `anthropic/claude-sonnet-4-20250514:medium`, sets the model and optional thinking level directly. |
217
217
  | `/scoped-models` | `[refs…\|reset]` | Show or set the models used by the model selector and cycling. Pass one or more `provider/model[:thinking]` references separated by spaces or commas. Use `reset` to restore the default favorites. |
218
- | `/thinking` | `[level]` | Select the thinking level. Without arguments, opens the thinking picker. Accepts an explicit level (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`). |
218
+ | `/thinking` | `[level\|auto]` | Select the thinking level. Without arguments, opens the thinking picker. Accepts an explicit level (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`) or `auto`, which chooses a supported level per prompt. |
219
219
  | `/enhance` | — | Improve the current prompt draft using the prompt enhancer model. |
220
220
  | `/export` | `[path]` | Export the session. Defaults to HTML. Pass a `.jsonl` path to export as JSONL. |
221
221
  | `/import` | `<path.jsonl>` | Import and resume a session from a JSONL file. |
package/dist/app/app.d.ts CHANGED
@@ -44,6 +44,7 @@ export declare class PiUiExtendApp {
44
44
  private readonly extensionShutdownHandler;
45
45
  private runtime;
46
46
  private readonly inputEditor;
47
+ private lastInputEditorContentVersion;
47
48
  private readonly requestHistory;
48
49
  /** Shortcut: get/set the editor text as a plain string. */
49
50
  private get input();
@@ -58,6 +59,7 @@ export declare class PiUiExtendApp {
58
59
  private resumeSessions;
59
60
  private resumeLoading;
60
61
  constructor(options: AppOptions);
62
+ private createRuntime;
61
63
  start(): Promise<void>;
62
64
  private checkPixUpdateOnStartup;
63
65
  private bindCurrentSession;
@@ -67,6 +69,7 @@ export declare class PiUiExtendApp {
67
69
  private afterSessionReplacement;
68
70
  private requireRuntime;
69
71
  private restoreSessionStatus;
72
+ private activeExtensionUiScope;
70
73
  private setInput;
71
74
  private resetInputAfterProgrammaticEdit;
72
75
  private restoreTabInputState;
@@ -77,6 +80,8 @@ export declare class PiUiExtendApp {
77
80
  private resetSessionView;
78
81
  private loadSessionHistory;
79
82
  private openSearchResultInNewTab;
83
+ private scrollToUserMessageJumpTarget;
84
+ private findUserEntryBySessionEntryId;
80
85
  private loadSessionHistoryAsync;
81
86
  private handleSessionEvent;
82
87
  private findEntry;
@@ -93,9 +98,11 @@ export declare class PiUiExtendApp {
93
98
  private toggleTerminalBellSound;
94
99
  private refreshModelUsageStatusFromClick;
95
100
  private showToast;
101
+ private toastNotifierForScope;
96
102
  private clearToastTimers;
97
103
  private render;
98
104
  private scheduleRender;
105
+ private syncScrollAfterInputEditorChange;
99
106
  private renderStatusLine;
100
107
  private terminalColumns;
101
108
  private terminalRows;
package/dist/app/app.js CHANGED
@@ -16,6 +16,7 @@ import { createId } from "./id.js";
16
16
  import { NerdFontController } from "./terminal/nerd-font-controller.js";
17
17
  import { AppPopupActionController } from "./popup/popup-action-controller.js";
18
18
  import { AppPopupMenuController } from "./popup/popup-menu-controller.js";
19
+ import { PopupMenuRenderer } from "./rendering/popup-menu-renderer.js";
19
20
  import { AppPromptEnhancerController } from "./input/prompt-enhancer-controller.js";
20
21
  import { AppAutocompleteController } from "./input/autocomplete-controller.js";
21
22
  import { AppQueuedMessageController } from "./session/queued-message-controller.js";
@@ -109,6 +110,7 @@ export class PiUiExtendApp {
109
110
  extensionShutdownHandler = () => { };
110
111
  runtime;
111
112
  inputEditor = new InputEditor();
113
+ lastInputEditorContentVersion = this.inputEditor.contentVersion;
112
114
  requestHistory;
113
115
  /** Shortcut: get/set the editor text as a plain string. */
114
116
  get input() { return this.inputEditor.text; }
@@ -125,6 +127,8 @@ export class PiUiExtendApp {
125
127
  constructor(options) {
126
128
  this.options = options;
127
129
  this.theme = THEMES[options.themeName];
130
+ this.pixConfig = loadPixConfig(this.options.cwd);
131
+ setAppIconTheme(this.pixConfig.iconTheme.name);
128
132
  const app = this;
129
133
  this.blinkController = new AppBlinkController({
130
134
  render: () => this.render(),
@@ -136,6 +140,7 @@ export class PiUiExtendApp {
136
140
  get mouseSelection() { return app.mouseController.mouseSelection; },
137
141
  });
138
142
  this.toastController = new AppToastController({
143
+ activeScope: () => this.activeExtensionUiScope(),
139
144
  render: () => this.render(),
140
145
  });
141
146
  this.nerdFontController = new NerdFontController({
@@ -155,14 +160,15 @@ export class PiUiExtendApp {
155
160
  });
156
161
  this.tabsController = new AppTabsController({
157
162
  options: this.options,
163
+ maxProjectSessions: this.pixConfig.maxProjectSessions,
158
164
  blinkController: this.blinkController,
159
165
  runtime: () => this.runtime,
160
- createRuntimeForNewSession: () => createPixRuntime(newTabRuntimeOptions(this.options), { eventBus: this.createExtensionEventBus() }),
161
- createRuntimeForSession: (sessionPath) => createPixRuntime({
166
+ createRuntimeForNewSession: () => this.createRuntime(newTabRuntimeOptions(this.options)),
167
+ createRuntimeForSession: (sessionPath) => this.createRuntime({
162
168
  ...this.options,
163
169
  noSession: false,
164
170
  sessionPath,
165
- }, { eventBus: this.createExtensionEventBus() }),
171
+ }),
166
172
  activateRuntime: (runtime) => this.activateRuntime(runtime),
167
173
  disposeRuntime: (runtime) => this.terminalController.disposeRuntime(runtime),
168
174
  isRunning: () => this.running,
@@ -173,16 +179,15 @@ export class PiUiExtendApp {
173
179
  loadSessionHistory: () => this.loadSessionHistory(),
174
180
  loadSessionHistoryAsync: (options) => this.loadSessionHistoryAsync(options),
175
181
  syncUserSessionEntryMetadata: () => this.workspaceActions.syncUserSessionEntryMetadata(),
176
- captureInputState: () => ({ text: this.inputEditor.text, cursor: this.inputEditor.cursor }),
177
- restoreInputState: (state) => this.restoreTabInputState(state.text, state.cursor),
182
+ captureInputState: () => this.inputEditor.draftState,
183
+ restoreInputState: (state) => this.restoreTabInputState(state),
184
+ closeMenusForTabSwitch: () => this.popupMenus.closeMenusForTabSwitch(),
178
185
  captureDeferredUserMessages: () => this.queuedMessages.captureDeferredUserMessages(),
179
186
  restoreDeferredUserMessages: (messages) => this.queuedMessages.restoreDeferredUserMessages(messages),
180
187
  addEntry: (entry) => this.addEntry(entry),
181
188
  showToast: (message, kind) => this.showToast(message, kind),
182
189
  render: () => this.render(),
183
190
  });
184
- this.pixConfig = loadPixConfig();
185
- setAppIconTheme(this.pixConfig.iconTheme.name);
186
191
  this.terminalBellSoundController = new TerminalBellSoundController();
187
192
  this.promptEnhancer = new AppPromptEnhancerController({
188
193
  runtime: () => this.runtime,
@@ -218,9 +223,17 @@ export class PiUiExtendApp {
218
223
  getEntries: () => this.entries,
219
224
  getResumeSessions: () => this.resumeSessions,
220
225
  });
221
- this.popupMenus = new AppPopupMenuController({
226
+ const popupMenuRenderer = new PopupMenuRenderer({
222
227
  theme: this.theme,
223
228
  screenStyler: this.screenStyler,
229
+ modelColors: this.pixConfig.modelColors,
230
+ get entries() { return app.entries; },
231
+ get session() { return app.runtime?.session; },
232
+ get resumeLoading() { return app.resumeLoading; },
233
+ get resumeSessionCount() { return app.resumeSessions.length; },
234
+ get userMessageJumpLoading() { return app.menuItems.isUserMessageJumpLoading(); },
235
+ });
236
+ this.popupMenus = new AppPopupMenuController({
224
237
  get entries() { return app.entries; },
225
238
  get session() { return app.runtime?.session; },
226
239
  get resumeLoading() { return app.resumeLoading; },
@@ -241,7 +254,7 @@ export class PiUiExtendApp {
241
254
  setStatus: (status) => this.setStatus(status),
242
255
  restoreSessionStatus: () => this.restoreSessionStatus(),
243
256
  render: () => this.render(),
244
- });
257
+ }, popupMenuRenderer);
245
258
  this.statusLineRenderer = new StatusLineRenderer({
246
259
  theme: this.theme,
247
260
  screenStyler: this.screenStyler,
@@ -277,10 +290,12 @@ export class PiUiExtendApp {
277
290
  });
278
291
  this.extensionUiController = new ExtensionUiController({
279
292
  theme: this.theme,
293
+ activeExtensionUiScope: () => this.activeExtensionUiScope(),
280
294
  isRunning: () => this.running,
281
295
  render: () => this.render(),
282
- showToast: (message, kind) => this.showToast(message, kind),
296
+ showToast: (message, kind, options) => this.showToast(message, kind, options),
283
297
  toastNotifier: this.toastNotifier,
298
+ toastNotifierForScope: (scopeKey) => this.toastNotifierForScope(scopeKey),
284
299
  menuController: this.popupMenus.menuController,
285
300
  setStatus: (status) => this.setStatus(status),
286
301
  restoreSessionStatus: () => this.restoreSessionStatus(),
@@ -328,11 +343,14 @@ export class PiUiExtendApp {
328
343
  showToast: (message, kind) => this.showToast(message, kind),
329
344
  render: () => this.render(),
330
345
  isRunning: () => this.running,
346
+ forkSessionEntryInNewTab: (sessionEntryId) => this.tabsController.forkSessionEntryInNewTab(sessionEntryId),
331
347
  });
332
348
  this.sessionEvents = new AppSessionEventController({
333
349
  entries: this.entries,
334
350
  runtime: () => this.runtime,
335
351
  conversationViewport: () => this.conversationViewport,
352
+ conversationViewportColumns: () => this.terminalColumns(),
353
+ onHistoryWindowPruned: (edge, lineCount) => this.scrollController.adjustForHistoryWindowPrune(edge, lineCount),
336
354
  isRunning: () => this.running,
337
355
  render: () => this.render(),
338
356
  scheduleRender: () => this.scheduleRender(),
@@ -408,11 +426,15 @@ export class PiUiExtendApp {
408
426
  terminalColumns: () => this.terminalColumns(),
409
427
  terminalRows: () => this.terminalRows(),
410
428
  tabPanelRows: (terminalRows) => this.tabsController.tabPanelRows(terminalRows),
429
+ hasOlderSessionHistory: () => this.sessionEvents.hasOlderSessionHistory(),
430
+ isLoadingOlderSessionHistory: () => this.sessionEvents.isLoadingOlderSessionHistory(),
431
+ loadOlderSessionHistory: (options) => this.sessionEvents.loadOlderSessionHistory(options),
411
432
  render: () => this.render(),
412
433
  });
413
434
  this.commandController = new AppCommandController({
414
435
  options: this.options,
415
436
  runtime: () => this.runtime,
437
+ requestHistory: () => this.requestHistory,
416
438
  getInput: () => this.input,
417
439
  setInput: (value) => this.setInput(value),
418
440
  promptEnhancerModelRef: () => this.pixConfig.promptEnhancer.modelRef,
@@ -421,6 +443,10 @@ export class PiUiExtendApp {
421
443
  this.pixConfig.autocomplete.modelRef = modelRef;
422
444
  this.autocompleteController.dispose();
423
445
  },
446
+ ignoreContextFiles: () => this.pixConfig.ignoreContextFiles,
447
+ setIgnoreContextFiles: (ignoreContextFiles) => {
448
+ this.pixConfig.ignoreContextFiles = ignoreContextFiles;
449
+ },
424
450
  enhancePrompt: () => this.promptEnhancer.enhancePrompt(),
425
451
  isRunning: () => this.running,
426
452
  stop: () => this.stop(),
@@ -452,6 +478,7 @@ export class PiUiExtendApp {
452
478
  setDirectPopupMenuQuery: (query) => {
453
479
  this.popupMenus.setDirectQuery(query);
454
480
  },
481
+ refreshUserMessageJumpMenuItems: () => this.menuItems.refreshUserMessageJumpMenuItems(),
455
482
  getResumeLoading: () => this.resumeLoading,
456
483
  getResumeSessions: () => this.resumeSessions,
457
484
  setResumeLoading: (loading) => {
@@ -481,6 +508,7 @@ export class PiUiExtendApp {
481
508
  bindCurrentSession: () => this.bindCurrentSession(),
482
509
  loadSessionHistory: () => this.loadSessionHistory(),
483
510
  scrollToConversationEntry: (entryId) => this.scrollController.scrollToConversationEntry(entryId),
511
+ scrollToUserMessageJumpTarget: (target) => this.scrollToUserMessageJumpTarget(target),
484
512
  }, this.popupMenus, this.commandController, this.menuItems, this.queuedMessages, this.workspaceActions);
485
513
  this.mouseController = new AppMouseController({
486
514
  terminalColumns: () => this.terminalColumns(),
@@ -519,10 +547,11 @@ export class PiUiExtendApp {
519
547
  closeTab: (tabId) => {
520
548
  void this.tabsController.closeTab(tabId);
521
549
  },
522
- toastEntry: (toastId) => this.toastController.toast.entry(toastId),
550
+ toastEntry: (toastId) => this.toastController.entry(toastId),
523
551
  showToast: (message, kind, options) => this.showToast(message, kind, options),
524
552
  dismissToast: (toastId) => this.toastController.dismissToast(toastId),
525
553
  refreshModelUsageStatus: () => this.refreshModelUsageStatusFromClick(),
554
+ refreshUserMessageJumpMenuItems: () => this.menuItems.refreshUserMessageJumpMenuItems(),
526
555
  queueInputFromStatus: () => {
527
556
  void this.inputActions.queueInputFromEditor().catch((error) => {
528
557
  this.addEntry({ id: createId("error"), kind: "error", text: `Queue input failed: ${error instanceof Error ? error.message : String(error)}` });
@@ -590,6 +619,7 @@ export class PiUiExtendApp {
590
619
  addEntry: (entry) => this.addEntry(entry),
591
620
  addSessionAbortedEntry: () => this.sessionEvents.addSessionAbortedEntry(),
592
621
  showToast: (message, kind) => this.showToast(message, kind),
622
+ dismissActiveDialog: () => this.toastController.dismissActiveDialog(),
593
623
  stopVoiceInput: () => this.voiceController.stopRecording(),
594
624
  isShellCommandRunning: () => this.shellController.isRunning(),
595
625
  runChatShellCommand: (command) => this.shellController.run(command),
@@ -653,7 +683,7 @@ export class PiUiExtendApp {
653
683
  });
654
684
  this.sessionLifecycle = new AppSessionLifecycleController({
655
685
  options: this.options,
656
- createRuntime: () => createPixRuntime(this.options, { eventBus: this.createExtensionEventBus() }),
686
+ createRuntime: () => this.createRuntime(this.options),
657
687
  entries: this.entries,
658
688
  runtime: () => this.runtime,
659
689
  setRuntime: (runtime) => {
@@ -671,8 +701,8 @@ export class PiUiExtendApp {
671
701
  loadRequestHistory: () => this.requestHistory.load(),
672
702
  startSubagentsPolling: () => this.subagentsWidgetController.startPolling(),
673
703
  closeSdkMenuForBind: () => this.popupMenus.closeSdkMenu(undefined, { render: false, restoreStatus: false }),
674
- clearExtensionWidgets: () => this.extensionUiController.clearWidgets(),
675
- createExtensionUIContext: () => this.extensionUiController.createExtensionUIContext(),
704
+ clearExtensionWidgets: (scopeKey, options) => this.extensionUiController.clearWidgets(scopeKey, options),
705
+ createExtensionUIContext: (scopeKey) => this.extensionUiController.createExtensionUIContext(scopeKey),
676
706
  extensionShutdownHandler: () => this.extensionShutdownHandler,
677
707
  createExtensionCommandContextActions: (runtime) => this.extensionActions.createCommandContextActions(runtime),
678
708
  handleExtensionError: (error) => this.extensionActions.handleExtensionError(error),
@@ -715,6 +745,11 @@ export class PiUiExtendApp {
715
745
  });
716
746
  this.slashCommands = this.commandController.slashCommands;
717
747
  }
748
+ createRuntime(options) {
749
+ return createPixRuntime(options, {
750
+ eventBus: this.createExtensionEventBus(),
751
+ });
752
+ }
718
753
  async start() {
719
754
  await this.sessionLifecycle.start();
720
755
  this.modelUsageController.startPolling();
@@ -766,6 +801,12 @@ export class PiUiExtendApp {
766
801
  if (this.runtime)
767
802
  this.setSessionStatus(this.runtime.session);
768
803
  }
804
+ activeExtensionUiScope() {
805
+ const session = this.runtime?.session;
806
+ if (!session)
807
+ return undefined;
808
+ return session.sessionFile ?? session.sessionId;
809
+ }
769
810
  setInput(value) {
770
811
  if (value !== this.input) {
771
812
  this.requestHistory.resetNavigation();
@@ -779,10 +820,10 @@ export class PiUiExtendApp {
779
820
  this.popupMenus.resetInputMenuDismissals();
780
821
  this.autocompleteController.dispose();
781
822
  }
782
- restoreTabInputState(text, cursor) {
823
+ restoreTabInputState(state) {
783
824
  this.requestHistory.resetNavigation();
784
825
  this.popupMenus.resetInputMenuDismissals();
785
- this.inputEditor.setText(text, cursor);
826
+ this.inputEditor.setDraftState(state);
786
827
  this.autocompleteController.dispose();
787
828
  }
788
829
  async clearPersistedInputDraft() {
@@ -818,7 +859,11 @@ export class PiUiExtendApp {
818
859
  this.sessionLifecycle.resetSessionView();
819
860
  }
820
861
  loadSessionHistory() {
821
- this.sessionLifecycle.loadSessionHistory();
862
+ void this.sessionEvents.loadSessionHistoryAsync({
863
+ isCancelled: () => !this.running,
864
+ render: () => this.render(),
865
+ lazyOlderHistory: true,
866
+ });
822
867
  }
823
868
  async openSearchResultInNewTab(result) {
824
869
  const opened = await this.tabsController.openSessionInNewTab(result.session.path);
@@ -838,6 +883,23 @@ export class PiUiExtendApp {
838
883
  this.showToast("Opened search result", "success");
839
884
  this.setSessionStatus(this.runtime?.session);
840
885
  }
886
+ async scrollToUserMessageJumpTarget(target) {
887
+ if (target.entryId && this.scrollController.scrollToConversationEntry(target.entryId))
888
+ return true;
889
+ if (!target.sessionEntryId)
890
+ return false;
891
+ let entry = this.findUserEntryBySessionEntryId(target.sessionEntryId);
892
+ while (!entry && this.sessionEvents.hasOlderSessionHistory() && !this.sessionEvents.isLoadingOlderSessionHistory()) {
893
+ const loaded = await this.sessionEvents.loadOlderSessionHistory({ render: false });
894
+ if (!loaded)
895
+ break;
896
+ entry = this.findUserEntryBySessionEntryId(target.sessionEntryId);
897
+ }
898
+ return entry ? this.scrollController.scrollToConversationEntry(entry.id) : false;
899
+ }
900
+ findUserEntryBySessionEntryId(sessionEntryId) {
901
+ return this.entries.find((entry) => entry.kind === "user" && entry.sessionEntryId === sessionEntryId);
902
+ }
841
903
  async loadSessionHistoryAsync(options) {
842
904
  return this.sessionEvents.loadSessionHistoryAsync(options);
843
905
  }
@@ -928,6 +990,20 @@ export class PiUiExtendApp {
928
990
  showToast(message, kind = "info", options) {
929
991
  this.toastController.showToast(message, kind, options);
930
992
  }
993
+ toastNotifierForScope(scopeKey) {
994
+ const options = () => {
995
+ if (scopeKey === undefined)
996
+ return {};
997
+ return { scopeKey };
998
+ };
999
+ return {
1000
+ show: (message, kind = "info") => this.showToast(message, kind, options()),
1001
+ success: (message) => this.showToast(message, "success", options()),
1002
+ error: (message) => this.showToast(message, "error", options()),
1003
+ warning: (message) => this.showToast(message, "warning", options()),
1004
+ info: (message) => this.showToast(message, "info", options()),
1005
+ };
1006
+ }
931
1007
  clearToastTimers() {
932
1008
  this.toastController.clearToastTimers();
933
1009
  }
@@ -937,6 +1013,7 @@ export class PiUiExtendApp {
937
1013
  this.scheduledRenderTimer = undefined;
938
1014
  }
939
1015
  this.autocompleteController.observeInput();
1016
+ this.syncScrollAfterInputEditorChange();
940
1017
  this.renderController.render();
941
1018
  }
942
1019
  scheduleRender() {
@@ -944,10 +1021,18 @@ export class PiUiExtendApp {
944
1021
  return;
945
1022
  this.scheduledRenderTimer = setTimeout(() => {
946
1023
  this.scheduledRenderTimer = undefined;
1024
+ this.syncScrollAfterInputEditorChange();
947
1025
  this.renderController.render();
948
1026
  }, COALESCED_RENDER_DELAY_MS);
949
1027
  this.scheduledRenderTimer.unref?.();
950
1028
  }
1029
+ syncScrollAfterInputEditorChange() {
1030
+ const contentVersion = this.inputEditor.contentVersion;
1031
+ if (contentVersion === this.lastInputEditorContentVersion)
1032
+ return;
1033
+ this.lastInputEditorContentVersion = contentVersion;
1034
+ this.scrollController.scrollToBottom();
1035
+ }
951
1036
  renderStatusLine() {
952
1037
  this.renderController.renderStatusLine();
953
1038
  }
@@ -30,6 +30,7 @@ export class AppCommandController {
30
30
  runModelSlashCommand: (argumentsText) => this.modelActions.runModelSlashCommand(argumentsText),
31
31
  runDefaultModelSlashCommand: (argumentsText) => this.modelActions.runDefaultModelSlashCommand(argumentsText),
32
32
  runAutocompleteSlashCommand: (argumentsText) => this.modelActions.runAutocompleteSlashCommand(argumentsText),
33
+ runNoContextFilesSlashCommand: (argumentsText) => this.modelActions.runNoContextFilesSlashCommand(argumentsText),
33
34
  runScopedModelsCommand: (argumentsText) => this.modelActions.runScopedModelsCommand(argumentsText),
34
35
  runThinkingSlashCommand: (argumentsText) => this.modelActions.runThinkingSlashCommand(argumentsText),
35
36
  runDefaultThinkingSlashCommand: (argumentsText) => this.modelActions.runDefaultThinkingSlashCommand(argumentsText),
@@ -53,6 +54,7 @@ export class AppCommandController {
53
54
  runCloneCommand: () => this.navigationActions.runCloneCommand(),
54
55
  runTreeCommand: (argumentsText) => this.navigationActions.runTreeCommand(argumentsText),
55
56
  runJumpCommand: (argumentsText) => this.navigationActions.runJumpCommand(argumentsText),
57
+ runHistoryCommand: (argumentsText) => this.navigationActions.runHistoryCommand(argumentsText),
56
58
  runSearchCommand: (argumentsText) => this.navigationActions.runSearchCommand(argumentsText),
57
59
  runUnsupportedBuiltinCommand: (commandName, message) => this.navigationActions.runUnsupportedBuiltinCommand(commandName, message),
58
60
  runResumePathCommand: (sessionPath) => this.navigationActions.runResumePathCommand(sessionPath),
@@ -1,16 +1,20 @@
1
1
  import type { AgentSession, AgentSessionRuntime, SessionInfo } from "@earendil-works/pi-coding-agent";
2
2
  import type { SessionSearchResult } from "../session/session-search.js";
3
+ import type { AppRequestHistory } from "../session/request-history.js";
3
4
  import type { ActivePopupMenu, AppOptions, Entry, ModelMenuValue, PixMenuItem, PixMenuOptions, PopupMenuPlacement, ScopedSessionModel, SessionModel, ThinkingMenuValue } from "../types.js";
4
5
  import type { ToastNotifier } from "../../ui.js";
5
6
  export type DirectPopupMenu = Exclude<ActivePopupMenu, "slash">;
6
7
  export type CommandControllerHost = {
7
8
  readonly options: AppOptions;
8
9
  runtime(): AgentSessionRuntime | undefined;
10
+ requestHistory(): AppRequestHistory;
9
11
  getInput(): string;
10
12
  setInput(value: string): void;
11
13
  promptEnhancerModelRef(): string;
12
14
  autocompleteModelRef(): string;
13
15
  setAutocompleteModelRef(modelRef: string): void;
16
+ ignoreContextFiles(): boolean;
17
+ setIgnoreContextFiles(ignoreContextFiles: boolean): void;
14
18
  enhancePrompt(): Promise<void>;
15
19
  isRunning(): boolean;
16
20
  stop(): void | Promise<void>;
@@ -37,6 +41,7 @@ export type CommandControllerHost = {
37
41
  setDirectPopupMenuPreserveStatus(preserveStatus: boolean): void;
38
42
  getDirectPopupMenuQuery(): string;
39
43
  setDirectPopupMenuQuery(query: string): void;
44
+ refreshUserMessageJumpMenuItems(): Promise<void>;
40
45
  getResumeLoading(): boolean;
41
46
  getResumeSessions(): readonly SessionInfo[];
42
47
  setResumeLoading(loading: boolean): void;
@@ -7,11 +7,13 @@ export declare class ModelCommandActions {
7
7
  runModelSlashCommand(argumentsText: string): Promise<void>;
8
8
  runDefaultModelSlashCommand(argumentsText: string): Promise<void>;
9
9
  runAutocompleteSlashCommand(argumentsText: string): Promise<void>;
10
+ runNoContextFilesSlashCommand(argumentsText: string): Promise<void>;
10
11
  runScopedModelsCommand(argumentsText: string): Promise<void>;
11
12
  runThinkingSlashCommand(argumentsText: string): Promise<void>;
12
13
  runDefaultThinkingSlashCommand(argumentsText: string): Promise<void>;
13
14
  runModelCommand(model: SessionModel): Promise<void>;
14
15
  runThinkingCommand(level: ThinkingLevel): Promise<void>;
16
+ private addPersistentSystemEntry;
15
17
  private saveDefaultModel;
16
18
  private saveDefaultThinking;
17
19
  }
@@ -1,7 +1,8 @@
1
1
  import { getIdleRuntime, getRuntime } from "./command-runtime.js";
2
- import { savePixAutocompleteModel, savePixDefaultModel, savePixDefaultThinking } from "../../config.js";
2
+ import { getProjectPixConfigPath, savePixAutocompleteModel, savePixDefaultModel, savePixDefaultThinking, saveProjectPixIgnoreContextFiles } from "../../config.js";
3
3
  import { createId } from "../id.js";
4
4
  import { isThinkingLevel, parseScopedModelRef } from "../model/model-ref.js";
5
+ import { appendPixSystemDisplayEntry } from "../session/pix-system-message.js";
5
6
  export class ModelCommandActions {
6
7
  host;
7
8
  constructor(host) {
@@ -24,6 +25,7 @@ export class ModelCommandActions {
24
25
  `model: ${currentModel}`,
25
26
  `prompt enhancer model: ${this.host.promptEnhancerModelRef()}`,
26
27
  `autocomplete model: ${this.host.autocompleteModelRef() || "disabled"}`,
28
+ `context files: ${this.host.ignoreContextFiles() ? "disabled" : "enabled"}`,
27
29
  `thinking: ${runtime.session.thinkingLevel}`,
28
30
  `theme: ${settings.getTheme() ?? this.host.options.themeName}`,
29
31
  `skill commands: ${settings.getEnableSkillCommands() ? "enabled" : "disabled"}`,
@@ -33,7 +35,7 @@ export class ModelCommandActions {
33
35
  "scoped models:",
34
36
  scopedModelText,
35
37
  "",
36
- "Use /model, /thinking, /scoped-models, /export, /import, /reload for editable settings in pix.",
38
+ "Use /model, /thinking, /no-context-files, /scoped-models, /export, /import, /reload for editable settings in pix.",
37
39
  ].join("\n");
38
40
  this.host.addEntry({ id: createId("system"), kind: "system", text });
39
41
  this.host.setSessionStatus(runtime.session);
@@ -68,7 +70,7 @@ export class ModelCommandActions {
68
70
  await this.runModelCommand(model);
69
71
  if (parsed.thinkingLevel !== undefined) {
70
72
  runtime.session.setThinkingLevel(parsed.thinkingLevel);
71
- this.host.addEntry({ id: createId("system"), kind: "system", text: `Selected thinking level ${runtime.session.thinkingLevel}` });
73
+ this.addPersistentSystemEntry(runtime.session, `Selected thinking level ${runtime.session.thinkingLevel}`);
72
74
  this.host.setSessionStatus(runtime.session);
73
75
  }
74
76
  }
@@ -125,6 +127,36 @@ export class ModelCommandActions {
125
127
  this.host.addEntry({ id: createId("system"), kind: "system", text: `Autocomplete model set to ${saved.modelRef}.` });
126
128
  this.host.setSessionStatus(runtime.session);
127
129
  }
130
+ async runNoContextFilesSlashCommand(argumentsText) {
131
+ const value = argumentsText.trim().toLowerCase();
132
+ if (!value) {
133
+ this.host.addEntry({
134
+ id: createId("system"),
135
+ kind: "system",
136
+ text: [
137
+ `Context file loading is currently ${this.host.ignoreContextFiles() ? "disabled" : "enabled"} for this project.`,
138
+ "Usage: /no-context-files <on|off>",
139
+ ].join("\n"),
140
+ });
141
+ this.host.setSessionStatus(this.host.runtime()?.session);
142
+ return;
143
+ }
144
+ if (value !== "on" && value !== "off")
145
+ throw new Error("Usage: /no-context-files <on|off>");
146
+ const ignoreContextFiles = value === "on";
147
+ const saved = saveProjectPixIgnoreContextFiles(this.host.options.cwd, ignoreContextFiles);
148
+ this.host.setIgnoreContextFiles(saved);
149
+ this.host.addEntry({
150
+ id: createId("system"),
151
+ kind: "system",
152
+ text: [
153
+ `Context file loading ${saved ? "disabled" : "enabled"} for this project.`,
154
+ `Saved ignoreContextFiles=${saved ? "true" : "false"} to ${getProjectPixConfigPath(this.host.options.cwd)}.`,
155
+ "Start a new session or restart Pix for the change to affect loaded AGENTS.md/CLAUDE.md context.",
156
+ ].join("\n"),
157
+ });
158
+ this.host.setSessionStatus(this.host.runtime()?.session);
159
+ }
128
160
  async runScopedModelsCommand(argumentsText) {
129
161
  const runtime = getIdleRuntime(this.host, "scoped-models");
130
162
  if (!runtime)
@@ -246,9 +278,13 @@ export class ModelCommandActions {
246
278
  this.host.setStatus(`selecting thinking ${level}`);
247
279
  this.host.render();
248
280
  runtime.session.setThinkingLevel(level);
249
- this.host.addEntry({ id: createId("system"), kind: "system", text: `Selected thinking level ${runtime.session.thinkingLevel}` });
281
+ this.addPersistentSystemEntry(runtime.session, `Selected thinking level ${runtime.session.thinkingLevel}`);
250
282
  this.host.setSessionStatus(runtime.session);
251
283
  }
284
+ addPersistentSystemEntry(session, text) {
285
+ appendPixSystemDisplayEntry(session, text);
286
+ this.host.addEntry({ id: createId("system"), kind: "system", text });
287
+ }
252
288
  saveDefaultModel(modelRef) {
253
289
  const saved = savePixDefaultModel(modelRef);
254
290
  if (!saved)
@@ -2,6 +2,14 @@ import type { SessionInfo } from "@earendil-works/pi-coding-agent";
2
2
  import type { CommandControllerHost } from "./command-host.js";
3
3
  import { type ResumeSessionLoaderOptions } from "../session/resume-session-loader.js";
4
4
  import type { PopupMenuPlacement } from "../types.js";
5
+ export declare function formatHistoryMenuLabel(text: string): string;
6
+ export declare function historyHighlightRanges(ranges: readonly {
7
+ start: number;
8
+ end: number;
9
+ }[], text: string): {
10
+ start: number;
11
+ end: number;
12
+ }[];
5
13
  export declare class NavigationCommandActions {
6
14
  private readonly host;
7
15
  private readonly resumeSessionLoader;
@@ -11,6 +19,7 @@ export declare class NavigationCommandActions {
11
19
  runCloneCommand(): Promise<void>;
12
20
  runTreeCommand(argumentsText: string): Promise<void>;
13
21
  runJumpCommand(argumentsText: string): Promise<void>;
22
+ runHistoryCommand(argumentsText: string): Promise<void>;
14
23
  runSearchCommand(argumentsText: string): Promise<void>;
15
24
  runUnsupportedBuiltinCommand(commandName: string, message: string): Promise<void>;
16
25
  runResumePathCommand(sessionPath: string): Promise<void>;
@@ -11,6 +11,24 @@ function nextTick() {
11
11
  setImmediate(resolve);
12
12
  });
13
13
  }
14
+ export function formatHistoryMenuLabel(text) {
15
+ return sanitizeText(text).replace(/\n/g, " ↵ ");
16
+ }
17
+ export function historyHighlightRanges(ranges, text) {
18
+ return ranges.map((range) => ({
19
+ start: historyLabelIndex(range.start, text),
20
+ end: historyLabelIndex(range.end, text),
21
+ })).filter((range) => range.end > range.start);
22
+ }
23
+ function historyLabelIndex(index, text) {
24
+ const before = text.slice(0, Math.max(0, Math.min(index, text.length)));
25
+ const newlineCount = before.split("\n").length - 1;
26
+ return before.length + newlineCount * 2;
27
+ }
28
+ function formatHistoryMenuDescription(text) {
29
+ const lines = sanitizeText(text).split("\n");
30
+ return lines.length > 1 ? `${lines.length} lines` : undefined;
31
+ }
14
32
  export class NavigationCommandActions {
15
33
  host;
16
34
  resumeSessionLoader;
@@ -101,6 +119,50 @@ export class NavigationCommandActions {
101
119
  this.host.openDirectPopupMenu("user-message-jump", { preserveStatus: true });
102
120
  this.host.setDirectPopupMenuQuery(argumentsText.trim());
103
121
  this.host.render();
122
+ try {
123
+ await this.host.refreshUserMessageJumpMenuItems();
124
+ }
125
+ catch (error) {
126
+ this.host.toast.error(`Could not load jump messages: ${error instanceof Error ? error.message : String(error)}`);
127
+ }
128
+ finally {
129
+ this.host.render();
130
+ }
131
+ }
132
+ async runHistoryCommand(argumentsText) {
133
+ const query = argumentsText.trim();
134
+ const matches = this.host.requestHistory().searchMatches(query, 100);
135
+ if (matches.length === 0) {
136
+ this.host.addEntry({ id: createId("system"), kind: "system", text: query ? `No command history found for: ${query}` : "Command history is empty." });
137
+ this.host.toast.info(query ? "No matching command history" : "Command history is empty");
138
+ this.host.setSessionStatus(this.host.runtime()?.session);
139
+ this.host.render();
140
+ return;
141
+ }
142
+ const selected = await this.host.showMenu(matches.map((match) => {
143
+ const description = formatHistoryMenuDescription(match.value);
144
+ return {
145
+ value: match.value,
146
+ label: formatHistoryMenuLabel(match.value),
147
+ labelHighlightRanges: match.matchedText === match.label ? historyHighlightRanges(match.matchedRanges, match.value) : [],
148
+ ...(description === undefined ? {} : { description }),
149
+ };
150
+ }), {
151
+ title: query ? `Search command history: ${query}` : "Command history",
152
+ placeholder: "Filter history",
153
+ emptyText: "No matching command history",
154
+ searchable: true,
155
+ minScorePerCharacter: 8,
156
+ preferKeyboardLayoutMatches: true,
157
+ });
158
+ if (!selected) {
159
+ this.host.setSessionStatus(this.host.runtime()?.session);
160
+ return;
161
+ }
162
+ this.host.setInput(selected);
163
+ this.host.toast.info("Restored command from history");
164
+ this.host.setSessionStatus(this.host.runtime()?.session);
165
+ this.host.render();
104
166
  }
105
167
  async runSearchCommand(argumentsText) {
106
168
  const runtime = getIdleRuntime(this.host, "search");
@@ -5,6 +5,7 @@ export type CommandRegistryActions = {
5
5
  runModelSlashCommand(argumentsText: string): Promise<void>;
6
6
  runDefaultModelSlashCommand(argumentsText: string): Promise<void>;
7
7
  runAutocompleteSlashCommand(argumentsText: string): Promise<void>;
8
+ runNoContextFilesSlashCommand(argumentsText: string): Promise<void>;
8
9
  runScopedModelsCommand(argumentsText: string): Promise<void>;
9
10
  runThinkingSlashCommand(argumentsText: string): Promise<void>;
10
11
  runDefaultThinkingSlashCommand(argumentsText: string): Promise<void>;
@@ -24,6 +25,7 @@ export type CommandRegistryActions = {
24
25
  runCloneCommand(): Promise<void>;
25
26
  runTreeCommand(argumentsText: string): Promise<void>;
26
27
  runJumpCommand(argumentsText: string): Promise<void>;
28
+ runHistoryCommand(argumentsText: string): Promise<void>;
27
29
  runSearchCommand(argumentsText: string): Promise<void>;
28
30
  runUnsupportedBuiltinCommand(commandName: string, message: string): Promise<void>;
29
31
  runReloadCommand(): Promise<void>;