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,8 @@
1
- import { readFile, readdir, realpath, stat } from "node:fs/promises";
1
+ import { open, readFile, readdir, realpath, stat } from "node:fs/promises";
2
2
  import { join, resolve } from "node:path";
3
3
  import { SUBAGENT_PARENT_SESSION_FILE, SUBAGENTS_REGISTRY_FILE, SUBAGENTS_RUN_ROOT } from "../constants.js";
4
4
  import { isSubagentRegistry } from "./subagents-model.js";
5
+ const LAST_ACTIVITY_TAIL_BYTES = 64 * 1024;
5
6
  export function subagentsRegistryPath(cwd) {
6
7
  return join(cwd, SUBAGENTS_RUN_ROOT, SUBAGENTS_REGISTRY_FILE);
7
8
  }
@@ -110,6 +111,9 @@ async function readSubagentAgentState(runDir, agentId, options) {
110
111
  const finishedAt = await readTrimmedFile(join(agentDir, "finished_at"));
111
112
  if (finishedAt)
112
113
  state.finishedAt = finishedAt;
114
+ const lastActivity = await readLastActivity(join(agentDir, "progress.jsonl"));
115
+ if (lastActivity)
116
+ state.lastActivity = lastActivity;
113
117
  const retryPending = await fileExists(join(agentDir, "retry_pending"));
114
118
  const stopRequested = await fileExists(join(agentDir, "stop_requested"));
115
119
  if (retryPending && !stopRequested && state.status !== "running" && state.status !== "done") {
@@ -179,6 +183,61 @@ async function readNumberFile(filePath) {
179
183
  const value = Number.parseInt(raw, 10);
180
184
  return Number.isFinite(value) ? value : undefined;
181
185
  }
186
+ async function readLastActivity(filePath) {
187
+ let handle;
188
+ try {
189
+ handle = await open(filePath, "r");
190
+ const size = (await handle.stat()).size;
191
+ if (size <= 0)
192
+ return undefined;
193
+ const start = Math.max(0, size - LAST_ACTIVITY_TAIL_BYTES);
194
+ const buffer = Buffer.allocUnsafe(size - start);
195
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, start);
196
+ if (bytesRead <= 0)
197
+ return undefined;
198
+ const lines = buffer.toString("utf8", 0, bytesRead).split(/\r?\n/);
199
+ if (start > 0) {
200
+ const previousByte = Buffer.allocUnsafe(1);
201
+ const previousRead = await handle.read(previousByte, 0, 1, start - 1);
202
+ if (previousRead.bytesRead === 1 && previousByte[0] !== 0x0a)
203
+ lines.shift();
204
+ }
205
+ for (let index = lines.length - 1; index >= 0; index--) {
206
+ const line = lines[index]?.trim();
207
+ if (!line)
208
+ continue;
209
+ try {
210
+ const value = JSON.parse(line);
211
+ if (!isProgressActivityRecord(value))
212
+ continue;
213
+ const toolName = typeof value.toolName === "string" ? value.toolName.trim() : "";
214
+ if (toolName)
215
+ return { label: toolName, at: value.at };
216
+ if (value.type === "message_start" || value.type === "message_end")
217
+ return { label: "Thinking", at: value.at };
218
+ }
219
+ catch {
220
+ // Ignore malformed or partially written tail lines and keep scanning.
221
+ }
222
+ }
223
+ }
224
+ catch {
225
+ return undefined;
226
+ }
227
+ finally {
228
+ await handle?.close().catch(() => undefined);
229
+ }
230
+ return undefined;
231
+ }
232
+ function isProgressActivityRecord(value) {
233
+ if (typeof value !== "object" || value === null || Array.isArray(value))
234
+ return false;
235
+ const record = value;
236
+ return record.stage === "rpc_event"
237
+ && typeof record.at === "string"
238
+ && record.at.trim().length > 0
239
+ && typeof record.type === "string";
240
+ }
182
241
  async function countFileLines(filePath) {
183
242
  try {
184
243
  const content = await readFile(filePath, "utf8");
@@ -32,5 +32,6 @@ export declare function allSubagentStatesTerminal(agents: readonly SubagentAgent
32
32
  export declare function taskPreviewMap(tasks: readonly SubagentTaskPreview[] | undefined): Map<string, SubagentTaskPreview>;
33
33
  export declare function formatSubagentTimestamp(value: string | undefined): string | undefined;
34
34
  export declare function formatElapsedSince(value: string | undefined, now?: number): string;
35
+ export declare function formatSubagentLastActivity(activity: SubagentAgentState["lastActivity"]): string | undefined;
35
36
  export declare function subagentRunName(runDir: string): string;
36
37
  export declare function resolveSubagentRunDir(cwd: string, runDir: string): string;
@@ -43,6 +43,14 @@ export function isSubagentAgentState(value) {
43
43
  return false;
44
44
  if (value.retryCount !== undefined && typeof value.retryCount !== "number")
45
45
  return false;
46
+ if (value.lastActivity !== undefined) {
47
+ if (!isRecord(value.lastActivity))
48
+ return false;
49
+ if (typeof value.lastActivity.label !== "string" || !value.lastActivity.label.trim())
50
+ return false;
51
+ if (typeof value.lastActivity.at !== "string" || !value.lastActivity.at.trim())
52
+ return false;
53
+ }
46
54
  return true;
47
55
  }
48
56
  export function isSubagentTaskPreview(value) {
@@ -219,11 +227,19 @@ export function formatSubagentTimestamp(value) {
219
227
  return new Date(time).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", second: "2-digit" });
220
228
  }
221
229
  export function formatElapsedSince(value, now = Date.now()) {
230
+ return formatDurationSince(value, now) ?? "elapsed:—";
231
+ }
232
+ export function formatSubagentLastActivity(activity) {
233
+ if (!activity)
234
+ return undefined;
235
+ return activity.label.trim() || undefined;
236
+ }
237
+ function formatDurationSince(value, now) {
222
238
  if (!value)
223
- return "elapsed:—";
239
+ return undefined;
224
240
  const started = Date.parse(value);
225
241
  if (!Number.isFinite(started))
226
- return "elapsed:—";
242
+ return undefined;
227
243
  const seconds = Math.max(0, Math.floor((now - started) / 1000));
228
244
  if (seconds < 60)
229
245
  return `${seconds}s`;
@@ -4,6 +4,7 @@ export type SubagentsToolResultObserveOptions = {
4
4
  };
5
5
  export type SubagentsWidgetControllerHost = {
6
6
  readonly cwd: string;
7
+ hasActiveRuntime(): boolean;
7
8
  sessionFile(): string | undefined;
8
9
  isRunning(): boolean;
9
10
  render(): void;
@@ -22,6 +22,8 @@ export class AppSubagentsWidgetController {
22
22
  this.host = host;
23
23
  }
24
24
  get widgetState() {
25
+ if (!this.host.hasActiveRuntime())
26
+ return undefined;
25
27
  return this.state;
26
28
  }
27
29
  startPolling() {
@@ -51,6 +53,8 @@ export class AppSubagentsWidgetController {
51
53
  return;
52
54
  if (!isSubagentRunRenderDetails(details))
53
55
  return;
56
+ if (!this.host.hasActiveRuntime())
57
+ return;
54
58
  const runDir = resolveSubagentRunDir(this.host.cwd, details.runDir);
55
59
  const normalizedDetails = { ...details, runDir };
56
60
  if (normalizedDetails.tasks)
@@ -372,6 +376,8 @@ export class AppSubagentsWidgetController {
372
376
  return activeSubagentStates(this.state?.agents ?? []).length > 0;
373
377
  }
374
378
  eventMatchesCurrentSession(eventSessionFile) {
379
+ if (!this.host.hasActiveRuntime())
380
+ return false;
375
381
  if (!eventSessionFile)
376
382
  return true;
377
383
  const sessionFile = this.host.sessionFile();
@@ -139,6 +139,10 @@ export type SubagentStatus = (typeof SUBAGENT_STATUSES)[number];
139
139
  export type SubagentActiveStatus = (typeof SUBAGENT_ACTIVE_STATUSES)[number];
140
140
  export type SubagentTerminalStatus = (typeof SUBAGENT_TERMINAL_STATUSES)[number];
141
141
  export type SubagentRenderMode = (typeof SUBAGENT_RENDER_MODES)[number];
142
+ export type SubagentActivity = {
143
+ label: string;
144
+ at: string;
145
+ };
142
146
  export type SubagentAgentState = {
143
147
  id: string;
144
148
  status: SubagentStatus;
@@ -151,6 +155,7 @@ export type SubagentAgentState = {
151
155
  stderrLines?: number;
152
156
  eventLines?: number;
153
157
  retryCount?: number;
158
+ lastActivity?: SubagentActivity;
154
159
  };
155
160
  export type SubagentTaskPreview = {
156
161
  id: string;
@@ -230,6 +235,8 @@ export type RenderedLine = {
230
235
  index: number;
231
236
  } | {
232
237
  kind: "popup-menu-close";
238
+ } | {
239
+ kind: "model-visibility-clear";
233
240
  } | {
234
241
  kind: "todo-panel";
235
242
  } | {
@@ -341,6 +348,7 @@ export type SessionTab = {
341
348
  id: string;
342
349
  title: string;
343
350
  titlePlaceholder?: "loading" | "new";
351
+ draft?: boolean;
344
352
  isFork?: boolean;
345
353
  status: SessionTabStatus;
346
354
  activity?: SessionActivity;
@@ -537,6 +545,7 @@ export type ModelMenuValue = {
537
545
  model: SessionModel;
538
546
  ref: string;
539
547
  current: boolean;
548
+ visible?: boolean;
540
549
  };
541
550
  export type ScopedSessionModel = {
542
551
  model: SessionModel;
@@ -546,6 +555,12 @@ export type ThinkingMenuValue = {
546
555
  level: ThinkingLevel;
547
556
  current: boolean;
548
557
  };
558
+ export type ModelThinkingMenuState = {
559
+ thinkingLevel: ThinkingLevel;
560
+ availableThinkingLevels: readonly ThinkingLevel[];
561
+ source: "model" | "thinking";
562
+ visibilityMode: boolean;
563
+ };
549
564
  export type UserMessageMenuValue = "copy" | "fork" | "fork-new-tab" | "undo";
550
565
  export type UserMessageJumpMenuValue = {
551
566
  entryId?: string;
@@ -562,7 +577,7 @@ export type ResumeMenuValue = {
562
577
  session: SessionInfo;
563
578
  };
564
579
  export type ActivePopupMenu = "slash" | "model" | "thinking" | "resume" | "user-message" | "user-message-jump" | "queue-message" | "sdk-menu";
565
- export type PopupMenuPlacement = "default" | "under-tabs";
580
+ export type PopupMenuPlacement = "default" | "under-tabs" | "draft-surface";
566
581
  export type StatusThinkingTarget = {
567
582
  row: number;
568
583
  startColumn: number;
@@ -2,7 +2,7 @@ import { getAgentDir } from "@earendil-works/pi-coding-agent";
2
2
  import { copyTextToClipboard } from "../screen/clipboard.js";
3
3
  import { isRecord } from "../guards.js";
4
4
  import { createId } from "../id.js";
5
- import { loadWorkspaceUndoIndex, prepareWorkspaceMutation, revertWorkspaceMutations, saveWorkspaceUndoIndex, workspaceMutationFromToolExecution, workspaceUndoIndexKey, } from "./workspace-undo.js";
5
+ import { loadWorkspaceUndoIndex, prepareWorkspaceMutation, revertWorkspaceMutations, saveWorkspaceUndoIndex, workspaceMutationFromToolExecution, WORKSPACE_MUTATION_ENTRY_TYPE, workspaceUndoIndexKey, } from "./workspace-undo.js";
6
6
  export class AppWorkspaceActionsController {
7
7
  host;
8
8
  workspaceUndoIndex = loadWorkspaceUndoIndex(getAgentDir());
@@ -23,8 +23,16 @@ export class AppWorkspaceActionsController {
23
23
  return;
24
24
  entry.workspaceMutations = [...(entry.workspaceMutations ?? []), mutation];
25
25
  const sessionEntryId = this.resolveUserSessionEntryId(entry);
26
- if (sessionEntryId)
26
+ if (sessionEntryId) {
27
27
  this.persistWorkspaceMutations(sessionEntryId, entry.workspaceMutations);
28
+ // Also persist each new mutation in the session branch so Desktop and
29
+ // future renderers can perform the same message-scoped undo without
30
+ // relying on this process-wide compatibility index.
31
+ this.host.runtime()?.session.sessionManager.appendCustomEntry?.(WORKSPACE_MUTATION_ENTRY_TYPE, {
32
+ userEntryId: sessionEntryId,
33
+ mutation,
34
+ });
35
+ }
28
36
  this.host.touchEntry(entry);
29
37
  }
30
38
  scheduleUserSessionEntryMetadataSync() {
@@ -36,6 +36,7 @@ export type WorkspaceRevertResult = {
36
36
  ok: false;
37
37
  error: string;
38
38
  };
39
+ export declare const WORKSPACE_MUTATION_ENTRY_TYPE = "pix-workspace-mutation";
39
40
  export declare function workspaceUndoIndexKey(_sessionFile: string | undefined, _sessionId: string, entryId: string): string;
40
41
  export declare function loadWorkspaceUndoIndex(agentDir: string): WorkspaceUndoIndex;
41
42
  export declare function saveWorkspaceUndoIndex(agentDir: string, index: WorkspaceUndoIndex): void;
@@ -3,6 +3,7 @@ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
3
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
4
  import { runProcess } from "../process.js";
5
5
  const UNDO_INDEX_VERSION = 1;
6
+ export const WORKSPACE_MUTATION_ENTRY_TYPE = "pix-workspace-mutation";
6
7
  export function workspaceUndoIndexKey(_sessionFile, _sessionId, entryId) {
7
8
  return entryId;
8
9
  }
@@ -3,6 +3,7 @@ import { questionParameters, normalizeQuestionInput } from "./contract.js";
3
3
  import { renderQuestionCall, renderQuestionResult } from "./render.js";
4
4
  import { createCanceledQuestionResult, createQuestionToolResult, createSuccessfulQuestionResult } from "./result.js";
5
5
  import { runDesktopQuestionnaire, shouldUseDesktopQuestionBridge } from "./desktop.js";
6
+ import { runRemoteQuestionnaire } from "./remote.js";
6
7
  import { runQuestionnaire } from "./tui.js";
7
8
  export default function questionExtension(pi) {
8
9
  pi.registerTool({
@@ -14,8 +15,15 @@ export default function questionExtension(pi) {
14
15
  renderResult(result, _options, theme, context) {
15
16
  return renderQuestionResult(result, theme, context.args);
16
17
  },
17
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
18
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
18
19
  const questions = normalizeQuestionInput(params);
20
+ const sessionId = ctx.sessionManager?.getSessionId();
21
+ const remoteSelections = sessionId ? await runRemoteQuestionnaire(sessionId, questions, signal) : undefined;
22
+ if (remoteSelections !== undefined) {
23
+ if (remoteSelections === null)
24
+ return createQuestionToolResult(createCanceledQuestionResult("user_canceled"), questions);
25
+ return createQuestionToolResult(createSuccessfulQuestionResult(questions, remoteSelections), questions);
26
+ }
19
27
  if (!ctx.hasUI)
20
28
  return createQuestionToolResult(createCanceledQuestionResult("ui_unavailable", questions), questions);
21
29
  const selections = shouldUseDesktopQuestionBridge(ctx)
@@ -0,0 +1,4 @@
1
+ import type { NormalizedQuestion, QuestionSelection } from "./types.js";
2
+ export type RemoteQuestionHandler = (questions: NormalizedQuestion[], signal?: AbortSignal) => Promise<QuestionSelection[] | null | undefined>;
3
+ export declare function registerRemoteQuestionHandler(sessionId: string, handler: RemoteQuestionHandler): () => void;
4
+ export declare function runRemoteQuestionnaire(sessionId: string, questions: NormalizedQuestion[], signal?: AbortSignal): Promise<QuestionSelection[] | null | undefined>;
@@ -0,0 +1,33 @@
1
+ const REGISTRY_KEY = Symbol.for("pix.question.remote-registry.v1");
2
+ export function registerRemoteQuestionHandler(sessionId, handler) {
3
+ const registry = remoteQuestionRegistry();
4
+ registry.set(sessionId, handler);
5
+ let active = true;
6
+ return () => {
7
+ if (!active)
8
+ return;
9
+ active = false;
10
+ if (registry.get(sessionId) === handler)
11
+ registry.delete(sessionId);
12
+ };
13
+ }
14
+ export async function runRemoteQuestionnaire(sessionId, questions, signal) {
15
+ const handler = remoteQuestionRegistry().get(sessionId);
16
+ if (!handler)
17
+ return undefined;
18
+ try {
19
+ return await handler(questions, signal);
20
+ }
21
+ catch {
22
+ if (signal?.aborted)
23
+ return null;
24
+ // Remote integrations are optional. If Telegram/networking fails before an
25
+ // answer is obtained, fall back to the normal local question UI.
26
+ return undefined;
27
+ }
28
+ }
29
+ function remoteQuestionRegistry() {
30
+ const root = globalThis;
31
+ root[REGISTRY_KEY] ??= new Map();
32
+ return root[REGISTRY_KEY];
33
+ }
@@ -0,0 +1,43 @@
1
+ export type TelegramBotClientOptions = {
2
+ sendTimeoutMs?: number;
3
+ pollRequestTimeoutMs?: number;
4
+ };
5
+ export type TelegramIncomingMessage = {
6
+ messageId: number;
7
+ chatId: string;
8
+ text: string;
9
+ replyToMessageId?: number;
10
+ };
11
+ type TelegramUpdate = {
12
+ update_id?: unknown;
13
+ message?: {
14
+ message_id?: unknown;
15
+ chat?: {
16
+ id?: unknown;
17
+ };
18
+ text?: unknown;
19
+ reply_to_message?: {
20
+ message_id?: unknown;
21
+ };
22
+ };
23
+ };
24
+ export type TelegramFetch = typeof fetch;
25
+ export declare class TelegramBotClient {
26
+ private readonly botToken;
27
+ private readonly chatId;
28
+ private readonly fetchFn;
29
+ private readonly options;
30
+ private running;
31
+ private abortController;
32
+ private nextOffset;
33
+ constructor(botToken: string, chatId: string, fetchFn?: TelegramFetch, options?: TelegramBotClientOptions);
34
+ start(onMessage: (message: TelegramIncomingMessage) => void | Promise<void>, onError?: (error: Error) => void): void;
35
+ stop(): void;
36
+ sendMessage(text: string, signal?: AbortSignal): Promise<number>;
37
+ private pollLoop;
38
+ private sanitizeError;
39
+ private call;
40
+ }
41
+ export declare function normalizeIncomingMessage(update: TelegramUpdate): TelegramIncomingMessage | undefined;
42
+ export declare function trimTelegramText(text: string): string;
43
+ export {};
@@ -0,0 +1,166 @@
1
+ const TELEGRAM_API_BASE = "https://api.telegram.org";
2
+ const MAX_TELEGRAM_TEXT = 4000;
3
+ const POLL_TIMEOUT_SECONDS = 25;
4
+ const DEFAULT_SEND_TIMEOUT_MS = 10_000;
5
+ const DEFAULT_POLL_REQUEST_TIMEOUT_MS = (POLL_TIMEOUT_SECONDS + 10) * 1000;
6
+ export class TelegramBotClient {
7
+ botToken;
8
+ chatId;
9
+ fetchFn;
10
+ options;
11
+ running = false;
12
+ abortController;
13
+ nextOffset;
14
+ constructor(botToken, chatId, fetchFn = fetch, options = {}) {
15
+ this.botToken = botToken;
16
+ this.chatId = chatId;
17
+ this.fetchFn = fetchFn;
18
+ this.options = options;
19
+ }
20
+ start(onMessage, onError = () => { }) {
21
+ if (this.running)
22
+ return;
23
+ this.running = true;
24
+ this.abortController = new AbortController();
25
+ void this.pollLoop(onMessage, onError, this.abortController.signal);
26
+ }
27
+ stop() {
28
+ this.running = false;
29
+ this.abortController?.abort();
30
+ this.abortController = undefined;
31
+ }
32
+ async sendMessage(text, signal) {
33
+ try {
34
+ const body = await this.call("sendMessage", {
35
+ chat_id: this.chatId,
36
+ text: trimTelegramText(text),
37
+ disable_web_page_preview: true,
38
+ }, signal);
39
+ if (!body || typeof body.message_id !== "number")
40
+ throw new Error("Telegram sendMessage returned no message id");
41
+ return body.message_id;
42
+ }
43
+ catch (error) {
44
+ throw this.sanitizeError(error);
45
+ }
46
+ }
47
+ async pollLoop(onMessage, onError, signal) {
48
+ let failures = 0;
49
+ let baselineReady = false;
50
+ while (this.running && !signal.aborted) {
51
+ try {
52
+ if (!baselineReady) {
53
+ const pending = await this.call("getUpdates", {
54
+ offset: -1,
55
+ limit: 1,
56
+ timeout: 0,
57
+ allowed_updates: ["message"],
58
+ }, signal);
59
+ const latestUpdateId = pending
60
+ ?.map((update) => update.update_id)
61
+ .filter((updateId) => typeof updateId === "number")
62
+ .at(-1);
63
+ if (latestUpdateId !== undefined)
64
+ this.nextOffset = latestUpdateId + 1;
65
+ baselineReady = true;
66
+ failures = 0;
67
+ continue;
68
+ }
69
+ const updates = await this.call("getUpdates", {
70
+ ...(this.nextOffset === undefined ? {} : { offset: this.nextOffset }),
71
+ timeout: POLL_TIMEOUT_SECONDS,
72
+ allowed_updates: ["message"],
73
+ }, signal);
74
+ failures = 0;
75
+ for (const update of updates ?? []) {
76
+ if (typeof update.update_id === "number")
77
+ this.nextOffset = update.update_id + 1;
78
+ const message = normalizeIncomingMessage(update);
79
+ if (!message || message.chatId !== this.chatId)
80
+ continue;
81
+ await onMessage(message);
82
+ }
83
+ }
84
+ catch (error) {
85
+ if (signal.aborted || !this.running)
86
+ return;
87
+ failures += 1;
88
+ onError(this.sanitizeError(error));
89
+ await sleep(Math.min(30_000, 500 * 2 ** Math.min(failures - 1, 6)), signal);
90
+ }
91
+ }
92
+ }
93
+ sanitizeError(error) {
94
+ const message = asError(error).message.replaceAll(this.botToken, "[redacted]");
95
+ return new Error(message);
96
+ }
97
+ async call(method, payload, signal) {
98
+ const requestSignal = combineAbortSignals([
99
+ signal,
100
+ this.abortController?.signal,
101
+ ], method === "getUpdates"
102
+ ? this.options.pollRequestTimeoutMs ?? DEFAULT_POLL_REQUEST_TIMEOUT_MS
103
+ : this.options.sendTimeoutMs ?? DEFAULT_SEND_TIMEOUT_MS);
104
+ const response = await this.fetchFn(`${TELEGRAM_API_BASE}/bot${this.botToken}/${method}`, {
105
+ method: "POST",
106
+ headers: { "Content-Type": "application/json" },
107
+ body: JSON.stringify(payload),
108
+ signal: requestSignal,
109
+ });
110
+ let parsed;
111
+ try {
112
+ parsed = await response.json();
113
+ }
114
+ catch {
115
+ // Fall through to the sanitized HTTP error below.
116
+ }
117
+ if (!response.ok || parsed?.ok !== true) {
118
+ const description = typeof parsed?.description === "string" ? `: ${parsed.description}` : "";
119
+ throw new Error(`Telegram ${method} failed (${response.status})${description}`);
120
+ }
121
+ return parsed.result;
122
+ }
123
+ }
124
+ function combineAbortSignals(signals, timeoutMs) {
125
+ const active = signals.filter((signal) => signal !== undefined);
126
+ active.push(AbortSignal.timeout(Math.max(1, timeoutMs)));
127
+ return active.length === 1 ? active[0] : AbortSignal.any(active);
128
+ }
129
+ export function normalizeIncomingMessage(update) {
130
+ const message = update.message;
131
+ if (!message || typeof message.message_id !== "number" || typeof message.text !== "string")
132
+ return undefined;
133
+ const rawChatId = message.chat?.id;
134
+ if (typeof rawChatId !== "number" && typeof rawChatId !== "string")
135
+ return undefined;
136
+ const text = message.text.trim();
137
+ if (!text)
138
+ return undefined;
139
+ const rawReplyId = message.reply_to_message?.message_id;
140
+ return {
141
+ messageId: message.message_id,
142
+ chatId: String(rawChatId),
143
+ text,
144
+ ...(typeof rawReplyId === "number" ? { replyToMessageId: rawReplyId } : {}),
145
+ };
146
+ }
147
+ export function trimTelegramText(text) {
148
+ if (text.length <= MAX_TELEGRAM_TEXT)
149
+ return text;
150
+ return `${text.slice(0, MAX_TELEGRAM_TEXT - 2)}…`;
151
+ }
152
+ function asError(error) {
153
+ return error instanceof Error ? error : new Error(String(error));
154
+ }
155
+ async function sleep(ms, signal) {
156
+ if (signal.aborted)
157
+ return;
158
+ await new Promise((resolve) => {
159
+ const timer = setTimeout(resolve, ms);
160
+ timer.unref?.();
161
+ signal.addEventListener("abort", () => {
162
+ clearTimeout(timer);
163
+ resolve();
164
+ }, { once: true });
165
+ });
166
+ }
@@ -0,0 +1,8 @@
1
+ export type TelegramConnectorConfig = {
2
+ enabled: boolean;
3
+ botToken?: string;
4
+ chatId?: string;
5
+ };
6
+ export declare function getTelegramConnectorConfigPath(homeDir?: string): string;
7
+ export declare function readTelegramConnectorConfig(configPath?: string): TelegramConnectorConfig;
8
+ export declare function resolveTelegramConnectorConfig(configPath?: string, environment?: NodeJS.ProcessEnv): TelegramConnectorConfig;
@@ -0,0 +1,87 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { parse as parseJsonc } from "jsonc-parser";
5
+ const CONFIG_KEY = "telegramConnector";
6
+ const ENV_ENABLED = "PIX_TELEGRAM_CONNECTOR";
7
+ const ENV_BOT_TOKEN = "PIX_TELEGRAM_BOT_TOKEN";
8
+ const ENV_CHAT_ID = "PIX_TELEGRAM_CHAT_ID";
9
+ export function getTelegramConnectorConfigPath(homeDir = homedir()) {
10
+ return join(homeDir, ".config", "pi", "pi-tools-suite.jsonc");
11
+ }
12
+ export function readTelegramConnectorConfig(configPath = getTelegramConnectorConfigPath()) {
13
+ return readTelegramConnectorConfigState(configPath).config;
14
+ }
15
+ function readTelegramConnectorConfigState(configPath) {
16
+ if (!existsSync(configPath))
17
+ return { config: { enabled: false }, explicitlyDisabled: false };
18
+ try {
19
+ const parsed = parseJsonc(readFileSync(configPath, "utf-8"));
20
+ if (!isRecord(parsed))
21
+ return { config: { enabled: false }, explicitlyDisabled: false };
22
+ const raw = parsed[CONFIG_KEY];
23
+ if (!isRecord(raw))
24
+ return { config: { enabled: false }, explicitlyDisabled: false };
25
+ const botToken = nonEmptyString(raw.botToken);
26
+ const chatId = normalizeChatId(raw.chatId);
27
+ const explicitlyDisabled = raw.enabled === false;
28
+ return {
29
+ config: {
30
+ enabled: !explicitlyDisabled && Boolean(botToken && chatId),
31
+ ...(botToken ? { botToken } : {}),
32
+ ...(chatId ? { chatId } : {}),
33
+ },
34
+ explicitlyDisabled,
35
+ };
36
+ }
37
+ catch {
38
+ return { config: { enabled: false }, explicitlyDisabled: false };
39
+ }
40
+ }
41
+ export function resolveTelegramConnectorConfig(configPath = getTelegramConnectorConfigPath(), environment = process.env) {
42
+ const { config: fromConfig, explicitlyDisabled } = readTelegramConnectorConfigState(configPath);
43
+ const envBotToken = nonEmptyString(environment[ENV_BOT_TOKEN]);
44
+ const envChatId = normalizeChatId(environment[ENV_CHAT_ID]);
45
+ const botToken = envBotToken ?? fromConfig.botToken;
46
+ const chatId = envChatId ?? fromConfig.chatId;
47
+ const envEnabled = parseBooleanEnv(environment[ENV_ENABLED]);
48
+ const enabled = envEnabled === false
49
+ ? false
50
+ : envEnabled === true
51
+ ? Boolean(botToken && chatId)
52
+ : explicitlyDisabled
53
+ ? false
54
+ : Boolean(botToken && chatId);
55
+ return {
56
+ enabled,
57
+ ...(botToken ? { botToken } : {}),
58
+ ...(chatId ? { chatId } : {}),
59
+ };
60
+ }
61
+ function normalizeChatId(value) {
62
+ if (typeof value === "number" && Number.isSafeInteger(value))
63
+ return String(value);
64
+ if (typeof value !== "string")
65
+ return undefined;
66
+ const trimmed = value.trim();
67
+ return /^-?\d+$/.test(trimmed) ? trimmed : undefined;
68
+ }
69
+ function nonEmptyString(value) {
70
+ if (typeof value !== "string")
71
+ return undefined;
72
+ const trimmed = value.trim();
73
+ return trimmed || undefined;
74
+ }
75
+ function parseBooleanEnv(value) {
76
+ if (value === undefined || value.trim() === "")
77
+ return undefined;
78
+ const normalized = value.trim().toLowerCase();
79
+ if (["1", "true", "yes", "on"].includes(normalized))
80
+ return true;
81
+ if (["0", "false", "no", "off"].includes(normalized))
82
+ return false;
83
+ return undefined;
84
+ }
85
+ function isRecord(value) {
86
+ return value !== null && typeof value === "object" && !Array.isArray(value);
87
+ }