pi-ui-extend 0.1.9 → 0.1.11

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 (92) hide show
  1. package/README.md +23 -2
  2. package/dist/app/app.d.ts +4 -0
  3. package/dist/app/app.js +74 -7
  4. package/dist/app/cli/install.d.ts +2 -0
  5. package/dist/app/cli/install.js +16 -1
  6. package/dist/app/commands/command-controller.js +4 -0
  7. package/dist/app/commands/command-host.d.ts +4 -0
  8. package/dist/app/commands/command-model-actions.d.ts +5 -0
  9. package/dist/app/commands/command-model-actions.js +104 -0
  10. package/dist/app/commands/command-navigation-actions.d.ts +6 -1
  11. package/dist/app/commands/command-navigation-actions.js +37 -14
  12. package/dist/app/commands/command-registry.d.ts +4 -0
  13. package/dist/app/commands/command-registry.js +32 -0
  14. package/dist/app/commands/command-session-actions.d.ts +1 -0
  15. package/dist/app/commands/command-session-actions.js +15 -5
  16. package/dist/app/commands/shell-controller.d.ts +1 -0
  17. package/dist/app/commands/shell-controller.js +1 -1
  18. package/dist/app/constants.d.ts +1 -1
  19. package/dist/app/constants.js +1 -1
  20. package/dist/app/icons.js +1 -1
  21. package/dist/app/input/autocomplete-controller.d.ts +52 -0
  22. package/dist/app/input/autocomplete-controller.js +352 -0
  23. package/dist/app/input/input-action-controller.d.ts +1 -0
  24. package/dist/app/input/input-action-controller.js +21 -0
  25. package/dist/app/input/input-controller.d.ts +1 -0
  26. package/dist/app/input/input-controller.js +2 -0
  27. package/dist/app/input/input-paste-handler.d.ts +1 -0
  28. package/dist/app/input/input-paste-handler.js +22 -18
  29. package/dist/app/input/voice-controller.d.ts +2 -0
  30. package/dist/app/input/voice-controller.js +27 -15
  31. package/dist/app/model/model-usage-status.d.ts +9 -0
  32. package/dist/app/model/model-usage-status.js +124 -34
  33. package/dist/app/popup/popup-action-controller.js +1 -1
  34. package/dist/app/process.d.ts +17 -0
  35. package/dist/app/process.js +68 -0
  36. package/dist/app/rendering/conversation-entry-renderer.js +17 -6
  37. package/dist/app/rendering/conversation-tool-renderer.js +3 -2
  38. package/dist/app/rendering/editor-layout-renderer.d.ts +1 -0
  39. package/dist/app/rendering/editor-layout-renderer.js +11 -1
  40. package/dist/app/rendering/message-content.js +65 -7
  41. package/dist/app/rendering/render-controller.js +6 -1
  42. package/dist/app/rendering/render-text.d.ts +3 -0
  43. package/dist/app/rendering/render-text.js +51 -3
  44. package/dist/app/rendering/status-line-renderer.d.ts +5 -1
  45. package/dist/app/rendering/status-line-renderer.js +69 -25
  46. package/dist/app/rendering/tool-block-renderer.js +13 -31
  47. package/dist/app/runtime.d.ts +6 -1
  48. package/dist/app/runtime.js +35 -2
  49. package/dist/app/screen/clipboard.d.ts +2 -2
  50. package/dist/app/screen/clipboard.js +13 -18
  51. package/dist/app/screen/mouse-controller.d.ts +5 -2
  52. package/dist/app/screen/mouse-controller.js +16 -1
  53. package/dist/app/screen/screen-styler.d.ts +4 -1
  54. package/dist/app/screen/screen-styler.js +3 -2
  55. package/dist/app/screen/status-controller.d.ts +3 -0
  56. package/dist/app/screen/status-controller.js +23 -8
  57. package/dist/app/session/queued-message-controller.d.ts +7 -1
  58. package/dist/app/session/queued-message-controller.js +32 -21
  59. package/dist/app/session/resume-session-loader.d.ts +15 -0
  60. package/dist/app/session/resume-session-loader.js +204 -0
  61. package/dist/app/session/session-event-controller.d.ts +5 -1
  62. package/dist/app/session/session-event-controller.js +72 -5
  63. package/dist/app/session/session-history.js +4 -3
  64. package/dist/app/session/session-lifecycle-controller.d.ts +5 -0
  65. package/dist/app/session/session-lifecycle-controller.js +9 -1
  66. package/dist/app/session/tabs-controller.d.ts +10 -1
  67. package/dist/app/session/tabs-controller.js +101 -5
  68. package/dist/app/terminal/nerd-font-controller.js +16 -17
  69. package/dist/app/terminal/terminal-controller.d.ts +1 -0
  70. package/dist/app/terminal/terminal-controller.js +1 -0
  71. package/dist/app/types.d.ts +14 -0
  72. package/dist/app/workspace/workspace-actions-controller.d.ts +1 -1
  73. package/dist/app/workspace/workspace-actions-controller.js +3 -3
  74. package/dist/app/workspace/workspace-undo.d.ts +1 -1
  75. package/dist/app/workspace/workspace-undo.js +22 -20
  76. package/dist/config.d.ts +27 -0
  77. package/dist/config.js +174 -1
  78. package/dist/default-pix-config.js +38 -353
  79. package/dist/input-editor.d.ts +7 -1
  80. package/dist/input-editor.js +47 -6
  81. package/dist/markdown-format.d.ts +1 -0
  82. package/dist/markdown-format.js +26 -1
  83. package/external/pi-tools-suite/src/dcp/compression-blocks.ts +1 -0
  84. package/external/pi-tools-suite/src/dcp/prompts.ts +1 -0
  85. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +45 -195
  86. package/external/pi-tools-suite/src/lib/lsp.ts +2 -1
  87. package/external/pi-tools-suite/src/lsp/_shared/output.ts +8 -7
  88. package/external/pi-tools-suite/src/lsp/manager.ts +4 -4
  89. package/external/pi-tools-suite/src/repo-discovery/index.ts +49 -2
  90. package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +9 -1
  91. package/external/pi-tools-suite/src/tool-descriptions.ts +1 -1
  92. package/package.json +1 -1
