@oh-my-pi/pi-coding-agent 16.1.23 → 16.2.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 (254) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/dist/cli.js +3817 -3735
  3. package/dist/types/advisor/__tests__/emission-guard.test.d.ts +1 -0
  4. package/dist/types/advisor/emission-guard.d.ts +73 -0
  5. package/dist/types/advisor/index.d.ts +1 -0
  6. package/dist/types/advisor/runtime.d.ts +7 -0
  7. package/dist/types/advisor/watchdog.d.ts +2 -0
  8. package/dist/types/cli/flag-tables.d.ts +1 -0
  9. package/dist/types/cli/gallery-fixtures/search.d.ts +1 -1
  10. package/dist/types/collab/display-name.d.ts +3 -0
  11. package/dist/types/collab/host.d.ts +0 -2
  12. package/dist/types/commands/worktree.d.ts +0 -3
  13. package/dist/types/config/inline-tool-descriptors-mode.d.ts +13 -0
  14. package/dist/types/config/model-discovery.d.ts +6 -1
  15. package/dist/types/config/model-registry.d.ts +6 -2
  16. package/dist/types/config/model-resolver.d.ts +12 -0
  17. package/dist/types/config/models-config-schema.d.ts +29 -2
  18. package/dist/types/config/models-config.d.ts +22 -1
  19. package/dist/types/config/settings-schema.d.ts +91 -21
  20. package/dist/types/config/settings.d.ts +2 -0
  21. package/dist/types/dap/config.d.ts +1 -1
  22. package/dist/types/edit/hashline/filesystem.d.ts +4 -2
  23. package/dist/types/edit/renderer.d.ts +4 -0
  24. package/dist/types/extensibility/extensions/types.d.ts +17 -17
  25. package/dist/types/extensibility/hooks/types.d.ts +10 -10
  26. package/dist/types/extensibility/plugins/marketplace/types.d.ts +1 -0
  27. package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +1 -0
  28. package/dist/types/internal-urls/index.d.ts +1 -0
  29. package/dist/types/internal-urls/registry-helpers.d.ts +2 -0
  30. package/dist/types/internal-urls/router.d.ts +1 -1
  31. package/dist/types/internal-urls/ssh-protocol.d.ts +10 -0
  32. package/dist/types/internal-urls/types.d.ts +19 -2
  33. package/dist/types/irc/bus.d.ts +6 -0
  34. package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +1 -0
  35. package/dist/types/modes/components/custom-editor.d.ts +7 -0
  36. package/dist/types/modes/components/move-overlay.d.ts +23 -0
  37. package/dist/types/modes/components/plugin-selector.d.ts +2 -1
  38. package/dist/types/modes/components/queue-mode-selector.d.ts +2 -1
  39. package/dist/types/modes/components/select-list-mouse-routing.d.ts +2 -0
  40. package/dist/types/modes/components/settings-defs.d.ts +4 -1
  41. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  42. package/dist/types/modes/components/show-images-selector.d.ts +2 -1
  43. package/dist/types/modes/components/status-line/component.d.ts +2 -0
  44. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  45. package/dist/types/modes/components/theme-selector.d.ts +2 -1
  46. package/dist/types/modes/components/thinking-selector.d.ts +2 -1
  47. package/dist/types/modes/controllers/command-controller.d.ts +10 -1
  48. package/dist/types/modes/controllers/streaming-reveal.d.ts +6 -0
  49. package/dist/types/modes/interactive-mode.d.ts +10 -1
  50. package/dist/types/modes/internal-url-autocomplete.d.ts +1 -1
  51. package/dist/types/modes/running-subagent-badge.d.ts +6 -0
  52. package/dist/types/modes/theme/theme.d.ts +1 -1
  53. package/dist/types/modes/types.d.ts +9 -1
  54. package/dist/types/sdk.d.ts +2 -2
  55. package/dist/types/session/agent-session.d.ts +2 -1
  56. package/dist/types/session/session-manager.d.ts +13 -0
  57. package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
  58. package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +1 -0
  59. package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +1 -0
  60. package/dist/types/ssh/connection-manager.d.ts +2 -0
  61. package/dist/types/ssh/file-transfer.d.ts +79 -0
  62. package/dist/types/ssh/utils.d.ts +6 -0
  63. package/dist/types/system-prompt.d.ts +3 -0
  64. package/dist/types/task/executor.d.ts +16 -0
  65. package/dist/types/tiny/text.d.ts +1 -1
  66. package/dist/types/tools/builtin-names.d.ts +5 -1
  67. package/dist/types/tools/{find.d.ts → glob.d.ts} +15 -15
  68. package/dist/types/tools/{search.d.ts → grep.d.ts} +14 -14
  69. package/dist/types/tools/index.d.ts +3 -3
  70. package/dist/types/tools/path-utils.d.ts +29 -0
  71. package/dist/types/tools/read.d.ts +2 -2
  72. package/dist/types/tools/render-utils.d.ts +8 -0
  73. package/dist/types/tools/renderers.d.ts +11 -0
  74. package/dist/types/tools/ssh.d.ts +2 -0
  75. package/dist/types/tools/todo.d.ts +0 -16
  76. package/dist/types/tools/write.d.ts +2 -2
  77. package/dist/types/utils/active-repo-context.d.ts +8 -0
  78. package/dist/types/utils/image-resize.d.ts +1 -0
  79. package/dist/types/utils/markit-cache.d.ts +23 -0
  80. package/dist/types/utils/markit.d.ts +5 -1
  81. package/dist/types/utils/prompt-path.d.ts +1 -0
  82. package/dist/types/web/search/providers/duckduckgo.d.ts +14 -0
  83. package/dist/types/web/search/providers/firecrawl.d.ts +28 -0
  84. package/dist/types/web/search/providers/tinyfish.d.ts +29 -0
  85. package/dist/types/web/search/providers/xai.d.ts +13 -0
  86. package/dist/types/web/search/types.d.ts +18 -2
  87. package/package.json +30 -15
  88. package/scripts/build-binary.ts +9 -9
  89. package/scripts/bundle-dist.ts +2 -2
  90. package/src/advisor/__tests__/advisor.test.ts +40 -4
  91. package/src/advisor/__tests__/emission-guard.test.ts +147 -0
  92. package/src/advisor/advise-tool.ts +1 -1
  93. package/src/advisor/emission-guard.ts +172 -0
  94. package/src/advisor/index.ts +1 -0
  95. package/src/advisor/runtime.ts +11 -0
  96. package/src/advisor/watchdog.ts +12 -1
  97. package/src/cli/args.ts +5 -2
  98. package/src/cli/auth-broker-cli.ts +17 -0
  99. package/src/cli/config-cli.ts +4 -0
  100. package/src/cli/flag-tables.ts +7 -4
  101. package/src/cli/gallery-cli.ts +14 -2
  102. package/src/cli/gallery-fixtures/edit.ts +60 -0
  103. package/src/cli/gallery-fixtures/fs.ts +17 -17
  104. package/src/cli/gallery-fixtures/search.ts +4 -4
  105. package/src/cli/usage-cli.ts +20 -1
  106. package/src/cli/web-search-cli.ts +1 -1
  107. package/src/collab/display-name.ts +13 -0
  108. package/src/collab/guest.ts +5 -1
  109. package/src/collab/host.ts +2 -13
  110. package/src/commands/worktree.ts +6 -0
  111. package/src/config/inline-tool-descriptors-mode.ts +27 -0
  112. package/src/config/model-discovery.ts +77 -8
  113. package/src/config/model-registry.ts +89 -11
  114. package/src/config/model-resolver.ts +39 -1
  115. package/src/config/models-config-schema.ts +41 -5
  116. package/src/config/models-config.ts +4 -1
  117. package/src/config/settings-schema.ts +85 -27
  118. package/src/config/settings.ts +172 -1
  119. package/src/cursor.ts +1 -1
  120. package/src/dap/config.ts +152 -8
  121. package/src/dap/session.ts +1 -1
  122. package/src/discovery/helpers.ts +3 -1
  123. package/src/edit/hashline/diff.ts +14 -3
  124. package/src/edit/hashline/execute.ts +42 -6
  125. package/src/edit/hashline/filesystem.ts +38 -2
  126. package/src/edit/index.ts +1 -0
  127. package/src/edit/modes/patch.ts +11 -1
  128. package/src/edit/renderer.ts +140 -13
  129. package/src/eval/__tests__/agent-bridge.test.ts +101 -0
  130. package/src/eval/agent-bridge.ts +26 -3
  131. package/src/eval/js/tool-bridge.ts +2 -2
  132. package/src/export/html/index.ts +1 -1
  133. package/src/export/html/template.js +3 -1
  134. package/src/export/html/tool-views.generated.js +20 -20
  135. package/src/extensibility/extensions/types.ts +22 -22
  136. package/src/extensibility/hooks/types.ts +11 -11
  137. package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +8 -3
  138. package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +25 -10
  139. package/src/extensibility/plugins/marketplace/manager.ts +22 -0
  140. package/src/extensibility/plugins/marketplace/types.ts +1 -0
  141. package/src/internal-urls/__tests__/ssh-protocol.test.ts +331 -0
  142. package/src/internal-urls/docs-index.generated.txt +2 -2
  143. package/src/internal-urls/docs-index.ts +2 -3
  144. package/src/internal-urls/index.ts +1 -0
  145. package/src/internal-urls/registry-helpers.ts +19 -4
  146. package/src/internal-urls/router.ts +5 -3
  147. package/src/internal-urls/ssh-protocol.ts +367 -0
  148. package/src/internal-urls/types.ts +19 -2
  149. package/src/irc/bus.ts +11 -3
  150. package/src/mcp/tool-bridge.ts +32 -2
  151. package/src/memories/index.ts +1 -1
  152. package/src/modes/acp/acp-event-mapper.ts +2 -2
  153. package/src/modes/components/__tests__/move-overlay.test.ts +166 -0
  154. package/src/modes/components/agent-hub.ts +3 -0
  155. package/src/modes/components/agent-transcript-viewer.ts +9 -7
  156. package/src/modes/components/custom-editor.ts +90 -5
  157. package/src/modes/components/move-overlay.ts +282 -0
  158. package/src/modes/components/plan-review-overlay.ts +35 -35
  159. package/src/modes/components/plugin-selector.ts +6 -1
  160. package/src/modes/components/queue-mode-selector.ts +6 -1
  161. package/src/modes/components/select-list-mouse-routing.ts +35 -0
  162. package/src/modes/components/session-selector.ts +11 -10
  163. package/src/modes/components/settings-defs.ts +14 -1
  164. package/src/modes/components/settings-selector.ts +196 -29
  165. package/src/modes/components/show-images-selector.ts +6 -1
  166. package/src/modes/components/status-line/component.ts +108 -28
  167. package/src/modes/components/status-line/segments.ts +5 -1
  168. package/src/modes/components/status-line/types.ts +2 -0
  169. package/src/modes/components/theme-selector.ts +6 -1
  170. package/src/modes/components/thinking-selector.ts +6 -1
  171. package/src/modes/components/tool-execution.ts +25 -9
  172. package/src/modes/components/tree-selector.ts +5 -5
  173. package/src/modes/controllers/command-controller.ts +140 -47
  174. package/src/modes/controllers/event-controller.ts +59 -3
  175. package/src/modes/controllers/input-controller.ts +68 -2
  176. package/src/modes/controllers/selector-controller.ts +5 -2
  177. package/src/modes/controllers/streaming-reveal.ts +17 -0
  178. package/src/modes/controllers/tool-args-reveal.ts +1 -1
  179. package/src/modes/interactive-mode.ts +116 -50
  180. package/src/modes/internal-url-autocomplete.ts +17 -2
  181. package/src/modes/prompt-action-autocomplete.ts +3 -1
  182. package/src/modes/running-subagent-badge.ts +13 -0
  183. package/src/modes/setup-wizard/scenes/glyph.ts +2 -13
  184. package/src/modes/setup-wizard/scenes/providers.ts +2 -1
  185. package/src/modes/setup-wizard/scenes/theme.ts +6 -12
  186. package/src/modes/setup-wizard/scenes/web-search.ts +8 -13
  187. package/src/modes/setup-wizard/wizard-overlay.ts +6 -5
  188. package/src/modes/theme/theme.ts +9 -1
  189. package/src/modes/types.ts +9 -1
  190. package/src/modes/utils/interactive-context-helpers.ts +1 -1
  191. package/src/priority.json +15 -0
  192. package/src/prompts/advisor/active-repo-watchdog.md +6 -0
  193. package/src/prompts/advisor/system.md +21 -7
  194. package/src/prompts/agents/designer.md +1 -1
  195. package/src/prompts/agents/explore.md +1 -1
  196. package/src/prompts/agents/librarian.md +2 -2
  197. package/src/prompts/agents/plan.md +2 -2
  198. package/src/prompts/agents/reviewer.md +1 -1
  199. package/src/prompts/agents/task.md +2 -2
  200. package/src/prompts/system/active-repo-context.md +4 -0
  201. package/src/prompts/system/gemini-tool-call-reminder.md +9 -0
  202. package/src/prompts/system/plan-mode-active.md +3 -3
  203. package/src/prompts/system/project-prompt.md +2 -2
  204. package/src/prompts/system/system-prompt.md +6 -6
  205. package/src/prompts/tools/bash.md +2 -2
  206. package/src/prompts/tools/checkpoint.md +1 -1
  207. package/src/prompts/tools/{find.md → glob.md} +1 -1
  208. package/src/prompts/tools/{search.md → grep.md} +3 -3
  209. package/src/prompts/tools/read.md +4 -2
  210. package/src/sdk.ts +55 -25
  211. package/src/session/agent-session.ts +508 -132
  212. package/src/session/session-history-format.ts +2 -2
  213. package/src/session/session-manager.ts +46 -0
  214. package/src/slash-commands/builtin-registry.ts +138 -20
  215. package/src/slash-commands/helpers/usage-report.ts +23 -2
  216. package/src/ssh/__tests__/connection-manager-args.test.ts +69 -0
  217. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +68 -0
  218. package/src/ssh/connection-manager.ts +12 -15
  219. package/src/ssh/file-transfer.ts +209 -0
  220. package/src/ssh/utils.ts +24 -0
  221. package/src/system-prompt.ts +56 -28
  222. package/src/task/executor.ts +96 -36
  223. package/src/task/index.ts +3 -3
  224. package/src/task/render.ts +14 -13
  225. package/src/task/worktree.ts +38 -1
  226. package/src/tiny/text.ts +49 -4
  227. package/src/tiny/worker.ts +3 -3
  228. package/src/tools/bash.ts +2 -2
  229. package/src/tools/builtin-names.ts +26 -2
  230. package/src/tools/{find.ts → glob.ts} +48 -42
  231. package/src/tools/{search.ts → grep.ts} +311 -129
  232. package/src/tools/index.ts +13 -14
  233. package/src/tools/irc.ts +6 -2
  234. package/src/tools/path-utils.ts +64 -1
  235. package/src/tools/read.ts +35 -6
  236. package/src/tools/render-utils.ts +14 -0
  237. package/src/tools/renderers.ts +15 -4
  238. package/src/tools/ssh.ts +17 -2
  239. package/src/tools/todo.ts +4 -26
  240. package/src/tools/write.ts +25 -9
  241. package/src/utils/active-repo-context.ts +143 -0
  242. package/src/utils/image-resize.ts +88 -7
  243. package/src/utils/lang-from-path.ts +3 -3
  244. package/src/utils/markit-cache.ts +166 -0
  245. package/src/utils/markit.ts +86 -18
  246. package/src/utils/prompt-path.ts +3 -0
  247. package/src/utils/title-generator.ts +1 -1
  248. package/src/web/search/provider.ts +54 -34
  249. package/src/web/search/providers/duckduckgo.ts +140 -0
  250. package/src/web/search/providers/firecrawl.ts +144 -0
  251. package/src/web/search/providers/tinyfish.ts +159 -0
  252. package/src/web/search/providers/xai.ts +292 -0
  253. package/src/web/search/providers/zai.ts +142 -56
  254. package/src/web/search/types.ts +6 -2
