@oh-my-pi/pi-coding-agent 3.30.0 → 3.31.0

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 (155) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/package.json +5 -5
  3. package/src/cli/args.ts +4 -0
  4. package/src/core/agent-session.ts +29 -2
  5. package/src/core/bash-executor.ts +2 -1
  6. package/src/core/custom-commands/bundled/review/index.ts +369 -14
  7. package/src/core/custom-commands/bundled/wt/index.ts +1 -1
  8. package/src/core/session-manager.ts +158 -246
  9. package/src/core/session-storage.ts +379 -0
  10. package/src/core/settings-manager.ts +155 -4
  11. package/src/core/system-prompt.ts +62 -64
  12. package/src/core/tools/ask.ts +5 -4
  13. package/src/core/tools/bash-interceptor.ts +26 -61
  14. package/src/core/tools/bash.ts +13 -8
  15. package/src/core/tools/edit-diff.ts +11 -4
  16. package/src/core/tools/edit.ts +7 -13
  17. package/src/core/tools/find.ts +111 -50
  18. package/src/core/tools/gemini-image.ts +128 -147
  19. package/src/core/tools/grep.ts +397 -415
  20. package/src/core/tools/index.test.ts +5 -1
  21. package/src/core/tools/index.ts +6 -8
  22. package/src/core/tools/ls.ts +12 -10
  23. package/src/core/tools/lsp/client.ts +58 -9
  24. package/src/core/tools/lsp/config.ts +205 -656
  25. package/src/core/tools/lsp/defaults.json +465 -0
  26. package/src/core/tools/lsp/index.ts +55 -32
  27. package/src/core/tools/lsp/rust-analyzer.ts +49 -10
  28. package/src/core/tools/lsp/types.ts +1 -0
  29. package/src/core/tools/lsp/utils.ts +1 -1
  30. package/src/core/tools/read.ts +150 -74
  31. package/src/core/tools/render-utils.ts +70 -10
  32. package/src/core/tools/review.ts +38 -126
  33. package/src/core/tools/task/artifacts.ts +5 -4
  34. package/src/core/tools/task/executor.ts +94 -83
  35. package/src/core/tools/task/index.ts +129 -92
  36. package/src/core/tools/task/parallel.ts +30 -3
  37. package/src/core/tools/task/render.ts +85 -39
  38. package/src/core/tools/task/types.ts +15 -6
  39. package/src/core/tools/task/worker.ts +124 -89
  40. package/src/core/tools/web-fetch.ts +112 -377
  41. package/src/core/tools/{web-fetch-handlers → web-scrapers}/artifacthub.ts +6 -1
  42. package/src/core/tools/{web-fetch-handlers → web-scrapers}/arxiv.ts +8 -4
  43. package/src/core/tools/{web-fetch-handlers → web-scrapers}/aur.ts +6 -2
  44. package/src/core/tools/{web-fetch-handlers → web-scrapers}/biorxiv.ts +6 -1
  45. package/src/core/tools/{web-fetch-handlers → web-scrapers}/bluesky.ts +10 -3
  46. package/src/core/tools/{web-fetch-handlers → web-scrapers}/brew.ts +6 -2
  47. package/src/core/tools/{web-fetch-handlers → web-scrapers}/cheatsh.ts +6 -1
  48. package/src/core/tools/{web-fetch-handlers → web-scrapers}/chocolatey.ts +6 -1
  49. package/src/core/tools/web-scrapers/choosealicense.ts +110 -0
  50. package/src/core/tools/web-scrapers/cisa-kev.ts +100 -0
  51. package/src/core/tools/web-scrapers/clojars.ts +180 -0
  52. package/src/core/tools/{web-fetch-handlers → web-scrapers}/coingecko.ts +6 -1
  53. package/src/core/tools/{web-fetch-handlers → web-scrapers}/crates-io.ts +7 -2
  54. package/src/core/tools/web-scrapers/crossref.ts +149 -0
  55. package/src/core/tools/{web-fetch-handlers → web-scrapers}/devto.ts +8 -4
  56. package/src/core/tools/{web-fetch-handlers → web-scrapers}/discogs.ts +6 -1
  57. package/src/core/tools/web-scrapers/discourse.ts +221 -0
  58. package/src/core/tools/{web-fetch-handlers → web-scrapers}/dockerhub.ts +7 -3
  59. package/src/core/tools/web-scrapers/fdroid.ts +158 -0
  60. package/src/core/tools/web-scrapers/firefox-addons.ts +214 -0
  61. package/src/core/tools/web-scrapers/flathub.ts +239 -0
  62. package/src/core/tools/{web-fetch-handlers → web-scrapers}/github-gist.ts +6 -2
  63. package/src/core/tools/{web-fetch-handlers → web-scrapers}/github.ts +63 -32
  64. package/src/core/tools/{web-fetch-handlers → web-scrapers}/gitlab.ts +31 -19
  65. package/src/core/tools/{web-fetch-handlers → web-scrapers}/go-pkg.ts +8 -4
  66. package/src/core/tools/{web-fetch-handlers → web-scrapers}/hackage.ts +6 -1
  67. package/src/core/tools/{web-fetch-handlers → web-scrapers}/hackernews.ts +18 -18
  68. package/src/core/tools/{web-fetch-handlers → web-scrapers}/hex.ts +3 -3
  69. package/src/core/tools/{web-fetch-handlers → web-scrapers}/huggingface.ts +10 -10
  70. package/src/core/tools/{web-fetch-handlers → web-scrapers}/iacr.ts +8 -4
  71. package/src/core/tools/web-scrapers/index.ts +250 -0
  72. package/src/core/tools/web-scrapers/jetbrains-marketplace.ts +169 -0
  73. package/src/core/tools/web-scrapers/lemmy.ts +220 -0
  74. package/src/core/tools/{web-fetch-handlers → web-scrapers}/lobsters.ts +3 -3
  75. package/src/core/tools/{web-fetch-handlers → web-scrapers}/mastodon.ts +11 -3
  76. package/src/core/tools/{web-fetch-handlers → web-scrapers}/maven.ts +6 -1
  77. package/src/core/tools/{web-fetch-handlers → web-scrapers}/mdn.ts +2 -2
  78. package/src/core/tools/{web-fetch-handlers → web-scrapers}/metacpan.ts +13 -7
  79. package/src/core/tools/web-scrapers/musicbrainz.ts +273 -0
  80. package/src/core/tools/{web-fetch-handlers → web-scrapers}/npm.ts +12 -5
  81. package/src/core/tools/{web-fetch-handlers → web-scrapers}/nuget.ts +9 -5
  82. package/src/core/tools/{web-fetch-handlers → web-scrapers}/nvd.ts +6 -1
  83. package/src/core/tools/web-scrapers/ollama.ts +267 -0
  84. package/src/core/tools/web-scrapers/open-vsx.ts +119 -0
  85. package/src/core/tools/{web-fetch-handlers → web-scrapers}/opencorporates.ts +2 -0
  86. package/src/core/tools/{web-fetch-handlers → web-scrapers}/openlibrary.ts +18 -12
  87. package/src/core/tools/web-scrapers/orcid.ts +299 -0
  88. package/src/core/tools/{web-fetch-handlers → web-scrapers}/osv.ts +6 -1
  89. package/src/core/tools/{web-fetch-handlers → web-scrapers}/packagist.ts +6 -2
  90. package/src/core/tools/{web-fetch-handlers → web-scrapers}/pub-dev.ts +3 -3
  91. package/src/core/tools/{web-fetch-handlers → web-scrapers}/pubmed.ts +8 -4
  92. package/src/core/tools/{web-fetch-handlers → web-scrapers}/pypi.ts +7 -3
  93. package/src/core/tools/web-scrapers/rawg.ts +124 -0
  94. package/src/core/tools/{web-fetch-handlers → web-scrapers}/readthedocs.ts +7 -3
  95. package/src/core/tools/{web-fetch-handlers → web-scrapers}/reddit.ts +6 -2
  96. package/src/core/tools/{web-fetch-handlers → web-scrapers}/repology.ts +6 -1
  97. package/src/core/tools/{web-fetch-handlers → web-scrapers}/rfc.ts +7 -3
  98. package/src/core/tools/{web-fetch-handlers → web-scrapers}/rubygems.ts +6 -1
  99. package/src/core/tools/web-scrapers/searchcode.ts +217 -0
  100. package/src/core/tools/{web-fetch-handlers → web-scrapers}/sec-edgar.ts +6 -1
  101. package/src/core/tools/{web-fetch-handlers → web-scrapers}/semantic-scholar.ts +2 -2
  102. package/src/core/tools/web-scrapers/snapcraft.ts +200 -0
  103. package/src/core/tools/web-scrapers/sourcegraph.ts +373 -0
  104. package/src/core/tools/web-scrapers/spdx.ts +121 -0
  105. package/src/core/tools/{web-fetch-handlers → web-scrapers}/spotify.ts +3 -3
  106. package/src/core/tools/{web-fetch-handlers → web-scrapers}/stackoverflow.ts +3 -2
  107. package/src/core/tools/{web-fetch-handlers → web-scrapers}/terraform.ts +11 -3
  108. package/src/core/tools/{web-fetch-handlers → web-scrapers}/tldr.ts +6 -2
  109. package/src/core/tools/{web-fetch-handlers → web-scrapers}/twitter.ts +15 -3
  110. package/src/core/tools/{web-fetch-handlers → web-scrapers}/types.ts +98 -27
  111. package/src/core/tools/web-scrapers/utils.ts +162 -0
  112. package/src/core/tools/{web-fetch-handlers → web-scrapers}/vimeo.ts +3 -3
  113. package/src/core/tools/web-scrapers/vscode-marketplace.ts +195 -0
  114. package/src/core/tools/web-scrapers/w3c.ts +163 -0
  115. package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikidata.ts +13 -5
  116. package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikipedia.ts +7 -3
  117. package/src/core/tools/{web-fetch-handlers → web-scrapers}/youtube.ts +72 -20
  118. package/src/core/tools/write.ts +21 -18
  119. package/src/core/voice.ts +3 -2
  120. package/src/lib/worktree/collapse.ts +2 -1
  121. package/src/lib/worktree/git.ts +2 -18
  122. package/src/main.ts +59 -3
  123. package/src/modes/interactive/components/extensions/extension-dashboard.ts +33 -19
  124. package/src/modes/interactive/components/extensions/extension-list.ts +15 -8
  125. package/src/modes/interactive/components/hook-editor.ts +2 -1
  126. package/src/modes/interactive/components/model-selector.ts +19 -4
  127. package/src/modes/interactive/interactive-mode.ts +41 -38
  128. package/src/modes/interactive/theme/theme.ts +58 -58
  129. package/src/modes/rpc/rpc-mode.ts +10 -9
  130. package/src/prompts/review-request.md +27 -0
  131. package/src/prompts/reviewer.md +64 -68
  132. package/src/prompts/tools/output.md +22 -3
  133. package/src/prompts/tools/task.md +32 -33
  134. package/src/utils/clipboard.ts +2 -1
  135. package/examples/extensions/subagent/agents/reviewer.md +0 -35
  136. package/src/core/tools/web-fetch-handlers/index.ts +0 -69
  137. package/src/core/tools/web-fetch-handlers/utils.ts +0 -91
  138. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/academic.test.ts +0 -0
  139. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/business.test.ts +0 -0
  140. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/dev-platforms.test.ts +0 -0
  141. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/documentation.test.ts +0 -0
  142. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/finance-media.test.ts +0 -0
  143. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/git-hosting.test.ts +0 -0
  144. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/media.test.ts +0 -0
  145. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-managers-2.test.ts +0 -0
  146. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-managers.test.ts +0 -0
  147. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-registries.test.ts +0 -0
  148. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/research.test.ts +0 -0
  149. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/security.test.ts +0 -0
  150. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/social-extended.test.ts +0 -0
  151. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/social.test.ts +0 -0
  152. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/stackexchange.test.ts +0 -0
  153. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/standards.test.ts +0 -0
  154. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikipedia.test.ts +0 -0
  155. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/youtube.test.ts +0 -0
