@oh-my-pi/pi-coding-agent 1.341.0 → 2.0.1337

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 (151) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +1 -1
  3. package/examples/custom-tools/subagent/index.ts +1 -1
  4. package/package.json +5 -3
  5. package/src/cli/args.ts +5 -6
  6. package/src/cli/file-processor.ts +3 -3
  7. package/src/cli/list-models.ts +2 -2
  8. package/src/cli/plugin-cli.ts +1 -1
  9. package/src/cli/session-picker.ts +2 -2
  10. package/src/cli.ts +1 -1
  11. package/src/config.ts +3 -3
  12. package/src/core/agent-session.ts +157 -15
  13. package/src/core/bash-executor.ts +50 -10
  14. package/src/core/compaction/branch-summarization.ts +5 -5
  15. package/src/core/compaction/compaction.ts +3 -3
  16. package/src/core/compaction/index.ts +3 -3
  17. package/src/core/custom-commands/bundled/review/index.ts +156 -0
  18. package/src/core/custom-commands/index.ts +15 -0
  19. package/src/core/custom-commands/loader.ts +232 -0
  20. package/src/core/custom-commands/types.ts +112 -0
  21. package/src/core/custom-tools/index.ts +3 -3
  22. package/src/core/custom-tools/loader.ts +10 -8
  23. package/src/core/custom-tools/types.ts +11 -6
  24. package/src/core/custom-tools/wrapper.ts +2 -1
  25. package/src/core/exec.ts +22 -12
  26. package/src/core/export-html/index.ts +5 -5
  27. package/src/core/file-mentions.ts +54 -0
  28. package/src/core/hooks/index.ts +5 -5
  29. package/src/core/hooks/loader.ts +21 -16
  30. package/src/core/hooks/runner.ts +6 -6
  31. package/src/core/hooks/tool-wrapper.ts +2 -2
  32. package/src/core/hooks/types.ts +12 -15
  33. package/src/core/index.ts +6 -6
  34. package/src/core/logger.ts +112 -0
  35. package/src/core/mcp/client.ts +3 -3
  36. package/src/core/mcp/config.ts +1 -1
  37. package/src/core/mcp/index.ts +12 -12
  38. package/src/core/mcp/loader.ts +2 -2
  39. package/src/core/mcp/manager.ts +6 -6
  40. package/src/core/mcp/tool-bridge.ts +3 -3
  41. package/src/core/mcp/transports/http.ts +1 -1
  42. package/src/core/mcp/transports/index.ts +2 -2
  43. package/src/core/mcp/transports/stdio.ts +1 -1
  44. package/src/core/messages.ts +22 -0
  45. package/src/core/model-registry.ts +2 -2
  46. package/src/core/model-resolver.ts +2 -2
  47. package/src/core/plugins/doctor.ts +1 -1
  48. package/src/core/plugins/index.ts +6 -6
  49. package/src/core/plugins/installer.ts +4 -4
  50. package/src/core/plugins/loader.ts +4 -9
  51. package/src/core/plugins/manager.ts +5 -5
  52. package/src/core/plugins/paths.ts +3 -3
  53. package/src/core/sdk.ts +77 -35
  54. package/src/core/session-manager.ts +6 -6
  55. package/src/core/settings-manager.ts +16 -3
  56. package/src/core/skills.ts +5 -5
  57. package/src/core/slash-commands.ts +60 -45
  58. package/src/core/system-prompt.ts +6 -6
  59. package/src/core/title-generator.ts +2 -2
  60. package/src/core/tools/bash.ts +32 -155
  61. package/src/core/tools/context.ts +2 -2
  62. package/src/core/tools/edit-diff.ts +3 -3
  63. package/src/core/tools/edit.ts +18 -5
  64. package/src/core/tools/exa/company.ts +3 -3
  65. package/src/core/tools/exa/index.ts +16 -17
  66. package/src/core/tools/exa/linkedin.ts +3 -3
  67. package/src/core/tools/exa/mcp-client.ts +9 -9
  68. package/src/core/tools/exa/render.ts +5 -5
  69. package/src/core/tools/exa/researcher.ts +3 -3
  70. package/src/core/tools/exa/search.ts +6 -5
  71. package/src/core/tools/exa/types.ts +5 -6
  72. package/src/core/tools/exa/websets.ts +3 -3
  73. package/src/core/tools/find.ts +3 -3
  74. package/src/core/tools/grep.ts +3 -3
  75. package/src/core/tools/index.ts +48 -34
  76. package/src/core/tools/ls.ts +4 -4
  77. package/src/core/tools/lsp/client.ts +161 -90
  78. package/src/core/tools/lsp/config.ts +1 -1
  79. package/src/core/tools/lsp/edits.ts +2 -2
  80. package/src/core/tools/lsp/index.ts +15 -13
  81. package/src/core/tools/lsp/render.ts +2 -2
  82. package/src/core/tools/lsp/rust-analyzer.ts +3 -3
  83. package/src/core/tools/lsp/utils.ts +1 -1
  84. package/src/core/tools/notebook.ts +1 -1
  85. package/src/core/tools/output.ts +175 -0
  86. package/src/core/tools/read.ts +7 -7
  87. package/src/core/tools/renderers.ts +92 -13
  88. package/src/core/tools/review.ts +268 -0
  89. package/src/core/tools/task/agents.ts +1 -1
  90. package/src/core/tools/task/bundled-agents/reviewer.md +52 -37
  91. package/src/core/tools/task/discovery.ts +2 -2
  92. package/src/core/tools/task/executor.ts +145 -28
  93. package/src/core/tools/task/index.ts +78 -30
  94. package/src/core/tools/task/model-resolver.ts +30 -20
  95. package/src/core/tools/task/parallel.ts +1 -1
  96. package/src/core/tools/task/render.ts +219 -30
  97. package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
  98. package/src/core/tools/task/types.ts +36 -2
  99. package/src/core/tools/web-fetch.ts +5 -3
  100. package/src/core/tools/web-search/auth.ts +1 -1
  101. package/src/core/tools/web-search/index.ts +17 -15
  102. package/src/core/tools/web-search/providers/anthropic.ts +2 -2
  103. package/src/core/tools/web-search/providers/exa.ts +3 -5
  104. package/src/core/tools/web-search/providers/perplexity.ts +1 -1
  105. package/src/core/tools/web-search/render.ts +3 -3
  106. package/src/core/tools/write.ts +4 -4
  107. package/src/index.ts +29 -18
  108. package/src/main.ts +37 -32
  109. package/src/migrations.ts +3 -3
  110. package/src/modes/index.ts +5 -5
  111. package/src/modes/interactive/components/armin.ts +1 -1
  112. package/src/modes/interactive/components/assistant-message.ts +1 -1
  113. package/src/modes/interactive/components/bash-execution.ts +4 -4
  114. package/src/modes/interactive/components/bordered-loader.ts +2 -2
  115. package/src/modes/interactive/components/branch-summary-message.ts +2 -2
  116. package/src/modes/interactive/components/compaction-summary-message.ts +2 -2
  117. package/src/modes/interactive/components/diff.ts +1 -1
  118. package/src/modes/interactive/components/dynamic-border.ts +1 -1
  119. package/src/modes/interactive/components/footer.ts +5 -5
  120. package/src/modes/interactive/components/hook-editor.ts +2 -2
  121. package/src/modes/interactive/components/hook-input.ts +2 -2
  122. package/src/modes/interactive/components/hook-message.ts +3 -3
  123. package/src/modes/interactive/components/hook-selector.ts +2 -2
  124. package/src/modes/interactive/components/model-selector.ts +281 -59
  125. package/src/modes/interactive/components/oauth-selector.ts +3 -3
  126. package/src/modes/interactive/components/plugin-settings.ts +4 -4
  127. package/src/modes/interactive/components/queue-mode-selector.ts +2 -2
  128. package/src/modes/interactive/components/session-selector.ts +4 -4
  129. package/src/modes/interactive/components/settings-defs.ts +1 -1
  130. package/src/modes/interactive/components/settings-selector.ts +5 -5
  131. package/src/modes/interactive/components/show-images-selector.ts +2 -2
  132. package/src/modes/interactive/components/theme-selector.ts +2 -2
  133. package/src/modes/interactive/components/thinking-selector.ts +2 -2
  134. package/src/modes/interactive/components/tool-execution.ts +26 -8
  135. package/src/modes/interactive/components/tree-selector.ts +3 -3
  136. package/src/modes/interactive/components/user-message-selector.ts +2 -2
  137. package/src/modes/interactive/components/user-message.ts +1 -1
  138. package/src/modes/interactive/components/welcome.ts +2 -2
  139. package/src/modes/interactive/interactive-mode.ts +85 -41
  140. package/src/modes/interactive/theme/theme.ts +8 -7
  141. package/src/modes/print-mode.ts +4 -3
  142. package/src/modes/rpc/rpc-client.ts +4 -4
  143. package/src/modes/rpc/rpc-mode.ts +21 -11
  144. package/src/modes/rpc/rpc-types.ts +3 -3
  145. package/src/utils/changelog.ts +2 -2
  146. package/src/utils/clipboard.ts +1 -1
  147. package/src/utils/shell-snapshot.ts +218 -0
  148. package/src/utils/shell.ts +93 -13
  149. package/src/utils/tools-manager.ts +1 -1
  150. package/examples/custom-tools/subagent/agents/reviewer.md +0 -35
  151. package/src/core/tools/exa/logger.ts +0 -56
