@oh-my-pi/pi-coding-agent 3.30.0 → 3.32.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 (158) hide show
  1. package/CHANGELOG.md +85 -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 +367 -14
  7. package/src/core/custom-commands/bundled/wt/index.ts +1 -1
  8. package/src/core/sdk.ts +10 -2
  9. package/src/core/session-manager.ts +158 -246
  10. package/src/core/session-storage.ts +379 -0
  11. package/src/core/settings-manager.ts +155 -4
  12. package/src/core/slash-commands.ts +39 -13
  13. package/src/core/system-prompt.ts +62 -64
  14. package/src/core/tools/ask.ts +5 -4
  15. package/src/core/tools/bash-interceptor.ts +26 -61
  16. package/src/core/tools/bash.ts +13 -8
  17. package/src/core/tools/edit-diff.ts +11 -4
  18. package/src/core/tools/edit.ts +7 -13
  19. package/src/core/tools/find.ts +111 -50
  20. package/src/core/tools/gemini-image.ts +128 -147
  21. package/src/core/tools/grep.ts +397 -415
  22. package/src/core/tools/index.test.ts +5 -1
  23. package/src/core/tools/index.ts +8 -4
  24. package/src/core/tools/ls.ts +12 -10
  25. package/src/core/tools/lsp/client.ts +84 -19
  26. package/src/core/tools/lsp/config.ts +205 -656
  27. package/src/core/tools/lsp/defaults.json +465 -0
  28. package/src/core/tools/lsp/index.ts +72 -35
  29. package/src/core/tools/lsp/rust-analyzer.ts +49 -10
  30. package/src/core/tools/lsp/types.ts +1 -0
  31. package/src/core/tools/lsp/utils.ts +1 -1
  32. package/src/core/tools/read.ts +150 -74
  33. package/src/core/tools/render-utils.ts +70 -10
  34. package/src/core/tools/review.ts +38 -126
  35. package/src/core/tools/task/artifacts.ts +5 -4
  36. package/src/core/tools/task/commands.ts +4 -0
  37. package/src/core/tools/task/executor.ts +94 -83
  38. package/src/core/tools/task/index.ts +130 -92
  39. package/src/core/tools/task/parallel.ts +30 -3
  40. package/src/core/tools/task/render.ts +85 -39
  41. package/src/core/tools/task/types.ts +15 -6
  42. package/src/core/tools/task/worker.ts +124 -89
  43. package/src/core/tools/web-fetch.ts +112 -377
  44. package/src/core/tools/{web-fetch-handlers → web-scrapers}/artifacthub.ts +6 -1
  45. package/src/core/tools/{web-fetch-handlers → web-scrapers}/arxiv.ts +8 -4
  46. package/src/core/tools/{web-fetch-handlers → web-scrapers}/aur.ts +6 -2
  47. package/src/core/tools/{web-fetch-handlers → web-scrapers}/biorxiv.ts +6 -1
  48. package/src/core/tools/{web-fetch-handlers → web-scrapers}/bluesky.ts +10 -3
  49. package/src/core/tools/{web-fetch-handlers → web-scrapers}/brew.ts +6 -2
  50. package/src/core/tools/{web-fetch-handlers → web-scrapers}/cheatsh.ts +6 -1
  51. package/src/core/tools/{web-fetch-handlers → web-scrapers}/chocolatey.ts +6 -1
  52. package/src/core/tools/web-scrapers/choosealicense.ts +110 -0
  53. package/src/core/tools/web-scrapers/cisa-kev.ts +100 -0
  54. package/src/core/tools/web-scrapers/clojars.ts +180 -0
  55. package/src/core/tools/{web-fetch-handlers → web-scrapers}/coingecko.ts +6 -1
  56. package/src/core/tools/{web-fetch-handlers → web-scrapers}/crates-io.ts +7 -2
  57. package/src/core/tools/web-scrapers/crossref.ts +149 -0
  58. package/src/core/tools/{web-fetch-handlers → web-scrapers}/devto.ts +8 -4
  59. package/src/core/tools/{web-fetch-handlers → web-scrapers}/discogs.ts +6 -1
  60. package/src/core/tools/web-scrapers/discourse.ts +221 -0
  61. package/src/core/tools/{web-fetch-handlers → web-scrapers}/dockerhub.ts +7 -3
  62. package/src/core/tools/web-scrapers/fdroid.ts +158 -0
  63. package/src/core/tools/web-scrapers/firefox-addons.ts +214 -0
  64. package/src/core/tools/web-scrapers/flathub.ts +239 -0
  65. package/src/core/tools/{web-fetch-handlers → web-scrapers}/github-gist.ts +6 -2
  66. package/src/core/tools/{web-fetch-handlers → web-scrapers}/github.ts +63 -32
  67. package/src/core/tools/{web-fetch-handlers → web-scrapers}/gitlab.ts +31 -19
  68. package/src/core/tools/{web-fetch-handlers → web-scrapers}/go-pkg.ts +8 -4
  69. package/src/core/tools/{web-fetch-handlers → web-scrapers}/hackage.ts +6 -1
  70. package/src/core/tools/{web-fetch-handlers → web-scrapers}/hackernews.ts +18 -18
  71. package/src/core/tools/{web-fetch-handlers → web-scrapers}/hex.ts +3 -3
  72. package/src/core/tools/{web-fetch-handlers → web-scrapers}/huggingface.ts +10 -10
  73. package/src/core/tools/{web-fetch-handlers → web-scrapers}/iacr.ts +8 -4
  74. package/src/core/tools/web-scrapers/index.ts +250 -0
  75. package/src/core/tools/web-scrapers/jetbrains-marketplace.ts +169 -0
  76. package/src/core/tools/web-scrapers/lemmy.ts +220 -0
  77. package/src/core/tools/{web-fetch-handlers → web-scrapers}/lobsters.ts +3 -3
  78. package/src/core/tools/{web-fetch-handlers → web-scrapers}/mastodon.ts +11 -3
  79. package/src/core/tools/{web-fetch-handlers → web-scrapers}/maven.ts +6 -1
  80. package/src/core/tools/{web-fetch-handlers → web-scrapers}/mdn.ts +2 -2
  81. package/src/core/tools/{web-fetch-handlers → web-scrapers}/metacpan.ts +13 -7
  82. package/src/core/tools/web-scrapers/musicbrainz.ts +273 -0
  83. package/src/core/tools/{web-fetch-handlers → web-scrapers}/npm.ts +12 -5
  84. package/src/core/tools/{web-fetch-handlers → web-scrapers}/nuget.ts +9 -5
  85. package/src/core/tools/{web-fetch-handlers → web-scrapers}/nvd.ts +6 -1
  86. package/src/core/tools/web-scrapers/ollama.ts +267 -0
  87. package/src/core/tools/web-scrapers/open-vsx.ts +119 -0
  88. package/src/core/tools/{web-fetch-handlers → web-scrapers}/opencorporates.ts +2 -0
  89. package/src/core/tools/{web-fetch-handlers → web-scrapers}/openlibrary.ts +18 -12
  90. package/src/core/tools/web-scrapers/orcid.ts +299 -0
  91. package/src/core/tools/{web-fetch-handlers → web-scrapers}/osv.ts +6 -1
  92. package/src/core/tools/{web-fetch-handlers → web-scrapers}/packagist.ts +6 -2
  93. package/src/core/tools/{web-fetch-handlers → web-scrapers}/pub-dev.ts +3 -3
  94. package/src/core/tools/{web-fetch-handlers → web-scrapers}/pubmed.ts +8 -4
  95. package/src/core/tools/{web-fetch-handlers → web-scrapers}/pypi.ts +7 -3
  96. package/src/core/tools/web-scrapers/rawg.ts +124 -0
  97. package/src/core/tools/{web-fetch-handlers → web-scrapers}/readthedocs.ts +7 -3
  98. package/src/core/tools/{web-fetch-handlers → web-scrapers}/reddit.ts +6 -2
  99. package/src/core/tools/{web-fetch-handlers → web-scrapers}/repology.ts +6 -1
  100. package/src/core/tools/{web-fetch-handlers → web-scrapers}/rfc.ts +7 -3
  101. package/src/core/tools/{web-fetch-handlers → web-scrapers}/rubygems.ts +6 -1
  102. package/src/core/tools/web-scrapers/searchcode.ts +217 -0
  103. package/src/core/tools/{web-fetch-handlers → web-scrapers}/sec-edgar.ts +6 -1
  104. package/src/core/tools/{web-fetch-handlers → web-scrapers}/semantic-scholar.ts +2 -2
  105. package/src/core/tools/web-scrapers/snapcraft.ts +200 -0
  106. package/src/core/tools/web-scrapers/sourcegraph.ts +373 -0
  107. package/src/core/tools/web-scrapers/spdx.ts +121 -0
  108. package/src/core/tools/{web-fetch-handlers → web-scrapers}/spotify.ts +3 -3
  109. package/src/core/tools/{web-fetch-handlers → web-scrapers}/stackoverflow.ts +3 -2
  110. package/src/core/tools/{web-fetch-handlers → web-scrapers}/terraform.ts +11 -3
  111. package/src/core/tools/{web-fetch-handlers → web-scrapers}/tldr.ts +6 -2
  112. package/src/core/tools/{web-fetch-handlers → web-scrapers}/twitter.ts +15 -3
  113. package/src/core/tools/{web-fetch-handlers → web-scrapers}/types.ts +98 -27
  114. package/src/core/tools/web-scrapers/utils.ts +162 -0
  115. package/src/core/tools/{web-fetch-handlers → web-scrapers}/vimeo.ts +3 -3
  116. package/src/core/tools/web-scrapers/vscode-marketplace.ts +195 -0
  117. package/src/core/tools/web-scrapers/w3c.ts +163 -0
  118. package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikidata.ts +13 -5
  119. package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikipedia.ts +7 -3
  120. package/src/core/tools/{web-fetch-handlers → web-scrapers}/youtube.ts +72 -20
  121. package/src/core/tools/write.ts +21 -18
  122. package/src/core/voice.ts +3 -2
  123. package/src/lib/worktree/collapse.ts +2 -1
  124. package/src/lib/worktree/git.ts +2 -18
  125. package/src/main.ts +59 -3
  126. package/src/modes/interactive/components/extensions/extension-dashboard.ts +33 -19
  127. package/src/modes/interactive/components/extensions/extension-list.ts +15 -8
  128. package/src/modes/interactive/components/hook-editor.ts +2 -1
  129. package/src/modes/interactive/components/model-selector.ts +19 -4
  130. package/src/modes/interactive/interactive-mode.ts +41 -63
  131. package/src/modes/interactive/theme/theme.ts +58 -58
  132. package/src/modes/rpc/rpc-mode.ts +10 -9
  133. package/src/prompts/review-request.md +27 -0
  134. package/src/prompts/reviewer.md +64 -68
  135. package/src/prompts/tools/output.md +22 -3
  136. package/src/prompts/tools/task.md +32 -33
  137. package/src/utils/clipboard.ts +2 -1
  138. package/examples/extensions/subagent/agents/reviewer.md +0 -35
  139. package/src/core/tools/web-fetch-handlers/index.ts +0 -69
  140. package/src/core/tools/web-fetch-handlers/utils.ts +0 -91
  141. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/academic.test.ts +0 -0
  142. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/business.test.ts +0 -0
  143. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/dev-platforms.test.ts +0 -0
  144. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/documentation.test.ts +0 -0
  145. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/finance-media.test.ts +0 -0
  146. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/git-hosting.test.ts +0 -0
  147. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/media.test.ts +0 -0
  148. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-managers-2.test.ts +0 -0
  149. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-managers.test.ts +0 -0
  150. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-registries.test.ts +0 -0
  151. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/research.test.ts +0 -0
  152. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/security.test.ts +0 -0
  153. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/social-extended.test.ts +0 -0
  154. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/social.test.ts +0 -0
  155. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/stackexchange.test.ts +0 -0
  156. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/standards.test.ts +0 -0
  157. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikipedia.test.ts +0 -0
  158. /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