@@ -38,30 +38,40 @@ export class ExtensionDashboard extends Container {
38
38
  private inspector: InspectorPanel;
39
39
  private settingsManager: SettingsManager | null;
40
40
  private cwd: string;
41
+ private terminalHeight: number;
41
42
 
42
43
  public onClose?: () => void;
43
44
 
44
- constructor(cwd: string, settingsManager: SettingsManager | null = null) {
45
+ constructor(cwd: string, settingsManager: SettingsManager | null = null, terminalHeight?: number) {
45
46
  super();
46
47
  this.cwd = cwd;
47
48
  this.settingsManager = settingsManager;
49
+ this.terminalHeight = terminalHeight ?? process.stdout.rows ?? 24;
48
50
  const disabledIds = settingsManager?.getDisabledExtensions() ?? [];
49
51
  this.state = createInitialState(cwd, disabledIds);
50
52
 
53
+ // Calculate max visible items based on terminal height
54
+ // Reserve ~10 lines for header, tabs, help text, borders
55
+ const maxVisible = Math.max(5, Math.floor((this.terminalHeight - 10) / 2));
56
+
51
57
  // Create main list - always focused
52
- this.mainList = new ExtensionList(this.state.searchFiltered, {
53
- onSelectionChange: (ext) => {
54
- this.state.selected = ext;
55
- this.inspector.setExtension(ext);
56
- },
57
- onToggle: (extensionId, enabled) => {
58
- this.handleExtensionToggle(extensionId, enabled);
59
- },
60
- onMasterToggle: (providerId) => {
61
- this.handleProviderToggle(providerId);
58
+ this.mainList = new ExtensionList(
59
+ this.state.searchFiltered,
60
+ {
61
+ onSelectionChange: (ext) => {
62
+ this.state.selected = ext;
63
+ this.inspector.setExtension(ext);
64
+ },
65
+ onToggle: (extensionId, enabled) => {
66
+ this.handleExtensionToggle(extensionId, enabled);
67
+ },
68
+ onMasterToggle: (providerId) => {
69
+ this.handleProviderToggle(providerId);
70
+ },
71
+ masterSwitchProvider: this.getActiveProviderId(),
62
72
  },
63
- masterSwitchProvider: this.getActiveProviderId(),
64
- });
73
+ maxVisible,
74
+ );
65
75
  this.mainList.setFocused(true);
66
76
 
67
77
  // Create inspector
@@ -91,9 +101,10 @@ export class ExtensionDashboard extends Container {
91
101
  this.addChild(new Text(this.renderTabBar(), 0, 0));
92
102
  this.addChild(new Spacer(1));
93
103
 
94
- // Help text
95
- // 2-column body
96
- this.addChild(new TwoColumnBody(this.mainList, this.inspector));
104
+ // 2-column body with height limit
105
+ // Reserve ~8 lines for header, tabs, help text, borders
106
+ const bodyMaxHeight = Math.max(5, this.terminalHeight - 8);
107
+ this.addChild(new TwoColumnBody(this.mainList, this.inspector, bodyMaxHeight));
97
108
 
98
109
  this.addChild(new Spacer(1));
99
110
  this.addChild(new Text(theme.fg("dim", " ↑/↓: navigate Space: toggle Tab: next provider Esc: close"), 0, 0));
@@ -262,10 +273,12 @@ export class ExtensionDashboard extends Container {
262
273
  class TwoColumnBody implements Component {
263
274
  private leftPane: ExtensionList;
264
275
  private rightPane: InspectorPanel;
276
+ private maxHeight: number;
265
277
 
266
- constructor(left: ExtensionList, right: InspectorPanel) {
278
+ constructor(left: ExtensionList, right: InspectorPanel, maxHeight: number) {
267
279
  this.leftPane = left;
268
280
  this.rightPane = right;
281
+ this.maxHeight = maxHeight;
269
282
  }
270
283
 
271
284
  render(width: number): string[] {
@@ -275,11 +288,12 @@ class TwoColumnBody implements Component {
275
288
  const leftLines = this.leftPane.render(leftWidth);
276
289
  const rightLines = this.rightPane.render(rightWidth);
277
290
 
278
- const maxLines = Math.max(leftLines.length, rightLines.length);
291
+ // Limit to maxHeight lines
292
+ const numLines = Math.min(this.maxHeight, Math.max(leftLines.length, rightLines.length));
279
293
  const combined: string[] = [];
280
294
  const separator = theme.fg("dim", ` ${theme.boxSharp.vertical} `);
281
295
 
282
- for (let i = 0; i < maxLines; i++) {
296
+ for (let i = 0; i < numLines; i++) {
283
297
  const left = truncateToWidth(leftLines[i] ?? "", leftWidth);
284
298
  const leftPadded = left + " ".repeat(Math.max(0, leftWidth - visibleWidth(left)));
285
299
  const right = truncateToWidth(rightLines[i] ?? "", rightWidth);
@@ -31,7 +31,7 @@ export interface ExtensionListCallbacks {
31
31
  masterSwitchProvider?: string | null;
32
32
  }
33
33
 
34
- const MAX_VISIBLE = 25;
34
+ const DEFAULT_MAX_VISIBLE = 15;
35
35
 
36
36
  /** Flattened list item for rendering */
37
37
  type ListItem =
@@ -48,14 +48,21 @@ export class ExtensionList implements Component {
48
48
  private focused = false;
49
49
  private callbacks: ExtensionListCallbacks;
50
50
  private masterSwitchProvider: string | null = null;
51
+ private maxVisible: number;
51
52
 
52
- constructor(extensions: Extension[], callbacks: ExtensionListCallbacks = {}) {
53
+ constructor(extensions: Extension[], callbacks: ExtensionListCallbacks = {}, maxVisible?: number) {
53
54
  this.extensions = extensions;
54
55
  this.callbacks = callbacks;
55
56
  this.masterSwitchProvider = callbacks.masterSwitchProvider ?? null;
57
+ this.maxVisible = maxVisible ?? DEFAULT_MAX_VISIBLE;
56
58
  this.rebuildList();
57
59
  }
58
60
 
61
+ setMaxVisible(maxVisible: number): void {
62
+ this.maxVisible = maxVisible;
63
+ this.clampSelection();
64
+ }
65
+
59
66
  setExtensions(extensions: Extension[]): void {
60
67
  this.extensions = extensions;
61
68
  this.rebuildList();
@@ -126,7 +133,7 @@ export class ExtensionList implements Component {
126
133
 
127
134
  // Calculate visible range
128
135
  const startIdx = this.scrollOffset;
129
- const endIdx = Math.min(startIdx + MAX_VISIBLE, this.listItems.length);
136
+ const endIdx = Math.min(startIdx + this.maxVisible, this.listItems.length);
130
137
 
131
138
  // Render visible items
132
139
  for (let i = startIdx; i < endIdx; i++) {
@@ -143,7 +150,7 @@ export class ExtensionList implements Component {
143
150
  }
144
151
 
145
152
  // Scroll indicator
146
- if (this.listItems.length > MAX_VISIBLE) {
153
+ if (this.listItems.length > this.maxVisible) {
147
154
  const indicator = theme.fg("muted", ` (${this.selectedIndex + 1}/${this.listItems.length})`);
148
155
  lines.push(indicator);
149
156
  }
@@ -388,8 +395,8 @@ export class ExtensionList implements Component {
388
395
  // Adjust scroll offset
389
396
  if (this.selectedIndex < this.scrollOffset) {
390
397
  this.scrollOffset = this.selectedIndex;
391
- } else if (this.selectedIndex >= this.scrollOffset + MAX_VISIBLE) {
392
- this.scrollOffset = this.selectedIndex - MAX_VISIBLE + 1;
398
+ } else if (this.selectedIndex >= this.scrollOffset + this.maxVisible) {
399
+ this.scrollOffset = this.selectedIndex - this.maxVisible + 1;
393
400
  }
394
401
  }
395
402
 
@@ -470,8 +477,8 @@ export class ExtensionList implements Component {
470
477
  private moveSelectionDown(): void {
471
478
  if (this.selectedIndex < this.listItems.length - 1) {
472
479
  this.selectedIndex++;
473
- if (this.selectedIndex >= this.scrollOffset + MAX_VISIBLE) {
474
- this.scrollOffset = this.selectedIndex - MAX_VISIBLE + 1;
480
+ if (this.selectedIndex >= this.scrollOffset + this.maxVisible) {
481
+ this.scrollOffset = this.selectedIndex - this.maxVisible + 1;
475
482
  }
476
483
  this.notifySelectionChange();
477
484
  }
@@ -7,6 +7,7 @@ import * as fs from "node:fs";
7
7
  import * as os from "node:os";
8
8
  import * as path from "node:path";
9
9
  import { Container, Editor, isCtrlG, isEscape, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
10
+ import { nanoid } from "nanoid";
10
11
  import { getEditorTheme, theme } from "../theme/theme";
11
12
  import { DynamicBorder } from "./dynamic-border";
12
13
 
@@ -90,7 +91,7 @@ export class HookEditorComponent extends Container {
90
91
  }
91
92
 
92
93
  const currentText = this.editor.getText();
93
- const tmpFile = path.join(os.tmpdir(), `omp-hook-editor-${Date.now()}.md`);
94
+ const tmpFile = path.join(os.tmpdir(), `omp-hook-editor-${nanoid()}.md`);
94
95
 
95
96
  try {
96
97
  fs.writeFileSync(tmpFile, currentText, "utf-8");
@@ -34,7 +34,7 @@ interface ScopedModelItem {
34
34
  thinkingLevel: string;
35
35
  }
36
36
 
37
- type ModelRole = "default" | "smol" | "slow";
37
+ type ModelRole = "default" | "smol" | "slow" | "temporary";
38
38
 
39
39
  interface MenuAction {
40
40
  label: string;
@@ -75,6 +75,7 @@ export class ModelSelectorComponent extends Container {
75
75
  private errorMessage?: string;
76
76
  private tui: TUI;
77
77
  private scopedModels: ReadonlyArray<ScopedModelItem>;
78
+ private temporaryOnly: boolean;
78
79
 
79
80
  // Tab state
80
81
  private providers: string[] = [ALL_TAB];
@@ -92,6 +93,7 @@ export class ModelSelectorComponent extends Container {
92
93
  scopedModels: ReadonlyArray<ScopedModelItem>,
93
94
  onSelect: (model: Model<any>, role: string) => void,
94
95
  onCancel: () => void,
96
+ options?: { temporaryOnly?: boolean },
95
97
  ) {
96
98
  super();
97
99
 
@@ -102,6 +104,7 @@ export class ModelSelectorComponent extends Container {
102
104
  this.scopedModels = scopedModels;
103
105
  this.onSelectCallback = onSelect;
104
106
  this.onCancelCallback = onCancel;
107
+ this.temporaryOnly = options?.temporaryOnly ?? false;
105
108
 
106
109
  // Load current role assignments from settings
107
110
  this._loadRoleModels();
@@ -483,10 +486,16 @@ export class ModelSelectorComponent extends Container {
483
486
  return;
484
487
  }
485
488
 
486
- // Enter - open context menu
489
+ // Enter - open context menu or select directly in temporary mode
487
490
  if (isEnter(keyData)) {
488
- if (this.filteredModels[this.selectedIndex]) {
489
- this.openMenu();
491
+ const selectedModel = this.filteredModels[this.selectedIndex];
492
+ if (selectedModel) {
493
+ if (this.temporaryOnly) {
494
+ // In temporary mode, skip menu and select directly
495
+ this.handleSelect(selectedModel.model, "temporary");
496
+ } else {
497
+ this.openMenu();
498
+ }
490
499
  }
491
500
  return;
492
501
  }
@@ -536,6 +545,12 @@ export class ModelSelectorComponent extends Container {
536
545
  }
537
546
 
538
547
  private handleSelect(model: Model<any>, role: ModelRole): void {
548
+ // For temporary role, don't save to settings - just notify caller
549
+ if (role === "temporary") {
550
+ this.onSelectCallback(model, role);
551
+ return;
552
+ }
553
+
539
554
  // Save to settings
540
555
  this.settingsManager.setModelRole(role, `${model.provider}/${model.id}`);
541
556
 
@@ -23,6 +23,7 @@ import {
23
23
  TUI,
24
24
  visibleWidth,
25
25
  } from "@oh-my-pi/pi-tui";
26
+ import { nanoid } from "nanoid";
26
27
  import { getAuthPath, getDebugLogPath } from "../../config";
27
28
  import type { AgentSession, AgentSessionEvent } from "../../core/agent-session";
28
29
  import type { ExtensionUIContext } from "../../core/extensions/index";
@@ -941,9 +942,9 @@ export class InteractiveMode {
941
942
  this.editor.onCtrlD = () => this.handleCtrlD();
942
943
  this.editor.onCtrlZ = () => this.handleCtrlZ();
943
944
  this.editor.onShiftTab = () => this.cycleThinkingLevel();
944
- this.editor.onCtrlP = () => this.cycleModel("forward");
945
- this.editor.onShiftCtrlP = () => this.cycleModel("backward");
946
- this.editor.onCtrlY = () => this.cycleRoleModel();
945
+ this.editor.onCtrlP = () => this.cycleRoleModel();
946
+ this.editor.onShiftCtrlP = () => this.cycleRoleModel({ temporary: true });
947
+ this.editor.onCtrlY = () => this.showModelSelector({ temporaryOnly: true });
947
948
 
948
949
  // Global debug handler on TUI (works regardless of focus)
949
950
  this.ui.onDebug = () => this.handleDebugCommand();
@@ -951,9 +952,6 @@ export class InteractiveMode {
951
952
  this.editor.onCtrlO = () => this.toggleToolOutputExpansion();
952
953
  this.editor.onCtrlT = () => this.toggleThinkingBlockVisibility();
953
954
  this.editor.onCtrlG = () => this.openExternalEditor();
954
- this.editor.onCtrlY = () => {
955
- void this.toggleVoiceListening();
956
- };
957
955
  this.editor.onQuestionMark = () => this.handleHotkeysCommand();
958
956
  this.editor.onCtrlV = () => this.handleImagePaste();
959
957
 
@@ -991,6 +989,14 @@ export class InteractiveMode {
991
989
  private setupEditorSubmitHandler(): void {
992
990
  this.editor.onSubmit = async (text: string) => {
993
991
  text = text.trim();
992
+
993
+ // Empty submit while streaming with queued messages: flush queues immediately
994
+ if (!text && this.session.isStreaming && this.session.queuedMessageCount > 0) {
995
+ // Abort current stream and let queued messages be processed
996
+ await this.session.abort();
997
+ return;
998
+ }
999
+
994
1000
  if (!text) return;
995
1001
 
996
1002
  // Handle slash commands
@@ -1994,27 +2000,9 @@ export class InteractiveMode {
1994
2000
  }
1995
2001
  }
1996
2002
 
1997
- private async cycleModel(direction: "forward" | "backward"): Promise<void> {
1998
- try {
1999
- const result = await this.session.cycleModel(direction);
2000
- if (result === undefined) {
2001
- const msg = this.session.scopedModels.length > 0 ? "Only one model in scope" : "Only one model available";
2002
- this.showStatus(msg);
2003
- } else {
2004
- this.statusLine.invalidate();
2005
- this.updateEditorBorderColor();
2006
- const thinkingStr =
2007
- result.model.reasoning && result.thinkingLevel !== "off" ? ` (thinking: ${result.thinkingLevel})` : "";
2008
- this.showStatus(`Switched to ${result.model.name || result.model.id}${thinkingStr}`);
2009
- }
2010
- } catch (error) {
2011
- this.showError(error instanceof Error ? error.message : String(error));
2012
- }
2013
- }
2014
-
2015
- private async cycleRoleModel(): Promise<void> {
2003
+ private async cycleRoleModel(options?: { temporary?: boolean }): Promise<void> {
2016
2004
  try {
2017
- const result = await this.session.cycleRoleModels(["slow", "default", "smol"]);
2005
+ const result = await this.session.cycleRoleModels(["slow", "default", "smol"], options);
2018
2006
  if (!result) {
2019
2007
  this.showStatus("Only one role model available");
2020
2008
  return;
@@ -2025,7 +2013,8 @@ export class InteractiveMode {
2025
2013
  const roleLabel = result.role === "default" ? "default" : result.role;
2026
2014
  const thinkingStr =
2027
2015
  result.model.reasoning && result.thinkingLevel !== "off" ? ` (thinking: ${result.thinkingLevel})` : "";
2028
- this.showStatus(`Switched to ${roleLabel}: ${result.model.name || result.model.id}${thinkingStr}`);
2016
+ const tempLabel = options?.temporary ? " (temporary)" : "";
2017
+ this.showStatus(`Switched to ${roleLabel}: ${result.model.name || result.model.id}${thinkingStr}${tempLabel}`);
2029
2018
  } catch (error) {
2030
2019
  this.showError(error instanceof Error ? error.message : String(error));
2031
2020
  }
@@ -2068,7 +2057,7 @@ export class InteractiveMode {
2068
2057
  }
2069
2058
 
2070
2059
  const currentText = this.editor.getText();
2071
- const tmpFile = path.join(os.tmpdir(), `omp-editor-${Date.now()}.omp.md`);
2060
+ const tmpFile = path.join(os.tmpdir(), `omp-editor-${nanoid()}.omp.md`);
2072
2061
 
2073
2062
  try {
2074
2063
  // Write current content to temp file
@@ -2255,7 +2244,7 @@ export class InteractiveMode {
2255
2244
  */
2256
2245
  private showExtensionsDashboard(): void {
2257
2246
  this.showSelector((done) => {
2258
- const dashboard = new ExtensionDashboard(process.cwd(), this.settingsManager);
2247
+ const dashboard = new ExtensionDashboard(process.cwd(), this.settingsManager, this.ui.terminal.rows);
2259
2248
  dashboard.onClose = () => {
2260
2249
  done();
2261
2250
  this.ui.requestRender();
@@ -2379,7 +2368,7 @@ export class InteractiveMode {
2379
2368
  }
2380
2369
  }
2381
2370
 
2382
- private showModelSelector(): void {
2371
+ private showModelSelector(options?: { temporaryOnly?: boolean }): void {
2383
2372
  this.showSelector((done) => {
2384
2373
  const selector = new ModelSelectorComponent(
2385
2374
  this.ui,
@@ -2389,24 +2378,36 @@ export class InteractiveMode {
2389
2378
  this.session.scopedModels,
2390
2379
  async (model, role) => {
2391
2380
  try {
2392
- // Only update agent state for default role
2393
- if (role === "default") {
2381
+ if (role === "temporary") {
2382
+ // Temporary: update agent state but don't persist to settings
2383
+ await this.session.setModelTemporary(model);
2384
+ this.statusLine.invalidate();
2385
+ this.updateEditorBorderColor();
2386
+ this.showStatus(`Temporary model: ${model.id}`);
2387
+ done();
2388
+ this.ui.requestRender();
2389
+ } else if (role === "default") {
2390
+ // Default: update agent state and persist
2394
2391
  await this.session.setModel(model, role);
2395
2392
  this.statusLine.invalidate();
2396
2393
  this.updateEditorBorderColor();
2394
+ this.showStatus(`Default model: ${model.id}`);
2395
+ // Don't call done() - selector stays open for role assignment
2396
+ } else {
2397
+ // Other roles (smol, slow): just update settings, not current model
2398
+ const roleLabel = role === "smol" ? "Smol" : role;
2399
+ this.showStatus(`${roleLabel} model: ${model.id}`);
2400
+ // Don't call done() - selector stays open
2397
2401
  }
2398
- // For other roles (small), just show status - settings already updated by selector
2399
- const roleLabel = role === "default" ? "Default" : role === "smol" ? "Smol" : role;
2400
- this.showStatus(`${roleLabel} model: ${model.id}`);
2401
2402
  } catch (error) {
2402
2403
  this.showError(error instanceof Error ? error.message : String(error));
2403
2404
  }
2404
- // Don't call done() - selector stays open
2405
2405
  },
2406
2406
  () => {
2407
2407
  done();
2408
2408
  this.ui.requestRender();
2409
2409
  },
2410
+ options,
2410
2411
  );
2411
2412
  return { component: selector, focus: selector };
2412
2413
  });
@@ -2994,8 +2995,10 @@ export class InteractiveMode {
2994
2995
  | \`Ctrl+D\` | Exit (when editor is empty) |
2995
2996
  | \`Ctrl+Z\` | Suspend to background |
2996
2997
  | \`Shift+Tab\` | Cycle thinking level |
2997
- | \`Ctrl+P\` | Cycle models |
2998
- | \`Ctrl+Y\` | Cycle role models (slow/default/smol) |
2998
+ | \`Ctrl+P\` | Cycle role models (slow/default/smol) |
2999
+ | \`Shift+Ctrl+P\` | Cycle role models (temporary) |
3000
+ | \`Ctrl+Y\` | Select model (temporary) |
3001
+ | \`Ctrl+L\` | Select model (set roles) |
2999
3002
  | \`Ctrl+O\` | Toggle tool output expansion |
3000
3003
  | \`Ctrl+T\` | Toggle thinking block visibility |
3001
3004
  | \`Ctrl+G\` | Edit message in external editor |
@@ -315,18 +315,18 @@ const UNICODE_SYMBOLS: SymbolMap = {
315
315
  "icon.rewind": "↩",
316
316
  // pick: ⚡ | alt: ✨ ✦
317
317
  "icon.auto": "⚡",
318
- // pick: SK | alt: 🧠 🎓
319
- "icon.extensionSkill": "SK",
320
- // pick: TL | alt: 🛠
321
- "icon.extensionTool": "TL",
318
+ // pick: | alt: ⚙ SK 🧠
319
+ "icon.extensionSkill": "",
320
+ // pick: | alt: ⛭ TL 🛠
321
+ "icon.extensionTool": "",
322
322
  // pick: / | alt: ⌘ ⌥
323
323
  "icon.extensionSlashCommand": "/",
324
- // pick: MCP | alt: 🔌 🧩
325
- "icon.extensionMcp": "MCP",
326
- // pick: RL | alt: ⚖ 📏
327
- "icon.extensionRule": "RL",
328
- // pick: HK | alt: 🪝
329
- "icon.extensionHook": "HK",
324
+ // pick: | alt: ⧫ MCP 🔌
325
+ "icon.extensionMcp": "",
326
+ // pick: § | alt: ⚖ RL 📏
327
+ "icon.extensionRule": "§",
328
+ // pick: | alt: ⚓ HK 🪝
329
+ "icon.extensionHook": "",
330
330
  // pick: PR | alt: 💬 ✎
331
331
  "icon.extensionPrompt": "PR",
332
332
  // pick: CF | alt: 📄 📎
@@ -356,10 +356,10 @@ const UNICODE_SYMBOLS: SymbolMap = {
356
356
  "format.bullet": "•",
357
357
  // pick: – | alt: — ― -
358
358
  "format.dash": "–",
359
- // pick: [ | alt: ⟦
360
- "format.bracketLeft": "[",
361
- // pick: ] | alt: ⟧
362
- "format.bracketRight": "]",
359
+ // pick: | alt: [
360
+ "format.bracketLeft": "",
361
+ // pick: | alt: ]
362
+ "format.bracketRight": "",
363
363
  // Markdown-specific
364
364
  // pick: │ | alt: ┃ ║
365
365
  "md.quoteBorder": "│",
@@ -574,15 +574,15 @@ const NERD_SYMBOLS: SymbolMap = {
574
574
  "icon.extensionInstruction": "\uf02d",
575
575
  // Thinking Levels - emoji labels
576
576
  // pick: 🤨 min | alt:  min  min
577
- "thinking.minimal": "🤨 min",
577
+ "thinking.minimal": "\u{F0E7} min",
578
578
  // pick: 🤔 low | alt:  low  low
579
- "thinking.low": "🤔 low",
579
+ "thinking.low": "\u{F10C} low",
580
580
  // pick: 🤓 med | alt:  med  med
581
- "thinking.medium": "🤓 med",
581
+ "thinking.medium": "\u{F192} med",
582
582
  // pick: 🤯 high | alt:  high  high
583
- "thinking.high": "🤯 high",
583
+ "thinking.high": "\u{F111} high",
584
584
  // pick: 🧠 xhi | alt:  xhi  xhi
585
- "thinking.xhigh": "🧠 xhi",
585
+ "thinking.xhigh": "\u{F06D} xhi",
586
586
  // Checkboxes
587
587
  // pick:  | alt:  
588
588
  "checkbox.checked": "\uf14a",
@@ -595,10 +595,10 @@ const NERD_SYMBOLS: SymbolMap = {
595
595
  "format.bullet": "\uf111",
596
596
  // pick: – | alt: — ― -
597
597
  "format.dash": "\u2013",
598
- // pick: [ | alt: ⟦
599
- "format.bracketLeft": "[",
600
- // pick: ] | alt: ⟧
601
- "format.bracketRight": "]",
598
+ // pick: | alt: [
599
+ "format.bracketLeft": "",
600
+ // pick: | alt: ]
601
+ "format.bracketRight": "",
602
602
  // Markdown-specific
603
603
  // pick: │ | alt: ┃ ║
604
604
  "md.quoteBorder": "\u2502",
@@ -608,41 +608,41 @@ const NERD_SYMBOLS: SymbolMap = {
608
608
  "md.bullet": "\uf111",
609
609
  // Language icons (nerd font devicons)
610
610
  "lang.default": "",
611
- "lang.typescript": "",
612
- "lang.javascript": "",
613
- "lang.python": "",
614
- "lang.rust": "",
615
- "lang.go": "",
616
- "lang.java": "",
617
- "lang.c": "",
618
- "lang.cpp": "",
619
- "lang.csharp": "",
620
- "lang.ruby": "",
621
- "lang.php": "",
622
- "lang.swift": "",
623
- "lang.kotlin": "",
624
- "lang.shell": "",
625
- "lang.html": "",
626
- "lang.css": "",
627
- "lang.json": "",
628
- "lang.yaml": "",
629
- "lang.markdown": "",
630
- "lang.sql": "",
631
- "lang.docker": "",
632
- "lang.lua": "",
633
- "lang.text": "",
634
- "lang.env": "",
635
- "lang.toml": "",
636
- "lang.xml": "󰗀",
637
- "lang.ini": "",
638
- "lang.conf": "",
639
- "lang.log": "󰌱",
640
- "lang.csv": "󰈛",
641
- "lang.tsv": "󰈛",
642
- "lang.image": "󰈟",
643
- "lang.pdf": "󰈦",
644
- "lang.archive": "",
645
- "lang.binary": "󰆚",
611
+ "lang.typescript": "\u{E628}",
612
+ "lang.javascript": "\u{E60C}",
613
+ "lang.python": "\u{E606}",
614
+ "lang.rust": "\u{E7A8}",
615
+ "lang.go": "\u{E627}",
616
+ "lang.java": "\u{E738}",
617
+ "lang.c": "\u{E61E}",
618
+ "lang.cpp": "\u{E61D}",
619
+ "lang.csharp": "\u{E7BC}",
620
+ "lang.ruby": "\u{E791}",
621
+ "lang.php": "\u{E608}",
622
+ "lang.swift": "\u{E755}",
623
+ "lang.kotlin": "\u{E634}",
624
+ "lang.shell": "\u{E795}",
625
+ "lang.html": "\u{E736}",
626
+ "lang.css": "\u{E749}",
627
+ "lang.json": "\u{E60B}",
628
+ "lang.yaml": "\u{E615}",
629
+ "lang.markdown": "\u{E609}",
630
+ "lang.sql": "\u{E706}",
631
+ "lang.docker": "\u{E7B0}",
632
+ "lang.lua": "\u{E620}",
633
+ "lang.text": "\u{E612}",
634
+ "lang.env": "\u{E615}",
635
+ "lang.toml": "\u{E615}",
636
+ "lang.xml": "\u{F05C0}",
637
+ "lang.ini": "\u{E615}",
638
+ "lang.conf": "\u{E615}",
639
+ "lang.log": "\u{F0331}",
640
+ "lang.csv": "\u{F021B}",
641
+ "lang.tsv": "\u{F021B}",
642
+ "lang.image": "\u{F021F}",
643
+ "lang.pdf": "\u{F0226}",
644
+ "lang.archive": "\u{F187}",
645
+ "lang.binary": "\u{F019A}",
646
646
  };
647
647
 
648
648
  const ASCII_SYMBOLS: SymbolMap = {
@@ -11,6 +11,7 @@
11
11
  * - Extension UI: Extension UI requests are emitted, client responds with extension_ui_response
12
12
  */
13
13
 
14
+ import { nanoid } from "nanoid";
14
15
  import type { AgentSession } from "../../core/agent-session";
15
16
  import type { ExtensionUIContext } from "../../core/extensions/index";
16
17
  import { theme } from "../interactive/theme/theme";
@@ -66,7 +67,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
66
67
  */
67
68
  const createExtensionUIContext = (): ExtensionUIContext => ({
68
69
  async select(title: string, options: string[]): Promise<string | undefined> {
69
- const id = globalThis.crypto.randomUUID();
70
+ const id = nanoid();
70
71
  return new Promise((resolve, reject) => {
71
72
  pendingExtensionRequests.set(id, {
72
73
  resolve: (response: RpcExtensionUIResponse) => {
@@ -85,7 +86,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
85
86
  },
86
87
 
87
88
  async confirm(title: string, message: string): Promise<boolean> {
88
- const id = globalThis.crypto.randomUUID();
89
+ const id = nanoid();
89
90
  return new Promise((resolve, reject) => {
90
91
  pendingExtensionRequests.set(id, {
91
92
  resolve: (response: RpcExtensionUIResponse) => {
@@ -104,7 +105,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
104
105
  },
105
106
 
106
107
  async input(title: string, placeholder?: string): Promise<string | undefined> {
107
- const id = globalThis.crypto.randomUUID();
108
+ const id = nanoid();
108
109
  return new Promise((resolve, reject) => {
109
110
  pendingExtensionRequests.set(id, {
110
111
  resolve: (response: RpcExtensionUIResponse) => {
@@ -126,7 +127,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
126
127
  // Fire and forget - no response needed
127
128
  output({
128
129
  type: "extension_ui_request",
129
- id: globalThis.crypto.randomUUID(),
130
+ id: nanoid(),
130
131
  method: "notify",
131
132
  message,
132
133
  notifyType: type,
@@ -137,7 +138,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
137
138
  // Fire and forget - no response needed
138
139
  output({
139
140
  type: "extension_ui_request",
140
- id: globalThis.crypto.randomUUID(),
141
+ id: nanoid(),
141
142
  method: "setStatus",
142
143
  statusKey: key,
143
144
  statusText: text,
@@ -149,7 +150,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
149
150
  if (content === undefined || Array.isArray(content)) {
150
151
  output({
151
152
  type: "extension_ui_request",
152
- id: globalThis.crypto.randomUUID(),
153
+ id: nanoid(),
153
154
  method: "setWidget",
154
155
  widgetKey: key,
155
156
  widgetLines: content as string[] | undefined,
@@ -162,7 +163,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
162
163
  // Fire and forget - host can implement terminal title control
163
164
  output({
164
165
  type: "extension_ui_request",
165
- id: globalThis.crypto.randomUUID(),
166
+ id: nanoid(),
166
167
  method: "setTitle",
167
168
  title,
168
169
  } as RpcExtensionUIRequest);
@@ -177,7 +178,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
177
178
  // Fire and forget - host can implement editor control
178
179
  output({
179
180
  type: "extension_ui_request",
180
- id: globalThis.crypto.randomUUID(),
181
+ id: nanoid(),
181
182
  method: "set_editor_text",
182
183
  text,
183
184
  } as RpcExtensionUIRequest);
@@ -190,7 +191,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
190
191
  },
191
192
 
192
193
  async editor(title: string, prefill?: string): Promise<string | undefined> {
193
- const id = globalThis.crypto.randomUUID();
194
+ const id = nanoid();
194
195
  return new Promise((resolve, reject) => {
195
196
  pendingExtensionRequests.set(id, {
196
197
  resolve: (response: RpcExtensionUIResponse) => {