@@ -23,7 +23,7 @@ import {
23
23
  Markdown,
24
24
  type MarkdownTheme,
25
25
  matchesKey,
26
- parseSgrMouse,
26
+ routeSgrMouseInput,
27
27
  ScrollView,
28
28
  truncateToWidth,
29
29
  visibleWidth,
@@ -333,42 +333,42 @@ export class PlanReviewOverlay implements Component {
333
333
  * the body.
334
334
  */
335
335
  #handleMouse(data: string): boolean {
336
- const event = parseSgrMouse(data);
337
- if (!event) return false;
338
- if (event.wheel !== null) {
339
- // Scroll wheel: three rows per notch.
340
- this.#scrollView.scroll(event.wheel * 3);
341
- return true;
342
- }
343
- if (event.release) return true;
344
- if (event.motion) {
345
- // Motion (hover or drag): light up the option row under the pointer so a
346
- // mouse user gets the same affordance the keyboard cursor gives. Any
347
- // non-option row clears the highlight.
348
- this.#setHoveredOption(this.#optionClickRows.get(event.row));
349
- return true;
350
- }
351
- if (!event.leftClick) return true;
352
- const optionIndex = this.#optionClickRows.get(event.row);
353
- if (optionIndex !== undefined) {
354
- if (!this.#disabled.has(optionIndex)) {
355
- this.#focus = "actions";
356
- this.#selectedIndex = optionIndex;
357
- this.#confirmSelection();
336
+ return routeSgrMouseInput(data, event => {
337
+ if (event.wheel !== null) {
338
+ // Scroll wheel: three rows per notch.
339
+ this.#scrollView.scroll(event.wheel * 3);
340
+ return true;
341
+ }
342
+ if (event.release) return true;
343
+ if (event.motion) {
344
+ // Motion (hover or drag): light up the option row under the pointer so a
345
+ // mouse user gets the same affordance the keyboard cursor gives. Any
346
+ // non-option row clears the highlight.
347
+ this.#setHoveredOption(this.#optionClickRows.get(event.row));
348
+ return true;
349
+ }
350
+ if (!event.leftClick) return true;
351
+ const optionIndex = this.#optionClickRows.get(event.row);
352
+ if (optionIndex !== undefined) {
353
+ if (!this.#disabled.has(optionIndex)) {
354
+ this.#focus = "actions";
355
+ this.#selectedIndex = optionIndex;
356
+ this.#confirmSelection();
357
+ }
358
+ return true;
359
+ }
360
+ const tocPos = this.#tocClickRows.get(event.row);
361
+ if (tocPos !== undefined && event.col < this.#sidebarClickMaxCol) {
362
+ this.#focus = "toc";
363
+ this.#tocCursor = tocPos;
364
+ this.#scrubBodyToToc();
365
+ return true;
366
+ }
367
+ if (this.#bodyClickRows.has(event.row)) {
368
+ this.#setFocus("body");
358
369
  }
359
370
  return true;
360
- }
361
- const tocPos = this.#tocClickRows.get(event.row);
362
- if (tocPos !== undefined && event.col < this.#sidebarClickMaxCol) {
363
- this.#focus = "toc";
364
- this.#tocCursor = tocPos;
365
- this.#scrubBodyToToc();
366
- return true;
367
- }
368
- if (this.#bodyClickRows.has(event.row)) {
369
- this.#setFocus("body");
370
- }
371
- return true;
371
+ });
372
372
  }
373
373
 
374
374
  /** Set the hovered option from a hit-tested row, ignoring disabled rows and
@@ -4,9 +4,10 @@
4
4
  * Shows available plugins from all configured marketplaces in a SelectList.
5
5
  * Selecting a plugin triggers installation. Esc cancels.
6
6
  */
7
- import { Container, type SelectItem, SelectList } from "@oh-my-pi/pi-tui";
7
+ import { Container, type SelectItem, SelectList, type SgrMouseEvent } from "@oh-my-pi/pi-tui";
8
8
  import { getSelectListTheme } from "../theme/theme";
9
9
  import { DynamicBorder } from "./dynamic-border";
10
+ import { routeSelectListMouseWithTopBorder } from "./select-list-mouse-routing";
10
11
 
11
12
  export interface PluginSelectorCallbacks {
12
13
  onSelect: (pluginName: string, marketplace: string, scope?: "user" | "project") => void;
@@ -82,6 +83,10 @@ export class PluginSelectorComponent extends Container {
82
83
  getSelectList(): SelectList {
83
84
  return this.#selectList;
84
85
  }
86
+
87
+ routeMouse(event: SgrMouseEvent, line: number, col: number): void {
88
+ routeSelectListMouseWithTopBorder(this.#selectList, event, line, col);
89
+ }
85
90
  }
86
91
 
87
92
  function splitPluginId(id: string): [string, string, "user" | "project" | undefined] | [null, null, null] {
@@ -1,6 +1,7 @@
1
- import { Container, type SelectItem, SelectList } from "@oh-my-pi/pi-tui";
1
+ import { Container, type SelectItem, SelectList, type SgrMouseEvent } from "@oh-my-pi/pi-tui";
2
2
  import { getSelectListTheme } from "../../modes/theme/theme";
3
3
  import { DynamicBorder } from "./dynamic-border";
4
+ import { routeSelectListMouseWithTopBorder } from "./select-list-mouse-routing";
4
5
 
5
6
  /**
6
7
  * Component that renders a queue mode selector with borders
@@ -53,4 +54,8 @@ export class QueueModeSelectorComponent extends Container {
53
54
  getSelectList(): SelectList {
54
55
  return this.#selectList;
55
56
  }
57
+
58
+ routeMouse(event: SgrMouseEvent, line: number, col: number): void {
59
+ routeSelectListMouseWithTopBorder(this.#selectList, event, line, col);
60
+ }
56
61
  }
@@ -0,0 +1,35 @@
1
+ import type { SelectList, SgrMouseEvent } from "@oh-my-pi/pi-tui";
2
+
3
+ interface RoutableSelectList {
4
+ routeMouse?: (event: SgrMouseEvent, line: number, col: number) => void;
5
+ handleWheel(delta: -1 | 1): void;
6
+ hitTest(line: number): number | undefined;
7
+ setHoverIndex(index: number | null): void;
8
+ clickItem(index: number): void;
9
+ }
10
+
11
+ export function routeSelectListMouseWithTopBorder(
12
+ selectList: SelectList,
13
+ event: SgrMouseEvent,
14
+ line: number,
15
+ col: number,
16
+ ): void {
17
+ const localLine = line - 1;
18
+ const target = selectList as RoutableSelectList;
19
+ if (typeof target.routeMouse === "function") {
20
+ target.routeMouse(event, localLine, col);
21
+ return;
22
+ }
23
+ if (event.wheel !== null) {
24
+ target.handleWheel(event.wheel);
25
+ return;
26
+ }
27
+ const index = target.hitTest(localLine);
28
+ if (event.motion) {
29
+ target.setHoverIndex(index ?? null);
30
+ return;
31
+ }
32
+ if (event.leftClick && index !== undefined) {
33
+ target.clickItem(index);
34
+ }
35
+ }
@@ -5,8 +5,8 @@ import {
5
5
  Input,
6
6
  matchesKey,
7
7
  padding,
8
- parseSgrMouse,
9
8
  replaceTabs,
9
+ routeSgrMouseInput,
10
10
  ScrollView,
11
11
  Spacer,
12
12
  Text,
@@ -726,15 +726,16 @@ export class SessionSelectorComponent extends Container {
726
726
  */
727
727
  #handleMouse(data: string): void {
728
728
  if (this.#confirmationDialog) return;
729
- const event = parseSgrMouse(data);
730
- if (!event) return;
731
- if (event.wheel !== null) {
732
- this.#sessionList.handleWheel(event.wheel);
733
- return;
734
- }
735
- if (!event.leftClick || event.row >= this.#footerStart) return;
736
- const index = this.#sessionList.hitTestSession(event.row - this.#listLineOffset);
737
- if (index !== undefined) this.#sessionList.selectAndConfirm(index);
729
+ routeSgrMouseInput(data, event => {
730
+ if (event.wheel !== null) {
731
+ this.#sessionList.handleWheel(event.wheel);
732
+ return true;
733
+ }
734
+ if (!event.leftClick || event.row >= this.#footerStart) return true;
735
+ const index = this.#sessionList.hitTestSession(event.row - this.#listLineOffset);
736
+ if (index !== undefined) this.#sessionList.selectAndConfirm(index);
737
+ return true;
738
+ });
738
739
  }
739
740
 
740
741
  getSessionList(): SessionList {
@@ -68,7 +68,16 @@ export interface TextInputSettingDef extends BaseSettingDef {
68
68
  type: "text";
69
69
  }
70
70
 
71
- export type SettingDef = BooleanSettingDef | EnumSettingDef | SubmenuSettingDef | TextInputSettingDef;
71
+ export interface ProviderLimitsSettingDef extends BaseSettingDef {
72
+ type: "providerLimits";
73
+ }
74
+
75
+ export type SettingDef =
76
+ | BooleanSettingDef
77
+ | EnumSettingDef
78
+ | SubmenuSettingDef
79
+ | TextInputSettingDef
80
+ | ProviderLimitsSettingDef;
72
81
 
73
82
  // ═══════════════════════════════════════════════════════════════════════════
74
83
  // Condition Functions
@@ -170,6 +179,10 @@ function pathToSettingDef(path: SettingPath): SettingDef | null {
170
179
  return { ...base, type: "text" };
171
180
  }
172
181
 
182
+ if (schemaType === "record") {
183
+ return path === "providers.maxInFlightRequests" ? { ...base, type: "providerLimits" } : null;
184
+ }
185
+
173
186
  return null;
174
187
  }
175
188
 
@@ -10,7 +10,9 @@ import {
10
10
  type ImageBudget,
11
11
  Input,
12
12
  matchesKey,
13
- parseSgrMouse,
13
+ replaceTabs,
14
+ routeSelectListMouse,
15
+ routeSgrMouseInput,
14
16
  type SelectItem,
15
17
  SelectList,
16
18
  type SettingItem,
@@ -24,7 +26,14 @@ import {
24
26
  visibleWidth,
25
27
  } from "@oh-my-pi/pi-tui";
26
28
  import type { ShapeTarget } from "@oh-my-pi/snapcompact";
27
- import { getDefault, type SettingPath, settings } from "../../config/settings";
29
+ import {
30
+ getDefault,
31
+ getType,
32
+ normalizeProviderMaxInFlightRequests,
33
+ type SettingPath,
34
+ settings,
35
+ validateProviderMaxInFlightRequests,
36
+ } from "../../config/settings";
28
37
  import type {
29
38
  SettingTab,
30
39
  StatusLinePreset,
@@ -50,6 +59,7 @@ import { getPreset } from "./status-line/presets";
50
59
  */
51
60
  class TextInputSubmenu extends Container {
52
61
  #input: Input;
62
+ #error: Text;
53
63
 
54
64
  constructor(
55
65
  label: string,
@@ -71,11 +81,18 @@ class TextInputSubmenu extends Container {
71
81
  if (currentValue) {
72
82
  this.#input.setValue(currentValue);
73
83
  }
84
+ this.#error = new Text("", 0, 0);
74
85
  this.#input.onSubmit = value => {
75
- this.onSubmit(value); // empty string clears the setting
86
+ try {
87
+ this.onSubmit(value); // empty string clears the setting
88
+ } catch (error) {
89
+ const message = error instanceof Error ? error.message : String(error);
90
+ this.#error.setText(theme.fg("error", truncateToWidth(replaceTabs(message).replace(/[\r\n]+/g, " "), 100)));
91
+ }
76
92
  };
77
93
  this.addChild(this.#input);
78
94
  this.addChild(new Spacer(1));
95
+ this.addChild(this.#error);
79
96
  this.addChild(new Text(theme.fg("dim", " Enter to save · Esc to cancel · Clear field to unset"), 0, 0));
80
97
  }
81
98
 
@@ -89,7 +106,6 @@ class SelectSubmenu extends Container {
89
106
  #previewText: Text | null = null;
90
107
  #previewUpdateRequestId: number = 0;
91
108
  #selectListLineOffset = 0;
92
- #selectListLineCount = 0;
93
109
 
94
110
  constructor(
95
111
  title: string,
@@ -187,7 +203,6 @@ class SelectSubmenu extends Container {
187
203
  const childLines = child.render(Math.max(1, width));
188
204
  if (child === this.#selectList) {
189
205
  this.#selectListLineOffset = lines.length;
190
- this.#selectListLineCount = childLines.length;
191
206
  }
192
207
  lines.push(...childLines);
193
208
  }
@@ -196,20 +211,7 @@ class SelectSubmenu extends Container {
196
211
 
197
212
  /** Mouse routed from the host: wheel steps, hover lights, click confirms. */
198
213
  routeMouse(event: SgrMouseEvent, line: number, _col: number): void {
199
- if (event.wheel !== null) {
200
- this.#selectList.handleWheel(event.wheel);
201
- return;
202
- }
203
- const listLine = line - this.#selectListLineOffset;
204
- const within = listLine >= 0 && listLine < this.#selectListLineCount;
205
- const index = within ? this.#selectList.hitTest(listLine) : undefined;
206
- if (event.motion) {
207
- this.#selectList.setHoverIndex(index ?? null);
208
- return;
209
- }
210
- if (event.leftClick && index !== undefined) {
211
- this.#selectList.clickItem(index);
212
- }
214
+ routeSelectListMouse(this.#selectList, event, line - this.#selectListLineOffset);
213
215
  }
214
216
 
215
217
  handleInput(data: string): void {
@@ -217,6 +219,113 @@ class SelectSubmenu extends Container {
217
219
  }
218
220
  }
219
221
 
222
+ class ProviderLimitsSubmenu extends Container {
223
+ #selectList: SelectList | undefined;
224
+
225
+ constructor(
226
+ private readonly providers: readonly string[],
227
+ private readonly onChange: (value: Record<string, number>) => void,
228
+ private readonly onCancel: () => void,
229
+ private readonly requestRender?: () => void,
230
+ ) {
231
+ super();
232
+ this.#showProviderList();
233
+ }
234
+
235
+ #providerIds(): string[] {
236
+ const limits = normalizeProviderMaxInFlightRequests(settings.get("providers.maxInFlightRequests"));
237
+ return [...new Set([...this.providers, ...Object.keys(limits)])].sort((a, b) => a.localeCompare(b));
238
+ }
239
+
240
+ #showProviderList(): void {
241
+ this.clear();
242
+ this.addChild(new Text(theme.bold(theme.fg("accent", "Max In-Flight Requests")), 0, 0));
243
+ this.addChild(new Spacer(1));
244
+ this.addChild(
245
+ new Text(
246
+ theme.fg(
247
+ "muted",
248
+ "Select a provider, enter a positive number to cap concurrent LLM requests, or clear it for unlimited.",
249
+ ),
250
+ 0,
251
+ 0,
252
+ ),
253
+ );
254
+ this.addChild(new Spacer(1));
255
+
256
+ const limits = normalizeProviderMaxInFlightRequests(settings.get("providers.maxInFlightRequests"));
257
+ const providerItems = this.#providerIds().map((provider): SelectItem => {
258
+ const limit = limits[provider];
259
+ return {
260
+ value: provider,
261
+ label: provider,
262
+ description: limit === undefined ? "Unlimited" : `Limit: ${limit}`,
263
+ };
264
+ });
265
+ const clearItem: SelectItem[] =
266
+ Object.keys(limits).length === 0
267
+ ? []
268
+ : [{ value: "__clear_all", label: "Clear all limits", description: "Make every provider unlimited" }];
269
+ const items = [...providerItems, ...clearItem];
270
+ this.#selectList = new SelectList(items, Math.min(Math.max(items.length, 1), 12), getSelectListTheme());
271
+ this.#selectList.onSelect = item => {
272
+ if (item.value === "__clear_all") {
273
+ settings.set("providers.maxInFlightRequests", {});
274
+ this.onChange({});
275
+ this.#showProviderList();
276
+ this.requestRender?.();
277
+ return;
278
+ }
279
+ this.#showProviderEditor(item.value);
280
+ };
281
+ this.#selectList.onCancel = this.onCancel;
282
+ this.addChild(this.#selectList);
283
+ this.addChild(new Spacer(1));
284
+ this.addChild(new Text(theme.fg("dim", " Enter to edit provider · Esc to go back"), 0, 0));
285
+ }
286
+
287
+ #showProviderEditor(provider: string): void {
288
+ const limits = normalizeProviderMaxInFlightRequests(settings.get("providers.maxInFlightRequests"));
289
+ this.clear();
290
+ this.#selectList = undefined;
291
+ this.addChild(
292
+ new TextInputSubmenu(
293
+ `Max In-Flight Requests: ${provider}`,
294
+ "Enter a positive number. Decimals round down. Clear the field to make this provider unlimited.",
295
+ limits[provider]?.toString() ?? "",
296
+ value => {
297
+ const next = { ...limits };
298
+ const trimmed = value.trim();
299
+ if (trimmed === "") {
300
+ delete next[provider];
301
+ } else {
302
+ const limit = Number(trimmed);
303
+ if (!Number.isFinite(limit) || limit <= 0) throw new Error("Limit must be a positive number.");
304
+ next[provider] = Math.max(1, Math.floor(limit));
305
+ }
306
+ const normalized = validateProviderMaxInFlightRequests(next);
307
+ settings.set("providers.maxInFlightRequests", normalized);
308
+ this.onChange(normalized);
309
+ this.#showProviderList();
310
+ this.requestRender?.();
311
+ },
312
+ () => {
313
+ this.#showProviderList();
314
+ this.requestRender?.();
315
+ },
316
+ ),
317
+ );
318
+ }
319
+
320
+ handleInput(data: string): void {
321
+ if (this.#selectList) {
322
+ this.#selectList.handleInput(data);
323
+ return;
324
+ }
325
+ this.children[0]?.handleInput?.(data);
326
+ }
327
+ }
328
+
220
329
  let cachedSidebarWidth: number | undefined;
221
330
  /**
222
331
  * Split-sidebar width derived from every group name in the schema (not just
@@ -258,6 +367,8 @@ export interface SettingsRuntimeContext {
258
367
  thinkingLevel: ThinkingLevel | undefined;
259
368
  /** Available themes */
260
369
  availableThemes: string[];
370
+ /** Provider/source ids shown in /model. */
371
+ providers: string[];
261
372
  /** Working directory for plugins tab */
262
373
  cwd: string;
263
374
  /** Active model (api + id); resolves what the snapcompact `auto` shape maps to. */
@@ -458,12 +569,14 @@ export class SettingsSelectorComponent implements Component {
458
569
  * activates it (toggle / open submenu).
459
570
  */
460
571
  #handleMouse(data: string): boolean {
461
- const event = parseSgrMouse(data);
462
- if (!event) return false;
572
+ return routeSgrMouseInput(data, event => this.#routeMouseEvent(event));
573
+ }
463
574
 
575
+ #routeMouseEvent(event: SgrMouseEvent): boolean {
464
576
  const list = this.#searchList ?? this.#currentList;
465
- // row() insets content by two columns (border + space).
466
- const innerCol = event.col - 2;
577
+ // row() insets content by the border column plus a space.
578
+ const contentColInset = 2;
579
+ const innerCol = event.col - contentColInset;
467
580
  const contentLine = event.row - this.#contentRowStart;
468
581
 
469
582
  // An open submenu owns the pointer: wheel, hover, and clicks route into
@@ -723,10 +836,20 @@ export class SettingsSelectorComponent implements Component {
723
836
  id: def.path,
724
837
  label: def.label,
725
838
  description: def.description,
726
- currentValue: String(currentValue ?? ""),
839
+ currentValue: this.#formatTextInputValue(def.path, currentValue),
727
840
  submenu: (cv, done) => this.#createTextInput(def, cv, done),
728
841
  changed,
729
842
  };
843
+
844
+ case "providerLimits":
845
+ return {
846
+ id: def.path,
847
+ label: def.label,
848
+ description: def.description,
849
+ currentValue: this.#formatProviderLimitsValue(currentValue),
850
+ submenu: (_cv, done) => this.#createProviderLimitsInput(done),
851
+ changed,
852
+ };
730
853
  }
731
854
  }
732
855
 
@@ -856,7 +979,7 @@ export class SettingsSelectorComponent implements Component {
856
979
  */
857
980
  #createTextInput(
858
981
  def: SettingDef & { type: "text" },
859
- currentValue: string,
982
+ _currentValue: string,
860
983
  done: (value?: string) => void,
861
984
  ): Container {
862
985
  this.#textInputActive = true;
@@ -867,28 +990,72 @@ export class SettingsSelectorComponent implements Component {
867
990
  return new TextInputSubmenu(
868
991
  def.label,
869
992
  def.description,
870
- currentValue,
993
+ this.#formatTextInputEditValue(def.path, settings.get(def.path)),
871
994
  value => {
872
995
  // Empty string clears the setting; undefined-typed string settings
873
996
  // store "" which the browser.ts expandPath ignores (no-op fallback).
874
997
  this.#setSettingValue(def.path, value);
875
- this.callbacks.onChange(def.path, value);
876
- wrappedDone(value);
998
+ this.callbacks.onChange(def.path, settings.get(def.path));
999
+ wrappedDone(this.#formatTextInputValue(def.path, settings.get(def.path)));
877
1000
  },
878
1001
  () => wrappedDone(),
879
1002
  );
880
1003
  }
881
1004
 
1005
+ #createProviderLimitsInput(done: (value?: string) => void): Container {
1006
+ return new ProviderLimitsSubmenu(
1007
+ this.context.providers,
1008
+ value => {
1009
+ this.callbacks.onChange("providers.maxInFlightRequests", value);
1010
+ done(this.#formatProviderLimitsValue(value));
1011
+ },
1012
+ () => done(),
1013
+ this.context.requestRender,
1014
+ );
1015
+ }
1016
+
1017
+ #formatProviderLimitsValue(value: unknown): string {
1018
+ const limits = normalizeProviderMaxInFlightRequests(value);
1019
+ const entries = Object.entries(limits).sort(([a], [b]) => a.localeCompare(b));
1020
+ if (entries.length === 0) return "Unlimited";
1021
+ return entries.map(([provider, limit]) => `${provider}: ${limit}`).join(", ");
1022
+ }
1023
+
1024
+ #formatTextInputValue(path: SettingPath, value: unknown): string {
1025
+ if (path === "providers.maxInFlightRequests") return this.#formatProviderLimitsValue(value);
1026
+ return this.#formatTextInputEditValue(path, value);
1027
+ }
1028
+
1029
+ #formatTextInputEditValue(_path: SettingPath, value: unknown): string {
1030
+ if (value === undefined || value === null) return "";
1031
+ if (typeof value === "object") return JSON.stringify(value);
1032
+ return String(value);
1033
+ }
1034
+
882
1035
  /**
883
1036
  * Set a setting value, handling type conversion.
884
1037
  */
885
1038
  #setSettingValue(path: SettingPath, value: string): void {
886
- // Handle number conversions
887
1039
  const currentValue = settings.get(path);
1040
+ const schemaType = getType(path);
888
1041
  if (path === "compaction.thresholdPercent" && value === "default") {
889
1042
  settings.set(path, -1 as never);
890
1043
  } else if (path === "compaction.thresholdTokens" && value === "default") {
891
1044
  settings.set(path, -1 as never);
1045
+ } else if (schemaType === "record") {
1046
+ let parsed: unknown;
1047
+ try {
1048
+ parsed = JSON.parse(value || "{}");
1049
+ } catch {
1050
+ throw new Error(`Invalid record JSON for ${path}`);
1051
+ }
1052
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1053
+ throw new Error(`Invalid record JSON for ${path}`);
1054
+ }
1055
+ if (path === "providers.maxInFlightRequests") {
1056
+ parsed = validateProviderMaxInFlightRequests(parsed);
1057
+ }
1058
+ settings.set(path, parsed as never);
892
1059
  } else if (typeof currentValue === "number") {
893
1060
  settings.set(path, Number(value) as never);
894
1061
  } else if (typeof currentValue === "boolean") {
@@ -1,6 +1,7 @@
1
- import { Container, type SelectItem, SelectList } from "@oh-my-pi/pi-tui";
1
+ import { Container, type SelectItem, SelectList, type SgrMouseEvent } from "@oh-my-pi/pi-tui";
2
2
  import { getSelectListTheme } from "../../modes/theme/theme";
3
3
  import { DynamicBorder } from "./dynamic-border";
4
+ import { routeSelectListMouseWithTopBorder } from "./select-list-mouse-routing";
4
5
 
5
6
  /**
6
7
  * Component that renders a show images selector with borders
@@ -42,4 +43,8 @@ export class ShowImagesSelectorComponent extends Container {
42
43
  getSelectList(): SelectList {
43
44
  return this.#selectList;
44
45
  }
46
+
47
+ routeMouse(event: SgrMouseEvent, line: number, col: number): void {
48
+ routeSelectListMouseWithTopBorder(this.#selectList, event, line, col);
49
+ }
45
50
  }