pi-ui-extend 1.0.42 → 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 (233) hide show
  1. package/README.md +41 -12
  2. package/dist/app/app.d.ts +10 -0
  3. package/dist/app/app.js +132 -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 +3 -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 +51 -34
  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 +14 -0
  17. package/dist/app/commands/reload-context-inventory.js +12 -1
  18. package/dist/app/input/autocomplete-controller.js +61 -34
  19. package/dist/app/input/input-action-controller.d.ts +2 -0
  20. package/dist/app/input/input-action-controller.js +8 -1
  21. package/dist/app/input/input-controller.d.ts +2 -1
  22. package/dist/app/input/input-controller.js +7 -2
  23. package/dist/app/input/prompt-enhancer-controller.js +53 -35
  24. package/dist/app/input/voice-controller.d.ts +41 -45
  25. package/dist/app/input/voice-controller.js +351 -419
  26. package/dist/app/model/model-usage-controller.js +17 -7
  27. package/dist/app/model/model-usage-status.d.ts +4 -1
  28. package/dist/app/model/model-usage-status.js +270 -46
  29. package/dist/app/popup/menu-items-controller.d.ts +13 -3
  30. package/dist/app/popup/menu-items-controller.js +37 -21
  31. package/dist/app/popup/popup-action-controller.d.ts +12 -2
  32. package/dist/app/popup/popup-action-controller.js +77 -24
  33. package/dist/app/popup/popup-menu-controller.d.ts +36 -14
  34. package/dist/app/popup/popup-menu-controller.js +239 -69
  35. package/dist/app/rendering/dcp-stats.d.ts +9 -4
  36. package/dist/app/rendering/dcp-stats.js +40 -425
  37. package/dist/app/rendering/popup-menu-renderer.d.ts +3 -5
  38. package/dist/app/rendering/popup-menu-renderer.js +42 -37
  39. package/dist/app/rendering/render-controller.js +23 -2
  40. package/dist/app/rendering/status-line-renderer.d.ts +5 -0
  41. package/dist/app/rendering/status-line-renderer.js +41 -5
  42. package/dist/app/rendering/tab-line-renderer.js +26 -20
  43. package/dist/app/runtime.d.ts +12 -1
  44. package/dist/app/runtime.js +120 -12
  45. package/dist/app/screen/mouse-controller.d.ts +2 -0
  46. package/dist/app/screen/mouse-controller.js +17 -7
  47. package/dist/app/screen/status-controller.d.ts +4 -0
  48. package/dist/app/screen/status-controller.js +5 -0
  49. package/dist/app/session/lazy-session-manager.js +34 -0
  50. package/dist/app/session/session-event-controller.d.ts +1 -0
  51. package/dist/app/session/session-event-controller.js +10 -1
  52. package/dist/app/session/session-history.d.ts +1 -0
  53. package/dist/app/session/session-history.js +12 -1
  54. package/dist/app/session/session-lifecycle-controller.d.ts +6 -1
  55. package/dist/app/session/session-lifecycle-controller.js +10 -1
  56. package/dist/app/session/tabs-controller.d.ts +25 -1
  57. package/dist/app/session/tabs-controller.js +442 -153
  58. package/dist/app/subagents/subagents-widget-controller.d.ts +1 -0
  59. package/dist/app/subagents/subagents-widget-controller.js +6 -0
  60. package/dist/app/types.d.ts +11 -1
  61. package/dist/app/workspace/workspace-actions-controller.js +10 -2
  62. package/dist/app/workspace/workspace-undo.d.ts +1 -0
  63. package/dist/app/workspace/workspace-undo.js +1 -0
  64. package/dist/bundled-extensions/question/index.js +9 -1
  65. package/dist/bundled-extensions/question/remote.d.ts +4 -0
  66. package/dist/bundled-extensions/question/remote.js +33 -0
  67. package/dist/bundled-extensions/telegram-connector/bot.d.ts +43 -0
  68. package/dist/bundled-extensions/telegram-connector/bot.js +166 -0
  69. package/dist/bundled-extensions/telegram-connector/config.d.ts +8 -0
  70. package/dist/bundled-extensions/telegram-connector/config.js +87 -0
  71. package/dist/bundled-extensions/telegram-connector/coordinator.d.ts +66 -0
  72. package/dist/bundled-extensions/telegram-connector/coordinator.js +413 -0
  73. package/dist/bundled-extensions/telegram-connector/index.d.ts +3 -0
  74. package/dist/bundled-extensions/telegram-connector/index.js +195 -0
  75. package/dist/bundled-extensions/terminal-bell/index.d.ts +0 -8
  76. package/dist/bundled-extensions/terminal-bell/index.js +0 -76
  77. package/dist/bundled-extensions/workspace-undo/index.d.ts +26 -0
  78. package/dist/bundled-extensions/workspace-undo/index.js +191 -0
  79. package/dist/config.d.ts +21 -2
  80. package/dist/config.js +159 -32
  81. package/dist/default-pix-config.js +20 -10
  82. package/dist/schemas/pi-tools-suite-schema.d.ts +8 -4
  83. package/dist/schemas/pi-tools-suite-schema.js +11 -5
  84. package/dist/schemas/pix-schema.d.ts +12 -2
  85. package/dist/schemas/pix-schema.js +37 -9
  86. package/dist/schemas/tasks-schema.js +1 -1
  87. package/docs/concurrency.md +3 -1
  88. package/docs/desktop-mvp.md +10 -5
  89. package/docs/desktop-task-manager.md +81 -28
  90. package/docs/release.md +30 -6
  91. package/external/pi-tools-suite/README.md +165 -39
  92. package/external/pi-tools-suite/docs/session-recovery.md +7 -0
  93. package/external/pi-tools-suite/docs/subagent-model-pools.md +12 -5
  94. package/external/pi-tools-suite/docs/ui-qa-subagent.md +440 -0
  95. package/external/pi-tools-suite/package.json +6 -1
  96. package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +2 -1
  97. package/external/pi-tools-suite/src/antigravity-auth/constants.ts +9 -3
  98. package/external/pi-tools-suite/src/antigravity-auth/headers.ts +41 -3
  99. package/external/pi-tools-suite/src/antigravity-auth/models.ts +94 -21
  100. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +5 -17
  101. package/external/pi-tools-suite/src/antigravity-auth/payload.ts +72 -7
  102. package/external/pi-tools-suite/src/antigravity-auth/stream.ts +13 -1
  103. package/external/pi-tools-suite/src/async-subagents/agents/implement.md +1 -1
  104. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/browser.mjs +346 -0
  105. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/desktop.mjs +1038 -0
  106. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/tui.mjs +759 -0
  107. package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/scripts/browser-qa-runner.mjs +32 -23
  108. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/chrome-devtools/chrome-devtools-provider.mjs +895 -0
  109. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/linux/linux-atspi.py +494 -0
  110. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/macos/macos-accessibility.swift +1085 -0
  111. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/native-terminal/bridge-client.mjs +50 -0
  112. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/native-terminal/native-terminal-host.mjs +501 -0
  113. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/windows/windows-uia.ps1 +454 -0
  114. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-auth.md +114 -0
  115. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-chrome-devtools.md +91 -0
  116. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-playwright.md +145 -0
  117. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser.md +82 -0
  118. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-linux.md +26 -0
  119. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-macos.md +29 -0
  120. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-windows.md +23 -0
  121. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop.md +54 -0
  122. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui-native-terminal.md +59 -0
  123. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui-pty.md +37 -0
  124. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui.md +60 -0
  125. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/scripts/ui-qa-runner.mjs +535 -0
  126. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa.md +75 -0
  127. package/external/pi-tools-suite/src/async-subagents/commands.ts +1 -1
  128. package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +1 -1
  129. package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +0 -2
  130. package/external/pi-tools-suite/src/async-subagents/core/browser-qa.ts +16 -2
  131. package/external/pi-tools-suite/src/async-subagents/core/config.ts +40 -17
  132. package/external/pi-tools-suite/src/async-subagents/core/model-selection.ts +2 -1
  133. package/external/pi-tools-suite/src/async-subagents/core/registry.ts +158 -33
  134. package/external/pi-tools-suite/src/async-subagents/core/routing.ts +7 -4
  135. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +34 -24
  136. package/external/pi-tools-suite/src/async-subagents/core/ultrawork-auto.ts +51 -45
  137. package/external/pi-tools-suite/src/async-subagents/lib.ts +9 -1
  138. package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +0 -1
  139. package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +2 -2
  140. package/external/pi-tools-suite/src/coding-discipline/index.ts +90 -59
  141. package/external/pi-tools-suite/src/config.ts +14 -0
  142. package/external/pi-tools-suite/src/context-gateway/accounting-log.ts +282 -0
  143. package/external/pi-tools-suite/src/context-gateway/config.ts +66 -1
  144. package/external/pi-tools-suite/src/context-gateway/efficiency.ts +414 -0
  145. package/external/pi-tools-suite/src/context-gateway/enforcement.ts +123 -3
  146. package/external/pi-tools-suite/src/context-gateway/index.ts +137 -4
  147. package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +5 -5
  148. package/external/pi-tools-suite/src/context-gateway/telemetry.ts +2 -2
  149. package/external/pi-tools-suite/src/context-gateway/types.ts +8 -1
  150. package/external/pi-tools-suite/src/dcp/auto-compress-budget.ts +39 -4
  151. package/external/pi-tools-suite/src/dcp/auto-compress.ts +41 -14
  152. package/external/pi-tools-suite/src/dcp/commands.ts +14 -156
  153. package/external/pi-tools-suite/src/dcp/compress-tool.ts +8 -2
  154. package/external/pi-tools-suite/src/dcp/compression-progress.ts +22 -0
  155. package/external/pi-tools-suite/src/dcp/config.ts +58 -4
  156. package/external/pi-tools-suite/src/dcp/conversation-index.ts +36 -9
  157. package/external/pi-tools-suite/src/dcp/diagnostics.ts +41 -0
  158. package/external/pi-tools-suite/src/dcp/fresh-tool-results.ts +38 -0
  159. package/external/pi-tools-suite/src/dcp/index.ts +338 -66
  160. package/external/pi-tools-suite/src/dcp/journal.ts +126 -4
  161. package/external/pi-tools-suite/src/dcp/progress-controller.ts +2 -1
  162. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +92 -2
  163. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +36 -33
  164. package/external/pi-tools-suite/src/dcp/pruner.ts +1 -0
  165. package/external/pi-tools-suite/src/dcp/routine-pressure.ts +86 -0
  166. package/external/pi-tools-suite/src/dcp/state.ts +9 -0
  167. package/external/pi-tools-suite/src/dcp/statistics.d.ts +8 -0
  168. package/external/pi-tools-suite/src/dcp/statistics.js +156 -0
  169. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +68 -16
  170. package/external/pi-tools-suite/src/lib/project.ts +36 -1
  171. package/external/pi-tools-suite/src/model-tools/index.ts +10 -7
  172. package/external/pi-tools-suite/src/repo-discovery/index.ts +304 -4
  173. package/external/pi-tools-suite/src/resource-registry/index.ts +282 -21
  174. package/external/pi-tools-suite/src/session-recovery/index.ts +17 -0
  175. package/external/pi-tools-suite/src/todo/index.ts +21 -0
  176. package/external/pi-tools-suite/src/todo/todo.ts +19 -1
  177. package/external/pi-tools-suite/src/tool-descriptions.ts +44 -15
  178. package/package.json +7 -8
  179. package/schemas/pi-tools-suite.json +44 -12
  180. package/schemas/pix.json +109 -15
  181. package/schemas/tasks.json +1 -2
  182. package/skills/simplify/SKILL.md +33 -5
  183. package/external/pi-tools-suite/docs/browser-qa-subagent.md +0 -196
  184. package/external/pi-tools-suite/src/async-subagents/agents/browser-qa.md +0 -598
  185. package/skills/playwright-cli/SKILL.md +0 -420
  186. package/skills/playwright-cli/references/element-attributes.md +0 -23
  187. package/skills/playwright-cli/references/playwright-tests.md +0 -50
  188. package/skills/playwright-cli/references/request-mocking.md +0 -87
  189. package/skills/playwright-cli/references/running-code.md +0 -241
  190. package/skills/playwright-cli/references/session-management.md +0 -273
  191. package/skills/playwright-cli/references/spec-driven-testing.md +0 -311
  192. package/skills/playwright-cli/references/storage-state.md +0 -290
  193. package/skills/playwright-cli/references/test-generation.md +0 -142
  194. package/skills/playwright-cli/references/tracing.md +0 -154
  195. package/skills/playwright-cli/references/video-recording.md +0 -147
  196. package/skills/spec-wiki/SKILL.md +0 -55
  197. package/skills/spec-wiki/evals/evals.json +0 -132
  198. package/skills/spec-wiki/evals/files/bootstrap/architecture/rfc-007.md +0 -14
  199. package/skills/spec-wiki/evals/files/bootstrap/docs/how-to-pay.md +0 -4
  200. package/skills/spec-wiki/evals/files/bootstrap/docs/overview.md +0 -9
  201. package/skills/spec-wiki/evals/files/bootstrap/requirements/payment-behavior.md +0 -17
  202. package/skills/spec-wiki/evals/files/bootstrap/src/payments.ts +0 -12
  203. package/skills/spec-wiki/evals/files/bootstrap/tests/payments.test.ts +0 -11
  204. package/skills/spec-wiki/evals/files/focused/src/session.ts +0 -5
  205. package/skills/spec-wiki/evals/files/focused/tests/session.test.ts +0 -17
  206. package/skills/spec-wiki/evals/files/maintenance/low-signal/incoming/capture.txt +0 -2
  207. package/skills/spec-wiki/evals/files/maintenance/low-signal/src/capture.ts +0 -3
  208. package/skills/spec-wiki/evals/files/maintenance/move/docs/old-job-contract.md +0 -18
  209. package/skills/spec-wiki/evals/files/maintenance/move/src/job.ts +0 -5
  210. package/skills/spec-wiki/evals/files/maintenance/new-relation/docs/session-refresh.md +0 -24
  211. package/skills/spec-wiki/evals/files/maintenance/new-relation/incoming/refresh-worker.ts +0 -7
  212. package/skills/spec-wiki/evals/files/maintenance/new-relation/src/session.ts +0 -7
  213. package/skills/spec-wiki/evals/files/maintenance/no-impact/docs/auth-contract.md +0 -17
  214. package/skills/spec-wiki/evals/files/maintenance/no-impact/incoming/telemetry-format.ts +0 -3
  215. package/skills/spec-wiki/evals/files/maintenance/no-impact/src/auth.ts +0 -3
  216. package/skills/spec-wiki/evals/files/query/docs/how-to-run-jobs.md +0 -4
  217. package/skills/spec-wiki/evals/files/query/docs/spec-overview.md +0 -9
  218. package/skills/spec-wiki/evals/files/query/features/jobs/cancellation-contract.md +0 -15
  219. package/skills/spec-wiki/evals/files/query/features/jobs/retry-spec.md +0 -6
  220. package/skills/spec-wiki/evals/maintenance_regression.py +0 -311
  221. package/skills/spec-wiki/evals/retrieval-eval.md +0 -37
  222. package/skills/spec-wiki/evals/retrieval-evals.json +0 -37
  223. package/skills/spec-wiki/evals/trigger-evals.json +0 -94
  224. package/skills/spec-wiki/references/bootstrap.md +0 -100
  225. package/skills/spec-wiki/references/focused-spec.md +0 -117
  226. package/skills/spec-wiki/references/index-schema.md +0 -330
  227. package/skills/spec-wiki/references/maintenance.md +0 -134
  228. package/skills/spec-wiki/references/query.md +0 -74
  229. package/skills/spec-wiki/scripts/spec_wiki.py +0 -2076
  230. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/examples/qa-auth.example.jsonc +0 -0
  231. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/examples/qa-flow.example.jsonc +0 -0
  232. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/vendor/fflate.LICENSE +0 -0
  233. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/vendor/fflate.mjs +0 -0
