pi-ui-extend 1.0.41 → 1.0.45

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 (236) hide show
  1. package/README.md +50 -13
  2. package/dist/app/app.d.ts +13 -0
  3. package/dist/app/app.js +158 -8
  4. package/dist/app/cli/install.js +2 -2
  5. package/dist/app/commands/command-controller.d.ts +4 -0
  6. package/dist/app/commands/command-controller.js +11 -0
  7. package/dist/app/commands/command-git-actions.d.ts +25 -0
  8. package/dist/app/commands/command-git-actions.js +381 -0
  9. package/dist/app/commands/command-host.d.ts +4 -0
  10. package/dist/app/commands/command-host.js +5 -2
  11. package/dist/app/commands/command-model-actions.d.ts +1 -0
  12. package/dist/app/commands/command-model-actions.js +57 -35
  13. package/dist/app/commands/command-navigation-actions.d.ts +1 -0
  14. package/dist/app/commands/command-navigation-actions.js +16 -3
  15. package/dist/app/commands/command-registry.d.ts +2 -0
  16. package/dist/app/commands/command-registry.js +15 -1
  17. package/dist/app/commands/command-session-actions.js +9 -3
  18. package/dist/app/commands/reload-context-inventory.d.ts +11 -0
  19. package/dist/app/commands/reload-context-inventory.js +70 -0
  20. package/dist/app/extensions/extension-actions-controller.d.ts +1 -0
  21. package/dist/app/extensions/extension-actions-controller.js +6 -0
  22. package/dist/app/extensions/subagent-catalog-state.d.ts +9 -0
  23. package/dist/app/extensions/subagent-catalog-state.js +23 -0
  24. package/dist/app/input/autocomplete-controller.js +61 -34
  25. package/dist/app/input/input-action-controller.d.ts +2 -0
  26. package/dist/app/input/input-action-controller.js +8 -1
  27. package/dist/app/input/input-controller.d.ts +2 -1
  28. package/dist/app/input/input-controller.js +7 -2
  29. package/dist/app/input/prompt-enhancer-controller.js +53 -35
  30. package/dist/app/input/voice-controller.d.ts +41 -45
  31. package/dist/app/input/voice-controller.js +351 -419
  32. package/dist/app/model/model-usage-controller.js +17 -7
  33. package/dist/app/model/model-usage-status.d.ts +4 -1
  34. package/dist/app/model/model-usage-status.js +270 -46
  35. package/dist/app/popup/menu-items-controller.d.ts +13 -3
  36. package/dist/app/popup/menu-items-controller.js +37 -21
  37. package/dist/app/popup/popup-action-controller.d.ts +12 -2
  38. package/dist/app/popup/popup-action-controller.js +77 -24
  39. package/dist/app/popup/popup-menu-controller.d.ts +36 -14
  40. package/dist/app/popup/popup-menu-controller.js +239 -69
  41. package/dist/app/rendering/dcp-stats.d.ts +9 -4
  42. package/dist/app/rendering/dcp-stats.js +40 -425
  43. package/dist/app/rendering/editor-panels.js +10 -4
  44. package/dist/app/rendering/popup-menu-renderer.d.ts +3 -5
  45. package/dist/app/rendering/popup-menu-renderer.js +42 -37
  46. package/dist/app/rendering/render-controller.js +23 -2
  47. package/dist/app/rendering/status-line-renderer.d.ts +5 -0
  48. package/dist/app/rendering/status-line-renderer.js +41 -5
  49. package/dist/app/rendering/tab-line-renderer.js +26 -20
  50. package/dist/app/runtime.d.ts +12 -1
  51. package/dist/app/runtime.js +120 -12
  52. package/dist/app/screen/mouse-controller.d.ts +2 -0
  53. package/dist/app/screen/mouse-controller.js +17 -7
  54. package/dist/app/screen/status-controller.d.ts +4 -0
  55. package/dist/app/screen/status-controller.js +5 -0
  56. package/dist/app/session/lazy-session-manager.js +34 -0
  57. package/dist/app/session/session-event-controller.d.ts +1 -0
  58. package/dist/app/session/session-event-controller.js +10 -1
  59. package/dist/app/session/session-history.d.ts +1 -0
  60. package/dist/app/session/session-history.js +12 -1
  61. package/dist/app/session/session-lifecycle-controller.d.ts +7 -1
  62. package/dist/app/session/session-lifecycle-controller.js +16 -1
  63. package/dist/app/session/tabs-controller.d.ts +26 -1
  64. package/dist/app/session/tabs-controller.js +454 -147
  65. package/dist/app/subagents/subagents-files.js +60 -1
  66. package/dist/app/subagents/subagents-model.d.ts +1 -0
  67. package/dist/app/subagents/subagents-model.js +18 -2
  68. package/dist/app/subagents/subagents-widget-controller.d.ts +1 -0
  69. package/dist/app/subagents/subagents-widget-controller.js +6 -0
  70. package/dist/app/types.d.ts +16 -1
  71. package/dist/app/workspace/workspace-actions-controller.js +10 -2
  72. package/dist/app/workspace/workspace-undo.d.ts +1 -0
  73. package/dist/app/workspace/workspace-undo.js +1 -0
  74. package/dist/bundled-extensions/question/index.js +9 -1
  75. package/dist/bundled-extensions/question/remote.d.ts +4 -0
  76. package/dist/bundled-extensions/question/remote.js +33 -0
  77. package/dist/bundled-extensions/telegram-connector/bot.d.ts +43 -0
  78. package/dist/bundled-extensions/telegram-connector/bot.js +166 -0
  79. package/dist/bundled-extensions/telegram-connector/config.d.ts +8 -0
  80. package/dist/bundled-extensions/telegram-connector/config.js +87 -0
  81. package/dist/bundled-extensions/telegram-connector/coordinator.d.ts +66 -0
  82. package/dist/bundled-extensions/telegram-connector/coordinator.js +413 -0
  83. package/dist/bundled-extensions/telegram-connector/index.d.ts +3 -0
  84. package/dist/bundled-extensions/telegram-connector/index.js +195 -0
  85. package/dist/bundled-extensions/terminal-bell/index.d.ts +0 -8
  86. package/dist/bundled-extensions/terminal-bell/index.js +0 -76
  87. package/dist/bundled-extensions/workspace-undo/index.d.ts +26 -0
  88. package/dist/bundled-extensions/workspace-undo/index.js +191 -0
  89. package/dist/config.d.ts +21 -2
  90. package/dist/config.js +159 -32
  91. package/dist/default-pix-config.js +25 -5
  92. package/dist/schemas/index.d.ts +1 -0
  93. package/dist/schemas/index.js +1 -0
  94. package/dist/schemas/pi-tools-suite-schema.d.ts +88 -62
  95. package/dist/schemas/pi-tools-suite-schema.js +54 -83
  96. package/dist/schemas/pix-schema.d.ts +19 -2
  97. package/dist/schemas/pix-schema.js +47 -5
  98. package/dist/schemas/tasks-schema.d.ts +18 -0
  99. package/dist/schemas/tasks-schema.js +41 -0
  100. package/docs/concurrency.md +9 -1
  101. package/docs/desktop-mvp.md +22 -6
  102. package/docs/desktop-task-manager.md +148 -84
  103. package/docs/release.md +30 -6
  104. package/external/pi-tools-suite/README.md +197 -84
  105. package/external/pi-tools-suite/docs/evals.md +1 -1
  106. package/external/pi-tools-suite/docs/session-recovery.md +47 -14
  107. package/external/pi-tools-suite/docs/subagent-model-pools.md +48 -30
  108. package/external/pi-tools-suite/docs/ui-qa-subagent.md +440 -0
  109. package/external/pi-tools-suite/package.json +6 -1
  110. package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +2 -1
  111. package/external/pi-tools-suite/src/antigravity-auth/constants.ts +9 -3
  112. package/external/pi-tools-suite/src/antigravity-auth/headers.ts +41 -3
  113. package/external/pi-tools-suite/src/antigravity-auth/models.ts +94 -21
  114. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +5 -17
  115. package/external/pi-tools-suite/src/antigravity-auth/payload.ts +72 -7
  116. package/external/pi-tools-suite/src/antigravity-auth/stream.ts +13 -1
  117. package/external/pi-tools-suite/src/async-subagents/agents/frontier-review.md +23 -0
  118. package/external/pi-tools-suite/src/async-subagents/agents/implement.md +1 -1
  119. package/external/pi-tools-suite/src/async-subagents/agents/presets.jsonc +16 -0
  120. package/external/pi-tools-suite/src/async-subagents/agents/research.md +5 -3
  121. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/browser.mjs +346 -0
  122. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/desktop.mjs +1038 -0
  123. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/tui.mjs +759 -0
  124. package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/scripts/browser-qa-runner.mjs +32 -23
  125. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/chrome-devtools/chrome-devtools-provider.mjs +895 -0
  126. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/linux/linux-atspi.py +494 -0
  127. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/macos/macos-accessibility.swift +1085 -0
  128. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/native-terminal/bridge-client.mjs +50 -0
  129. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/native-terminal/native-terminal-host.mjs +501 -0
  130. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/windows/windows-uia.ps1 +454 -0
  131. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-auth.md +114 -0
  132. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-chrome-devtools.md +91 -0
  133. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-playwright.md +145 -0
  134. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser.md +82 -0
  135. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-linux.md +26 -0
  136. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-macos.md +29 -0
  137. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-windows.md +23 -0
  138. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop.md +54 -0
  139. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui-native-terminal.md +59 -0
  140. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui-pty.md +37 -0
  141. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui.md +60 -0
  142. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/scripts/ui-qa-runner.mjs +535 -0
  143. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa.md +75 -0
  144. package/external/pi-tools-suite/src/async-subagents/commands.ts +15 -71
  145. package/external/pi-tools-suite/src/async-subagents/core/activity.ts +33 -0
  146. package/external/pi-tools-suite/src/async-subagents/core/agent-catalog.ts +5 -4
  147. package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +1 -1
  148. package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +8 -5
  149. package/external/pi-tools-suite/src/async-subagents/core/browser-qa.ts +16 -2
  150. package/external/pi-tools-suite/src/async-subagents/core/config.ts +160 -327
  151. package/external/pi-tools-suite/src/async-subagents/core/model-selection.ts +2 -1
  152. package/external/pi-tools-suite/src/async-subagents/core/registry.ts +158 -33
  153. package/external/pi-tools-suite/src/async-subagents/core/routing.ts +21 -5
  154. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +40 -26
  155. package/external/pi-tools-suite/src/async-subagents/core/state.ts +40 -0
  156. package/external/pi-tools-suite/src/async-subagents/core/types.ts +7 -0
  157. package/external/pi-tools-suite/src/async-subagents/core/ultrawork-auto.ts +51 -45
  158. package/external/pi-tools-suite/src/async-subagents/index.ts +69 -5
  159. package/external/pi-tools-suite/src/async-subagents/lib.ts +16 -9
  160. package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +8 -5
  161. package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +5 -4
  162. package/external/pi-tools-suite/src/async-subagents/types.ts +1 -0
  163. package/external/pi-tools-suite/src/coding-discipline/index.ts +90 -59
  164. package/external/pi-tools-suite/src/config.ts +55 -1
  165. package/external/pi-tools-suite/src/context-gateway/accounting-log.ts +282 -0
  166. package/external/pi-tools-suite/src/context-gateway/config.ts +77 -3
  167. package/external/pi-tools-suite/src/context-gateway/efficiency.ts +414 -0
  168. package/external/pi-tools-suite/src/context-gateway/enforcement.ts +222 -0
  169. package/external/pi-tools-suite/src/context-gateway/index.ts +206 -19
  170. package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +7 -7
  171. package/external/pi-tools-suite/src/context-gateway/telemetry.ts +101 -28
  172. package/external/pi-tools-suite/src/context-gateway/types.ts +16 -3
  173. package/external/pi-tools-suite/src/context-inventory.ts +99 -0
  174. package/external/pi-tools-suite/src/dcp/auto-compress-budget.ts +39 -4
  175. package/external/pi-tools-suite/src/dcp/auto-compress.ts +128 -49
  176. package/external/pi-tools-suite/src/dcp/commands.ts +32 -156
  177. package/external/pi-tools-suite/src/dcp/compress-tool.ts +177 -46
  178. package/external/pi-tools-suite/src/dcp/compression-blocks.ts +6 -58
  179. package/external/pi-tools-suite/src/dcp/compression-preview.ts +9 -0
  180. package/external/pi-tools-suite/src/dcp/compression-progress.ts +22 -0
  181. package/external/pi-tools-suite/src/dcp/config.ts +114 -17
  182. package/external/pi-tools-suite/src/dcp/conversation-index.ts +36 -9
  183. package/external/pi-tools-suite/src/dcp/diagnostics.ts +41 -0
  184. package/external/pi-tools-suite/src/dcp/fresh-tool-results.ts +38 -0
  185. package/external/pi-tools-suite/src/dcp/index.ts +338 -66
  186. package/external/pi-tools-suite/src/dcp/journal.ts +126 -4
  187. package/external/pi-tools-suite/src/dcp/progress-controller.ts +2 -1
  188. package/external/pi-tools-suite/src/dcp/prompts.ts +83 -192
  189. package/external/pi-tools-suite/src/dcp/protected-continuity.ts +175 -0
  190. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +93 -3
  191. package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +6 -10
  192. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +36 -33
  193. package/external/pi-tools-suite/src/dcp/pruner-tools.ts +6 -3
  194. package/external/pi-tools-suite/src/dcp/pruner.ts +1 -0
  195. package/external/pi-tools-suite/src/dcp/routine-pressure.ts +86 -0
  196. package/external/pi-tools-suite/src/dcp/state.ts +9 -0
  197. package/external/pi-tools-suite/src/dcp/statistics.d.ts +8 -0
  198. package/external/pi-tools-suite/src/dcp/statistics.js +156 -0
  199. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +78 -22
  200. package/external/pi-tools-suite/src/index.ts +7 -1
  201. package/external/pi-tools-suite/src/lib/project.ts +36 -1
  202. package/external/pi-tools-suite/src/model-tools/index.ts +10 -7
  203. package/external/pi-tools-suite/src/repo-discovery/index.ts +304 -4
  204. package/external/pi-tools-suite/src/resource-registry/index.ts +2551 -0
  205. package/external/pi-tools-suite/src/session-recovery/index.ts +17 -0
  206. package/external/pi-tools-suite/src/shell-command-policy.ts +219 -0
  207. package/external/pi-tools-suite/src/todo/index.ts +21 -0
  208. package/external/pi-tools-suite/src/todo/todo.ts +19 -1
  209. package/external/pi-tools-suite/src/tool-descriptions.ts +47 -19
  210. package/package.json +10 -9
  211. package/schemas/pi-tools-suite.json +466 -287
  212. package/schemas/pix.json +129 -11
  213. package/schemas/tasks.json +131 -0
  214. package/skills/simplify/SKILL.md +33 -5
  215. package/docs/desktop-markdown-media.md +0 -77
  216. package/external/pi-tools-suite/docs/browser-qa-subagent.md +0 -177
  217. package/external/pi-tools-suite/docs/dcp-emergency-current-turn.md +0 -102
  218. package/external/pi-tools-suite/src/async-subagents/agents/browser-qa.md +0 -598
  219. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +0 -54
  220. package/external/pi-tools-suite/src/skill-installer/index.ts +0 -333
  221. package/skills/playwright-cli/SKILL.md +0 -420
  222. package/skills/playwright-cli/references/element-attributes.md +0 -23
  223. package/skills/playwright-cli/references/playwright-tests.md +0 -50
  224. package/skills/playwright-cli/references/request-mocking.md +0 -87
  225. package/skills/playwright-cli/references/running-code.md +0 -241
  226. package/skills/playwright-cli/references/session-management.md +0 -273
  227. package/skills/playwright-cli/references/spec-driven-testing.md +0 -311
  228. package/skills/playwright-cli/references/storage-state.md +0 -290
  229. package/skills/playwright-cli/references/test-generation.md +0 -142
  230. package/skills/playwright-cli/references/tracing.md +0 -154
  231. package/skills/playwright-cli/references/video-recording.md +0 -147
  232. package/skills/spec-lite/SKILL.md +0 -140
  233. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/examples/qa-auth.example.jsonc +0 -0
  234. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/examples/qa-flow.example.jsonc +0 -0
  235. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/vendor/fflate.LICENSE +0 -0
  236. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/vendor/fflate.mjs +0 -0