@@ -1900,31 +1906,6 @@ export class InteractiveMode {
1900
1906
  this.voiceSupervisor.notifyProgress(text);
1901
1907
  }
1902
1908
 
1903
- private async toggleVoiceListening(): Promise<void> {
1904
- if (!this.settingsManager.getVoiceEnabled()) {
1905
- this.settingsManager.setVoiceEnabled(true);
1906
- this.showStatus("Voice mode enabled.");
1907
- }
1908
-
1909
- if (this.voiceAutoModeEnabled) {
1910
- this.voiceAutoModeEnabled = false;
1911
- this.stopVoiceProgressTimer();
1912
- await this.voiceSupervisor.stop();
1913
- this.setVoiceStatus(undefined);
1914
- this.showStatus("Voice mode disabled.");
1915
- return;
1916
- }
1917
-
1918
- this.voiceAutoModeEnabled = true;
1919
- try {
1920
- await this.voiceSupervisor.start();
1921
- } catch (error) {
1922
- this.voiceAutoModeEnabled = false;
1923
- this.setVoiceStatus(undefined);
1924
- this.showError(error instanceof Error ? error.message : String(error));
1925
- }
1926
- }
1927
-
1928
1909
  private async submitVoiceText(text: string): Promise<void> {
1929
1910
  const cleaned = text.trim();
1930
1911
  if (!cleaned) {
@@ -1994,27 +1975,9 @@ export class InteractiveMode {
1994
1975
  }
1995
1976
  }
1996
1977
 
1997
- private async cycleModel(direction: "forward" | "backward"): Promise<void> {
1978
+ private async cycleRoleModel(options?: { temporary?: boolean }): Promise<void> {
1998
1979
  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> {
2016
- try {
2017
- const result = await this.session.cycleRoleModels(["slow", "default", "smol"]);
1980
+ const result = await this.session.cycleRoleModels(["slow", "default", "smol"], options);
2018
1981
  if (!result) {
2019
1982
  this.showStatus("Only one role model available");
2020
1983
  return;
@@ -2025,7 +1988,8 @@ export class InteractiveMode {
2025
1988
  const roleLabel = result.role === "default" ? "default" : result.role;
2026
1989
  const thinkingStr =
2027
1990
  result.model.reasoning && result.thinkingLevel !== "off" ? ` (thinking: ${result.thinkingLevel})` : "";
2028
- this.showStatus(`Switched to ${roleLabel}: ${result.model.name || result.model.id}${thinkingStr}`);
1991
+ const tempLabel = options?.temporary ? " (temporary)" : "";
1992
+ this.showStatus(`Switched to ${roleLabel}: ${result.model.name || result.model.id}${thinkingStr}${tempLabel}`);
2029
1993
  } catch (error) {
2030
1994
  this.showError(error instanceof Error ? error.message : String(error));
2031
1995
  }
@@ -2068,7 +2032,7 @@ export class InteractiveMode {
2068
2032
  }
2069
2033
 
2070
2034
  const currentText = this.editor.getText();
2071
- const tmpFile = path.join(os.tmpdir(), `omp-editor-${Date.now()}.omp.md`);
2035
+ const tmpFile = path.join(os.tmpdir(), `omp-editor-${nanoid()}.omp.md`);
2072
2036
 
2073
2037
  try {
2074
2038
  // Write current content to temp file
@@ -2255,7 +2219,7 @@ export class InteractiveMode {
2255
2219
  */
2256
2220
  private showExtensionsDashboard(): void {
2257
2221
  this.showSelector((done) => {
2258
- const dashboard = new ExtensionDashboard(process.cwd(), this.settingsManager);
2222
+ const dashboard = new ExtensionDashboard(process.cwd(), this.settingsManager, this.ui.terminal.rows);
2259
2223
  dashboard.onClose = () => {
2260
2224
  done();
2261
2225
  this.ui.requestRender();
@@ -2379,7 +2343,7 @@ export class InteractiveMode {
2379
2343
  }
2380
2344
  }
2381
2345
 
2382
- private showModelSelector(): void {
2346
+ private showModelSelector(options?: { temporaryOnly?: boolean }): void {
2383
2347
  this.showSelector((done) => {
2384
2348
  const selector = new ModelSelectorComponent(
2385
2349
  this.ui,
@@ -2389,24 +2353,36 @@ export class InteractiveMode {
2389
2353
  this.session.scopedModels,
2390
2354
  async (model, role) => {
2391
2355
  try {
2392
- // Only update agent state for default role
2393
- if (role === "default") {
2356
+ if (role === "temporary") {
2357
+ // Temporary: update agent state but don't persist to settings
2358
+ await this.session.setModelTemporary(model);
2359
+ this.statusLine.invalidate();
2360
+ this.updateEditorBorderColor();
2361
+ this.showStatus(`Temporary model: ${model.id}`);
2362
+ done();
2363
+ this.ui.requestRender();
2364
+ } else if (role === "default") {
2365
+ // Default: update agent state and persist
2394
2366
  await this.session.setModel(model, role);
2395
2367
  this.statusLine.invalidate();
2396
2368
  this.updateEditorBorderColor();
2369
+ this.showStatus(`Default model: ${model.id}`);
2370
+ // Don't call done() - selector stays open for role assignment
2371
+ } else {
2372
+ // Other roles (smol, slow): just update settings, not current model
2373
+ const roleLabel = role === "smol" ? "Smol" : role;
2374
+ this.showStatus(`${roleLabel} model: ${model.id}`);
2375
+ // Don't call done() - selector stays open
2397
2376
  }
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
2377
  } catch (error) {
2402
2378
  this.showError(error instanceof Error ? error.message : String(error));
2403
2379
  }
2404
- // Don't call done() - selector stays open
2405
2380
  },
2406
2381
  () => {
2407
2382
  done();
2408
2383
  this.ui.requestRender();
2409
2384
  },
2385
+ options,
2410
2386
  );
2411
2387
  return { component: selector, focus: selector };
2412
2388
  });
@@ -2994,8 +2970,10 @@ export class InteractiveMode {
2994
2970
  | \`Ctrl+D\` | Exit (when editor is empty) |
2995
2971
  | \`Ctrl+Z\` | Suspend to background |
2996
2972
  | \`Shift+Tab\` | Cycle thinking level |
2997
- | \`Ctrl+P\` | Cycle models |
2998
- | \`Ctrl+Y\` | Cycle role models (slow/default/smol) |
2973
+ | \`Ctrl+P\` | Cycle role models (slow/default/smol) |
2974
+ | \`Shift+Ctrl+P\` | Cycle role models (temporary) |
2975
+ | \`Ctrl+Y\` | Select model (temporary) |
2976
+ | \`Ctrl+L\` | Select model (set roles) |
2999
2977
  | \`Ctrl+O\` | Toggle tool output expansion |
3000
2978
  | \`Ctrl+T\` | Toggle thinking block visibility |
3001
2979
  | \`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 = {