package/README.md CHANGED
@@ -14,6 +14,23 @@ The npm package is currently named `pi-ui-extend` and installs the `pix` CLI.
14
14
  - Pix extension UI helpers for toasts, menus, and rows above the input editor.
15
15
  - Bundled `pi-tools-suite` payload. On startup, Pix links it into Pi's standard user extension directory.
16
16
 
17
+ ## Why use Pix instead of bare Pi?
18
+
19
+ Pix keeps the same Pi agent engine and SDK, but replaces the default terminal experience with a faster, more interactive coding cockpit:
20
+
21
+ - **Tabs for real work.** Keep several workspace-scoped Pi sessions open in one terminal, switch between tasks instantly, and restore tab state after restart.
22
+ - **A readable tool-call UI.** Tool output is grouped into clickable rows, so long reads, patches, shell logs, and errors can be collapsed or expanded instead of flooding the conversation.
23
+ - **Local shell without leaving the chat.** Prefix a command with `!` to run it in an ephemeral in-chat shell block, or use `!!` for raw TTY programs.
24
+ - **Voice prompts.** Dictate prompts locally in Russian or English with Vosk; no cloud speech service is required.
25
+ - **Prompt drafting assistance.** Inline AI autocomplete suggests the next words while you type, and `/enhance` rewrites a rough draft into a clearer agent prompt.
26
+ - **Workspace safety net.** Pix tracks supported file mutations and lets you roll back the latest workspace change with `/undo`.
27
+ - **Clickable files and images.** File paths in the conversation become clickable links that open in Zed, including line and column when available; image labels can be opened in the system viewer.
28
+ - **Session power tools.** Search across saved sessions, fork from an earlier message, clone the current branch, rename sessions, export/import JSONL, or share a private gist.
29
+ - **Queued follow-ups.** Add the next prompt while the agent is still running; Pix sends it automatically when the current turn finishes.
30
+ - **Live status panels.** Built-in panels surface todo state, async sub-agent activity, model/context usage, and compression stats above the editor.
31
+ - **Renderer-aware extensions.** Pix extensions can draw rows above/below the input, show stackable toasts, open searchable popup menus, and react through an isolated event bus.
32
+ - **Nicer terminal ergonomics.** Mouse scrolling, PageUp/PageDown scrollback, tab attention, status lines, terminal-output buffering, Nerd Font icons, and fallback icon mode are handled by the Pix renderer.
33
+
17
34
  ## Requirements
18
35
 
19
36
  - Node.js `>=22.19.0 <25` (`24.16.0` is pinned for development).
@@ -43,6 +60,12 @@ pix install --check
43
60
 
44
61
  `pix install` checks the icon font, `pi` CLI availability, and clipboard helpers. The `pix` launcher also prepends Pix's bundled dependency bin directory to `PATH`, so a package-manager install can use the bundled Pi CLI even when a separate global `pi` command is not present.
45
62
 
63
+ After the setup check, review the user config files:
64
+
65
+ - `~/.config/pi/pix.jsonc`: choose the voice-input language with `dictation.language` and keep only the `dictation.languages` models you want enabled.
66
+ - `~/.config/pi/pi-tools-suite.jsonc`: enable/configure your LSP servers under `lsp.servers` and adjust pi-tools-suite modules or sub-agent presets if needed.
67
+ - In Pix, run `/opencode-import` to import opencode accounts. For Antigravity OAuth accounts, run `/antigravity-import` or add another account with `/antigravity-add-account`.
68
+
46
69
  On startup, Pix ensures the bundled suite is available at:
47
70
 