@@ -1,7 +1,7 @@
1
1
  import { colorLine } from "../../theme.js";
2
2
  import { stringDisplayWidth } from "../../terminal-width.js";
3
3
  import { resolveColor, resolveModelColor } from "../../config.js";
4
- import { SLASH_COMMAND_DESCRIPTION_COLUMN, THINKING_LEVELS, } from "../constants.js";
4
+ import { SLASH_COMMAND_DESCRIPTION_COLUMN, } from "../constants.js";
5
5
  import { APP_ICONS } from "../icons.js";
6
6
  import { ellipsizeDisplay, padOrTrimPlain, sanitizeText } from "./render-text.js";
7
7
  import { modelProviderThemeColor, thinkingLevelThemeColor } from "./status-line-renderer.js";
@@ -96,8 +96,8 @@ export class PopupMenuRenderer {
96
96
  }
97
97
  return lines;
98
98
  }
99
- renderModelMenu(width, menu) {
100
- const lines = [this.popupMenuHeader("Select model", width)];
99
+ renderModelMenu(width, menu, state) {
100
+ const lines = [this.popupMenuHeader(state.visibilityMode ? "Manage visible models" : "Select model & thinking", width)];
101
101
  const visibleItems = menu.visibleItems();
102
102
  if (!this.hasPopupActionItems(menu.items)) {
103
103
  lines.push({
@@ -106,37 +106,55 @@ export class PopupMenuRenderer {
106
106
  });
107
107
  }
108
108
  for (const item of visibleItems) {
109
- const marker = item.selected ? "▶ " : " ";
110
- const text = `${marker}${this.labelDescriptionText(item.label, item.description, width - 2)}`;
109
+ const selectionMarker = item.selected ? "▶ " : " ";
110
+ const visibilityMarker = state.visibilityMode ? `${item.value.visible === false ? "○" : APP_ICONS.check} ` : "";
111
+ const marker = `${selectionMarker}${visibilityMarker}`;
112
+ const text = `${marker}${this.labelDescriptionText(item.label, item.description, width - marker.length)}`;
111
113
  lines.push({
112
114
  text,
113
115
  variant: this.selectableItemVariant(item.value),
114
- segments: [...this.modelMenuItemSegments(item.value), ...this.itemHighlightSegments(item, text)],
116
+ segments: [
117
+ ...this.modelMenuItemSegments(item.value, marker.length),
118
+ ...this.itemHighlightSegments(item, text, marker.length),
119
+ ],
115
120
  target: { kind: "popup-menu", index: item.index },
116
121
  });
117
122
  }
118
- return lines;
119
- }
120
- renderThinkingMenu(width, menu) {
121
- const lines = [this.popupMenuHeader("Thinking level", width)];
122
- const visibleItems = menu.visibleItems();
123
- if (!this.hasPopupActionItems(menu.items)) {
124
- lines.push({ text: " No matching thinking levels", variant: "muted" });
123
+ if (state.visibilityMode && menu.selectedItem()) {
124
+ const selected = menu.selectedItem().value;
125
+ lines.push({
126
+ text: selected.current
127
+ ? " Current model stays visible until another model is selected"
128
+ : " Enter toggles whether this model appears in model pickers",
129
+ variant: "muted",
130
+ });
131
+ lines.push({
132
+ text: " Clear all",
133
+ variant: "error",
134
+ target: { kind: "model-visibility-clear" },
135
+ });
136
+ lines.push({ text: " ↑/↓ model · Enter show/hide · Shift+Tab select", variant: "muted" });
125
137
  }
126
- for (const item of visibleItems) {
127
- const marker = item.selected ? "▶ " : " ";
128
- const text = `${marker}${this.labelDescriptionText(item.label, item.description, width - 2)}`;
138
+ else if (menu.selectedItem()) {
139
+ const thinkingPrefix = " Thinking ← ";
140
+ const thinkingSuffix = " →";
129
141
  lines.push({
130
- text,
131
- variant: this.selectableItemVariant(item.value),
132
- segments: this.thinkingMenuItemSegments(item.value),
133
- target: { kind: "popup-menu", index: item.index },
142
+ text: `${thinkingPrefix}${state.thinkingLevel}${thinkingSuffix}`,
143
+ variant: "normal",
144
+ segments: [{
145
+ start: thinkingPrefix.length,
146
+ end: thinkingPrefix.length + state.thinkingLevel.length,
147
+ foreground: thinkingLevelThemeColor(state.thinkingLevel, this.host.theme.colors, state.availableThinkingLevels),
148
+ bold: true,
149
+ }],
134
150
  });
151
+ lines.push({ text: " ↑/↓ model · ←/→ thinking · Enter apply · Shift+Tab manage", variant: "muted" });
135
152
  }
136
153
  return lines;
137
154
  }
138
155
  renderResumeMenu(width, menu, state) {
139
- const title = this.host.resumeLoading ? `Resume session ${APP_ICONS.timerSand}` : "Resume session";
156
+ const titleBase = state.draft ? "Open a conversation" : "Resume session";
157
+ const title = this.host.resumeLoading ? `${titleBase} ${APP_ICONS.timerSand}` : titleBase;
140
158
  const lines = [this.popupMenuHeader(title, width)];
141
159
  const visibleItems = menu.visibleItems();
142
160
  if (!this.host.resumeLoading && !this.hasPopupActionItems(menu.items)) {
@@ -261,16 +279,7 @@ export class PopupMenuRenderer {
261
279
  selectableItemVariant(value) {
262
280
  return value.current ? "muted" : "normal";
263
281
  }
264
- thinkingMenuItemSegments(value) {
265
- const markerOffset = 2; // "▶ " or " "
266
- return [{
267
- start: markerOffset,
268
- end: markerOffset + value.level.length,
269
- foreground: thinkingLevelThemeColor(value.level, this.host.theme.colors, this.availableThinkingLevels()),
270
- }];
271
- }
272
- modelMenuItemSegments(value) {
273
- const markerOffset = 2; // "▶ " or " "
282
+ modelMenuItemSegments(value, markerOffset = 2) {
274
283
  return [{
275
284
  start: markerOffset,
276
285
  end: markerOffset + value.ref.length,
@@ -285,10 +294,6 @@ export class PopupMenuRenderer {
285
294
  ? resolveColor(configuredColor, this.host.theme.colors)
286
295
  : modelProviderThemeColor(value.model.provider, this.host.theme.colors);
287
296
  }
288
- availableThinkingLevels() {
289
- const levels = this.host.session?.getAvailableThinkingLevels();
290
- return Array.isArray(levels) && levels.length > 0 ? levels.map(String) : [...THINKING_LEVELS];
291
- }
292
297
  queueMessageItemVariant(value) {
293
298
  return value === "cancel" ? "error" : "normal";
294
299
  }
@@ -301,8 +306,8 @@ export class PopupMenuRenderer {
301
306
  ...this.descriptionHighlightSegments(item.description, item.descriptionHighlightRanges ?? item.value.descriptionHighlightRanges ?? [], text),
302
307
  ];
303
308
  }
304
- itemHighlightSegments(item, text) {
305
- return this.highlightSegments(item.labelHighlightRanges ?? [], text, 2);
309
+ itemHighlightSegments(item, text, markerOffset = 2) {
310
+ return this.highlightSegments(item.labelHighlightRanges ?? [], text, markerOffset);
306
311
  }
307
312
  highlightSegments(ranges, text, markerOffset) {
308
313
  if (ranges.length === 0)
@@ -52,6 +52,7 @@ export class AppRenderController {
52
52
  const defaultOverlayLines = menuLines.slice(0, Math.max(0, inputSeparatorRow - 1));
53
53
  const underTabsOverlayStartRow = Math.min(rows, topReservedRows + 1);
54
54
  const underTabsOverlayLines = menuLines.slice(0, Math.max(0, statusRow - underTabsOverlayStartRow));
55
+ const draftSurfaceLines = menuLines.slice(0, Math.max(0, bodyHeight));
55
56
  const { lines: visible, metrics: scrollMetrics } = this.deps.scrollController.conversationView(columns, bodyHeight);
56
57
  const conversationColumns = Math.max(1, Math.min(columns, scrollMetrics.viewportColumns));
57
58
  this.deps.mouseController.syncConversationSelectionForRender(scrollMetrics.start, bodyHeight, topReservedRows, conversationColumns);
@@ -104,9 +105,27 @@ export class AppRenderController {
104
105
  .filter((target) => target.kind === "new-tab")
105
106
  .map((target) => ({ ...target, row: tabRow })));
106
107
  }
108
+ const draftSurfaceMenu = popupMenuPlacement === "draft-surface"
109
+ ? this.deps.popupMenus.getActivePopupMenu(activePopupMenu ?? this.deps.popupMenus.syncActivePopupMenu() ?? "slash")
110
+ : undefined;
107
111
  for (let index = 0; index < bodyHeight; index += 1) {
108
- const rendered = visible[index];
109
112
  const row = toScreenRow(index + 1);
113
+ if (draftSurfaceMenu) {
114
+ const line = draftSurfaceLines[index];
115
+ if (line) {
116
+ const fallbackTarget = { kind: "popup-menu", index: draftSurfaceMenu.selectedIndex };
117
+ this.deps.mouseController.renderedTargets.set(row, line.target ?? fallbackTarget);
118
+ this.deps.mouseController.renderedRowTexts.set(row, this.deps.popupMenus.overlayPlainText(line, columns));
119
+ setRenderedBackground(row, line.backgroundOverride);
120
+ appendFrameOutput(row, this.renderFrameRow(row, this.deps.popupMenus.styleOverlayLine(row, line, columns)));
121
+ }
122
+ else {
123
+ this.deps.mouseController.renderedRowTexts.set(row, "");
124
+ appendFrameOutput(row, this.renderFrameRow(row, this.deps.screenStyler.styleBaseLine(row, undefined, columns)));
125
+ }
126
+ continue;
127
+ }
128
+ const rendered = visible[index];
110
129
  if (rendered?.target)
111
130
  this.deps.mouseController.renderedTargets.set(row, rendered.target);
112
131
  if (rendered?.imageTargets?.length)
@@ -117,7 +136,9 @@ export class AppRenderController {
117
136
  setRenderedBackground(row, rendered?.backgroundOverride);
118
137
  appendFrameOutput(row, this.renderFrameRow(row, this.deps.screenStyler.styleBaseLine(row, rendered, conversationColumns)));
119
138
  }
120
- const loadingConversationOverlay = this.renderConversationLoadingOverlay(this.deps.loadingConversationOverlayText?.(), conversationColumns, topReservedRows, bodyHeight);
139
+ const loadingConversationOverlay = popupMenuPlacement === "draft-surface"
140
+ ? undefined
141
+ : this.renderConversationLoadingOverlay(this.deps.loadingConversationOverlayText?.(), conversationColumns, topReservedRows, bodyHeight);
121
142
  if (loadingConversationOverlay) {
122
143
  this.deps.mouseController.renderedRowTexts.set(loadingConversationOverlay.row, loadingConversationOverlay.text);
123
144
  appendFrameOutput(loadingConversationOverlay.row, this.renderFrameRow(loadingConversationOverlay.row, loadingConversationOverlay.output));
@@ -7,6 +7,10 @@ export type StatusLineRendererHost = {
7
7
  readonly theme: Theme;
8
8
  readonly screenStyler: ScreenStyler;
9
9
  readonly session: AgentSession | undefined;
10
+ draftModelStatus?(): {
11
+ modelLabel: string;
12
+ thinkingLabel: string;
13
+ } | undefined;
10
14
  readonly modelColors?: ModelColorsConfig;
11
15
  readonly sessionActivity: SessionActivity;
12
16
  readonly statusDotBright: boolean;
@@ -76,6 +80,7 @@ export declare class StatusLineRenderer {
76
80
  private pushContextBarSegments;
77
81
  private modelUsageProgressColor;
78
82
  private modelProviderColor;
83
+ private modelRefColor;
79
84
  private thinkingLevelColor;
80
85
  private statusThinkingDisplayLabel;
81
86
  private availableThinkingLevels;
@@ -180,8 +180,16 @@ export class StatusLineRenderer {
180
180
  }
181
181
  modelTarget(statusText, row) {
182
182
  const session = this.host.session;
183
- if (!session)
184
- return undefined;
183
+ if (!session) {
184
+ const draft = this.host.draftModelStatus?.();
185
+ if (!draft)
186
+ return undefined;
187
+ const marker = `${draft.modelLabel} ${APP_ICONS.lightbulb} ${draft.thinkingLabel}`;
188
+ const startIndex = statusText.indexOf(marker);
189
+ if (startIndex < 0)
190
+ return undefined;
191
+ return { row, startColumn: startIndex + 1, endColumn: startIndex + draft.modelLabel.length + 1 };
192
+ }
185
193
  const label = this.host.statusModelLabel(session);
186
194
  const marker = `${label} ${this.statusThinkingDisplayLabel(session)} `;
187
195
  const startIndex = statusText.indexOf(marker);
@@ -191,8 +199,17 @@ export class StatusLineRenderer {
191
199
  }
192
200
  thinkingTarget(statusText, row) {
193
201
  const session = this.host.session;
194
- if (!session)
195
- return undefined;
202
+ if (!session) {
203
+ const draft = this.host.draftModelStatus?.();
204
+ if (!draft)
205
+ return undefined;
206
+ const marker = `${APP_ICONS.lightbulb} ${draft.thinkingLabel}`;
207
+ const markerIndex = statusText.indexOf(marker);
208
+ if (markerIndex < 0)
209
+ return undefined;
210
+ const startIndex = markerIndex + APP_ICONS.lightbulb.length + 1;
211
+ return { row, startColumn: startIndex + 1, endColumn: startIndex + draft.thinkingLabel.length + 1 };
212
+ }
196
213
  const label = this.statusThinkingDisplayLabel(session);
197
214
  const marker = ` ${label} ${this.host.formatContextUsagePercent(session)}`;
198
215
  const markerIndex = statusText.indexOf(marker);
@@ -327,8 +344,17 @@ export class StatusLineRenderer {
327
344
  if (layout.modelUsageLabel)
328
345
  this.pushModelUsageSegments(segments, statusText, layout.modelUsageLabel);
329
346
  const session = this.host.session;
330
- if (!session)
347
+ if (!session) {
348
+ const draft = this.host.draftModelStatus?.();
349
+ if (!draft)
350
+ return segments;
351
+ const modelStart = statusText.indexOf(`${draft.modelLabel} ${APP_ICONS.lightbulb} ${draft.thinkingLabel}`);
352
+ this.pushSegment(segments, modelStart, draft.modelLabel.length, this.modelRefColor(draft.modelLabel));
353
+ const thinkingMarkerStart = statusText.indexOf(`${APP_ICONS.lightbulb} ${draft.thinkingLabel}`);
354
+ const thinkingStart = thinkingMarkerStart >= 0 ? thinkingMarkerStart + APP_ICONS.lightbulb.length + 1 : -1;
355
+ this.pushSegment(segments, thinkingStart, draft.thinkingLabel.length, this.thinkingLevelColor(draft.thinkingLabel));
331
356
  return segments;
357
+ }
332
358
  const modelLabel = this.host.statusModelLabel(session);
333
359
  const thinkingLabel = this.host.statusThinkingLabel(session);
334
360
  const thinkingDisplayLabel = this.statusThinkingDisplayLabel(session);
@@ -511,6 +537,16 @@ export class StatusLineRenderer {
511
537
  ? resolveColor(configuredColor, this.host.theme.colors)
512
538
  : modelProviderThemeColor(provider, this.host.theme.colors);
513
539
  }
540
+ modelRefColor(ref) {
541
+ const slash = ref.indexOf("/");
542
+ const provider = slash > 0 ? ref.slice(0, slash) : undefined;
543
+ if (!provider)
544
+ return this.host.theme.colors.selectionForeground;
545
+ const configuredColor = this.host.modelColors ? resolveModelColor(ref, this.host.modelColors) : undefined;
546
+ return configuredColor
547
+ ? resolveColor(configuredColor, this.host.theme.colors)
548
+ : modelProviderThemeColor(provider, this.host.theme.colors);
549
+ }
514
550
  thinkingLevelColor(label) {
515
551
  return thinkingLevelThemeColor(label, this.host.theme.colors, this.availableThinkingLevels());
516
552
  }
@@ -29,13 +29,15 @@ export class TabLineRenderer {
29
29
  const newTabPrefix = tabs.length > 0 ? separator : EMPTY_NEW_TAB_PREFIX;
30
30
  const newTabPrefixWidth = stringDisplayWidth(newTabPrefix);
31
31
  const tabsWidth = Math.max(0, width - newTabWidth - newTabPrefixWidth);
32
- const naturalButtons = tabs.map((tab) => this.buttonLayout(tab));
32
+ const naturalButtons = tabs.map((tab) => this.buttonLayout(tab, undefined, tabs.length > 1 || tab.draft !== true));
33
33
  const naturalWidth = naturalButtons.reduce((sum, button) => sum + stringDisplayWidth(button.text), 0)
34
34
  + separatorCount * separatorWidth;
35
35
  const buttonMaxWidth = naturalWidth <= tabsWidth
36
36
  ? undefined
37
37
  : Math.max(7, Math.floor(Math.max(1, tabsWidth - separatorCount * separatorWidth) / tabs.length));
38
- const buttons = buttonMaxWidth === undefined ? naturalButtons : tabs.map((tab) => this.buttonLayout(tab, buttonMaxWidth));
38
+ const buttons = buttonMaxWidth === undefined
39
+ ? naturalButtons
40
+ : tabs.map((tab) => this.buttonLayout(tab, buttonMaxWidth, tabs.length > 1 || tab.draft !== true));
39
41
  const segments = [];
40
42
  const targets = [];
41
43
  const separatorColumns = [];
@@ -61,15 +63,17 @@ export class TabLineRenderer {
61
63
  const buttonWidth = stringDisplayWidth(button.text);
62
64
  text += button.text;
63
65
  this.addButtonSegments(tab, button, textOffset, segments);
64
- const closeColumnOffset = stringDisplayWidth(button.text.slice(0, button.closeStart));
65
- // Keep the more specific close target before the tab target because the
66
- // tab target intentionally spans the whole button, including the close icon.
67
- targets.push({
68
- kind: "close",
69
- tabId: tab.id,
70
- startColumn: displayColumn + closeColumnOffset,
71
- endColumn: displayColumn + closeColumnOffset + stringDisplayWidth(button.text.slice(button.closeStart, button.closeEnd)),
72
- });
66
+ if (button.closable) {
67
+ const closeColumnOffset = stringDisplayWidth(button.text.slice(0, button.closeStart));
68
+ // Keep the more specific close target before the tab target because the
69
+ // tab target intentionally spans the whole button, including the close icon.
70
+ targets.push({
71
+ kind: "close",
72
+ tabId: tab.id,
73
+ startColumn: displayColumn + closeColumnOffset,
74
+ endColumn: displayColumn + closeColumnOffset + stringDisplayWidth(button.text.slice(button.closeStart, button.closeEnd)),
75
+ });
76
+ }
73
77
  targets.push({
74
78
  kind: "tab",
75
79
  tabId: tab.id,
@@ -142,31 +146,33 @@ export class TabLineRenderer {
142
146
  }
143
147
  return chars.join("");
144
148
  }
145
- buttonLayout(tab, maxWidth) {
149
+ buttonLayout(tab, maxWidth, closable = true) {
146
150
  const statusText = this.statusIndicatorIcon(tab);
147
151
  const prefix = `${statusText} `;
148
- const suffix = ` ${APP_ICONS.close}`;
152
+ const suffix = closable ? ` ${APP_ICONS.close}` : "";
149
153
  const title = this.displayTitle(tab);
150
154
  const naturalText = `${prefix}${title}${suffix}`;
151
155
  const naturalWidth = stringDisplayWidth(naturalText);
152
156
  if (maxWidth === undefined || naturalWidth <= maxWidth)
153
- return this.buttonLayoutFromText(naturalText, 0, statusText.length);
157
+ return this.buttonLayoutFromText(naturalText, 0, statusText.length, closable);
154
158
  const titleWidth = maxWidth - stringDisplayWidth(prefix) - stringDisplayWidth(suffix);
155
- if (titleWidth <= 0)
156
- return this.buttonLayoutFromText(ellipsizeDisplay(`${statusText}${APP_ICONS.close}`, maxWidth), 0, statusText.length);
157
- return this.buttonLayoutFromText(`${prefix}${ellipsizeDisplay(title, titleWidth)}${suffix}`, 0, statusText.length);
159
+ if (titleWidth <= 0) {
160
+ return this.buttonLayoutFromText(ellipsizeDisplay(`${statusText}${closable ? APP_ICONS.close : ""}`, maxWidth), 0, statusText.length, closable);
161
+ }
162
+ return this.buttonLayoutFromText(`${prefix}${ellipsizeDisplay(title, titleWidth)}${suffix}`, 0, statusText.length, closable);
158
163
  }
159
164
  displayTitle(tab) {
160
165
  return tab.isFork ? `${APP_ICONS.fork}${tab.title}` : tab.title;
161
166
  }
162
- buttonLayoutFromText(text, statusStart, statusLength) {
163
- const closeStart = Math.max(0, text.lastIndexOf(APP_ICONS.close));
167
+ buttonLayoutFromText(text, statusStart, statusLength, closable) {
168
+ const closeStart = closable ? Math.max(0, text.lastIndexOf(APP_ICONS.close)) : text.length;
164
169
  return {
165
170
  text,
166
171
  statusStart,
167
172
  statusEnd: Math.min(text.length, statusStart + statusLength),
168
173
  closeStart,
169
- closeEnd: closeStart + APP_ICONS.close.length,
174
+ closeEnd: closable ? closeStart + APP_ICONS.close.length : closeStart,
175
+ closable,
170
176
  };
171
177
  }
172
178
  addButtonSegments(tab, button, textOffset, segments) {
@@ -1,6 +1,6 @@
1
1
  import { SessionManager, type EventBus, type AgentSessionRuntime, type AgentSessionServices, type LoadExtensionsResult, type SessionEntry } from "@earendil-works/pi-coding-agent";
2
2
  import { type PixConfig } from "../config.js";
3
- import type { AppOptions, ThinkingLevel } from "./types.js";
3
+ import type { AppOptions, SessionModel, ThinkingLevel } from "./types.js";
4
4
  export type PiToolsSuiteInstallAction = "installed" | "already-installed" | "existing-kept" | "missing-source";
5
5
  export type PiToolsSuiteInstallResult = {
6
6
  action: PiToolsSuiteInstallAction;
@@ -28,6 +28,7 @@ export type BundledSkillsInstallOptions = {
28
28
  export declare function bundledQuestionExtensionPath(): string;
29
29
  export declare function bundledSessionTitleExtensionPath(): string;
30
30
  export declare function bundledTerminalBellExtensionPath(): string;
31
+ export declare function bundledTelegramConnectorExtensionPath(): string;
31
32
  export declare function piToolsSuiteExtensionSourcePath(): string;
32
33
  export declare function piToolsSuiteExtensionInstallPath(agentDir?: string): string;
33
34
  export declare function bundledSkillsSourcePath(): string;
@@ -44,8 +45,18 @@ export type CreatePixRuntimeOptions = {
44
45
  };
45
46
  type RuntimeSessionManagerModelState = Pick<SessionManager, "getEntries" | "getBranch">;
46
47
  export declare function resolvePixRuntimeModelRef(options: Pick<AppOptions, "modelRef">, sessionManager: RuntimeSessionManagerModelState, config?: PixConfig): string | undefined;
48
+ export declare function resolvePixRuntimeModelRefs(options: Pick<AppOptions, "modelRef">, sessionManager: RuntimeSessionManagerModelState, config?: PixConfig): string[];
47
49
  export declare function resolvePixRuntimeInitialThinkingLevel(options: Pick<AppOptions, "modelRef">, sessionManager: RuntimeSessionManagerModelState, config: PixConfig): ThinkingLevel | undefined;
48
50
  export declare function resolveSessionModelRefFromTail(entries: readonly SessionEntry[]): string | undefined;
49
51
  export declare function refreshPixModelRuntimeForStartup(modelRuntime: Pick<AgentSessionServices["modelRuntime"], "refresh">): Promise<void>;
52
+ /**
53
+ * Build the model catalogue used by UI-only draft tabs without allocating an
54
+ * AgentSession or session file. Drafts can therefore expose model selection
55
+ * while preserving lazy session materialization until the first prompt.
56
+ */
57
+ export declare function createPixDraftModelCatalog(options: {
58
+ cwd: string;
59
+ agentDir?: string;
60
+ }): Promise<SessionModel[]>;
50
61
  export declare function createPixRuntime(options: AppOptions, runtimeOptions?: CreatePixRuntimeOptions): Promise<AgentSessionRuntime>;
51
62
  export {};
@@ -1,9 +1,9 @@
1
1
  import { existsSync } from "node:fs";
2
- import { access, cp, lstat, mkdir, readdir, readlink, realpath, rm, symlink } from "node:fs/promises";
2
+ import { access, cp, lstat, mkdir, readFile, readdir, readlink, realpath, rm, symlink } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
- import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, getAgentDir, SessionManager, } from "@earendil-works/pi-coding-agent";
6
+ import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, getAgentDir, ModelRuntime, SessionManager, } from "@earendil-works/pi-coding-agent";
7
7
  import { loadPixConfig, resolveDefaultModelRef } from "../config.js";
8
8
  import { isThinkingLevel, parseModelRef, parseScopedModelRef } from "./model/model-ref.js";
9
9
  import { openLazySessionManager } from "./session/lazy-session-manager.js";
@@ -13,6 +13,7 @@ const BUNDLED_EXTENSIONS_DIR = resolve(dirname(fileURLToPath(import.meta.url)),
13
13
  const BUNDLED_QUESTION_EXTENSION_DIR = resolve(BUNDLED_EXTENSIONS_DIR, BUNDLED_QUESTION_EXTENSION_NAME);
14
14
  const BUNDLED_SESSION_TITLE_EXTENSION_DIR = resolve(BUNDLED_EXTENSIONS_DIR, "session-title");
15
15
  const BUNDLED_TERMINAL_BELL_EXTENSION_DIR = resolve(BUNDLED_EXTENSIONS_DIR, "terminal-bell");
16
+ const BUNDLED_TELEGRAM_CONNECTOR_EXTENSION_DIR = resolve(BUNDLED_EXTENSIONS_DIR, "telegram-connector");
16
17
  const PI_TOOLS_SUITE_SOURCE_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "../..", "external", PI_TOOLS_SUITE_EXTENSION_NAME);
17
18
  const BUNDLED_SKILLS_SOURCE_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "../..", "skills");
18
19
  export function bundledQuestionExtensionPath() {
@@ -24,6 +25,9 @@ export function bundledSessionTitleExtensionPath() {
24
25
  export function bundledTerminalBellExtensionPath() {
25
26
  return BUNDLED_TERMINAL_BELL_EXTENSION_DIR;
26
27
  }
28
+ export function bundledTelegramConnectorExtensionPath() {
29
+ return BUNDLED_TELEGRAM_CONNECTOR_EXTENSION_DIR;
30
+ }
27
31
  export function piToolsSuiteExtensionSourcePath() {
28
32
  return PI_TOOLS_SUITE_SOURCE_DIR;
29
33
  }
@@ -60,8 +64,22 @@ export async function ensurePiToolsSuiteExtensionInstalled(options = {}) {
60
64
  if (await pathsReferToSameEntry(targetPath, sourcePath)) {
61
65
  return { action: "already-installed", sourcePath, targetPath };
62
66
  }
67
+ if (targetStat.isDirectory() && await isLegacyManagedPiToolsSuite(targetPath)) {
68
+ await rm(targetPath, { recursive: true, force: true });
69
+ await symlink(sourcePath, targetPath, extensionSymlinkType());
70
+ return { action: "installed", sourcePath, targetPath };
71
+ }
63
72
  return { action: "existing-kept", sourcePath, targetPath };
64
73
  }
74
+ async function isLegacyManagedPiToolsSuite(targetPath) {
75
+ try {
76
+ const packageJson = JSON.parse(await readFile(join(targetPath, "package.json"), "utf8"));
77
+ return packageJson.name === "pi-tools-suite-local";
78
+ }
79
+ catch {
80
+ return false;
81
+ }
82
+ }
65
83
  export async function ensureBundledSkillsInstalled(options = {}) {
66
84
  const sourcePath = resolve(options.sourcePath ?? bundledSkillsSourcePath());
67
85
  const targetPath = resolve(options.targetPath ?? bundledSkillsInstallPath(options.homeDir));
@@ -98,6 +116,7 @@ export function getBundledExtensionPaths() {
98
116
  bundledQuestionExtensionPath(),
99
117
  bundledSessionTitleExtensionPath(),
100
118
  bundledTerminalBellExtensionPath(),
119
+ bundledTelegramConnectorExtensionPath(),
101
120
  ].filter(extensionEntryExists);
102
121
  }
103
122
  export async function getBundledExtensionPathsAsync() {
@@ -105,6 +124,7 @@ export async function getBundledExtensionPathsAsync() {
105
124
  bundledQuestionExtensionPath(),
106
125
  bundledSessionTitleExtensionPath(),
107
126
  bundledTerminalBellExtensionPath(),
127
+ bundledTelegramConnectorExtensionPath(),
108
128
  ].map(async (extensionPath) => await extensionEntryExistsAsync(extensionPath) ? extensionPath : undefined));
109
129
  return paths.filter((path) => path !== undefined);
110
130
  }
@@ -199,12 +219,32 @@ async function ensurePiToolsSuiteExtensionInstalledOnce(options = {}) {
199
219
  return await pending;
200
220
  }
201
221
  export function resolvePixRuntimeModelRef(options, sessionManager, config = loadPixConfig()) {
222
+ return resolvePixRuntimeModelRefs(options, sessionManager, config)[0];
223
+ }
224
+ export function resolvePixRuntimeModelRefs(options, sessionManager, config = loadPixConfig()) {
202
225
  if (options.modelRef)
203
- return options.modelRef;
226
+ return [options.modelRef];
204
227
  const existingEntryCount = sessionManager.getEntries().length;
205
- if (existingEntryCount > 0)
206
- return resolveSessionModelRefFromTail(sessionManager.getBranch());
207
- return resolveDefaultModelRef(config);
228
+ if (existingEntryCount > 0) {
229
+ const sessionRef = resolveSessionModelRefFromTail(sessionManager.getBranch());
230
+ return sessionRef ? [sessionRef] : [];
231
+ }
232
+ const primary = resolveDefaultModelRef(config);
233
+ if (!primary)
234
+ return [];
235
+ const defaultThinking = config.defaultModel?.thinking;
236
+ const fallbacks = (config.defaultModel?.fallbackModels ?? []).map((modelRef) => {
237
+ const trimmed = modelRef.trim();
238
+ if (!trimmed || !defaultThinking)
239
+ return trimmed;
240
+ try {
241
+ return parseModelRef(trimmed).thinkingLevel === undefined ? `${trimmed}:${defaultThinking}` : trimmed;
242
+ }
243
+ catch {
244
+ return trimmed;
245
+ }
246
+ });
247
+ return [...new Set([primary, ...fallbacks].map((ref) => ref.trim()).filter(Boolean))];
208
248
  }
209
249
  export function resolvePixRuntimeInitialThinkingLevel(options, sessionManager, config) {
210
250
  const effectiveModelRef = resolvePixRuntimeModelRef(options, sessionManager, config);
@@ -239,14 +279,52 @@ export async function refreshPixModelRuntimeForStartup(modelRuntime) {
239
279
  // refreshes belong to explicit model-management flows and must not block boot.
240
280
  await modelRuntime.refresh({ allowNetwork: false });
241
281
  }
282
+ /**
283
+ * Build the model catalogue used by UI-only draft tabs without allocating an
284
+ * AgentSession or session file. Drafts can therefore expose model selection
285
+ * while preserving lazy session materialization until the first prompt.
286
+ */
287
+ export async function createPixDraftModelCatalog(options) {
288
+ const agentDir = options.agentDir ?? getAgentDir();
289
+ await ensurePiToolsSuiteExtensionInstalledOnce({ agentDir });
290
+ const bundledExtensionPaths = await getBundledExtensionPathsAsync();
291
+ const modelRuntime = await ModelRuntime.create({
292
+ authPath: join(agentDir, "auth.json"),
293
+ modelsPath: join(agentDir, "models.json"),
294
+ allowModelNetwork: false,
295
+ refreshOnCreate: false,
296
+ });
297
+ const services = await createAgentSessionServices({
298
+ cwd: options.cwd,
299
+ agentDir,
300
+ modelRuntime,
301
+ resourceLoaderOptions: {
302
+ noContextFiles: true,
303
+ noSkills: true,
304
+ noPromptTemplates: true,
305
+ noThemes: true,
306
+ ...(bundledExtensionPaths.length === 0 ? {} : {
307
+ additionalExtensionPaths: bundledExtensionPaths,
308
+ extensionsOverride: prioritizeBundledQuestionExtension,
309
+ }),
310
+ },
311
+ });
312
+ try {
313
+ return [...services.modelRuntime.getAvailableSnapshot()];
314
+ }
315
+ finally {
316
+ // Draft discovery needs extension provider registrations, but it does not
317
+ // own a session. Invalidate the temporary extension runtime so event-bus
318
+ // subscriptions and captured ctx/pi handles cannot outlive discovery.
319
+ services.resourceLoader.getExtensions().runtime.invalidate("Draft model catalog discovery completed");
320
+ }
321
+ }
242
322
  export async function createPixRuntime(options, runtimeOptions = {}) {
243
323
  const agentDir = getAgentDir();
244
324
  const reusableServices = reusableRuntimeServices(runtimeOptions.reuseServicesFrom, options.cwd, agentDir);
245
325
  const createRuntime = async ({ cwd, sessionManager, sessionStartEvent }) => {
246
326
  const config = runtimeOptions.config ?? loadPixConfig(cwd);
247
- const effectiveModelRef = resolvePixRuntimeModelRef(options, sessionManager, config);
248
- const parsedModel = effectiveModelRef ? parseModelRef(effectiveModelRef) : undefined;
249
- const initialThinkingLevel = resolvePixRuntimeInitialThinkingLevel(options, sessionManager, config);
327
+ const modelRefs = resolvePixRuntimeModelRefs(options, sessionManager, config);
250
328
  // Only reuse services for the initial session. Session replacements
251
329
  // (switchSession/newSession/fork) must get fresh services so extensions
252
330
  // are re-loaded with a fresh pi — otherwise handlers capture the old,
@@ -261,10 +339,36 @@ export async function createPixRuntime(options, runtimeOptions = {}) {
261
339
  ...(runtimeOptions.eventBus === undefined ? {} : { eventBus: runtimeOptions.eventBus }),
262
340
  });
263
341
  await refreshPixModelRuntimeForStartup(services.modelRuntime);
264
- const model = parsedModel ? services.modelRuntime.getModel(parsedModel.provider, parsedModel.modelId) : undefined;
265
- if (parsedModel && !model) {
266
- throw new Error(`Model not found: ${parsedModel.provider}/${parsedModel.modelId}`);
342
+ let model;
343
+ let selectedRef;
344
+ let selectedThinkingLevel;
345
+ let primaryError;
346
+ for (let index = 0; index < modelRefs.length; index++) {
347
+ const modelRef = modelRefs[index];
348
+ let parsedModel;
349
+ try {
350
+ parsedModel = parseModelRef(modelRef);
351
+ }
352
+ catch (error) {
353
+ if (index === 0)
354
+ throw error;
355
+ continue;
356
+ }
357
+ const candidate = services.modelRuntime.getModel(parsedModel.provider, parsedModel.modelId);
358
+ if (!candidate) {
359
+ if (index === 0)
360
+ primaryError = new Error(`Model not found: ${parsedModel.provider}/${parsedModel.modelId}`);
361
+ continue;
362
+ }
363
+ model = candidate;
364
+ selectedRef = modelRef;
365
+ selectedThinkingLevel = parsedModel.thinkingLevel;
366
+ break;
367
+ }
368
+ if (modelRefs.length > 0 && !model) {
369
+ throw primaryError ?? new Error(`No configured model is available: ${modelRefs.join(", ")}`);
267
370
  }
371
+ const initialThinkingLevel = selectedThinkingLevel;
268
372
  const enabledModelRefs = services.settingsManager.getEnabledModels();
269
373
  const scopedModels = (enabledModelRefs ?? []).flatMap((modelRef) => {
270
374
  const scoped = parseScopedModelRef(modelRef);
@@ -288,9 +392,13 @@ export async function createPixRuntime(options, runtimeOptions = {}) {
288
392
  ...(initialThinkingLevel === undefined ? {} : { thinkingLevel: initialThinkingLevel }),
289
393
  ...(scopedModels.length === 0 ? {} : { scopedModels }),
290
394
  });
395
+ const modelFallbackMessage = selectedRef && selectedRef !== modelRefs[0]
396
+ ? `Default model ${modelRefs[0]} is unavailable; using fallback ${selectedRef}.`
397
+ : undefined;
291
398
  return {
292
399
  ...created,
293
400
  services,
401
+ ...(modelFallbackMessage ? { modelFallbackMessage } : {}),
294
402
  // Snapshot diagnostics per-runtime. When services are reused across
295
403
  // tabs (reuseServicesFrom), services.diagnostics is a shared array —
296
404
  // giving each runtime its own copy prevents one tab's creation
@@ -86,6 +86,8 @@ export declare class AppMouseController {
86
86
  index: number;
87
87
  } | {
88
88
  kind: "popup-menu-close";
89
+ } | {
90
+ kind: "model-visibility-clear";
89
91
  } | {
90
92
  kind: "todo-panel";
91
93
  } | {