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
@@ -7,6 +7,7 @@ import {
7
7
  getRunState,
8
8
  getRunRoot,
9
9
  getSubagentRegistryPath,
10
+ filterSubagentConfigForParentModel,
10
11
  isBlindModelRef,
11
12
  loadSubagentConfig,
12
13
  listSubagentSessionRecords,
@@ -14,9 +15,11 @@ import {
14
15
  removeSubagentRunsFromRegistry,
15
16
  stopAgents,
16
17
  type AgentCompletionHandler,
18
+ type RpcEventRecord,
17
19
  type StopSignal,
18
20
  type SubagentSessionRecord,
19
21
  } from "./lib.js";
22
+ import { activityFromRpcEvent } from "./core/activity.js";
20
23
  import { buildUltraworkPrompt, isUltraworkEnvEnabled, registerCommands } from "./commands.js";
21
24
  import { agentStrategyPrompt, appendAgentStrategyPrompt } from "./core/agent-strategy.js";
22
25
  import { buildSubagentCatalogPrompt } from "./core/agent-catalog.js";
@@ -41,6 +44,7 @@ function isTerminalAgentStatus(status: AgentState["status"]): boolean {
41
44
 
42
45
  const SUBAGENTS_LIVE_COUNT_EVENT = "pi-tools-suite:async-subagents:live-count";
43
46
  const SUBAGENTS_LIVE_STATE_EVENT = "pi-tools-suite:async-subagents:live-state";
47
+ export const SUBAGENTS_CATALOG_STATE_EVENT = "pi-tools-suite:async-subagents:catalog";
44
48
  const SESSION_SHUTDOWN_KILL_GRACE_MS = 500;
45
49
  const COMPLETION_WATCH_INTERVAL_MS = 2_000;
46
50
 
@@ -54,6 +58,14 @@ interface ShutdownPlan {
54
58
  runDirsToDelete: string[];
55
59
  }
56
60
 
61
+ interface SubagentCatalogStateEvent {
62
+ version: 1;
63
+ sessionId?: string;
64
+ sessionFile?: string;
65
+ model?: string;
66
+ types: string[];
67
+ }
68
+
57
69
  function createLiveStatePayload(
58
70
  liveAgents: Map<string, Map<string, LiveAgent>>,
59
71
  sessionFile: string | undefined,
@@ -65,7 +77,13 @@ function createLiveStatePayload(
65
77
  if (matchingLiveAgents.length === 0) continue;
66
78
  const agentIds = matchingLiveAgents.map((agent) => agent.agentId);
67
79
  const state = getRunState(runDir, agentIds, { includeLineCounts: false, checkRpcPromptFailure: false });
68
- const activeAgents = state.agents.filter((agent) => !isTerminalAgentStatus(agent.status));
80
+ const liveAgentsById = new Map(matchingLiveAgents.map((agent) => [agent.agentId, agent]));
81
+ const activeAgents = state.agents
82
+ .filter((agent) => !isTerminalAgentStatus(agent.status))
83
+ .map((agent) => {
84
+ const lastActivity = liveAgentsById.get(agent.id)?.lastActivity;
85
+ return lastActivity ? { ...agent, lastActivity } : agent;
86
+ });
69
87
  if (activeAgents.length === 0) continue;
70
88
  count += activeAgents.length;
71
89
  const tasks = matchingLiveAgents.map((agent) => agent.preview).filter((preview): preview is NonNullable<typeof preview> => Boolean(preview));
@@ -84,6 +102,25 @@ function createLiveStatePayload(
84
102
  };
85
103
  }
86
104
 
105
+ function createSubagentCatalogState(ctx: unknown): SubagentCatalogStateEvent {
106
+ const cwd = (ctx as { cwd?: string } | undefined)?.cwd ?? process.cwd();
107
+ const model = modelRefFromContext(ctx);
108
+ const config = safeLoadSubagentConfig(cwd);
109
+ const effective = config ? filterSubagentConfigForParentModel(config, model) : undefined;
110
+ const sessionManager = (ctx as {
111
+ sessionManager?: { getSessionId?: () => string; getSessionFile?: () => string | undefined };
112
+ } | undefined)?.sessionManager;
113
+ const sessionId = typeof sessionManager?.getSessionId === "function" ? sessionManager.getSessionId() : undefined;
114
+ const sessionFile = typeof sessionManager?.getSessionFile === "function" ? sessionManager.getSessionFile() : undefined;
115
+ return {
116
+ version: 1,
117
+ ...(sessionId ? { sessionId } : {}),
118
+ ...(sessionFile ? { sessionFile } : {}),
119
+ ...(model ? { model } : {}),
120
+ types: Object.keys(effective?.types ?? {}).sort(),
121
+ };
122
+ }
123
+
87
124
  function agentMatchesSession(agent: LiveAgent, sessionFile: string | undefined): boolean {
88
125
  if (!sessionFile) return true;
89
126
  return agent.parentSession !== undefined && pathsEqual(sessionFile, agent.parentSession);
@@ -106,6 +143,12 @@ export default function (pi: ExtensionAPI) {
106
143
  let completionWatchTimer: ReturnType<typeof setInterval> | undefined;
107
144
  publishSubagentPresetsStartupSection();
108
145
 
146
+ function publishSubagentCatalogState(ctx: unknown): void {
147
+ const state = createSubagentCatalogState(ctx);
148
+ pi.events?.emit?.(SUBAGENTS_CATALOG_STATE_EVENT, state);
149
+ publishRpcSessionState(ctx as Parameters<typeof publishRpcSessionState>[0], SUBAGENTS_CATALOG_STATE_EVENT, state);
150
+ }
151
+
109
152
  function refreshSubagentOverlay(): void {
110
153
  try {
111
154
  reconcileLiveAgentCompletions();
@@ -165,7 +208,16 @@ export default function (pi: ExtensionAPI) {
165
208
  refreshSubagentOverlay();
166
209
  };
167
210
 
168
- registerSubagentsTool(pi, liveAgents, handleAgentCompletion, refreshSubagentOverlay);
211
+ function handleAgentRpcEvent(runDir: string, agentId: string, event: RpcEventRecord): void {
212
+ const activity = activityFromRpcEvent(event);
213
+ if (!activity) return;
214
+ const liveAgent = liveAgents.get(runDir)?.get(agentId);
215
+ if (!liveAgent) return;
216
+ liveAgent.lastActivity = activity;
217
+ refreshSubagentOverlay();
218
+ }
219
+
220
+ registerSubagentsTool(pi, liveAgents, handleAgentCompletion, refreshSubagentOverlay, handleAgentRpcEvent);
169
221
  registerCommands(pi);
170
222
 
171
223
  pi.on("session_start", async (_event, ctx) => {
@@ -173,6 +225,7 @@ export default function (pi: ExtensionAPI) {
173
225
  sawAutoUltraworkCandidate = false;
174
226
  currentSessionFile = sessionFileFromContext(ctx);
175
227
  currentSessionStateContext = ctx;
228
+ publishSubagentCatalogState(ctx);
176
229
  subagentOverlay.restoreRunningAgents(ctx.cwd, currentSessionFile);
177
230
  refreshSubagentOverlay();
178
231
  } catch (error) {
@@ -180,6 +233,14 @@ export default function (pi: ExtensionAPI) {
180
233
  }
181
234
  });
182
235
 
236
+ pi.on("model_select", (_event, ctx) => {
237
+ try {
238
+ publishSubagentCatalogState(ctx);
239
+ } catch (error) {
240
+ ignoreStaleExtensionContextError(error);
241
+ }
242
+ });
243
+
183
244
  pi.on("tool_execution_end", async (event) => {
184
245
  if (event.toolName !== "subagents" && !event.toolName.startsWith("async_subagents_")) return;
185
246
  refreshSubagentOverlay();
@@ -192,7 +253,10 @@ export default function (pi: ExtensionAPI) {
192
253
  });
193
254
  const visionPrompt = visionCapabilityPrompt(event, ctx);
194
255
  const catalogPrompt = selectedToolsInclude(event, "subagents")
195
- ? subagentCatalogPrompt((ctx as { cwd?: string } | undefined)?.cwd ?? process.cwd())
256
+ ? subagentCatalogPrompt(
257
+ (ctx as { cwd?: string } | undefined)?.cwd ?? process.cwd(),
258
+ modelRefFromContext(ctx),
259
+ )
196
260
  : undefined;
197
261
  if (!strategyPrompt && !visionPrompt && !catalogPrompt) return undefined;
198
262
  let systemPrompt = event.systemPrompt ?? "";
@@ -286,9 +350,9 @@ function safeLoadSubagentConfig(cwd: string) {
286
350
  }
287
351
  }
288
352
 
289
- function subagentCatalogPrompt(cwd: string): string | undefined {
353
+ function subagentCatalogPrompt(cwd: string, parentModelRef?: string): string | undefined {
290
354
  const config = safeLoadSubagentConfig(cwd);
291
- return config ? buildSubagentCatalogPrompt(config) : undefined;
355
+ return config ? buildSubagentCatalogPrompt(config, parentModelRef) : undefined;
292
356
  }
293
357
 
294
358
  function startupSubagentPresetList(cwd = process.cwd()): string {
@@ -1,4 +1,5 @@
1
1
  export type {
2
+ AgentActivity,
2
3
  AgentCompletionHandler,
3
4
  AgentResult,
4
5
  AgentState,
@@ -12,21 +13,19 @@ export type {
12
13
  } from "./core/types.js";
13
14
 
14
15
  export { createRunDir, getRunRoot, hasAgentPrompt, hasLaunchedAgentPrompt, hasQueuedAgentPrompt, resolveRunDir, validateBasename } from "./core/paths.js";
15
- export type { CopySubagentConfigSampleResult, ModelByParentEntry, ResolvedAgentTaskConfig, ResolvedSubagentRoutingConfig, ResolveAgentTaskOptions, SubagentConfig, SubagentPreset, SubagentRoutingConfig, SubagentTypeConfig, SubagentVisionConfig } from "./core/config.js";
16
+ export type { ModelByParentEntry, ResolvedAgentTaskConfig, ResolvedSubagentRoutingConfig, ResolveAgentTaskOptions, SubagentConfig, SubagentPreset, SubagentRoutingConfig, SubagentTypeConfig, SubagentVisionConfig } from "./core/config.js";
16
17
  export {
17
- configFiles,
18
- copySubagentConfigSample,
19
18
  currentModelRef,
20
19
  defaultSubagentType,
21
20
  DEFAULT_MAX_CONCURRENT,
22
21
  DEFAULT_RETRY_CONFIG,
23
22
  DEFAULT_ROUTING_CONFIG,
24
- existingSubagentConfigFiles,
25
- getDefaultSubagentConfigPath,
23
+ filterSubagentConfigForParentModel,
26
24
  getBuiltinSubagentDefinitionsDir,
27
- getSubagentConfigInitTargetPath,
28
- getSubagentConfigSamplePath,
25
+ getBuiltinSubagentPresetsPath,
26
+ getProjectSubagentPresetsPath,
29
27
  isBlindModelRef,
28
+ isSubagentTypeAvailableForParent,
30
29
  loadSubagentConfig,
31
30
  resolveAgentTaskConfig,
32
31
  resolveRetryConfig,
@@ -35,9 +34,17 @@ export {
35
34
  shouldForceCurrentSubagentModel,
36
35
  } from "./core/config.js";
37
36
  export { routeSubagentTasks, SubagentRoutingError } from "./core/routing.js";
38
- export { BROWSER_QA_RUNNER_ENV, getBrowserQaRunnerPath } from "./core/browser-qa.js";
37
+ export {
38
+ BROWSER_QA_RUNNER_ENV,
39
+ UI_QA_RUNNER_ENV,
40
+ LEGACY_BROWSER_QA_TYPE,
41
+ UI_QA_TYPE,
42
+ getBrowserQaRunnerPath,
43
+ getUiQaRunnerPath,
44
+ isUiQaType,
45
+ } from "./core/browser-qa.js";
39
46
  export type { RoutedSubagentTasks, SubagentRoutingContext } from "./core/routing.js";
40
- export { agentDefinitionFiles, agentNameFromFilename, parseAgentMarkdown, projectAgentDefinitionFiles, readAgentDefinitionsFromDir, readProjectAgentDefinitions } from "./core/agents-dir.js";
47
+ export { agentDefinitionFiles, agentNameFromFilename, parseAgentMarkdown, projectAgentDefinitionFiles, projectAgentsDir, readAgentDefinitionsFromDir, readProjectAgentDefinitions } from "./core/agents-dir.js";
41
48
  export type { AgentDefinition, ProjectAgentDefinition } from "./core/agents-dir.js";
42
49
  export { buildSubagentCatalogPrompt } from "./core/agent-catalog.js";
43
50
  export type { SubagentPresetSelectionState } from "./core/presets.js";
@@ -7,7 +7,7 @@ import { ASYNC_SUBAGENT_TOOL_DESCRIPTIONS } from "../../tool-descriptions.js";
7
7
  import { SUBAGENT_TYPE_SELECTION_GUIDANCE } from "../core/agent-catalog.js";
8
8
  import { selectAvailableAgentModels } from "../core/model-selection.js";
9
9
  import { SubagentModelSelectionError } from "../core/config.js";
10
- import type { AgentCompletionHandler, AgentTask, ResolvedAgentTaskConfig, Semaphore, SpawnedAgent } from "../lib.js";
10
+ import type { AgentCompletionHandler, AgentTask, ResolvedAgentTaskConfig, RpcEventRecord, Semaphore, SpawnedAgent } from "../lib.js";
11
11
  import {
12
12
  createRunDir,
13
13
  createSemaphore,
@@ -52,6 +52,7 @@ interface LaunchQueuedAgentOptions {
52
52
  onCancelled: (reason: string) => void;
53
53
  onLaunchError: (error: unknown) => void;
54
54
  onUpdate: () => void;
55
+ onRpcEvent?: (event: RpcEventRecord) => void;
55
56
  }
56
57
 
57
58
  const PROJECT_SEMAPHORES = new Map<string, Semaphore>();
@@ -71,7 +72,7 @@ function normalizedLimit(limit: number): number {
71
72
  }
72
73
 
73
74
  async function launchQueuedAgent(options: LaunchQueuedAgentOptions): Promise<void> {
74
- const { resolved, runDir, cwd, parentSession, semaphore, signal, onResult, onComplete, onCancelled, onLaunchError, onUpdate } = options;
75
+ const { resolved, runDir, cwd, parentSession, semaphore, signal, onResult, onComplete, onCancelled, onLaunchError, onUpdate, onRpcEvent } = options;
75
76
  let slotAcquired = false;
76
77
  try {
77
78
  await semaphore.acquire(signal);
@@ -98,7 +99,6 @@ async function launchQueuedAgent(options: LaunchQueuedAgentOptions): Promise<voi
98
99
  parentSession,
99
100
  maxResultBytes: resolved.maxResultBytes,
100
101
  timeoutMs: resolved.timeoutMs,
101
- isolatedSkills: resolved.isolatedSkills,
102
102
  };
103
103
  if (resolved.retry.maxRetries > 0 || resolved.fallbackModels.length > 0) {
104
104
  const retryResult = spawnAgentWithRetry(
@@ -111,13 +111,14 @@ async function launchQueuedAgent(options: LaunchQueuedAgentOptions): Promise<voi
111
111
  extraArgs: resolved.extraArgs,
112
112
  fallbackModels: resolved.fallbackModels,
113
113
  signal,
114
+ onRpcEvent,
114
115
  ...spawnOptions,
115
116
  },
116
117
  );
117
118
  onResult(retryResult.initial);
118
119
  retryResult.done.catch(onLaunchError);
119
120
  } else {
120
- const result = spawnAgent(runDir, resolved.task, cwd, resolved.extraArgs, undefined, completionHandler, spawnOptions);
121
+ const result = spawnAgent(runDir, resolved.task, cwd, resolved.extraArgs, onRpcEvent, completionHandler, spawnOptions);
121
122
  onResult(result);
122
123
  }
123
124
  onUpdate();
@@ -195,6 +196,7 @@ export function registerSpawnTool(
195
196
  liveAgents: Map<string, Map<string, LiveAgent>>,
196
197
  handleAgentCompletion: AgentCompletionHandler,
197
198
  onLiveAgentsChange?: () => void,
199
+ onAgentRpcEvent?: (runDir: string, agentId: string, event: RpcEventRecord) => void,
198
200
  ): void {
199
201
  pi.registerTool({
200
202
  ...ASYNC_SUBAGENT_TOOL_DESCRIPTIONS.spawnAction,
@@ -226,7 +228,7 @@ export function registerSpawnTool(
226
228
  const activePreset = activePresetName ? config.presets?.[activePresetName] : undefined;
227
229
  if (getSessionSubagentPresetOverride() && !activePreset) {
228
230
  return {
229
- content: [{ type: "text", text: `AGENTS_PRESET=${activePresetName} does not match any preset in asyncSubagents config.` }],
231
+ content: [{ type: "text", text: `AGENTS_PRESET=${activePresetName} does not match any available sub-agent preset.` }],
230
232
  details: {},
231
233
  isError: true,
232
234
  };
@@ -342,6 +344,7 @@ export function registerSpawnTool(
342
344
  details: partialDetails,
343
345
  });
344
346
  },
347
+ onRpcEvent: (event) => onAgentRpcEvent?.(runDir, task.id, event),
345
348
  });
346
349
  }
347
350
 
@@ -3,8 +3,8 @@ import { Type } from "@earendil-works/pi-ai";
3
3
  import { Text } from "@earendil-works/pi-tui";
4
4
  import { asyncSubagentToolDescriptions } from "../../tool-descriptions.js";
5
5
  import { SUBAGENT_TYPE_SELECTION_GUIDANCE } from "../core/agent-catalog.js";
6
- import { hasIndexedProjectRoot } from "../../lib/project.js";
7
- import type { AgentCompletionHandler } from "../lib.js";
6
+ import { hasAvailableIndexedProjectRoot } from "../../lib/project.js";
7
+ import type { AgentCompletionHandler, RpcEventRecord } from "../lib.js";
8
8
  import { DEFAULT_SPAWN_WATCH_SECONDS, INLINE_RENDERING } from "../constants.js";
9
9
  import { renderSubagentSpawnPrompts } from "../render.js";
10
10
  import { emptyToolSlot } from "../ui.js";
@@ -73,10 +73,11 @@ export function registerSubagentsTool(
73
73
  liveAgents: Map<string, Map<string, LiveAgent>>,
74
74
  handleAgentCompletion: AgentCompletionHandler,
75
75
  onLiveAgentsChange?: () => void,
76
+ onAgentRpcEvent?: (runDir: string, agentId: string, event: RpcEventRecord) => void,
76
77
  ): void {
77
- const toolDescriptions = asyncSubagentToolDescriptions(hasIndexedProjectRoot());
78
+ const toolDescriptions = asyncSubagentToolDescriptions(hasAvailableIndexedProjectRoot());
78
79
  const collector = new ToolCollector();
79
- registerSpawnTool(collector as any, liveAgents, handleAgentCompletion, onLiveAgentsChange);
80
+ registerSpawnTool(collector as any, liveAgents, handleAgentCompletion, onLiveAgentsChange, onAgentRpcEvent);
80
81
  registerStatusTool(collector as any);
81
82
  registerStopTool(collector as any, liveAgents, onLiveAgentsChange);
82
83
  registerWaitTool(collector as any);
@@ -6,6 +6,7 @@ export interface LiveAgent {
6
6
  preview?: AgentTaskPreview;
7
7
  /** Parent pi session that spawned/adopted this sub-agent, when known. */
8
8
  parentSession?: string;
9
+ lastActivity?: AgentState["lastActivity"];
9
10
  completed: Promise<void>;
10
11
  }
11
12
 
@@ -22,6 +22,11 @@ type LookupDetails = {
22
22
  contextChars: number;
23
23
  stopReason?: string;
24
24
  error?: string;
25
+ attempts?: Array<{
26
+ ref: string;
27
+ outcome: "ok" | "unavailable" | "error" | "empty";
28
+ error?: string;
29
+ }>;
25
30
  };
26
31
 
27
32
  type ResolvedLookupModel = {
@@ -473,8 +478,9 @@ function createLookupTool() {
473
478
  executionMode: "sequential" as const,
474
479
  async execute(_toolCallId: string, params: LookupParams, signal: AbortSignal | undefined, _onUpdate: unknown, ctx: unknown) {
475
480
  const cwd = contextCwd(ctx) ?? process.cwd();
476
- const lookupModelRef = lookupModelFromConfig(cwd);
477
- if (!lookupModelRef) {
481
+ const lookupModelRefs = lookupModelRefsFromConfig(cwd);
482
+ const primaryLookupModelRef = lookupModelRefs[0];
483
+ if (!primaryLookupModelRef) {
478
484
  return lookupToolTextResult("lookupModel is not configured in pi-tools-suite config; lookup is disabled.", {
479
485
  model: "",
480
486
  imageCount: 0,
@@ -484,69 +490,86 @@ function createLookupTool() {
484
490
  });
485
491
  }
486
492
 
487
- const resolved = await resolveLookupModel(ctx, lookupModelRef);
488
- if (!resolved) {
489
- return lookupToolTextResult(`Lookup model is unavailable or unauthenticated: ${lookupModelRef}`, {
490
- model: lookupModelRef,
491
- imageCount: 0,
492
- imagePathCount: params.imagePaths?.length ?? 0,
493
- contextChars: 0,
494
- error: "lookup model unavailable",
495
- });
496
- }
497
-
498
493
  const recentContext = buildRecentSessionContext(ctx, DEFAULT_LOOKUP_MAX_CONTEXT_CHARS);
499
494
  const sessionImages = extractRecentSessionImages(ctx, DEFAULT_LOOKUP_MAX_IMAGES);
500
495
  const pathImages = readImagePathContents(cwd, params.imagePaths ?? [], Math.max(0, DEFAULT_LOOKUP_MAX_IMAGES - sessionImages.length));
501
496
  const images = [...sessionImages, ...pathImages.images].slice(0, DEFAULT_LOOKUP_MAX_IMAGES);
502
497
  const promptText = buildLookupPrompt(params, recentContext, images.length, pathImages.warnings);
498
+ const attempts: NonNullable<LookupDetails["attempts"]> = [];
499
+
500
+ for (const lookupModelRef of lookupModelRefs) {
501
+ if (signal?.aborted) throw signal.reason ?? new Error("Lookup aborted");
502
+ let resolved: ResolvedLookupModel | undefined;
503
+ try {
504
+ resolved = await resolveLookupModel(ctx, lookupModelRef);
505
+ } catch (error) {
506
+ if (signal?.aborted || isAbortError(error)) throw error;
507
+ attempts.push({ ref: lookupModelRef, outcome: "error", error: error instanceof Error ? error.message : String(error) });
508
+ continue;
509
+ }
510
+ if (!resolved) {
511
+ attempts.push({ ref: lookupModelRef, outcome: "unavailable" });
512
+ continue;
513
+ }
503
514
 
504
- try {
505
- const response = await completeWithModelRegistry(
506
- resolved.modelRegistry,
507
- resolved.model,
508
- {
509
- systemPrompt: LOOKUP_SYSTEM_PROMPT,
510
- messages: [
511
- {
512
- role: "user" as const,
513
- content: [{ type: "text" as const, text: promptText }, ...images],
514
- timestamp: Date.now(),
515
- },
516
- ],
517
- },
518
- {
519
- apiKey: resolved.apiKey,
520
- headers: resolved.headers,
521
- env: resolved.env,
522
- cacheRetention: "none",
523
- maxRetries: 1,
524
- maxTokens: DEFAULT_LOOKUP_MAX_TOKENS,
525
- signal,
526
- timeoutMs: DEFAULT_LOOKUP_TIMEOUT_MS,
527
- },
528
- );
529
- const text = responseText(response).trim() || "Lookup returned no text.";
530
- const suffix = response.stopReason === "error" && response.errorMessage ? `\n\nLookup error: ${response.errorMessage}` : "";
531
- return lookupToolTextResult(`${text}${suffix}`, {
532
- model: lookupModelRef,
533
- imageCount: images.length,
534
- imagePathCount: pathImages.images.length,
535
- contextChars: recentContext.length,
536
- stopReason: response.stopReason,
537
- ...(response.stopReason === "error" && response.errorMessage ? { error: response.errorMessage } : {}),
538
- });
539
- } catch (error) {
540
- if (signal?.aborted || isAbortError(error)) throw error;
541
- const message = error instanceof Error ? error.message : String(error);
542
- return lookupToolTextResult(`Lookup failed: ${message}`, {
543
- model: lookupModelRef,
544
- imageCount: images.length,
545
- imagePathCount: pathImages.images.length,
546
- contextChars: recentContext.length,
547
- error: message,
548
- });
515
+ try {
516
+ const response = await completeWithModelRegistry(
517
+ resolved.modelRegistry,
518
+ resolved.model,
519
+ {
520
+ systemPrompt: LOOKUP_SYSTEM_PROMPT,
521
+ messages: [
522
+ {
523
+ role: "user" as const,
524
+ content: [{ type: "text" as const, text: promptText }, ...images],
525
+ timestamp: Date.now(),
526
+ },
527
+ ],
528
+ },
529
+ {
530
+ apiKey: resolved.apiKey,
531
+ headers: resolved.headers,
532
+ env: resolved.env,
533
+ cacheRetention: "none",
534
+ maxRetries: 1,
535
+ maxTokens: DEFAULT_LOOKUP_MAX_TOKENS,
536
+ signal,
537
+ timeoutMs: DEFAULT_LOOKUP_TIMEOUT_MS,
538
+ },
539
+ );
540
+ if (signal?.aborted || response.stopReason === "aborted") throw signal?.reason ?? new Error("Lookup aborted");
541
+ if (response.stopReason === "error") {
542
+ attempts.push({ ref: lookupModelRef, outcome: "error", ...(response.errorMessage ? { error: response.errorMessage } : {}) });
543
+ continue;
544
+ }
545
+ const text = responseText(response).trim();
546
+ if (!text) {
547
+ attempts.push({ ref: lookupModelRef, outcome: "empty" });
548
+ continue;
549
+ }
550
+ attempts.push({ ref: lookupModelRef, outcome: "ok" });
551
+ return lookupToolTextResult(text, {
552
+ model: lookupModelRef,
553
+ imageCount: images.length,
554
+ imagePathCount: pathImages.images.length,
555
+ contextChars: recentContext.length,
556
+ stopReason: response.stopReason,
557
+ attempts,
558
+ });
559
+ } catch (error) {
560
+ if (signal?.aborted || isAbortError(error)) throw error;
561
+ attempts.push({ ref: lookupModelRef, outcome: "error", error: error instanceof Error ? error.message : String(error) });
562
+ }
549
563
  }
564
+
565
+ return lookupToolTextResult("Lookup failed across configured models.", {
566
+ model: primaryLookupModelRef,
567
+ imageCount: images.length,
568
+ imagePathCount: pathImages.images.length,
569
+ contextChars: recentContext.length,
570
+ error: "all lookup models failed",
571
+ attempts,
572
+ });
550
573
  },
551
574
  };
552
575
  }
@@ -585,7 +608,15 @@ function isInstructionMessage(message: unknown): boolean {
585
608
  }
586
609
 
587
610
  function lookupModelFromConfig(cwd?: string): string | undefined {
588
- return loadPiToolsSuiteConfig(["coding-discipline"], { cwd: cwd ?? process.cwd() }).lookupModel;
611
+ return lookupModelRefsFromConfig(cwd)[0];
612
+ }
613
+
614
+ function lookupModelRefsFromConfig(cwd?: string): string[] {
615
+ const config = loadPiToolsSuiteConfig(["coding-discipline"], { cwd: cwd ?? process.cwd() });
616
+ if (!config.lookupModel) return [];
617
+ return [...new Set([config.lookupModel, ...config.lookupFallbackModels]
618
+ .map((modelRef) => modelRef.trim())
619
+ .filter(Boolean))];
589
620
  }
590
621
 
591
622
  function buildLookupPrompt(params: LookupParams, recentContext: string, imageCount: number, warnings: string[]): string {
@@ -12,6 +12,18 @@ export interface PiToolsSuiteConfig {
12
12
  todoThinkingOverrides: Record<string, TodoThinkingLevel>;
13
13
  /** Vision-capable model used by the coding-discipline lookup tool; unset disables lookup. */
14
14
  lookupModel?: string;
15
+ /** Ordered lookup fallbacks tried after lookupModel. Always present, even when empty. */
16
+ lookupFallbackModels: string[];
17
+ resourceRegistry: ResourceRegistryConfig;
18
+ }
19
+
20
+ export interface ResourceRegistryConfig {
21
+ /** Git remote containing reusable skills/agents and project-scoped state under projects/. */
22
+ remote?: string;
23
+ /** Branch used for installs, updates, and pushes. */
24
+ branch: string;
25
+ /** Optional explicit project key for project-scoped registry state when Git remote discovery is unavailable or should be overridden. */
26
+ projectKey?: string;
15
27
  }
16
28
 
17
29
  type MutableConfig = {
@@ -20,6 +32,8 @@ type MutableConfig = {
20
32
  todoThinking: boolean;
21
33
  todoThinkingOverrides: Map<string, TodoThinkingLevel>;
22
34
  lookupModel: string | undefined;
35
+ lookupFallbackModels: string[];
36
+ resourceRegistry: ResourceRegistryConfig;
23
37
  };
24
38
 
25
39
  const TODO_THINKING_OVERRIDE_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
@@ -35,8 +49,9 @@ const ENABLED_LIST_KEYS = ["enabledModules", "enabledExtensions"];
35
49
  const MODULE_MAP_KEYS = ["modules", "extensions"];
36
50
  const DEFAULT_DISABLED_MODULES = new Set<string>(["credential-firewall", "truncation-metadata-normalizer"]);
37
51
  const DEFAULT_TODO_THINKING_OVERRIDES = new Map<string, TodoThinkingLevel>([["zai/glm-5.3", "max"]]);
52
+ const DEFAULT_RESOURCE_REGISTRY_BRANCH = "main";
38
53
 
39
- export function getPiToolsSuiteUserConfigPath(homeDir = homedir()): string {
54
+ export function getPiToolsSuiteUserConfigPath(homeDir = process.env.HOME?.trim() || homedir()): string {
40
55
  return join(homeDir, ".config", "pi", "pi-tools-suite.jsonc");
41
56
  }
42
57
 
@@ -68,6 +83,32 @@ function normalizeLookupModel(raw: unknown): string | undefined {
68
83
  return trimmed ? trimmed : undefined;
69
84
  }
70
85
 
86
+ function normalizeModelList(raw: unknown): string[] {
87
+ if (!Array.isArray(raw)) return [];
88
+ return [...new Set(raw
89
+ .filter((value): value is string => typeof value === "string")
90
+ .map((value) => value.trim())
91
+ .filter(Boolean))];
92
+ }
93
+
94
+ function normalizeNonEmptyString(raw: unknown): string | undefined {
95
+ if (typeof raw !== "string") return undefined;
96
+ const trimmed = raw.trim();
97
+ return trimmed ? trimmed : undefined;
98
+ }
99
+
100
+ function mergeResourceRegistry(config: MutableConfig, raw: unknown): void {
101
+ if (!isRecord(raw)) return;
102
+ if (Object.prototype.hasOwnProperty.call(raw, "remote")) {
103
+ config.resourceRegistry.remote = normalizeNonEmptyString(raw.remote);
104
+ }
105
+ const branch = normalizeNonEmptyString(raw.branch);
106
+ if (branch) config.resourceRegistry.branch = branch;
107
+ if (Object.prototype.hasOwnProperty.call(raw, "projectKey")) {
108
+ config.resourceRegistry.projectKey = normalizeNonEmptyString(raw.projectKey);
109
+ }
110
+ }
111
+
71
112
  function isTodoThinkingLevel(raw: unknown): raw is TodoThinkingLevel {
72
113
  return TODO_THINKING_OVERRIDE_LEVELS.includes(raw as TodoThinkingLevel);
73
114
  }
@@ -145,6 +186,8 @@ function mergeConfigLayer(config: MutableConfig, raw: Record<string, unknown>, k
145
186
  if (typeof raw.todoThinking === "boolean") config.todoThinking = raw.todoThinking;
146
187
  mergeTodoThinkingOverrides(config, raw.todoThinkingOverrides);
147
188
  if (Object.prototype.hasOwnProperty.call(raw, "lookupModel")) config.lookupModel = normalizeLookupModel(raw.lookupModel);
189
+ if (Object.prototype.hasOwnProperty.call(raw, "lookupFallbackModels")) config.lookupFallbackModels = normalizeModelList(raw.lookupFallbackModels);
190
+ mergeResourceRegistry(config, raw.resourceRegistry);
148
191
 
149
192
  for (const key of DISABLED_LIST_KEYS) addDisabled(config, raw[key], knownModules);
150
193
  for (const key of ENABLED_LIST_KEYS) removeDisabled(config, raw[key], knownModules);
@@ -191,6 +234,13 @@ function applyEnv(config: MutableConfig, env: Env, knownModules: ReadonlySet<str
191
234
  const todoThinking = boolFromEnv(env.PI_TOOLS_SUITE_TODO_THINKING);
192
235
  if (todoThinking !== undefined) config.todoThinking = todoThinking;
193
236
 
237
+ const registryRemote = normalizeNonEmptyString(env.PI_RESOURCE_REGISTRY_REMOTE);
238
+ if (registryRemote) config.resourceRegistry.remote = registryRemote;
239
+ const registryBranch = normalizeNonEmptyString(env.PI_RESOURCE_REGISTRY_BRANCH);
240
+ if (registryBranch) config.resourceRegistry.branch = registryBranch;
241
+ const registryProjectKey = normalizeNonEmptyString(env.PI_RESOURCE_REGISTRY_PROJECT_KEY);
242
+ if (registryProjectKey) config.resourceRegistry.projectKey = registryProjectKey;
243
+
194
244
  return config;
195
245
  }
196
246
 
@@ -203,6 +253,8 @@ export function loadPiToolsSuiteConfig(moduleNames: readonly string[], options:
203
253
  todoThinking: false,
204
254
  todoThinkingOverrides: new Map(DEFAULT_TODO_THINKING_OVERRIDES),
205
255
  lookupModel: undefined,
256
+ lookupFallbackModels: [],
257
+ resourceRegistry: { branch: DEFAULT_RESOURCE_REGISTRY_BRANCH },
206
258
  };
207
259
  const userConfigPath = getPiToolsSuiteUserConfigPath(options.homeDir);
208
260
 
@@ -223,5 +275,7 @@ export function loadPiToolsSuiteConfig(moduleNames: readonly string[], options:
223
275
  todoThinking: config.todoThinking,
224
276
  todoThinkingOverrides: Object.fromEntries(config.todoThinkingOverrides),
225
277
  ...(config.lookupModel ? { lookupModel: config.lookupModel } : {}),
278
+ lookupFallbackModels: [...config.lookupFallbackModels],
279
+ resourceRegistry: { ...config.resourceRegistry },
226
280
  };
227
281
  }