48
71
  ```text
@@ -153,8 +176,6 @@ Useful environment variables:
153
176
  - `PIX_DISABLE_TERMINAL_OUTPUT_BUFFER=1` or `PIX_TERMINAL_OUTPUT_BUFFER=0`: disable Pix terminal output region buffering.
154
177
  - `PIX_USE_FALLBACK_ICONS=1` or `PIX_ICON_THEME=fallback`: use plain fallback icons when Nerd Font glyphs are unavailable.
155
178
  - `PIX_ICON_THEME=nerdFont`: force the Nerd Font icon theme.
156
- - `PIX_ANTIGRAVITY_GOOGLE_CLIENT_ID` / `ANTIGRAVITY_GOOGLE_CLIENT_ID`: Google OAuth client ID used for Antigravity quota/login integrations.
157
- - `PIX_ANTIGRAVITY_GOOGLE_CLIENT_SECRET` / `ANTIGRAVITY_GOOGLE_CLIENT_SECRET`: Google OAuth client secret used for Antigravity quota/login integrations.
158
179
 
159
180
  Pix user configuration is read from:
160
181
 
package/dist/app/app.d.ts CHANGED
@@ -31,6 +31,7 @@ export declare class PiUiExtendApp {
31
31
  private readonly nerdFontController;
32
32
  private readonly popupActions;
33
33
  private readonly promptEnhancer;
34
+ private readonly autocompleteController;
34
35
  private readonly mouseController;
35
36
  private readonly popupMenus;
36
37
  private readonly voiceController;
@@ -48,6 +49,7 @@ export declare class PiUiExtendApp {
48
49
  private get input();
49
50
  private set input(value);
50
51
  private running;
52
+ private scheduledRenderTimer;
51
53
  private todoPanelExpanded;
52
54
  private subagentsPanelExpanded;
53
55
  private allThinkingExpanded;
@@ -85,6 +87,7 @@ export declare class PiUiExtendApp {
85
87
  private setSessionStatus;
86
88
  private setSessionActivity;
87
89
  private toolDefaultExpanded;
90
+ private toggleSuperCompactTools;
88
91
  private stopBlinking;
89
92
  private stop;
90
93
  private toggleTerminalBellSound;
@@ -92,6 +95,7 @@ export declare class PiUiExtendApp {
92
95
  private showToast;
93
96
  private clearToastTimers;
94
97
  private render;
98
+ private scheduleRender;
95
99
  private renderStatusLine;
96
100
  private terminalColumns;
97
101
  private terminalRows;
package/dist/app/app.js CHANGED
@@ -17,6 +17,7 @@ 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
19
  import { AppPromptEnhancerController } from "./input/prompt-enhancer-controller.js";
20
+ import { AppAutocompleteController } from "./input/autocomplete-controller.js";
20
21
  import { AppQueuedMessageController } from "./session/queued-message-controller.js";
21
22
  import { AppRequestHistory } from "./session/request-history.js";
22
23
  import { AppRenderController } from "./rendering/render-controller.js";
@@ -46,6 +47,7 @@ import { setAppIconTheme } from "./icons.js";
46
47
  const TERMINAL_BELL_ATTENTION_EVENT = "pix:terminal-bell:attention";
47
48
  const SUBAGENTS_LIVE_STATE_EVENT = "pi-tools-suite:async-subagents:live-state";
48
49
  const TODO_STATE_EVENT = "pi-tools-suite:todo:state";
50
+ const COALESCED_RENDER_DELAY_MS = 16;
49
51
  export class PiUiExtendApp {
50
52
  entries = [];
51
53
  options;
@@ -78,6 +80,7 @@ export class PiUiExtendApp {
78
80
  nerdFontController;
79
81
  popupActions;
80
82
  promptEnhancer;
83
+ autocompleteController;
81
84
  mouseController;
82
85
  popupMenus;
83
86
  voiceController;
@@ -111,6 +114,7 @@ export class PiUiExtendApp {
111
114
  get input() { return this.inputEditor.text; }
112
115
  set input(value) { this.inputEditor.setText(value); }
113
116
  running = false;
117
+ scheduledRenderTimer;
114
118
  todoPanelExpanded = true;
115
119
  subagentsPanelExpanded = true;
116
120
  allThinkingExpanded = false;
@@ -143,6 +147,7 @@ export class PiUiExtendApp {
143
147
  theme: this.theme,
144
148
  blinkController: this.blinkController,
145
149
  runtimeSession: () => this.runtime?.session,
150
+ render: () => this.scheduleRender(),
146
151
  });
147
152
  this.modelUsageController = new AppModelUsageController({
148
153
  runtimeSession: () => this.runtime?.session,
@@ -170,6 +175,8 @@ export class PiUiExtendApp {
170
175
  syncUserSessionEntryMetadata: () => this.workspaceActions.syncUserSessionEntryMetadata(),
171
176
  captureInputState: () => ({ text: this.inputEditor.text, cursor: this.inputEditor.cursor }),
172
177
  restoreInputState: (state) => this.restoreTabInputState(state.text, state.cursor),
178
+ captureDeferredUserMessages: () => this.queuedMessages.captureDeferredUserMessages(),
179
+ restoreDeferredUserMessages: (messages) => this.queuedMessages.restoreDeferredUserMessages(messages),
173
180
  addEntry: (entry) => this.addEntry(entry),
174
181
  showToast: (message, kind) => this.showToast(message, kind),
175
182
  render: () => this.render(),
@@ -191,6 +198,13 @@ export class PiUiExtendApp {
191
198
  toast: this.toastNotifier,
192
199
  render: () => this.render(),
193
200
  });
201
+ this.autocompleteController = new AppAutocompleteController({
202
+ runtime: () => this.runtime,
203
+ inputEditor: () => this.inputEditor,
204
+ autocompleteConfig: () => this.pixConfig.autocomplete,
205
+ isRunning: () => this.running,
206
+ render: () => this.render(),
207
+ });
194
208
  this.voiceController = new AppVoiceController({
195
209
  insertTranscript: (text) => this.insertVoiceTranscript(text),
196
210
  setPartialTranscript: (text) => this.setVoicePartialTranscript(text),
@@ -251,6 +265,7 @@ export class PiUiExtendApp {
251
265
  terminalBellSoundStatusWidgetEnabled: () => this.terminalBellSoundController.isEnabled(),
252
266
  voiceStatusWidgetText: () => this.voiceController.statusWidgetText(),
253
267
  voiceStatusWidgetActive: () => this.voiceController.statusWidgetActive(),
268
+ queueableInputActive: () => this.inputEditor.promptText.trimEnd().length > 0 || this.inputEditor.images.length > 0,
254
269
  userMessageJumpMenuActive: () => this.popupMenus.directMenu === "user-message-jump",
255
270
  allThinkingExpandedActive: () => this.allThinkingExpanded,
256
271
  superCompactToolsActive: () => this.superCompactTools,
@@ -291,12 +306,12 @@ export class PiUiExtendApp {
291
306
  cwd: this.options.cwd,
292
307
  sessionFile: () => this.runtime?.session.sessionFile,
293
308
  isRunning: () => this.running,
294
- render: () => this.render(),
309
+ render: () => this.scheduleRender(),
295
310
  });
296
311
  this.todoWidgetController = new AppTodoWidgetController({
297
312
  sessionFile: () => this.runtime?.session.sessionFile,
298
313
  isRunning: () => this.running,
299
- render: () => this.render(),
314
+ render: () => this.scheduleRender(),
300
315
  });
301
316
  this.workspaceActions = new AppWorkspaceActionsController({
302
317
  entries: this.entries,
@@ -320,13 +335,11 @@ export class PiUiExtendApp {
320
335
  conversationViewport: () => this.conversationViewport,
321
336
  isRunning: () => this.running,
322
337
  render: () => this.render(),
338
+ scheduleRender: () => this.scheduleRender(),
323
339
  setStatus: (status) => this.setStatus(status),
324
340
  restoreSessionStatus: () => this.restoreSessionStatus(),
325
341
  setSessionStatus: (session) => this.setSessionStatus(session),
326
342
  setSessionActivity: (activity) => this.setSessionActivity(activity),
327
- flushDeferredUserMessages: () => {
328
- void this.queuedMessages.flushDeferredUserMessages();
329
- },
330
343
  updateQueuedMessageStatus: () => this.queuedMessages.updateQueuedMessageStatus(),
331
344
  prepareWorkspaceMutation: (toolName, args) => this.workspaceActions.prepareWorkspaceMutation(toolName, args),
332
345
  workspaceMutationFromToolExecution: (input) => this.workspaceActions.workspaceMutationFromToolExecution(input),
@@ -355,6 +368,7 @@ export class PiUiExtendApp {
355
368
  setInput: (value) => this.inputEditor.setText(value),
356
369
  insertInput: (value) => this.inputEditor.insert(value),
357
370
  attachImage: (data, mimeType) => this.inputEditor.attachImage(data, mimeType),
371
+ onDeferredUserMessagesChanged: () => this.tabsController.persistActiveDeferredUserMessages(),
358
372
  });
359
373
  this.editorLayoutRenderer = new EditorLayoutRenderer({
360
374
  theme: this.theme,
@@ -365,6 +379,7 @@ export class PiUiExtendApp {
365
379
  get subagentsPanelExpanded() { return app.subagentsPanelExpanded; },
366
380
  get subagentsWidgetState() { return app.subagentsWidgetController.widgetState; },
367
381
  get voicePartialText() { return app.voicePartialText; },
382
+ get autocompleteSuggestion() { return app.autocompleteController.suggestionText(); },
368
383
  renderExtensionInputComponent: (width) => this.extensionUiController.renderActiveCustomUi(width),
369
384
  extensionInputUsesEditor: () => this.extensionUiController.activeCustomUiUsesEditor(),
370
385
  widgetTuiHandle: () => this.extensionUiController.widgetTuiHandle(),
@@ -401,6 +416,11 @@ export class PiUiExtendApp {
401
416
  getInput: () => this.input,
402
417
  setInput: (value) => this.setInput(value),
403
418
  promptEnhancerModelRef: () => this.pixConfig.promptEnhancer.modelRef,
419
+ autocompleteModelRef: () => this.pixConfig.autocomplete.modelRef,
420
+ setAutocompleteModelRef: (modelRef) => {
421
+ this.pixConfig.autocomplete.modelRef = modelRef;
422
+ this.autocompleteController.dispose();
423
+ },
404
424
  enhancePrompt: () => this.promptEnhancer.enhancePrompt(),
405
425
  isRunning: () => this.running,
406
426
  stop: () => this.stop(),
@@ -414,6 +434,9 @@ export class PiUiExtendApp {
414
434
  modelRef: (model) => this.menuItems.modelRef(model),
415
435
  getFavoriteScopedModels: () => this.menuItems.getFavoriteScopedModels(),
416
436
  setSessionStatus: (session) => this.setSessionStatus(session),
437
+ queueUserMessage: (text) => {
438
+ this.queuedMessages.deferUserMessage(this.queuedMessages.createSubmittedUserMessage(text, text, []));
439
+ },
417
440
  resetSessionView: () => this.resetSessionView(),
418
441
  loadSessionHistory: () => this.loadSessionHistory(),
419
442
  afterSessionReplacement: (message) => this.afterSessionReplacement(message),
@@ -430,6 +453,7 @@ export class PiUiExtendApp {
430
453
  this.popupMenus.setDirectQuery(query);
431
454
  },
432
455
  getResumeLoading: () => this.resumeLoading,
456
+ getResumeSessions: () => this.resumeSessions,
433
457
  setResumeLoading: (loading) => {
434
458
  this.resumeLoading = loading;
435
459
  },
@@ -499,12 +523,20 @@ export class PiUiExtendApp {
499
523
  showToast: (message, kind, options) => this.showToast(message, kind, options),
500
524
  dismissToast: (toastId) => this.toastController.dismissToast(toastId),
501
525
  refreshModelUsageStatus: () => this.refreshModelUsageStatusFromClick(),
526
+ queueInputFromStatus: () => {
527
+ void this.inputActions.queueInputFromEditor().catch((error) => {
528
+ this.addEntry({ id: createId("error"), kind: "error", text: `Queue input failed: ${error instanceof Error ? error.message : String(error)}` });
529
+ this.showToast("Queue input failed", "error");
530
+ this.setSessionStatus(this.runtime?.session);
531
+ this.render();
532
+ });
533
+ },
502
534
  toggleAllThinkingExpanded: () => {
503
535
  this.allThinkingExpanded = !this.allThinkingExpanded;
504
536
  this.render();
505
537
  },
506
538
  toggleSuperCompactTools: () => {
507
- this.superCompactTools = !this.superCompactTools;
539
+ this.toggleSuperCompactTools();
508
540
  this.render();
509
541
  },
510
542
  toggleTerminalBellSound: () => this.toggleTerminalBellSound(),
@@ -543,6 +575,7 @@ export class PiUiExtendApp {
543
575
  setSessionActivity: (activity) => this.setSessionActivity(activity),
544
576
  restoreSessionStatus: () => this.restoreSessionStatus(),
545
577
  render: () => this.render(),
578
+ scheduleRender: () => this.scheduleRender(),
546
579
  });
547
580
  this.inputActions = new AppInputActionController({
548
581
  runtime: () => this.runtime,
@@ -587,6 +620,7 @@ export class PiUiExtendApp {
587
620
  handleDirectPopupInput: (char) => this.popupMenus.handleDirectPopupInput(char),
588
621
  autocompleteModel: () => this.popupMenus.autocompleteModel(),
589
622
  autocompleteThinking: () => this.popupMenus.autocompleteThinking(),
623
+ acceptAutocompleteSuggestion: () => this.autocompleteController.acceptSuggestion(),
590
624
  autocompleteSlashCommand: () => this.popupMenus.autocompleteSlashCommand(),
591
625
  toggleVoiceRecording: () => {
592
626
  void this.voiceController.toggleRecording();
@@ -609,6 +643,7 @@ export class PiUiExtendApp {
609
643
  stopSubagentsPolling: () => this.subagentsWidgetController.stopPolling(),
610
644
  stopModelUsagePolling: () => this.modelUsageController.stopPolling(),
611
645
  stopVoiceInput: () => this.voiceController.dispose(),
646
+ stopAutocomplete: () => this.autocompleteController.dispose(),
612
647
  stopShellCommand: () => this.shellController.dispose(),
613
648
  unsubscribeSession: () => {
614
649
  this.sessionLifecycle.unsubscribeSession();
@@ -660,6 +695,7 @@ export class PiUiExtendApp {
660
695
  this.mouseController.statusThinkingTarget = undefined;
661
696
  this.mouseController.statusContextTarget = undefined;
662
697
  this.mouseController.statusModelUsageTarget = undefined;
698
+ this.mouseController.statusDraftQueueTarget = undefined;
663
699
  this.mouseController.statusUserJumpTarget = undefined;
664
700
  this.mouseController.statusThinkingExpandTarget = undefined;
665
701
  this.mouseController.statusCompactToolsTarget = undefined;
@@ -672,6 +708,7 @@ export class PiUiExtendApp {
672
708
  },
673
709
  scrollReset: () => this.scrollController.reset(),
674
710
  loadSessionHistoryEntries: () => this.sessionEvents.loadSessionHistory(),
711
+ loadSessionHistoryEntriesAsync: (options) => this.sessionEvents.loadSessionHistoryAsync(options),
675
712
  syncUserSessionEntryMetadata: () => this.workspaceActions.syncUserSessionEntryMetadata(),
676
713
  restoreTabsAfterStartup: () => this.tabsController.restoreAfterStartup(),
677
714
  render: () => this.render(),
@@ -735,15 +772,18 @@ export class PiUiExtendApp {
735
772
  this.popupMenus.resetInputMenuDismissals();
736
773
  }
737
774
  this.input = value;
775
+ this.autocompleteController.dispose();
738
776
  }
739
777
  resetInputAfterProgrammaticEdit() {
740
778
  this.requestHistory.resetNavigation();
741
779
  this.popupMenus.resetInputMenuDismissals();
780
+ this.autocompleteController.dispose();
742
781
  }
743
782
  restoreTabInputState(text, cursor) {
744
783
  this.requestHistory.resetNavigation();
745
784
  this.popupMenus.resetInputMenuDismissals();
746
785
  this.inputEditor.setText(text, cursor);
786
+ this.autocompleteController.dispose();
747
787
  }
748
788
  async clearPersistedInputDraft() {
749
789
  await this.tabsController.setInputStateForTab(this.tabsController.activeInputTabId(), { text: "", cursor: 0 });
@@ -768,7 +808,7 @@ export class PiUiExtendApp {
768
808
  if (this.voicePartialText === text)
769
809
  return;
770
810
  this.voicePartialText = text;
771
- this.render();
811
+ this.scheduleRender();
772
812
  }
773
813
  addVoiceSystemMessage(message) {
774
814
  this.addEntry({ id: createId("system"), kind: "system", text: message });
@@ -828,8 +868,21 @@ export class PiUiExtendApp {
828
868
  this.statusController.setSessionActivity(activity);
829
869
  }
830
870
  toolDefaultExpanded(toolName) {
871
+ if (this.superCompactTools)
872
+ return false;
831
873
  return resolveToolRule(toolName, this.pixConfig.toolRenderer).defaultExpanded === true;
832
874
  }
875
+ toggleSuperCompactTools() {
876
+ this.superCompactTools = !this.superCompactTools;
877
+ if (!this.superCompactTools)
878
+ return;
879
+ for (const entry of this.entries) {
880
+ if (entry.kind !== "tool" || !entry.expanded)
881
+ continue;
882
+ entry.expanded = false;
883
+ this.touchEntry(entry);
884
+ }
885
+ }
833
886
  stopBlinking() {
834
887
  this.blinkController.dispose();
835
888
  }
@@ -877,8 +930,22 @@ export class PiUiExtendApp {
877
930
  this.toastController.clearToastTimers();
878
931
  }
879
932
  render() {
933
+ if (this.scheduledRenderTimer) {
934
+ clearTimeout(this.scheduledRenderTimer);
935
+ this.scheduledRenderTimer = undefined;
936
+ }
937
+ this.autocompleteController.observeInput();
880
938
  this.renderController.render();
881
939
  }
940
+ scheduleRender() {
941
+ if (!this.running || this.scheduledRenderTimer)
942
+ return;
943
+ this.scheduledRenderTimer = setTimeout(() => {
944
+ this.scheduledRenderTimer = undefined;
945
+ this.renderController.render();
946
+ }, COALESCED_RENDER_DELAY_MS);
947
+ this.scheduledRenderTimer.unref?.();
948
+ }
882
949
  renderStatusLine() {
883
950
  this.renderController.renderStatusLine();
884
951
  }
@@ -4,7 +4,9 @@ export type PixInstallCliOptions = {
4
4
  };
5
5
  export type PixInstallCliContext = {
6
6
  env?: NodeJS.ProcessEnv;
7
+ homeDir?: string;
7
8
  };
9
+ export declare function formatPixInstallNextSteps(homeDir?: string): string;
8
10
  export declare function pixInstallUsage(): string;
9
11
  export declare function parsePixInstallArgs(argv: readonly string[]): PixInstallCliOptions;
10
12
  export declare function runPixInstallCli(argv?: readonly string[], context?: PixInstallCliContext): Promise<number>;
@@ -1,8 +1,22 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
+ import { homedir } from "node:os";
3
4
  import { join } from "node:path";
4
5
  import { FONT_FAMILY_NAME, installJetBrainsNerdFont, isJetBrainsNerdFontInstalled, } from "../terminal/nerd-font-controller.js";
5
6
  import { clipboardInstallHint, clipboardSupportAvailable } from "../screen/clipboard.js";
7
+ import { getPixConfigPath } from "../../config.js";
8
+ export function formatPixInstallNextSteps(homeDir = homedir()) {
9
+ const pixConfigPath = getPixConfigPath(homeDir);
10
+ const toolsConfigPath = join(homeDir, ".config", "pi", "pi-tools-suite.jsonc");
11
+ return [
12
+ "",
13
+ "Next steps:",
14
+ ` 1. Edit ${pixConfigPath} and set dictation.language / dictation.languages for voice input.`,
15
+ ` 2. Edit ${toolsConfigPath} and enable the LSP servers you use under lsp.servers.`,
16
+ " 3. Start pix, then run /opencode-import to import opencode accounts.",
17
+ " For Antigravity accounts, run /antigravity-import or /antigravity-add-account.",
18
+ ].join("\n");
19
+ }
6
20
  export function pixInstallUsage() {
7
21
  return `Usage: pix install [--check]