@@ -1,11 +1,25 @@
1
1
  import { type Model, modelsAreEqual } from "@oh-my-pi/pi-ai";
2
- import { Container, Input, isArrowDown, isArrowUp, isEnter, isEscape, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
3
- import type { ModelRegistry } from "../../../core/model-registry.js";
4
- import { parseModelString } from "../../../core/model-resolver.js";
5
- import type { SettingsManager } from "../../../core/settings-manager.js";
6
- import { fuzzyFilter } from "../../../utils/fuzzy.js";
7
- import { theme } from "../theme/theme.js";
8
- import { DynamicBorder } from "./dynamic-border.js";
2
+ import {
3
+ Container,
4
+ Input,
5
+ isArrowDown,
6
+ isArrowLeft,
7
+ isArrowRight,
8
+ isArrowUp,
9
+ isEnter,
10
+ isEscape,
11
+ isShiftTab,
12
+ isTab,
13
+ Spacer,
14
+ Text,
15
+ type TUI,
16
+ } from "@oh-my-pi/pi-tui";
17
+ import type { ModelRegistry } from "../../../core/model-registry";
18
+ import { parseModelString } from "../../../core/model-resolver";
19
+ import type { SettingsManager } from "../../../core/settings-manager";
20
+ import { fuzzyFilter } from "../../../utils/fuzzy";
21
+ import { theme } from "../theme/theme";
22
+ import { DynamicBorder } from "./dynamic-border";
9
23
 
10
24
  interface ModelItem {
11
25
  provider: string;
@@ -18,16 +32,33 @@ interface ScopedModelItem {
18
32
  thinkingLevel: string;
19
33
  }
20
34
 
35
+ type ModelRole = "default" | "smol" | "slow";
36
+
37
+ interface MenuAction {
38
+ label: string;
39
+ role: ModelRole;
40
+ }
41
+
42
+ const MENU_ACTIONS: MenuAction[] = [
43
+ { label: "Set as Default", role: "default" },
44
+ { label: "Set as Smol (Fast)", role: "smol" },
45
+ { label: "Set as Slow (Thinking)", role: "slow" },
46
+ ];
47
+
48
+ const ALL_TAB = "ALL";
49
+
21
50
  /**
22
- * Component that renders a model selector with search.
23
- * - Enter: Set selected model as default
24
- * - S: Set selected model as smol
25
- * - L: Set selected model as slow
26
- * - Escape: Close selector
51
+ * Component that renders a model selector with provider tabs and context menu.
52
+ * - Tab/Arrow Left/Right: Switch between provider tabs
53
+ * - Arrow Up/Down: Navigate model list
54
+ * - Enter: Open context menu to select action
55
+ * - Escape: Close menu or selector
27
56
  */
28
57
  export class ModelSelectorComponent extends Container {
29
58
  private searchInput: Input;
59
+ private headerContainer: Container;
30
60
  private listContainer: Container;
61
+ private menuContainer: Container;
31
62
  private allModels: ModelItem[] = [];
32
63
  private filteredModels: ModelItem[] = [];
33
64
  private selectedIndex: number = 0;
@@ -43,6 +74,14 @@ export class ModelSelectorComponent extends Container {
43
74
  private tui: TUI;
44
75
  private scopedModels: ReadonlyArray<ScopedModelItem>;
45
76
 
77
+ // Tab state
78
+ private providers: string[] = [ALL_TAB];
79
+ private activeTabIndex: number = 0;
80
+
81
+ // Context menu state
82
+ private isMenuOpen: boolean = false;
83
+ private menuSelectedIndex: number = 0;
84
+
46
85
  constructor(
47
86
  tui: TUI,
48
87
  currentModel: Model<any> | undefined,
@@ -69,21 +108,26 @@ export class ModelSelectorComponent extends Container {
69
108
  this.addChild(new DynamicBorder());
70
109
  this.addChild(new Spacer(1));
71
110
 
72
- // Add hint about model filtering and key bindings
111
+ // Add hint about model filtering
73
112
  const hintText =
74
113
  scopedModels.length > 0
75
114
  ? "Showing models from --models scope"
76
115
  : "Only showing models with configured API keys (see README for details)";
77
116
  this.addChild(new Text(theme.fg("warning", hintText), 0, 0));
78
- this.addChild(new Text(theme.fg("muted", "Enter: default S: smol L: slow Esc: close"), 0, 0));
117
+ this.addChild(new Spacer(1));
118
+
119
+ // Create header container for tab bar
120
+ this.headerContainer = new Container();
121
+ this.addChild(this.headerContainer);
122
+
79
123
  this.addChild(new Spacer(1));
80
124
 
81
125
  // Create search input
82
126
  this.searchInput = new Input();
83
127
  this.searchInput.onSubmit = () => {
84
- // Enter on search input sets as default
128
+ // Enter on search input opens menu if we have a selection
85
129
  if (this.filteredModels[this.selectedIndex]) {
86
- this.handleSelect(this.filteredModels[this.selectedIndex].model, "default");
130
+ this.openMenu();
87
131
  }
88
132
  };
89
133
  this.addChild(this.searchInput);
@@ -94,6 +138,10 @@ export class ModelSelectorComponent extends Container {
94
138
  this.listContainer = new Container();
95
139
  this.addChild(this.listContainer);
96
140
 
141
+ // Create menu container (hidden by default)
142
+ this.menuContainer = new Container();
143
+ this.addChild(this.menuContainer);
144
+
97
145
  this.addChild(new Spacer(1));
98
146
 
99
147
  // Add bottom border
@@ -101,6 +149,8 @@ export class ModelSelectorComponent extends Container {
101
149
 
102
150
  // Load models and do initial render
103
151
  this.loadModels().then(() => {
152
+ this.buildProviderTabs();
153
+ this.updateTabBar();
104
154
  this.updateList();
105
155
  // Request re-render after models are loaded
106
156
  this.tui.requestRender();
@@ -175,13 +225,15 @@ export class ModelSelectorComponent extends Container {
175
225
  }
176
226
  }
177
227
 
178
- // Sort: current model first, then by provider
228
+ // Sort: current model first, then by provider, then by id
179
229
  models.sort((a, b) => {
180
230
  const aIsCurrent = modelsAreEqual(this.currentModel, a.model);
181
231
  const bIsCurrent = modelsAreEqual(this.currentModel, b.model);
182
232
  if (aIsCurrent && !bIsCurrent) return -1;
183
233
  if (!aIsCurrent && bIsCurrent) return 1;
184
- return a.provider.localeCompare(b.provider);
234
+ const providerCmp = a.provider.localeCompare(b.provider);
235
+ if (providerCmp !== 0) return providerCmp;
236
+ return a.id.localeCompare(b.id);
185
237
  });
186
238
 
187
239
  this.allModels = models;
@@ -189,12 +241,81 @@ export class ModelSelectorComponent extends Container {
189
241
  this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, models.length - 1));
190
242
  }
191
243
 
244
+ private buildProviderTabs(): void {
245
+ // Extract unique providers from models
246
+ const providerSet = new Set<string>();
247
+ for (const item of this.allModels) {
248
+ providerSet.add(item.provider.toUpperCase());
249
+ }
250
+ // Sort providers alphabetically
251
+ const sortedProviders = Array.from(providerSet).sort();
252
+ this.providers = [ALL_TAB, ...sortedProviders];
253
+ }
254
+
255
+ private updateTabBar(): void {
256
+ this.headerContainer.clear();
257
+
258
+ // Build tab bar line
259
+ const parts: string[] = [];
260
+ parts.push(theme.fg("muted", "Provider:"));
261
+ parts.push(" ");
262
+
263
+ for (let i = 0; i < this.providers.length; i++) {
264
+ const provider = this.providers[i]!;
265
+ const isActive = i === this.activeTabIndex;
266
+
267
+ if (isActive) {
268
+ parts.push(theme.fg("accent", `[ ${provider} ]`));
269
+ } else {
270
+ parts.push(theme.fg("muted", ` ${provider} `));
271
+ }
272
+
273
+ if (i < this.providers.length - 1) {
274
+ parts.push(" ");
275
+ }
276
+ }
277
+
278
+ parts.push(" ");
279
+ parts.push(theme.fg("dim", "(←/→ or Tab to switch)"));
280
+
281
+ this.headerContainer.addChild(new Text(parts.join(""), 0, 0));
282
+ }
283
+
284
+ private getActiveProvider(): string {
285
+ return this.providers[this.activeTabIndex] ?? ALL_TAB;
286
+ }
287
+
192
288
  private filterModels(query: string): void {
193
- this.filteredModels = fuzzyFilter(this.allModels, query, ({ id, provider }) => `${id} ${provider}`);
289
+ const activeProvider = this.getActiveProvider();
290
+
291
+ // Start with all models or filter by provider
292
+ let baseModels = this.allModels;
293
+ if (activeProvider !== ALL_TAB) {
294
+ baseModels = this.allModels.filter((m) => m.provider.toUpperCase() === activeProvider);
295
+ }
296
+
297
+ // Apply fuzzy filter if query is present
298
+ if (query.trim()) {
299
+ // If user is searching, auto-switch to ALL tab to show global results
300
+ if (activeProvider !== ALL_TAB) {
301
+ this.activeTabIndex = 0;
302
+ this.updateTabBar();
303
+ baseModels = this.allModels;
304
+ }
305
+ this.filteredModels = fuzzyFilter(baseModels, query, ({ id, provider }) => `${id} ${provider}`);
306
+ } else {
307
+ this.filteredModels = baseModels;
308
+ }
309
+
194
310
  this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredModels.length - 1));
195
311
  this.updateList();
196
312
  }
197
313
 
314
+ private applyTabFilter(): void {
315
+ const query = this.searchInput.getValue();
316
+ this.filterModels(query);
317
+ }
318
+
198
319
  private updateList(): void {
199
320
  this.listContainer.clear();
200
321
 
@@ -205,6 +326,9 @@ export class ModelSelectorComponent extends Container {
205
326
  );
206
327
  const endIndex = Math.min(startIndex + maxVisible, this.filteredModels.length);
207
328
 
329
+ const activeProvider = this.getActiveProvider();
330
+ const showProvider = activeProvider === ALL_TAB;
331
+
208
332
  // Show visible slice of filtered models
209
333
  for (let i = startIndex; i < endIndex; i++) {
210
334
  const item = this.filteredModels[i];
@@ -215,22 +339,31 @@ export class ModelSelectorComponent extends Container {
215
339
  const isSmol = modelsAreEqual(this.smolModel, item.model);
216
340
  const isSlow = modelsAreEqual(this.slowModel, item.model);
217
341
 
218
- // Build role markers: for default, ⚡ for smol, 🧠 for slow
219
- let markers = "";
220
- if (isDefault) markers += theme.fg("success", " ");
221
- if (isSmol) markers += theme.fg("warning", " ");
222
- if (isSlow) markers += theme.fg("accent", " 🧠");
342
+ // Build role badges (right-aligned style)
343
+ const badges: string[] = [];
344
+ if (isDefault) badges.push(theme.fg("success", "[ DEFAULT ]"));
345
+ if (isSmol) badges.push(theme.fg("warning", "[ SMOL ]"));
346
+ if (isSlow) badges.push(theme.fg("accent", "[ SLOW ]"));
347
+ const badgeText = badges.length > 0 ? ` ${badges.join(" ")}` : "";
223
348
 
224
349
  let line = "";
225
350
  if (isSelected) {
226
351
  const prefix = theme.fg("accent", "→ ");
227
- const modelText = `${item.id}`;
228
- const providerBadge = theme.fg("muted", `[${item.provider}]`);
229
- line = `${prefix + theme.fg("accent", modelText)} ${providerBadge}${markers}`;
352
+ const modelText = item.id;
353
+ if (showProvider) {
354
+ const providerBadge = theme.fg("muted", `[${item.provider}]`);
355
+ line = `${prefix}${theme.fg("accent", modelText)} ${providerBadge}${badgeText}`;
356
+ } else {
357
+ line = `${prefix}${theme.fg("accent", modelText)}${badgeText}`;
358
+ }
230
359
  } else {
231
- const modelText = ` ${item.id}`;
232
- const providerBadge = theme.fg("muted", `[${item.provider}]`);
233
- line = `${modelText} ${providerBadge}${markers}`;
360
+ const prefix = " ";
361
+ if (showProvider) {
362
+ const providerBadge = theme.fg("muted", `[${item.provider}]`);
363
+ line = `${prefix}${item.id} ${providerBadge}${badgeText}`;
364
+ } else {
365
+ line = `${prefix}${item.id}${badgeText}`;
366
+ }
234
367
  }
235
368
 
236
369
  this.listContainer.addChild(new Text(line, 0, 0));
@@ -244,7 +377,6 @@ export class ModelSelectorComponent extends Container {
244
377
 
245
378
  // Show error message or "no results" if empty
246
379
  if (this.errorMessage) {
247
- // Show error in red
248
380
  const errorLines = this.errorMessage.split("\n");
249
381
  for (const line of errorLines) {
250
382
  this.listContainer.addChild(new Text(theme.fg("error", line), 0, 0));
@@ -254,52 +386,142 @@ export class ModelSelectorComponent extends Container {
254
386
  }
255
387
  }
256
388
 
389
+ private openMenu(): void {
390
+ if (this.filteredModels.length === 0) return;
391
+
392
+ this.isMenuOpen = true;
393
+ this.menuSelectedIndex = 0;
394
+ this.updateMenu();
395
+ }
396
+
397
+ private closeMenu(): void {
398
+ this.isMenuOpen = false;
399
+ this.menuContainer.clear();
400
+ }
401
+
402
+ private updateMenu(): void {
403
+ this.menuContainer.clear();
404
+
405
+ const selectedModel = this.filteredModels[this.selectedIndex];
406
+ if (!selectedModel) return;
407
+
408
+ // Menu header
409
+ this.menuContainer.addChild(new Spacer(1));
410
+ this.menuContainer.addChild(new Text(theme.fg("border", "─".repeat(40)), 0, 0));
411
+ this.menuContainer.addChild(new Text(theme.fg("text", ` Action for: ${theme.bold(selectedModel.id)}`), 0, 0));
412
+ this.menuContainer.addChild(new Spacer(1));
413
+
414
+ // Menu options
415
+ for (let i = 0; i < MENU_ACTIONS.length; i++) {
416
+ const action = MENU_ACTIONS[i]!;
417
+ const isSelected = i === this.menuSelectedIndex;
418
+
419
+ let line: string;
420
+ if (isSelected) {
421
+ line = theme.fg("accent", ` → ${action.label}`);
422
+ } else {
423
+ line = theme.fg("muted", ` ${action.label}`);
424
+ }
425
+ this.menuContainer.addChild(new Text(line, 0, 0));
426
+ }
427
+
428
+ this.menuContainer.addChild(new Spacer(1));
429
+ this.menuContainer.addChild(new Text(theme.fg("dim", " Enter: confirm Esc: cancel"), 0, 0));
430
+ this.menuContainer.addChild(new Text(theme.fg("border", "─".repeat(40)), 0, 0));
431
+ }
432
+
257
433
  handleInput(keyData: string): void {
258
- // Up arrow - wrap to bottom when at top
434
+ if (this.isMenuOpen) {
435
+ this.handleMenuInput(keyData);
436
+ return;
437
+ }
438
+
439
+ // Tab bar navigation: Left/Right arrows or Tab/Shift+Tab
440
+ if (isArrowLeft(keyData) || isShiftTab(keyData)) {
441
+ this.activeTabIndex = (this.activeTabIndex - 1 + this.providers.length) % this.providers.length;
442
+ this.updateTabBar();
443
+ this.selectedIndex = 0;
444
+ this.applyTabFilter();
445
+ return;
446
+ }
447
+
448
+ if (isArrowRight(keyData) || isTab(keyData)) {
449
+ this.activeTabIndex = (this.activeTabIndex + 1) % this.providers.length;
450
+ this.updateTabBar();
451
+ this.selectedIndex = 0;
452
+ this.applyTabFilter();
453
+ return;
454
+ }
455
+
456
+ // Up arrow - navigate list (wrap to bottom when at top)
259
457
  if (isArrowUp(keyData)) {
260
458
  if (this.filteredModels.length === 0) return;
261
459
  this.selectedIndex = this.selectedIndex === 0 ? this.filteredModels.length - 1 : this.selectedIndex - 1;
262
460
  this.updateList();
461
+ return;
263
462
  }
264
- // Down arrow - wrap to top when at bottom
265
- else if (isArrowDown(keyData)) {
463
+
464
+ // Down arrow - navigate list (wrap to top when at bottom)
465
+ if (isArrowDown(keyData)) {
266
466
  if (this.filteredModels.length === 0) return;
267
467
  this.selectedIndex = this.selectedIndex === this.filteredModels.length - 1 ? 0 : this.selectedIndex + 1;
268
468
  this.updateList();
469
+ return;
269
470
  }
270
- // Enter - set as default model (don't close)
271
- else if (isEnter(keyData)) {
272
- const selectedModel = this.filteredModels[this.selectedIndex];
273
- if (selectedModel) {
274
- this.handleSelect(selectedModel.model, "default");
471
+
472
+ // Enter - open context menu
473
+ if (isEnter(keyData)) {
474
+ if (this.filteredModels[this.selectedIndex]) {
475
+ this.openMenu();
275
476
  }
477
+ return;
276
478
  }
277
- // S key - set as smol model (don't close)
278
- else if (keyData === "s" || keyData === "S") {
279
- const selectedModel = this.filteredModels[this.selectedIndex];
280
- if (selectedModel) {
281
- this.handleSelect(selectedModel.model, "smol");
282
- }
479
+
480
+ // Escape - close selector
481
+ if (isEscape(keyData)) {
482
+ this.onCancelCallback();
483
+ return;
484
+ }
485
+
486
+ // Pass everything else to search input
487
+ this.searchInput.handleInput(keyData);
488
+ this.filterModels(this.searchInput.getValue());
489
+ }
490
+
491
+ private handleMenuInput(keyData: string): void {
492
+ // Up arrow - navigate menu
493
+ if (isArrowUp(keyData)) {
494
+ this.menuSelectedIndex = (this.menuSelectedIndex - 1 + MENU_ACTIONS.length) % MENU_ACTIONS.length;
495
+ this.updateMenu();
496
+ return;
497
+ }
498
+
499
+ // Down arrow - navigate menu
500
+ if (isArrowDown(keyData)) {
501
+ this.menuSelectedIndex = (this.menuSelectedIndex + 1) % MENU_ACTIONS.length;
502
+ this.updateMenu();
503
+ return;
283
504
  }
284
- // L key - set as slow model (don't close)
285
- else if (keyData === "l" || keyData === "L") {
505
+
506
+ // Enter - confirm selection
507
+ if (isEnter(keyData)) {
286
508
  const selectedModel = this.filteredModels[this.selectedIndex];
287
- if (selectedModel) {
288
- this.handleSelect(selectedModel.model, "slow");
509
+ const action = MENU_ACTIONS[this.menuSelectedIndex];
510
+ if (selectedModel && action) {
511
+ this.handleSelect(selectedModel.model, action.role);
512
+ this.closeMenu();
289
513
  }
514
+ return;
290
515
  }
291
- // Escape - close
292
- else if (isEscape(keyData)) {
293
- this.onCancelCallback();
294
- }
295
- // Pass everything else to search input
296
- else {
297
- this.searchInput.handleInput(keyData);
298
- this.filterModels(this.searchInput.getValue());
516
+
517
+ // Escape - close menu only
518
+ if (isEscape(keyData)) {
519
+ this.closeMenu();
520
+ return;
299
521
  }
300
522
  }
301
523
 
302
- private handleSelect(model: Model<any>, role: string): void {
524
+ private handleSelect(model: Model<any>, role: ModelRole): void {
303
525
  // Save to settings
304
526
  this.settingsManager.setModelRole(role, `${model.provider}/${model.id}`);
305
527
 
@@ -315,7 +537,7 @@ export class ModelSelectorComponent extends Container {
315
537
  // Notify caller (for updating agent state if needed)
316
538
  this.onSelectCallback(model, role);
317
539
 
318
- // Update list to show new markers
540
+ // Update list to show new badges
319
541
  this.updateList();
320
542
  }
321
543
 
@@ -1,8 +1,8 @@
1
1
  import { getOAuthProviders, type OAuthProviderInfo } from "@oh-my-pi/pi-ai";
2
2
  import { Container, isArrowDown, isArrowUp, isEnter, isEscape, Spacer, TruncatedText } from "@oh-my-pi/pi-tui";
3
- import type { AuthStorage } from "../../../core/auth-storage.js";
4
- import { theme } from "../theme/theme.js";
5
- import { DynamicBorder } from "./dynamic-border.js";
3
+ import type { AuthStorage } from "../../../core/auth-storage";
4
+ import { theme } from "../theme/theme";
5
+ import { DynamicBorder } from "./dynamic-border";
6
6
 
7
7
  /**
8
8
  * Component that renders an OAuth provider selector
@@ -18,10 +18,10 @@ import {
18
18
  Spacer,
19
19
  Text,
20
20
  } from "@oh-my-pi/pi-tui";
21
- import { PluginManager } from "../../../core/plugins/manager.js";
22
- import type { InstalledPlugin, PluginSettingSchema } from "../../../core/plugins/types.js";
23
- import { getSelectListTheme, getSettingsListTheme, theme } from "../theme/theme.js";
24
- import { DynamicBorder } from "./dynamic-border.js";
21
+ import { PluginManager } from "../../../core/plugins/manager";
22
+ import type { InstalledPlugin, PluginSettingSchema } from "../../../core/plugins/types";
23
+ import { getSelectListTheme, getSettingsListTheme, theme } from "../theme/theme";
24
+ import { DynamicBorder } from "./dynamic-border";
25
25
 
26
26
  // =============================================================================
27
27
  // Plugin List Component
@@ -1,6 +1,6 @@
1
1
  import { Container, type SelectItem, SelectList } from "@oh-my-pi/pi-tui";
2
- import { getSelectListTheme } from "../theme/theme.js";
3
- import { DynamicBorder } from "./dynamic-border.js";
2
+ import { getSelectListTheme } from "../theme/theme";
3
+ import { DynamicBorder } from "./dynamic-border";
4
4
 
5
5
  /**
6
6
  * Component that renders a queue mode selector with borders
@@ -11,10 +11,10 @@ import {
11
11
  Text,
12
12
  truncateToWidth,
13
13
  } from "@oh-my-pi/pi-tui";
14
- import type { SessionInfo } from "../../../core/session-manager.js";
15
- import { fuzzyFilter } from "../../../utils/fuzzy.js";
16
- import { theme } from "../theme/theme.js";
17
- import { DynamicBorder } from "./dynamic-border.js";
14
+ import type { SessionInfo } from "../../../core/session-manager";
15
+ import { fuzzyFilter } from "../../../utils/fuzzy";
16
+ import { theme } from "../theme/theme";
17
+ import { DynamicBorder } from "./dynamic-border";
18
18
 
19
19
  /**
20
20
  * Custom session list component with multi-line items and search
@@ -10,7 +10,7 @@
10
10
 
11
11
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
12
12
  import { getCapabilities } from "@oh-my-pi/pi-tui";
13
- import type { SettingsManager } from "../../../core/settings-manager.js";
13
+ import type { SettingsManager } from "../../../core/settings-manager";
14
14
 
15
15
  // Setting value types
16
16
  export type SettingValue = boolean | string;
@@ -16,11 +16,11 @@ import {
16
16
  type TabBarTheme,
17
17
  Text,
18
18
  } from "@oh-my-pi/pi-tui";
19
- import type { SettingsManager } from "../../../core/settings-manager.js";
20
- import { getSelectListTheme, getSettingsListTheme, theme } from "../theme/theme.js";
21
- import { DynamicBorder } from "./dynamic-border.js";
22
- import { PluginSettingsComponent } from "./plugin-settings.js";
23
- import { getSettingsForTab, type SettingDef } from "./settings-defs.js";
19
+ import type { SettingsManager } from "../../../core/settings-manager";
20
+ import { getSelectListTheme, getSettingsListTheme, theme } from "../theme/theme";
21
+ import { DynamicBorder } from "./dynamic-border";
22
+ import { PluginSettingsComponent } from "./plugin-settings";
23
+ import { getSettingsForTab, type SettingDef } from "./settings-defs";
24
24
 
25
25
  function getTabBarTheme(): TabBarTheme {
26
26
  return {
@@ -1,6 +1,6 @@
1
1
  import { Container, type SelectItem, SelectList } from "@oh-my-pi/pi-tui";
2
- import { getSelectListTheme } from "../theme/theme.js";
3
- import { DynamicBorder } from "./dynamic-border.js";
2
+ import { getSelectListTheme } from "../theme/theme";
3
+ import { DynamicBorder } from "./dynamic-border";
4
4
 
5
5
  /**
6
6
  * Component that renders a show images selector with borders
@@ -1,6 +1,6 @@
1
1
  import { Container, type SelectItem, SelectList } from "@oh-my-pi/pi-tui";
2
- import { getAvailableThemes, getSelectListTheme } from "../theme/theme.js";
3
- import { DynamicBorder } from "./dynamic-border.js";
2
+ import { getAvailableThemes, getSelectListTheme } from "../theme/theme";
3
+ import { DynamicBorder } from "./dynamic-border";
4
4
 
5
5
  /**
6
6
  * Component that renders a theme selector
@@ -1,7 +1,7 @@
1
1
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
2
  import { Container, type SelectItem, SelectList } from "@oh-my-pi/pi-tui";
3
- import { getSelectListTheme } from "../theme/theme.js";
4
- import { DynamicBorder } from "./dynamic-border.js";
3
+ import { getSelectListTheme } from "../theme/theme";
4
+ import { DynamicBorder } from "./dynamic-border";
5
5
 
6
6
  const LEVEL_DESCRIPTIONS: Record<ThinkingLevel, string> = {
7
7
  off: "No reasoning",
@@ -11,14 +11,14 @@ import {
11
11
  type TUI,
12
12
  } from "@oh-my-pi/pi-tui";
13
13
  import stripAnsi from "strip-ansi";
14
- import type { CustomTool } from "../../../core/custom-tools/types.js";
15
- import { computeEditDiff, type EditDiffError, type EditDiffResult } from "../../../core/tools/edit-diff.js";
16
- import { toolRenderers } from "../../../core/tools/renderers.js";
17
- import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize } from "../../../core/tools/truncate.js";
18
- import { sanitizeBinaryOutput } from "../../../utils/shell.js";
19
- import { getLanguageFromPath, highlightCode, theme } from "../theme/theme.js";
20
- import { renderDiff } from "./diff.js";
21
- import { truncateToVisualLines } from "./visual-truncate.js";
14
+ import type { CustomTool } from "../../../core/custom-tools/types";
15
+ import { computeEditDiff, type EditDiffError, type EditDiffResult } from "../../../core/tools/edit-diff";
16
+ import { toolRenderers } from "../../../core/tools/renderers";
17
+ import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize } from "../../../core/tools/truncate";
18
+ import { sanitizeBinaryOutput } from "../../../utils/shell";
19
+ import { getLanguageFromPath, highlightCode, theme } from "../theme/theme";
20
+ import { renderDiff } from "./diff";
21
+ import { truncateToVisualLines } from "./visual-truncate";
22
22
 
23
23
  // Preview line limit for bash when not expanded
24
24
  const BASH_PREVIEW_LINES = 5;
@@ -550,6 +550,24 @@ export class ToolExecutionComponent extends Container {
550
550
  text += `\n\n${renderDiff(this.editDiffPreview.diff, { filePath: rawPath })}`;
551
551
  }
552
552
  }
553
+
554
+ // Show LSP diagnostics if available
555
+ if (this.result?.details?.diagnostics?.available) {
556
+ const diag = this.result.details.diagnostics;
557
+ if (diag.diagnostics.length > 0) {
558
+ const icon = diag.hasErrors ? theme.fg("error", "●") : theme.fg("warning", "●");
559
+ text += `\n\n${icon} ${theme.fg("toolTitle", "LSP Diagnostics")} ${theme.fg("dim", `(${diag.summary})`)}`;
560
+ const maxDiags = this.expanded ? diag.diagnostics.length : 5;
561
+ const displayDiags = diag.diagnostics.slice(0, maxDiags);
562
+ for (const d of displayDiags) {
563
+ const color = d.includes("[error]") ? "error" : d.includes("[warning]") ? "warning" : "dim";
564
+ text += `\n ${theme.fg(color, d)}`;
565
+ }
566
+ if (diag.diagnostics.length > maxDiags) {
567
+ text += theme.fg("dim", `\n ... (${diag.diagnostics.length - maxDiags} more)`);
568
+ }
569
+ }
570
+ }
553
571
  } else if (this.toolName === "ls") {
554
572
  const path = shortenPath(this.args?.path || ".");
555
573
  const limit = this.args?.limit;
@@ -17,9 +17,9 @@ import {
17
17
  TruncatedText,
18
18
  truncateToWidth,
19
19
  } from "@oh-my-pi/pi-tui";
20
- import type { SessionTreeNode } from "../../../core/session-manager.js";
21
- import { theme } from "../theme/theme.js";
22
- import { DynamicBorder } from "./dynamic-border.js";
20
+ import type { SessionTreeNode } from "../../../core/session-manager";
21
+ import { theme } from "../theme/theme";
22
+ import { DynamicBorder } from "./dynamic-border";
23
23
 
24
24
  /** Gutter info: position (displayIndent where connector was) and whether to show │ */
25
25
  interface GutterInfo {
@@ -10,8 +10,8 @@ import {
10
10
  Text,
11
11
  truncateToWidth,
12
12
  } from "@oh-my-pi/pi-tui";
13
- import { theme } from "../theme/theme.js";
14
- import { DynamicBorder } from "./dynamic-border.js";
13
+ import { theme } from "../theme/theme";
14
+ import { DynamicBorder } from "./dynamic-border";
15
15
 
16
16
  interface UserMessageItem {
17
17
  id: string; // Entry ID in the session