@@ -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();
@@ -235,6 +235,8 @@ export type RenderedLine = {
235
235
  index: number;
236
236
  } | {
237
237
  kind: "popup-menu-close";
238
+ } | {
239
+ kind: "model-visibility-clear";
238
240
  } | {
239
241
  kind: "todo-panel";
240
242
  } | {
@@ -346,6 +348,7 @@ export type SessionTab = {
346
348
  id: string;
347
349
  title: string;
348
350
  titlePlaceholder?: "loading" | "new";
351
+ draft?: boolean;
349
352
  isFork?: boolean;
350
353
  status: SessionTabStatus;
351
354
  activity?: SessionActivity;
@@ -542,6 +545,7 @@ export type ModelMenuValue = {
542
545
  model: SessionModel;
543
546
  ref: string;
544
547
  current: boolean;
548
+ visible?: boolean;
545
549
  };
546
550
  export type ScopedSessionModel = {
547
551
  model: SessionModel;
@@ -551,6 +555,12 @@ export type ThinkingMenuValue = {
551
555
  level: ThinkingLevel;
552
556
  current: boolean;
553
557
  };
558
+ export type ModelThinkingMenuState = {
559
+ thinkingLevel: ThinkingLevel;
560
+ availableThinkingLevels: readonly ThinkingLevel[];
561
+ source: "model" | "thinking";
562
+ visibilityMode: boolean;
563
+ };
554
564
  export type UserMessageMenuValue = "copy" | "fork" | "fork-new-tab" | "undo";
555
565
  export type UserMessageJumpMenuValue = {
556
566
  entryId?: string;
@@ -567,7 +577,7 @@ export type ResumeMenuValue = {
567
577
  session: SessionInfo;
568
578
  };
569
579
  export type ActivePopupMenu = "slash" | "model" | "thinking" | "resume" | "user-message" | "user-message-jump" | "queue-message" | "sdk-menu";
570
- export type PopupMenuPlacement = "default" | "under-tabs";
580
+ export type PopupMenuPlacement = "default" | "under-tabs" | "draft-surface";
571
581
  export type StatusThinkingTarget = {
572
582
  row: number;
573
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
+ }
@@ -0,0 +1,66 @@
1
+ import type { NormalizedQuestion, QuestionSelection } from "../question/types.js";
2
+ import { TelegramBotClient, type TelegramIncomingMessage } from "./bot.js";
3
+ import type { TelegramConnectorConfig } from "./config.js";
4
+ export type TelegramCompletionKind = "complete" | "error";
5
+ export type TelegramSessionEndpoint = {
6
+ sessionId: string;
7
+ cwd: string;
8
+ getTitle(): string;
9
+ isIdle(): boolean;
10
+ sendUserMessage(text: string): void;
11
+ dispatchNewSession(requestId: string): void;
12
+ };
13
+ type BotLike = Pick<TelegramBotClient, "start" | "stop" | "sendMessage">;
14
+ type BotFactory = (botToken: string, chatId: string) => BotLike;
15
+ export declare class TelegramConnectorCoordinator {
16
+ private readonly createBot;
17
+ private readonly reportError;
18
+ private readonly sessions;
19
+ private readonly completionRoutes;
20
+ private readonly pendingQuestions;
21
+ private readonly newSessionRequests;
22
+ private readonly newSessionSources;
23
+ private bot;
24
+ private botKey;
25
+ private focusSessionId;
26
+ private stopTimer;
27
+ constructor(createBot?: BotFactory, reportError?: (message: string) => void);
28
+ attachSession(endpoint: TelegramSessionEndpoint, config: TelegramConnectorConfig): void;
29
+ detachSession(sessionId: string, endpoint?: TelegramSessionEndpoint): void;
30
+ notifyCompletion(sessionId: string, kind: TelegramCompletionKind, summary?: string): Promise<void>;
31
+ askQuestions(sessionId: string, questions: NormalizedQuestion[], signal?: AbortSignal): Promise<QuestionSelection[] | null | undefined>;
32
+ createNewSessionRequest(sourceSessionId: string, task: string): string | undefined;
33
+ consumeNewSessionRequest(requestId: string, sourceSessionId: string): string | undefined;
34
+ finishNewSessionRequest(sourceSessionId: string): void;
35
+ announceNewSession(sessionId: string): Promise<void>;
36
+ sendStatus(): Promise<void>;
37
+ sendHelp(): Promise<void>;
38
+ sendNotice(text: string): Promise<void>;
39
+ handleIncoming(message: TelegramIncomingMessage): Promise<void>;
40
+ stop(): void;
41
+ private ensureBot;
42
+ private waitForQuestionReply;
43
+ private resolvePendingQuestion;
44
+ private cancelPendingQuestion;
45
+ private rememberCompletionRoute;
46
+ private hasPendingQuestionFor;
47
+ private pruneExpiredNewSessionRequests;
48
+ private scheduleStop;
49
+ private clearStopTimer;
50
+ }
51
+ type ParsedTelegramCommand = {
52
+ kind: "new";
53
+ task: string;
54
+ } | {
55
+ kind: "help";
56
+ } | {
57
+ kind: "status";
58
+ } | {
59
+ kind: "message";
60
+ text: string;
61
+ };
62
+ export declare function parseTelegramCommand(text: string): ParsedTelegramCommand;
63
+ export declare function parseQuestionAnswer(question: NormalizedQuestion, answer: string): QuestionSelection | undefined;
64
+ export declare function getTelegramConnectorCoordinator(): TelegramConnectorCoordinator;
65
+ export declare function resetTelegramConnectorCoordinatorForTests(): void;
66
+ export {};