8
22
  pix setup [--check]
@@ -86,7 +100,7 @@ export async function runPixInstallCli(argv = process.argv.slice(2), context = {
86
100
  failures += 1;
87
101
  }
88
102
  }
89
- if (clipboardSupportAvailable(env)) {
103
+ if (await clipboardSupportAvailable(env)) {
90
104
  console.log("✓ Clipboard support is available");
91
105
  }
92
106
  else {
@@ -94,6 +108,7 @@ export async function runPixInstallCli(argv = process.argv.slice(2), context = {
94
108
  if (process.platform === "linux")
95
109
  failures += 1;
96
110
  }
111
+ console.log(formatPixInstallNextSteps(context.homeDir));
97
112
  return failures === 0 ? 0 : 1;
98
113
  }
99
114
  async function resolvePiCliStatus(env) {
@@ -28,13 +28,17 @@ export class AppCommandController {
28
28
  return {
29
29
  runSettingsCommand: () => this.modelActions.runSettingsCommand(),
30
30
  runModelSlashCommand: (argumentsText) => this.modelActions.runModelSlashCommand(argumentsText),
31
+ runDefaultModelSlashCommand: (argumentsText) => this.modelActions.runDefaultModelSlashCommand(argumentsText),
32
+ runAutocompleteSlashCommand: (argumentsText) => this.modelActions.runAutocompleteSlashCommand(argumentsText),
31
33
  runScopedModelsCommand: (argumentsText) => this.modelActions.runScopedModelsCommand(argumentsText),
32
34
  runThinkingSlashCommand: (argumentsText) => this.modelActions.runThinkingSlashCommand(argumentsText),
35
+ runDefaultThinkingSlashCommand: (argumentsText) => this.modelActions.runDefaultThinkingSlashCommand(argumentsText),
33
36
  runEnhanceCommand: () => this.host.enhancePrompt(),
34
37
  runExportCommand: (argumentsText) => this.sessionActions.runExportCommand(argumentsText),
35
38
  runImportCommand: (argumentsText) => this.sessionActions.runImportCommand(argumentsText),
36
39
  runShareCommand: () => this.sessionActions.runShareCommand(),
37
40
  runCopyCommand: () => this.sessionActions.runCopyCommand(),
41
+ runQueueCommand: (argumentsText) => this.sessionActions.runQueueCommand(argumentsText),
38
42
  runNameCommand: (argumentsText) => this.sessionActions.runNameCommand(argumentsText),
39
43
  runSessionInfoCommand: () => this.sessionActions.runSessionInfoCommand(),
40
44
  runUsageCommand: () => this.sessionActions.runUsageCommand(),
@@ -9,6 +9,8 @@ export type CommandControllerHost = {
9
9
  getInput(): string;
10
10
  setInput(value: string): void;
11
11
  promptEnhancerModelRef(): string;
12
+ autocompleteModelRef(): string;
13
+ setAutocompleteModelRef(modelRef: string): void;
12
14
  enhancePrompt(): Promise<void>;
13
15
  isRunning(): boolean;
14
16
  stop(): void | Promise<void>;
@@ -22,6 +24,7 @@ export type CommandControllerHost = {
22
24
  modelRef(model: SessionModel): string;
23
25
  getFavoriteScopedModels(): ScopedSessionModel[];
24
26
  setSessionStatus(session: AgentSession | undefined): void;
27
+ queueUserMessage(text: string): void;
25
28
  resetSessionView(): void;
26
29
  loadSessionHistory(): void;
27
30
  afterSessionReplacement(message?: string): void;
@@ -35,6 +38,7 @@ export type CommandControllerHost = {
35
38
  getDirectPopupMenuQuery(): string;
36
39
  setDirectPopupMenuQuery(query: string): void;
37
40
  getResumeLoading(): boolean;
41
+ getResumeSessions(): readonly SessionInfo[];
38
42
  setResumeLoading(loading: boolean): void;
39
43
  setResumeSessions(sessions: SessionInfo[]): void;
40
44
  openResumeMenuWithQuery(query: string): void;
@@ -5,8 +5,13 @@ export declare class ModelCommandActions {
5
5
  constructor(host: CommandControllerHost);
6
6
  runSettingsCommand(): Promise<void>;
7
7
  runModelSlashCommand(argumentsText: string): Promise<void>;
8
+ runDefaultModelSlashCommand(argumentsText: string): Promise<void>;
9
+ runAutocompleteSlashCommand(argumentsText: string): Promise<void>;
8
10
  runScopedModelsCommand(argumentsText: string): Promise<void>;
9
11
  runThinkingSlashCommand(argumentsText: string): Promise<void>;
12
+ runDefaultThinkingSlashCommand(argumentsText: string): Promise<void>;
10
13
  runModelCommand(model: SessionModel): Promise<void>;
11
14
  runThinkingCommand(level: ThinkingLevel): Promise<void>;
15
+ private saveDefaultModel;
16
+ private saveDefaultThinking;
12
17
  }
@@ -1,4 +1,5 @@
1
1
  import { getIdleRuntime, getRuntime } from "./command-runtime.js";
2
+ import { savePixAutocompleteModel, savePixDefaultModel, savePixDefaultThinking } from "../../config.js";
2
3
  import { createId } from "../id.js";
3
4
  import { isThinkingLevel, parseScopedModelRef } from "../model/model-ref.js";
4
5
  export class ModelCommandActions {
@@ -22,6 +23,7 @@ export class ModelCommandActions {
22
23
  `session: ${runtime.session.sessionFile ?? "in-memory"}`,
23
24
  `model: ${currentModel}`,
24
25
  `prompt enhancer model: ${this.host.promptEnhancerModelRef()}`,
26
+ `autocomplete model: ${this.host.autocompleteModelRef() || "disabled"}`,
25
27
  `thinking: ${runtime.session.thinkingLevel}`,
26
28
  `theme: ${settings.getTheme() ?? this.host.options.themeName}`,
27
29
  `skill commands: ${settings.getEnableSkillCommands() ? "enabled" : "disabled"}`,
@@ -70,6 +72,59 @@ export class ModelCommandActions {
70
72
  this.host.setSessionStatus(runtime.session);
71
73
  }
72
74
  }
75
+ async runDefaultModelSlashCommand(argumentsText) {
76
+ const modelRef = argumentsText.trim();
77
+ if (!modelRef) {
78
+ const selected = await this.host.showMenu(this.host.getModelMenuItems(""), {
79
+ title: "Select default model",
80
+ placeholder: "Search models",
81
+ emptyText: "No matching models",
82
+ });
83
+ if (!selected) {
84
+ this.host.setSessionStatus(this.host.runtime()?.session);
85
+ this.host.render();
86
+ return;
87
+ }
88
+ this.saveDefaultModel(this.host.modelRef(selected.model));
89
+ this.host.render();
90
+ return;
91
+ }
92
+ const runtime = getRuntime(this.host, "default-model");
93
+ if (!runtime)
94
+ return;
95
+ const parsed = parseScopedModelRef(modelRef);
96
+ if (!parsed)
97
+ throw new Error("Model must use provider/model[:thinking] format");
98
+ runtime.services.modelRegistry.refresh();
99
+ const model = runtime.services.modelRegistry.find(parsed.provider, parsed.modelId);
100
+ if (!model)
101
+ throw new Error(`Model not found: ${parsed.provider}/${parsed.modelId}`);
102
+ this.saveDefaultModel(modelRef);
103
+ this.host.setSessionStatus(runtime.session);
104
+ }
105
+ async runAutocompleteSlashCommand(argumentsText) {
106
+ const modelRef = argumentsText.trim();
107
+ if (!modelRef) {
108
+ const saved = savePixAutocompleteModel("");
109
+ this.host.setAutocompleteModelRef(saved.modelRef);
110
+ this.host.addEntry({ id: createId("system"), kind: "system", text: saved.modelRef ? `Autocomplete model set to ${saved.modelRef}` : "Inline autocomplete disabled." });
111
+ return;
112
+ }
113
+ const runtime = getRuntime(this.host, "autocomplete");
114
+ if (!runtime)
115
+ return;
116
+ const parsed = parseScopedModelRef(modelRef);
117
+ if (!parsed)
118
+ throw new Error("Model must use provider/model[:thinking] format, or run /autocomplete with no arguments to disable");
119
+ runtime.services.modelRegistry.refresh();
120
+ const model = runtime.services.modelRegistry.find(parsed.provider, parsed.modelId);
121
+ if (!model)
122
+ throw new Error(`Model not found: ${parsed.provider}/${parsed.modelId}`);
123
+ const saved = savePixAutocompleteModel(modelRef);
124
+ this.host.setAutocompleteModelRef(saved.modelRef);
125
+ this.host.addEntry({ id: createId("system"), kind: "system", text: `Autocomplete model set to ${saved.modelRef}.` });
126
+ this.host.setSessionStatus(runtime.session);
127
+ }
73
128
  async runScopedModelsCommand(argumentsText) {
74
129
  const runtime = getIdleRuntime(this.host, "scoped-models");
75
130
  if (!runtime)
@@ -152,6 +207,27 @@ export class ModelCommandActions {
152
207
  throw new Error(`Unknown thinking level: ${level}`);
153
208
  await this.runThinkingCommand(level);
154
209
  }
210
+ async runDefaultThinkingSlashCommand(argumentsText) {
211
+ const level = argumentsText.trim();
212
+ if (!level) {
213
+ const selected = await this.host.showMenu(this.host.getThinkingMenuItems(""), {
214
+ title: "Select default thinking level",
215
+ placeholder: "Search thinking levels",
216
+ emptyText: "No matching thinking levels",
217
+ });
218
+ if (!selected) {
219
+ this.host.setSessionStatus(this.host.runtime()?.session);
220
+ this.host.render();
221
+ return;
222
+ }
223
+ this.saveDefaultThinking(selected.level);
224
+ this.host.render();
225
+ return;
226
+ }
227
+ if (!isThinkingLevel(level))
228
+ throw new Error(`Unknown thinking level: ${level}`);
229
+ this.saveDefaultThinking(level);
230
+ }
155
231
  async runModelCommand(model) {
156
232
  const runtime = getRuntime(this.host, "model");
157
233
  if (!runtime)
@@ -173,4 +249,32 @@ export class ModelCommandActions {
173
249
  this.host.addEntry({ id: createId("system"), kind: "system", text: `Selected thinking level ${runtime.session.thinkingLevel}` });
174
250
  this.host.setSessionStatus(runtime.session);
175
251
  }
252
+ saveDefaultModel(modelRef) {
253
+ const saved = savePixDefaultModel(modelRef);
254
+ if (!saved)
255
+ throw new Error("Model must use provider/model[:thinking] format");
256
+ this.host.addEntry({
257
+ id: createId("system"),
258
+ kind: "system",
259
+ text: `Default model set to ${formatDefaultModelRef(saved)}. New sessions will use it unless --model is provided.`,
260
+ });
261
+ }
262
+ saveDefaultThinking(level) {
263
+ const runtime = getRuntime(this.host, "default-thinking");
264
+ if (!runtime)
265
+ return;
266
+ const fallbackModelRef = runtime.session.model ? this.host.modelRef(runtime.session.model) : undefined;
267
+ const saved = savePixDefaultThinking(level, fallbackModelRef);
268
+ if (!saved)
269
+ throw new Error("Set /default-model first or select a session model before setting default thinking");
270
+ this.host.addEntry({
271
+ id: createId("system"),
272
+ kind: "system",
273
+ text: `Default thinking level set to ${saved.thinking ?? level} for ${saved.modelRef}. New sessions will use it unless --model is provided.`,
274
+ });
275
+ this.host.setSessionStatus(runtime.session);
276
+ }
277
+ }
278
+ function formatDefaultModelRef(defaultModel) {
279
+ return defaultModel.thinking ? `${defaultModel.modelRef}:${defaultModel.thinking}` : defaultModel.modelRef;
176
280
  }
@@ -1,8 +1,12 @@
1
+ import type { SessionInfo } from "@earendil-works/pi-coding-agent";
1
2
  import type { CommandControllerHost } from "./command-host.js";
3
+ import { type ResumeSessionLoaderOptions } from "../session/resume-session-loader.js";
2
4
  import type { PopupMenuPlacement } from "../types.js";
3
5
  export declare class NavigationCommandActions {
4
6
  private readonly host;
5
- constructor(host: CommandControllerHost);
7
+ private readonly resumeSessionLoader;
8
+ private resumeLoadId;
9
+ constructor(host: CommandControllerHost, resumeSessionLoader?: (options: ResumeSessionLoaderOptions) => Promise<readonly SessionInfo[]>);
6
10
  runForkCommand(argumentsText: string): Promise<void>;
7
11
  runCloneCommand(): Promise<void>;
8
12
  runTreeCommand(argumentsText: string): Promise<void>;
@@ -14,6 +18,7 @@ export declare class NavigationCommandActions {
14
18
  preserveStatus?: boolean;
15
19
  placement?: PopupMenuPlacement;
16
20
  }): Promise<void>;
21
+ private loadResumeSessionsInBackground;
17
22
  private formatSessionTree;
18
23
  private describeSessionTreeEntry;
19
24
  }