@getpaseo/server 0.1.58 → 0.1.60

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 (276) hide show
  1. package/dist/scripts/dev-runner.js +26 -7
  2. package/dist/scripts/dev-runner.js.map +1 -1
  3. package/dist/server/client/daemon-client-runtime-metrics.d.ts +39 -0
  4. package/dist/server/client/daemon-client-runtime-metrics.d.ts.map +1 -0
  5. package/dist/server/client/daemon-client-runtime-metrics.js +173 -0
  6. package/dist/server/client/daemon-client-runtime-metrics.js.map +1 -0
  7. package/dist/server/client/daemon-client.d.ts +58 -9
  8. package/dist/server/client/daemon-client.d.ts.map +1 -1
  9. package/dist/server/client/daemon-client.js +151 -10
  10. package/dist/server/client/daemon-client.js.map +1 -1
  11. package/dist/server/server/agent/agent-manager.d.ts +55 -48
  12. package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
  13. package/dist/server/server/agent/agent-manager.js +541 -331
  14. package/dist/server/server/agent/agent-manager.js.map +1 -1
  15. package/dist/server/server/agent/agent-metadata-generator.d.ts +3 -2
  16. package/dist/server/server/agent/agent-metadata-generator.d.ts.map +1 -1
  17. package/dist/server/server/agent/agent-metadata-generator.js +31 -16
  18. package/dist/server/server/agent/agent-metadata-generator.js.map +1 -1
  19. package/dist/server/server/agent/agent-projections.d.ts +2 -1
  20. package/dist/server/server/agent/agent-projections.d.ts.map +1 -1
  21. package/dist/server/server/agent/agent-projections.js +29 -1
  22. package/dist/server/server/agent/agent-projections.js.map +1 -1
  23. package/dist/server/server/agent/agent-sdk-types.d.ts +9 -5
  24. package/dist/server/server/agent/agent-sdk-types.d.ts.map +1 -1
  25. package/dist/server/server/agent/agent-sdk-types.js.map +1 -1
  26. package/dist/server/server/agent/agent-storage.d.ts +76 -69
  27. package/dist/server/server/agent/agent-storage.d.ts.map +1 -1
  28. package/dist/server/server/agent/agent-storage.js +13 -6
  29. package/dist/server/server/agent/agent-storage.js.map +1 -1
  30. package/dist/server/server/agent/agent-stream-coalescer.d.ts +41 -0
  31. package/dist/server/server/agent/agent-stream-coalescer.d.ts.map +1 -0
  32. package/dist/server/server/agent/agent-stream-coalescer.js +166 -0
  33. package/dist/server/server/agent/agent-stream-coalescer.js.map +1 -0
  34. package/dist/server/server/agent/agent-timeline-store-types.d.ts +54 -0
  35. package/dist/server/server/agent/agent-timeline-store-types.d.ts.map +1 -0
  36. package/dist/server/server/agent/agent-timeline-store-types.js +2 -0
  37. package/dist/server/server/agent/agent-timeline-store-types.js.map +1 -0
  38. package/dist/server/server/agent/agent-timeline-store.d.ts +32 -0
  39. package/dist/server/server/agent/agent-timeline-store.d.ts.map +1 -0
  40. package/dist/server/server/agent/agent-timeline-store.js +245 -0
  41. package/dist/server/server/agent/agent-timeline-store.js.map +1 -0
  42. package/dist/server/server/agent/mcp-server.d.ts +12 -1
  43. package/dist/server/server/agent/mcp-server.d.ts.map +1 -1
  44. package/dist/server/server/agent/mcp-server.js +276 -65
  45. package/dist/server/server/agent/mcp-server.js.map +1 -1
  46. package/dist/server/server/agent/mcp-shared.d.ts +196 -152
  47. package/dist/server/server/agent/mcp-shared.d.ts.map +1 -1
  48. package/dist/server/server/agent/mcp-shared.js +40 -42
  49. package/dist/server/server/agent/mcp-shared.js.map +1 -1
  50. package/dist/server/server/agent/model-resolver.d.ts.map +1 -1
  51. package/dist/server/server/agent/model-resolver.js +3 -1
  52. package/dist/server/server/agent/model-resolver.js.map +1 -1
  53. package/dist/server/server/agent/prompt-attachments.d.ts +6 -0
  54. package/dist/server/server/agent/prompt-attachments.d.ts.map +1 -0
  55. package/dist/server/server/agent/prompt-attachments.js +31 -0
  56. package/dist/server/server/agent/prompt-attachments.js.map +1 -0
  57. package/dist/server/server/agent/provider-launch-config.d.ts +12 -10
  58. package/dist/server/server/agent/provider-launch-config.d.ts.map +1 -1
  59. package/dist/server/server/agent/provider-launch-config.js +34 -0
  60. package/dist/server/server/agent/provider-launch-config.js.map +1 -1
  61. package/dist/server/server/agent/provider-manifest.d.ts +1 -0
  62. package/dist/server/server/agent/provider-manifest.d.ts.map +1 -1
  63. package/dist/server/server/agent/provider-manifest.js +22 -1
  64. package/dist/server/server/agent/provider-manifest.js.map +1 -1
  65. package/dist/server/server/agent/provider-registry.d.ts +5 -2
  66. package/dist/server/server/agent/provider-registry.d.ts.map +1 -1
  67. package/dist/server/server/agent/provider-registry.js +20 -9
  68. package/dist/server/server/agent/provider-registry.js.map +1 -1
  69. package/dist/server/server/agent/provider-snapshot-manager.d.ts +17 -5
  70. package/dist/server/server/agent/provider-snapshot-manager.d.ts.map +1 -1
  71. package/dist/server/server/agent/provider-snapshot-manager.js +150 -61
  72. package/dist/server/server/agent/provider-snapshot-manager.js.map +1 -1
  73. package/dist/server/server/agent/providers/acp-agent.d.ts +8 -4
  74. package/dist/server/server/agent/providers/acp-agent.d.ts.map +1 -1
  75. package/dist/server/server/agent/providers/acp-agent.js +73 -8
  76. package/dist/server/server/agent/providers/acp-agent.js.map +1 -1
  77. package/dist/server/server/agent/providers/claude/claude-models.d.ts.map +1 -1
  78. package/dist/server/server/agent/providers/claude/claude-models.js +21 -0
  79. package/dist/server/server/agent/providers/claude/claude-models.js.map +1 -1
  80. package/dist/server/server/agent/providers/claude/task-notification-tool-call.d.ts +2 -2
  81. package/dist/server/server/agent/providers/claude-agent.d.ts +1 -1
  82. package/dist/server/server/agent/providers/claude-agent.d.ts.map +1 -1
  83. package/dist/server/server/agent/providers/claude-agent.js +15 -8
  84. package/dist/server/server/agent/providers/claude-agent.js.map +1 -1
  85. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts +37 -4
  86. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts.map +1 -1
  87. package/dist/server/server/agent/providers/codex-app-server-agent.js +61 -31
  88. package/dist/server/server/agent/providers/codex-app-server-agent.js.map +1 -1
  89. package/dist/server/server/agent/providers/copilot-acp-agent.d.ts.map +1 -1
  90. package/dist/server/server/agent/providers/copilot-acp-agent.js +3 -2
  91. package/dist/server/server/agent/providers/copilot-acp-agent.js.map +1 -1
  92. package/dist/server/server/agent/providers/mock-load-test-agent.d.ts +64 -0
  93. package/dist/server/server/agent/providers/mock-load-test-agent.d.ts.map +1 -0
  94. package/dist/server/server/agent/providers/mock-load-test-agent.js +585 -0
  95. package/dist/server/server/agent/providers/mock-load-test-agent.js.map +1 -0
  96. package/dist/server/server/agent/providers/opencode-agent.d.ts +19 -4
  97. package/dist/server/server/agent/providers/opencode-agent.d.ts.map +1 -1
  98. package/dist/server/server/agent/providers/opencode-agent.js +227 -118
  99. package/dist/server/server/agent/providers/opencode-agent.js.map +1 -1
  100. package/dist/server/server/agent/providers/pi-direct-agent.d.ts +69 -0
  101. package/dist/server/server/agent/providers/pi-direct-agent.d.ts.map +1 -0
  102. package/dist/server/server/agent/providers/pi-direct-agent.js +1177 -0
  103. package/dist/server/server/agent/providers/pi-direct-agent.js.map +1 -0
  104. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts +7 -4
  105. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts.map +1 -1
  106. package/dist/server/server/agent-attention-policy.d.ts +13 -13
  107. package/dist/server/server/agent-attention-policy.d.ts.map +1 -1
  108. package/dist/server/server/agent-attention-policy.js +20 -36
  109. package/dist/server/server/agent-attention-policy.js.map +1 -1
  110. package/dist/server/server/bootstrap.d.ts +6 -0
  111. package/dist/server/server/bootstrap.d.ts.map +1 -1
  112. package/dist/server/server/bootstrap.js +113 -11
  113. package/dist/server/server/bootstrap.js.map +1 -1
  114. package/dist/server/server/chat/chat-rpc-schemas.d.ts +44 -44
  115. package/dist/server/server/chat/chat-types.d.ts +6 -6
  116. package/dist/server/server/checkout-diff-manager.d.ts +0 -1
  117. package/dist/server/server/checkout-diff-manager.d.ts.map +1 -1
  118. package/dist/server/server/checkout-diff-manager.js +6 -4
  119. package/dist/server/server/checkout-diff-manager.js.map +1 -1
  120. package/dist/server/server/config.d.ts.map +1 -1
  121. package/dist/server/server/config.js +1 -0
  122. package/dist/server/server/config.js.map +1 -1
  123. package/dist/server/server/file-explorer/service.d.ts.map +1 -1
  124. package/dist/server/server/file-explorer/service.js +2 -1
  125. package/dist/server/server/file-explorer/service.js.map +1 -1
  126. package/dist/server/server/loop/rpc-schemas.d.ts +392 -392
  127. package/dist/server/server/loop-service.d.ts +52 -52
  128. package/dist/server/server/paseo-worktree-archive-service.d.ts +41 -0
  129. package/dist/server/server/paseo-worktree-archive-service.d.ts.map +1 -0
  130. package/dist/server/server/paseo-worktree-archive-service.js +137 -0
  131. package/dist/server/server/paseo-worktree-archive-service.js.map +1 -0
  132. package/dist/server/server/paseo-worktree-service.d.ts +24 -0
  133. package/dist/server/server/paseo-worktree-service.d.ts.map +1 -0
  134. package/dist/server/server/paseo-worktree-service.js +94 -0
  135. package/dist/server/server/paseo-worktree-service.js.map +1 -0
  136. package/dist/server/server/path-utils.d.ts +1 -0
  137. package/dist/server/server/path-utils.d.ts.map +1 -1
  138. package/dist/server/server/path-utils.js +9 -0
  139. package/dist/server/server/path-utils.js.map +1 -1
  140. package/dist/server/server/persisted-config.d.ts +73 -73
  141. package/dist/server/server/persistence-hooks.d.ts.map +1 -1
  142. package/dist/server/server/persistence-hooks.js +3 -0
  143. package/dist/server/server/persistence-hooks.js.map +1 -1
  144. package/dist/server/server/resolve-worktree-creation-intent.d.ts +30 -0
  145. package/dist/server/server/resolve-worktree-creation-intent.d.ts.map +1 -0
  146. package/dist/server/server/resolve-worktree-creation-intent.js +163 -0
  147. package/dist/server/server/resolve-worktree-creation-intent.js.map +1 -0
  148. package/dist/server/server/schedule/rpc-schemas.d.ts +192 -192
  149. package/dist/server/server/schedule/service.d.ts +1 -1
  150. package/dist/server/server/schedule/service.d.ts.map +1 -1
  151. package/dist/server/server/schedule/types.d.ts +44 -44
  152. package/dist/server/server/script-health-monitor.d.ts +39 -0
  153. package/dist/server/server/script-health-monitor.d.ts.map +1 -0
  154. package/dist/server/server/script-health-monitor.js +158 -0
  155. package/dist/server/server/script-health-monitor.js.map +1 -0
  156. package/dist/server/server/script-proxy.d.ts +40 -0
  157. package/dist/server/server/script-proxy.d.ts.map +1 -0
  158. package/dist/server/server/script-proxy.js +245 -0
  159. package/dist/server/server/script-proxy.js.map +1 -0
  160. package/dist/server/server/script-route-branch-handler.d.ts +10 -0
  161. package/dist/server/server/script-route-branch-handler.d.ts.map +1 -0
  162. package/dist/server/server/script-route-branch-handler.js +45 -0
  163. package/dist/server/server/script-route-branch-handler.js.map +1 -0
  164. package/dist/server/server/script-status-projection.d.ts +29 -0
  165. package/dist/server/server/script-status-projection.d.ts.map +1 -0
  166. package/dist/server/server/script-status-projection.js +133 -0
  167. package/dist/server/server/script-status-projection.js.map +1 -0
  168. package/dist/server/server/session.d.ts +77 -13
  169. package/dist/server/server/session.d.ts.map +1 -1
  170. package/dist/server/server/session.js +1290 -548
  171. package/dist/server/server/session.js.map +1 -1
  172. package/dist/server/server/websocket-server.d.ts +27 -3
  173. package/dist/server/server/websocket-server.d.ts.map +1 -1
  174. package/dist/server/server/websocket-server.js +112 -29
  175. package/dist/server/server/websocket-server.js.map +1 -1
  176. package/dist/server/server/workspace-archive-service.d.ts +8 -0
  177. package/dist/server/server/workspace-archive-service.d.ts.map +1 -0
  178. package/dist/server/server/workspace-archive-service.js +17 -0
  179. package/dist/server/server/workspace-archive-service.js.map +1 -0
  180. package/dist/server/server/workspace-git-metadata.d.ts +24 -0
  181. package/dist/server/server/workspace-git-metadata.d.ts.map +1 -0
  182. package/dist/server/server/workspace-git-metadata.js +78 -0
  183. package/dist/server/server/workspace-git-metadata.js.map +1 -0
  184. package/dist/server/server/workspace-git-service.d.ts +104 -5
  185. package/dist/server/server/workspace-git-service.d.ts.map +1 -1
  186. package/dist/server/server/workspace-git-service.js +442 -56
  187. package/dist/server/server/workspace-git-service.js.map +1 -1
  188. package/dist/server/server/workspace-reconciliation-service.d.ts +54 -0
  189. package/dist/server/server/workspace-reconciliation-service.d.ts.map +1 -0
  190. package/dist/server/server/workspace-reconciliation-service.js +176 -0
  191. package/dist/server/server/workspace-reconciliation-service.js.map +1 -0
  192. package/dist/server/server/workspace-registry-bootstrap.d.ts.map +1 -1
  193. package/dist/server/server/workspace-registry-bootstrap.js +4 -3
  194. package/dist/server/server/workspace-registry-bootstrap.js.map +1 -1
  195. package/dist/server/server/workspace-registry.d.ts +8 -8
  196. package/dist/server/server/workspace-registry.test-helpers.d.ts +37 -0
  197. package/dist/server/server/workspace-registry.test-helpers.d.ts.map +1 -0
  198. package/dist/server/server/workspace-registry.test-helpers.js +121 -0
  199. package/dist/server/server/workspace-registry.test-helpers.js.map +1 -0
  200. package/dist/server/server/workspace-script-runtime-store.d.ts +28 -0
  201. package/dist/server/server/workspace-script-runtime-store.d.ts.map +1 -0
  202. package/dist/server/server/workspace-script-runtime-store.js +78 -0
  203. package/dist/server/server/workspace-script-runtime-store.js.map +1 -0
  204. package/dist/server/server/workspace-service-env.d.ts +17 -0
  205. package/dist/server/server/workspace-service-env.d.ts.map +1 -0
  206. package/dist/server/server/workspace-service-env.js +80 -0
  207. package/dist/server/server/workspace-service-env.js.map +1 -0
  208. package/dist/server/server/workspace-service-port-registry.d.ts +19 -0
  209. package/dist/server/server/workspace-service-port-registry.d.ts.map +1 -0
  210. package/dist/server/server/workspace-service-port-registry.js +59 -0
  211. package/dist/server/server/workspace-service-port-registry.js.map +1 -0
  212. package/dist/server/server/worktree-bootstrap.d.ts +55 -10
  213. package/dist/server/server/worktree-bootstrap.d.ts.map +1 -1
  214. package/dist/server/server/worktree-bootstrap.js +290 -112
  215. package/dist/server/server/worktree-bootstrap.js.map +1 -1
  216. package/dist/server/server/worktree-core.d.ts +25 -0
  217. package/dist/server/server/worktree-core.d.ts.map +1 -0
  218. package/dist/server/server/worktree-core.js +75 -0
  219. package/dist/server/server/worktree-core.js.map +1 -0
  220. package/dist/server/server/worktree-errors.d.ts +12 -0
  221. package/dist/server/server/worktree-errors.d.ts.map +1 -0
  222. package/dist/server/server/worktree-errors.js +31 -0
  223. package/dist/server/server/worktree-errors.js.map +1 -0
  224. package/dist/server/server/worktree-session.d.ts +56 -70
  225. package/dist/server/server/worktree-session.d.ts.map +1 -1
  226. package/dist/server/server/worktree-session.js +176 -251
  227. package/dist/server/server/worktree-session.js.map +1 -1
  228. package/dist/server/services/github-service.d.ts +225 -0
  229. package/dist/server/services/github-service.d.ts.map +1 -0
  230. package/dist/server/services/github-service.js +1381 -0
  231. package/dist/server/services/github-service.js.map +1 -0
  232. package/dist/server/shared/messages.d.ts +29408 -12268
  233. package/dist/server/shared/messages.d.ts.map +1 -1
  234. package/dist/server/shared/messages.js +391 -65
  235. package/dist/server/shared/messages.js.map +1 -1
  236. package/dist/server/terminal/shell-integration/zsh/.zshenv +17 -0
  237. package/dist/server/terminal/shell-integration/zsh/paseo-integration.zsh +32 -0
  238. package/dist/server/terminal/terminal-manager.d.ts +9 -0
  239. package/dist/server/terminal/terminal-manager.d.ts.map +1 -1
  240. package/dist/server/terminal/terminal-manager.js +27 -0
  241. package/dist/server/terminal/terminal-manager.js.map +1 -1
  242. package/dist/server/terminal/terminal-output-coalescer.d.ts +30 -0
  243. package/dist/server/terminal/terminal-output-coalescer.d.ts.map +1 -0
  244. package/dist/server/terminal/terminal-output-coalescer.js +55 -0
  245. package/dist/server/terminal/terminal-output-coalescer.js.map +1 -0
  246. package/dist/server/terminal/terminal.d.ts +32 -1
  247. package/dist/server/terminal/terminal.d.ts.map +1 -1
  248. package/dist/server/terminal/terminal.js +397 -17
  249. package/dist/server/terminal/terminal.js.map +1 -1
  250. package/dist/server/utils/checkout-git.d.ts +63 -10
  251. package/dist/server/utils/checkout-git.d.ts.map +1 -1
  252. package/dist/server/utils/checkout-git.js +321 -229
  253. package/dist/server/utils/checkout-git.js.map +1 -1
  254. package/dist/server/utils/promise-timeout.d.ts +9 -0
  255. package/dist/server/utils/promise-timeout.d.ts.map +1 -0
  256. package/dist/server/utils/promise-timeout.js +25 -0
  257. package/dist/server/utils/promise-timeout.js.map +1 -0
  258. package/dist/server/utils/script-hostname.d.ts +8 -0
  259. package/dist/server/utils/script-hostname.d.ts.map +1 -0
  260. package/dist/server/utils/script-hostname.js +14 -0
  261. package/dist/server/utils/script-hostname.js.map +1 -0
  262. package/dist/server/utils/string-command-shell.d.ts +10 -0
  263. package/dist/server/utils/string-command-shell.d.ts.map +1 -0
  264. package/dist/server/utils/string-command-shell.js +21 -0
  265. package/dist/server/utils/string-command-shell.js.map +1 -0
  266. package/dist/server/utils/worktree.d.ts +54 -7
  267. package/dist/server/utils/worktree.d.ts.map +1 -1
  268. package/dist/server/utils/worktree.js +434 -129
  269. package/dist/server/utils/worktree.js.map +1 -1
  270. package/dist/src/terminal/shell-integration/zsh/.zshenv +17 -0
  271. package/dist/src/terminal/shell-integration/zsh/paseo-integration.zsh +32 -0
  272. package/package.json +11 -14
  273. package/dist/server/server/agent/providers/pi-acp-agent.d.ts +0 -28
  274. package/dist/server/server/agent/providers/pi-acp-agent.d.ts.map +0 -1
  275. package/dist/server/server/agent/providers/pi-acp-agent.js +0 -302
  276. package/dist/server/server/agent/providers/pi-acp-agent.js.map +0 -1
@@ -1,13 +1,13 @@
1
1
  import equal from "fast-deep-equal";
2
2
  import { v4 as uuidv4 } from "uuid";
3
- import { stat } from "fs/promises";
4
- import { exec } from "node:child_process";
5
- import { promisify } from "util";
3
+ import { TTLCache } from "@isaacs/ttlcache";
4
+ import pMemoize from "p-memoize";
6
5
  import { resolve, sep } from "path";
7
6
  import { homedir } from "node:os";
8
7
  import { z } from "zod";
9
8
  import { isLegacyEditorTargetId, serializeAgentStreamEvent, } from "./messages.js";
10
9
  import { captureTerminalLines } from "../terminal/terminal.js";
10
+ import { TerminalOutputCoalescer } from "../terminal/terminal-output-coalescer.js";
11
11
  import { TerminalStreamOpcode, encodeTerminalSnapshotPayload, encodeTerminalStreamFrame, decodeTerminalResizePayload, } from "../shared/terminal-stream-protocol.js";
12
12
  import { TTSManager } from "./agent/tts-manager.js";
13
13
  import { STTManager } from "./agent/stt-manager.js";
@@ -21,32 +21,43 @@ import { ensureAgentLoaded } from "./agent/agent-loading.js";
21
21
  import { sendPromptToAgent, unarchiveAgentState } from "./agent/mcp-shared.js";
22
22
  import { experimental_createMCPClient } from "ai";
23
23
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
24
+ import { buildWorkspaceScriptPayloads } from "./script-status-projection.js";
25
+ import { deriveProjectSlug } from "./workspace-git-metadata.js";
26
+ import { spawnWorkspaceScript } from "./worktree-bootstrap.js";
24
27
  import { buildProviderRegistry } from "./agent/provider-registry.js";
28
+ import { resolveSnapshotCwd } from "./agent/provider-snapshot-manager.js";
25
29
  import { scheduleAgentMetadataGeneration } from "./agent/agent-metadata-generator.js";
26
30
  import { buildStoredAgentPayload, resolveEffectiveThinkingOptionId, resolveStoredAgentPayloadUpdatedAt, toAgentPayload, } from "./agent/agent-projections.js";
27
31
  import { MAX_EXPLICIT_AGENT_TITLE_CHARS } from "./agent/agent-title-limits.js";
28
32
  import { appendTimelineItemIfAgentKnown, emitLiveTimelineItemIfAgentKnown, } from "./agent/timeline-append.js";
29
33
  import { projectTimelineRows, selectTimelineWindowByProjectedLimit, } from "./agent/timeline-projection.js";
30
34
  import { DEFAULT_STRUCTURED_GENERATION_PROVIDERS, StructuredAgentFallbackError, StructuredAgentResponseError, generateStructuredAgentResponseWithFallback, } from "./agent/agent-response-loop.js";
31
- import { buildProjectPlacementForCwd, checkoutLiteFromGitSnapshot, detectStaleWorkspaces, deriveProjectKind, deriveProjectRootPath, deriveWorkspaceId, deriveWorkspaceDisplayName, deriveWorkspaceKind, normalizeWorkspaceId as normalizePersistedWorkspaceId, } from "./workspace-registry-model.js";
35
+ import { checkoutLiteFromGitSnapshot, normalizeWorkspaceId as normalizePersistedWorkspaceId, deriveProjectGroupingName, deriveWorkspaceId, deriveProjectRootPath, deriveProjectKind, deriveWorkspaceKind, deriveWorkspaceDisplayName, buildProjectPlacementForCwd as buildProjectPlacementForCwdStandalone, } from "./workspace-registry-model.js";
32
36
  import { createPersistedProjectRecord, createPersistedWorkspaceRecord, } from "./workspace-registry.js";
33
37
  import { buildVoiceModeSystemPrompt, stripVoiceModeSystemPrompt, wrapSpokenInput, } from "./voice-config.js";
34
38
  import { isVoicePermissionAllowed } from "./voice-permission-policy.js";
35
39
  import { listDirectoryEntries, readExplorerFile, getDownloadableFileInfo, } from "./file-explorer/service.js";
36
40
  import { runAsyncWorktreeBootstrap } from "./worktree-bootstrap.js";
37
- import { getCheckoutDiff, getCheckoutStatus, listBranchSuggestions, commitChanges, mergeToBase, mergeFromBase, pullCurrentBranch, pushCurrentBranch, createPullRequest, } from "../utils/checkout-git.js";
41
+ import { archivePersistedWorkspaceRecord } from "./workspace-archive-service.js";
42
+ import { WorkspaceReconciliationService } from "./workspace-reconciliation-service.js";
43
+ import { checkoutResolvedBranch, commitChanges, mergeToBase, mergeFromBase, pullCurrentBranch, pushCurrentBranch, createPullRequest, } from "../utils/checkout-git.js";
38
44
  import { getProjectIcon } from "../utils/project-icon.js";
39
45
  import { expandTilde } from "../utils/path.js";
40
46
  import { searchHomeDirectories, searchWorkspaceEntries } from "../utils/directory-suggestions.js";
41
- import { READ_ONLY_GIT_ENV, toCheckoutError } from "./checkout-git-utils.js";
47
+ import { toCheckoutError } from "./checkout-git-utils.js";
42
48
  import { toResolver } from "./speech/provider-resolver.js";
43
49
  import { resolveClientMessageId } from "./client-message-id.js";
44
50
  import { ChatServiceError } from "./chat/chat-service.js";
45
51
  import { notifyChatMentions } from "./chat/chat-mentions.js";
46
52
  import { execCommand } from "../utils/spawn.js";
47
- import { assertSafeGitRef as assertWorktreeSafeGitRef, buildAgentSessionConfig as buildWorktreeAgentSessionConfig, runWorktreeSetupInBackground as runWorktreeSetupInBackgroundSession, handleCreatePaseoWorktreeRequest as handleCreateWorktreeRequest, handlePaseoWorktreeArchiveRequest as handleWorktreeArchiveRequest, handlePaseoWorktreeListRequest as handleWorktreeListRequest, killTerminalsUnderPath as killWorktreeTerminalsUnderPath, registerPendingWorktreeWorkspace as registerPendingWorktreeWorkspaceSession, } from "./worktree-session.js";
48
- const execAsync = promisify(exec);
53
+ import { createGitHubService, } from "../services/github-service.js";
54
+ import { createPaseoWorktree, } from "./paseo-worktree-service.js";
55
+ import { createWorktreeCoreDeps } from "./worktree-core.js";
56
+ import { assertSafeGitRef as assertWorktreeSafeGitRef, buildAgentSessionConfig as buildWorktreeAgentSessionConfig, runWorktreeSetupInBackground as runWorktreeSetupInBackgroundSession, handleCreatePaseoWorktreeRequest as handleCreateWorktreeRequest, handlePaseoWorktreeArchiveRequest as handleWorktreeArchiveRequest, handlePaseoWorktreeListRequest as handleWorktreeListRequest, handleWorkspaceSetupStatusRequest as handleWorkspaceSetupStatusRequestMessage, } from "./worktree-session.js";
57
+ import { killTerminalsUnderPath as killWorktreeTerminalsUnderPath } from "./paseo-worktree-archive-service.js";
58
+ import { toWorktreeWireError } from "./worktree-errors.js";
49
59
  const MAX_INITIAL_AGENT_TITLE_CHARS = Math.min(60, MAX_EXPLICIT_AGENT_TITLE_CHARS);
60
+ const WORKSPACE_GIT_WATCH_REMOVED_FINGERPRINT = "__removed__";
50
61
  // TODO: Remove once all app store clients are on >=0.1.45 and understand arbitrary provider strings.
51
62
  // Clients before 0.1.45 validate providers with z.enum(["claude", "codex", "opencode"]) and reject
52
63
  // the entire session message if they encounter an unknown provider.
@@ -56,7 +67,7 @@ const MIN_VERSION_FLEXIBLE_EDITOR_IDS = "0.1.50";
56
67
  function isAppVersionAtLeast(appVersion, minVersion) {
57
68
  if (!appVersion)
58
69
  return false;
59
- // Strip RC/prerelease suffix: "0.1.45-rc.4" "0.1.45"
70
+ // Strip prerelease suffix: "0.1.45-beta.4" -> "0.1.45"
60
71
  const base = appVersion.replace(/-.*$/, "");
61
72
  const parts = base.split(".").map(Number);
62
73
  const minParts = minVersion.split(".").map(Number);
@@ -77,6 +88,11 @@ function clientSupportsFlexibleEditorIds(appVersion) {
77
88
  return isAppVersionAtLeast(appVersion, MIN_VERSION_FLEXIBLE_EDITOR_IDS);
78
89
  }
79
90
  const MAX_TERMINAL_STREAM_SLOTS = 256;
91
+ function beginAgentDeleteIfSupported(agentStorage, agentId) {
92
+ if ("beginDelete" in agentStorage && typeof agentStorage.beginDelete === "function") {
93
+ agentStorage.beginDelete(agentId);
94
+ }
95
+ }
80
96
  function deriveInitialAgentTitle(prompt) {
81
97
  const firstContentLine = prompt
82
98
  .split(/\r?\n/)
@@ -146,6 +162,8 @@ const MIN_STREAMING_SEGMENT_DURATION_MS = 1000;
146
162
  const MIN_STREAMING_SEGMENT_BYTES = Math.round(PCM_BYTES_PER_MS * MIN_STREAMING_SEGMENT_DURATION_MS);
147
163
  const AgentIdSchema = z.string().uuid();
148
164
  const VOICE_INTERRUPT_CONFIRMATION_MS = 500;
165
+ const AVAILABLE_EDITOR_TARGETS_CACHE_TTL_MS = 60000;
166
+ const AVAILABLE_EDITOR_TARGETS_CACHE_KEY = "available";
149
167
  class VoiceFeatureUnavailableError extends Error {
150
168
  constructor(context) {
151
169
  super(context.message);
@@ -216,7 +234,18 @@ export class Session {
216
234
  this.nextTerminalSlot = 0;
217
235
  this.inflightRequests = 0;
218
236
  this.peakInflightRequests = 0;
237
+ this.availableEditorTargetsCache = new TTLCache({
238
+ ttl: AVAILABLE_EDITOR_TARGETS_CACHE_TTL_MS,
239
+ max: 1,
240
+ checkAgeOnGet: true,
241
+ });
242
+ this.getMemoizedAvailableEditorTargets = pMemoize(async () => this.resolveAvailableEditorTargets(), {
243
+ cache: this.availableEditorTargetsCache,
244
+ cacheKey: () => AVAILABLE_EDITOR_TARGETS_CACHE_KEY,
245
+ });
219
246
  this.checkoutDiffSubscriptions = new Map();
247
+ this.workspaceGitWatchTargets = new Map();
248
+ this.workspaceGitFetchSubscriptions = new Map();
220
249
  this.workspaceGitSubscriptions = new Map();
221
250
  this.voiceModeAgentId = null;
222
251
  this.voiceModeBaseConfig = null;
@@ -227,9 +256,9 @@ export class Session {
227
256
  attention: 3,
228
257
  done: 4,
229
258
  };
230
- const { clientId, appVersion, onMessage, onBinaryMessage, onLifecycleIntent, logger, downloadTokenStore, pushTokenStore, paseoHome, agentManager, agentStorage, projectRegistry, workspaceRegistry, chatService, scheduleService, loopService, checkoutDiffManager, workspaceGitService, daemonConfigStore, mcpBaseUrl, stt, tts, terminalManager, providerSnapshotManager, voice, voiceBridge, dictation, agentProviderRuntimeSettings, providerOverrides, } = options;
259
+ const { clientId, appVersion, onMessage, onBinaryMessage, onLifecycleIntent, logger, downloadTokenStore, pushTokenStore, paseoHome, agentManager, agentStorage, projectRegistry, workspaceRegistry, chatService, scheduleService, loopService, checkoutDiffManager, github, workspaceGitService, daemonConfigStore, mcpBaseUrl, stt, tts, terminalManager, providerSnapshotManager, scriptRouteStore, scriptRuntimeStore, workspaceSetupSnapshots, onBranchChanged, getDaemonTcpPort, getDaemonTcpHost, resolveScriptHealth, voice, voiceBridge, dictation, agentProviderRuntimeSettings, providerOverrides, isDev, } = options;
231
260
  this.clientId = clientId;
232
- this.appVersion = appVersion;
261
+ this.appVersion = appVersion ?? null;
233
262
  this.sessionId = uuidv4();
234
263
  this.onMessage = onMessage;
235
264
  this.onBinaryMessage = onBinaryMessage ?? null;
@@ -237,6 +266,11 @@ export class Session {
237
266
  this.downloadTokenStore = downloadTokenStore;
238
267
  this.pushTokenStore = pushTokenStore;
239
268
  this.paseoHome = paseoHome;
269
+ this.sessionLogger = logger.child({
270
+ module: "session",
271
+ clientId: this.clientId,
272
+ sessionId: this.sessionId,
273
+ });
240
274
  this.agentManager = agentManager;
241
275
  this.agentStorage = agentStorage;
242
276
  this.projectRegistry = projectRegistry;
@@ -245,11 +279,19 @@ export class Session {
245
279
  this.scheduleService = scheduleService;
246
280
  this.loopService = loopService;
247
281
  this.checkoutDiffManager = checkoutDiffManager;
282
+ this.github = github ?? createGitHubService();
248
283
  this.workspaceGitService = workspaceGitService;
249
284
  this.daemonConfigStore = daemonConfigStore;
250
285
  this.mcpBaseUrl = mcpBaseUrl ?? null;
251
286
  this.terminalManager = terminalManager;
252
287
  this.providerSnapshotManager = providerSnapshotManager ?? null;
288
+ this.scriptRouteStore = scriptRouteStore ?? null;
289
+ this.scriptRuntimeStore = scriptRuntimeStore ?? null;
290
+ this.workspaceSetupSnapshots = workspaceSetupSnapshots ?? new Map();
291
+ this.onBranchChanged = onBranchChanged;
292
+ this.getDaemonTcpPort = getDaemonTcpPort ?? null;
293
+ this.getDaemonTcpHost = getDaemonTcpHost ?? null;
294
+ this.resolveScriptHealth = resolveScriptHealth ?? null;
253
295
  if (this.terminalManager) {
254
296
  this.unsubscribeTerminalsChanged = this.terminalManager.subscribeTerminalsChanged((event) => this.handleTerminalsChanged(event));
255
297
  }
@@ -279,15 +321,13 @@ export class Session {
279
321
  this.getSpeechReadiness = dictation?.getSpeechReadiness;
280
322
  this.agentProviderRuntimeSettings = agentProviderRuntimeSettings;
281
323
  this.providerOverrides = providerOverrides;
324
+ this.isDev = isDev === true;
282
325
  this.abortController = new AbortController();
283
- this.sessionLogger = logger.child({
284
- module: "session",
285
- clientId: this.clientId,
286
- sessionId: this.sessionId,
287
- });
288
326
  this.providerRegistry = buildProviderRegistry(this.sessionLogger, {
289
327
  runtimeSettings: this.agentProviderRuntimeSettings,
290
328
  providerOverrides: this.providerOverrides,
329
+ workspaceGitService: this.workspaceGitService,
330
+ isDev: this.isDev,
291
331
  });
292
332
  // Initialize per-session managers
293
333
  this.ttsManager = new TTSManager(this.sessionId, this.sessionLogger, tts);
@@ -309,6 +349,23 @@ export class Session {
309
349
  this.appVersion = appVersion;
310
350
  }
311
351
  }
352
+ async primeWorkspaceGitWatchFingerprintForWorkspace(workspace) {
353
+ const descriptor = await this.describeWorkspaceRecordWithGitData(workspace);
354
+ await this.primeWorkspaceGitWatchFingerprints([descriptor]);
355
+ }
356
+ async emitWorkspaceUpdateForWorkspaceId(workspaceId) {
357
+ await this.emitWorkspaceUpdatesForWorkspaceIds([workspaceId], { skipReconcile: true });
358
+ }
359
+ async archiveWorkspaceRecordForExternalMutation(workspaceId) {
360
+ await this.archiveWorkspaceRecord(workspaceId);
361
+ }
362
+ async emitWorkspaceUpdatesForExternalCwds(cwds) {
363
+ await this.emitWorkspaceUpdatesForCwds(cwds);
364
+ }
365
+ async warmWorkspaceGitDataForWorkspace(workspace) {
366
+ await this.primeWorkspaceGitWatchFingerprintForWorkspace(workspace);
367
+ await this.emitWorkspaceUpdateForWorkspaceId(workspace.workspaceId);
368
+ }
312
369
  /**
313
370
  * Get the client's current activity state
314
371
  */
@@ -323,6 +380,9 @@ export class Session {
323
380
  peakInflightRequests: this.peakInflightRequests,
324
381
  };
325
382
  }
383
+ emitServerMessage(message) {
384
+ this.emit(message);
385
+ }
326
386
  /**
327
387
  * Send initial state to client after connection
328
388
  */
@@ -332,18 +392,23 @@ export class Session {
332
392
  /**
333
393
  * Normalize a user prompt (with optional image metadata) for AgentManager
334
394
  */
335
- buildAgentPrompt(text, images) {
395
+ buildAgentPrompt(text, images, attachments) {
336
396
  const normalized = text?.trim() ?? "";
337
- if (!images || images.length === 0) {
397
+ const hasImages = Boolean(images && images.length > 0);
398
+ const hasAttachments = Boolean(attachments && attachments.length > 0);
399
+ if (!hasImages && !hasAttachments) {
338
400
  return normalized;
339
401
  }
340
402
  const blocks = [];
341
403
  if (normalized.length > 0) {
342
404
  blocks.push({ type: "text", text: normalized });
343
405
  }
344
- for (const image of images) {
406
+ for (const image of images ?? []) {
345
407
  blocks.push({ type: "image", data: image.data, mimeType: image.mimeType });
346
408
  }
409
+ for (const attachment of attachments ?? []) {
410
+ blocks.push(attachment);
411
+ }
347
412
  return blocks;
348
413
  }
349
414
  /**
@@ -546,8 +611,7 @@ export class Session {
546
611
  if (storedUpdatedAt) {
547
612
  const liveUpdatedAt = Date.parse(payload.updatedAt);
548
613
  const persistedUpdatedAt = Date.parse(storedUpdatedAt);
549
- if (!Number.isNaN(persistedUpdatedAt) &&
550
- (Number.isNaN(liveUpdatedAt) || persistedUpdatedAt > liveUpdatedAt)) {
614
+ if (Number.isNaN(liveUpdatedAt) || persistedUpdatedAt > liveUpdatedAt) {
551
615
  payload.updatedAt = storedUpdatedAt;
552
616
  }
553
617
  }
@@ -557,10 +621,10 @@ export class Session {
557
621
  buildStoredAgentPayload(record) {
558
622
  return buildStoredAgentPayload(record, this.providerRegistry, this.sessionLogger);
559
623
  }
560
- // TODO: Remove once all app store clients are on >=0.1.45.
561
624
  isProviderVisibleToClient(provider) {
562
- if (clientSupportsAllProviders(this.appVersion))
625
+ if (clientSupportsAllProviders(this.appVersion)) {
563
626
  return true;
627
+ }
564
628
  return LEGACY_PROVIDER_IDS.has(provider);
565
629
  }
566
630
  filterEditorsForClient(editors) {
@@ -618,7 +682,6 @@ export class Session {
618
682
  return update.kind === "remove" ? update.agentId : update.agent.id;
619
683
  }
620
684
  bufferOrEmitAgentUpdate(subscription, payload) {
621
- // TODO: Remove once all app store clients are on >=0.1.45.
622
685
  if (payload.kind === "upsert" && !this.isProviderVisibleToClient(payload.agent.provider)) {
623
686
  return;
624
687
  }
@@ -655,156 +718,103 @@ export class Session {
655
718
  });
656
719
  }
657
720
  }
658
- async buildProjectPlacement(cwd) {
659
- return buildProjectPlacementForCwd({
660
- cwd,
661
- workspaceGitService: this.workspaceGitService,
662
- });
663
- }
664
- buildPersistedProjectRecord(input) {
665
- return createPersistedProjectRecord({
666
- projectId: input.placement.projectKey,
667
- rootPath: deriveProjectRootPath({
668
- cwd: input.workspaceId,
669
- checkout: input.placement.checkout,
670
- }),
671
- kind: deriveProjectKind(input.placement.checkout),
672
- displayName: input.placement.projectName,
673
- createdAt: input.createdAt,
674
- updatedAt: input.updatedAt,
675
- archivedAt: null,
676
- });
721
+ async findWorkspaceByDirectory(cwd, options) {
722
+ const normalizedCwd = await this.resolveWorkspaceDirectory(cwd, options);
723
+ const workspaces = await this.workspaceRegistry.list();
724
+ const workspaceId = this.resolveRegisteredWorkspaceIdForCwd(normalizedCwd, workspaces);
725
+ return workspaces.find((workspace) => workspace.workspaceId === workspaceId) ?? null;
677
726
  }
678
- buildPersistedWorkspaceRecord(input) {
679
- return createPersistedWorkspaceRecord({
680
- workspaceId: input.workspaceId,
681
- projectId: input.placement.projectKey,
682
- cwd: input.workspaceId,
683
- kind: deriveWorkspaceKind(input.placement.checkout),
684
- displayName: deriveWorkspaceDisplayName({
685
- cwd: input.workspaceId,
686
- checkout: input.placement.checkout,
687
- }),
688
- createdAt: input.createdAt,
689
- updatedAt: input.updatedAt,
690
- archivedAt: null,
691
- });
692
- }
693
- async archiveProjectRecordIfEmpty(projectId, archivedAt) {
694
- const siblingWorkspaces = (await this.workspaceRegistry.list()).filter((workspace) => workspace.projectId === projectId && !workspace.archivedAt);
695
- if (siblingWorkspaces.length === 0) {
696
- await this.projectRegistry.archive(projectId, archivedAt);
697
- }
698
- }
699
- async reconcileWorkspaceRecord(workspaceId) {
700
- const normalizedCwd = normalizePersistedWorkspaceId(workspaceId);
701
- const placement = await this.buildProjectPlacement(normalizedCwd);
702
- const resolvedWorkspaceId = deriveWorkspaceId(normalizedCwd, placement.checkout);
703
- const staleWorkspace = resolvedWorkspaceId === normalizedCwd
704
- ? null
705
- : await this.workspaceRegistry.get(normalizedCwd);
706
- const existing = (await this.workspaceRegistry.get(resolvedWorkspaceId)) ?? staleWorkspace;
707
- await this.syncWorkspaceGitWatchTarget(resolvedWorkspaceId, {
708
- isGit: placement.checkout.isGit,
709
- });
710
- const now = new Date().toISOString();
711
- const nextProjectCreatedAt = existing?.createdAt ?? now;
712
- const nextWorkspaceCreatedAt = existing?.createdAt ?? now;
713
- const currentProjectRecord = await this.projectRegistry.get(placement.projectKey);
714
- const nextProjectRecord = this.buildPersistedProjectRecord({
715
- workspaceId: resolvedWorkspaceId,
716
- placement,
717
- createdAt: currentProjectRecord?.createdAt ?? nextProjectCreatedAt,
718
- updatedAt: now,
719
- });
720
- const nextWorkspaceRecord = this.buildPersistedWorkspaceRecord({
721
- workspaceId: resolvedWorkspaceId,
722
- placement,
723
- createdAt: nextWorkspaceCreatedAt,
724
- updatedAt: now,
725
- });
726
- const needsWorkspaceUpdate = !existing ||
727
- existing.archivedAt ||
728
- existing.projectId !== nextWorkspaceRecord.projectId ||
729
- existing.kind !== nextWorkspaceRecord.kind ||
730
- existing.displayName !== nextWorkspaceRecord.displayName;
731
- const needsProjectUpdate = !currentProjectRecord ||
732
- currentProjectRecord.archivedAt ||
733
- currentProjectRecord.rootPath !== nextProjectRecord.rootPath ||
734
- currentProjectRecord.kind !== nextProjectRecord.kind ||
735
- currentProjectRecord.displayName !== nextProjectRecord.displayName;
736
- const needsStaleWorkspaceCleanup = !!staleWorkspace &&
737
- !staleWorkspace.archivedAt &&
738
- staleWorkspace.workspaceId !== resolvedWorkspaceId;
739
- let removedWorkspaceId = null;
740
- if (needsStaleWorkspaceCleanup) {
741
- await this.workspaceRegistry.archive(staleWorkspace.workspaceId, now);
742
- this.removeWorkspaceGitSubscription(staleWorkspace.workspaceId);
743
- removedWorkspaceId = staleWorkspace.workspaceId;
744
- }
745
- if (!needsWorkspaceUpdate && !needsProjectUpdate && !needsStaleWorkspaceCleanup) {
746
- return {
747
- workspace: existing,
748
- changed: false,
749
- removedWorkspaceId: null,
750
- };
727
+ async resolveWorkspaceDirectory(cwd, options) {
728
+ const normalizedCwd = normalizePersistedWorkspaceId(cwd);
729
+ if (options?.refreshGit === false) {
730
+ const snapshot = this.workspaceGitService.peekSnapshot(normalizedCwd);
731
+ return normalizePersistedWorkspaceId(snapshot?.git.repoRoot ?? normalizedCwd);
751
732
  }
752
- await this.projectRegistry.upsert(nextProjectRecord);
753
- await this.workspaceRegistry.upsert(nextWorkspaceRecord);
754
- if (existing && existing.workspaceId !== resolvedWorkspaceId) {
755
- await this.workspaceRegistry.archive(existing.workspaceId, now);
756
- this.removeWorkspaceGitSubscription(existing.workspaceId);
757
- removedWorkspaceId ?? (removedWorkspaceId = existing.workspaceId);
733
+ try {
734
+ const snapshot = await this.workspaceGitService.getSnapshot(normalizedCwd);
735
+ return normalizePersistedWorkspaceId(snapshot.git.repoRoot ?? normalizedCwd);
758
736
  }
759
- if (existing && !existing.archivedAt && existing.projectId !== nextWorkspaceRecord.projectId) {
760
- await this.archiveProjectRecordIfEmpty(existing.projectId, now);
737
+ catch {
738
+ return normalizedCwd;
761
739
  }
740
+ }
741
+ async buildProjectPlacementForWorkspace(workspace, projectRecord) {
742
+ const project = projectRecord ?? (await this.projectRegistry.get(workspace.projectId));
743
+ if (!project) {
744
+ throw new Error(`Project not found for workspace ${workspace.workspaceId}`);
745
+ }
746
+ const checkout = project.kind !== "git"
747
+ ? {
748
+ cwd: workspace.cwd,
749
+ isGit: false,
750
+ currentBranch: null,
751
+ remoteUrl: null,
752
+ worktreeRoot: null,
753
+ isPaseoOwnedWorktree: false,
754
+ mainRepoRoot: null,
755
+ }
756
+ : workspace.kind === "worktree"
757
+ ? {
758
+ cwd: workspace.cwd,
759
+ isGit: true,
760
+ currentBranch: workspace.displayName,
761
+ remoteUrl: null,
762
+ worktreeRoot: workspace.cwd,
763
+ isPaseoOwnedWorktree: true,
764
+ mainRepoRoot: project.rootPath,
765
+ }
766
+ : {
767
+ cwd: workspace.cwd,
768
+ isGit: true,
769
+ currentBranch: workspace.displayName,
770
+ remoteUrl: null,
771
+ worktreeRoot: workspace.cwd,
772
+ isPaseoOwnedWorktree: false,
773
+ mainRepoRoot: null,
774
+ };
762
775
  return {
763
- workspace: nextWorkspaceRecord,
764
- changed: true,
765
- removedWorkspaceId,
776
+ projectKey: project.projectId,
777
+ projectName: project.displayName,
778
+ checkout,
766
779
  };
767
780
  }
768
- async reconcileActiveWorkspaceRecords() {
769
- const changedWorkspaceIds = new Set();
770
- const activeWorkspaces = (await this.workspaceRegistry.list()).filter((workspace) => !workspace.archivedAt);
771
- const staleWorkspaceIds = await detectStaleWorkspaces({
772
- activeWorkspaces,
773
- checkDirectoryExists: async (cwd) => {
774
- try {
775
- await stat(cwd);
776
- return true;
777
- }
778
- catch {
779
- return false;
780
- }
781
- },
781
+ async buildProjectPlacementForCwd(cwd, options) {
782
+ const workspace = await this.findWorkspaceByDirectory(cwd, {
783
+ refreshGit: options?.refreshGit,
782
784
  });
783
- for (const workspaceId of staleWorkspaceIds) {
784
- await this.archiveWorkspaceRecord(workspaceId);
785
- changedWorkspaceIds.add(workspaceId);
786
- }
787
- for (const workspace of activeWorkspaces) {
788
- if (staleWorkspaceIds.has(workspace.workspaceId)) {
789
- continue;
790
- }
791
- const result = await this.reconcileWorkspaceRecord(workspace.workspaceId);
792
- if (result.changed) {
793
- changedWorkspaceIds.add(result.workspace.workspaceId);
794
- if (result.removedWorkspaceId) {
795
- changedWorkspaceIds.add(result.removedWorkspaceId);
796
- }
785
+ if (!workspace) {
786
+ if (!options?.fallback) {
787
+ return null;
797
788
  }
789
+ const normalizedCwd = normalizePersistedWorkspaceId(cwd);
790
+ return {
791
+ projectKey: normalizedCwd,
792
+ projectName: deriveProjectGroupingName(normalizedCwd),
793
+ checkout: {
794
+ cwd: normalizedCwd,
795
+ isGit: false,
796
+ currentBranch: null,
797
+ remoteUrl: null,
798
+ worktreeRoot: null,
799
+ isPaseoOwnedWorktree: false,
800
+ mainRepoRoot: null,
801
+ },
802
+ };
798
803
  }
799
- return changedWorkspaceIds;
804
+ return this.buildProjectPlacementForWorkspace(workspace);
800
805
  }
801
806
  async forwardAgentUpdate(agent) {
802
807
  try {
803
- await this.ensureWorkspaceRegistered(agent.cwd);
804
808
  const subscription = this.agentUpdatesSubscription;
805
809
  const payload = await this.buildAgentPayload(agent);
806
810
  if (subscription) {
807
- const project = await this.buildProjectPlacement(payload.cwd);
811
+ const project = await this.buildProjectPlacementForCwd(payload.cwd, {
812
+ refreshGit: false,
813
+ fallback: true,
814
+ });
815
+ if (!project) {
816
+ throw new Error(`Workspace not found for agent ${payload.id}`);
817
+ }
808
818
  const matches = this.matchesAgentFilter({
809
819
  agent: payload,
810
820
  project,
@@ -854,6 +864,9 @@ export class Session {
854
864
  case "fetch_agents_request":
855
865
  await this.handleFetchAgents(msg);
856
866
  break;
867
+ case "fetch_agent_history_request":
868
+ await this.handleFetchAgentHistory(msg);
869
+ break;
857
870
  case "fetch_workspaces_request":
858
871
  await this.handleFetchWorkspacesRequest(msg);
859
872
  break;
@@ -1019,6 +1032,12 @@ export class Session {
1019
1032
  case "checkout_pr_status_request":
1020
1033
  await this.handleCheckoutPrStatusRequest(msg);
1021
1034
  break;
1035
+ case "pull_request_timeline_request":
1036
+ await this.handlePullRequestTimelineRequest(msg);
1037
+ break;
1038
+ case "github_search_request":
1039
+ await this.handleGitHubSearchRequest(msg);
1040
+ break;
1022
1041
  case "paseo_worktree_list_request":
1023
1042
  await this.handlePaseoWorktreeListRequest(msg);
1024
1043
  break;
@@ -1028,6 +1047,9 @@ export class Session {
1028
1047
  case "create_paseo_worktree_request":
1029
1048
  await this.handleCreatePaseoWorktreeRequest(msg);
1030
1049
  break;
1050
+ case "workspace_setup_status_request":
1051
+ await this.handleWorkspaceSetupStatusRequest(msg);
1052
+ break;
1031
1053
  case "list_available_editors_request":
1032
1054
  await this.handleListAvailableEditorsRequest(msg);
1033
1055
  break;
@@ -1107,6 +1129,9 @@ export class Session {
1107
1129
  case "create_terminal_request":
1108
1130
  await this.handleCreateTerminalRequest(msg);
1109
1131
  break;
1132
+ case "start_workspace_script_request":
1133
+ await this.handleStartWorkspaceScriptRequest(msg);
1134
+ break;
1110
1135
  case "subscribe_terminal_request":
1111
1136
  await this.handleSubscribeTerminalRequest(msg);
1112
1137
  break;
@@ -1306,19 +1331,23 @@ export class Session {
1306
1331
  const knownCwd = this.agentManager.getAgent(agentId)?.cwd ??
1307
1332
  (await this.agentStorage.get(agentId))?.cwd ??
1308
1333
  null;
1309
- // Prevent the persistence hook from re-creating the record while we close/delete.
1310
- this.agentStorage.beginDelete(agentId);
1334
+ // File-backed storage still needs an early delete fence before closeAgent().
1335
+ beginAgentDeleteIfSupported(this.agentStorage, agentId);
1311
1336
  try {
1312
1337
  await this.agentManager.closeAgent(agentId);
1313
1338
  }
1314
1339
  catch (error) {
1315
1340
  this.sessionLogger.warn({ err: error, agentId }, `Failed to close agent ${agentId} during delete`);
1316
1341
  }
1342
+ // Drain queued persistence from the just-closed agent before removing its
1343
+ // durable snapshot, otherwise an in-flight background write can recreate it.
1344
+ await this.agentManager.flush();
1317
1345
  try {
1318
1346
  await this.agentStorage.remove(agentId);
1347
+ await this.agentManager.deleteCommittedTimeline(agentId);
1319
1348
  }
1320
1349
  catch (error) {
1321
- this.sessionLogger.error({ err: error, agentId }, `Failed to remove agent ${agentId} from registry`);
1350
+ this.sessionLogger.error({ err: error, agentId }, `Failed to fully delete agent ${agentId}`);
1322
1351
  }
1323
1352
  this.emit({
1324
1353
  type: "agent_deleted",
@@ -1338,12 +1367,13 @@ export class Session {
1338
1367
  }
1339
1368
  }
1340
1369
  async handleArchiveAgentRequest(agentId, requestId) {
1341
- const result = await this.archiveAgentForClose(agentId);
1370
+ this.sessionLogger.info({ agentId }, `Archiving agent ${agentId}`);
1371
+ const { archivedAt } = await this.archiveAgentForClose(agentId);
1342
1372
  this.emit({
1343
1373
  type: "agent_archived",
1344
1374
  payload: {
1345
- agentId: result.agentId,
1346
- archivedAt: result.archivedAt,
1375
+ agentId,
1376
+ archivedAt,
1347
1377
  requestId,
1348
1378
  },
1349
1379
  });
@@ -1360,22 +1390,10 @@ export class Session {
1360
1390
  };
1361
1391
  }
1362
1392
  const archivedAt = new Date().toISOString();
1363
- const normalizedStatus = existing.lastStatus === "running" || existing.lastStatus === "initializing"
1364
- ? "idle"
1365
- : existing.lastStatus;
1366
- await this.agentStorage.upsert({
1367
- ...existing,
1368
- archivedAt,
1369
- updatedAt: archivedAt,
1370
- lastStatus: normalizedStatus,
1371
- requiresAttention: false,
1372
- attentionReason: null,
1373
- attentionTimestamp: null,
1374
- });
1393
+ await this.agentManager.archiveSnapshot(agentId, archivedAt);
1375
1394
  return { agentId, archivedAt };
1376
1395
  }
1377
1396
  async archiveAgentForClose(agentId) {
1378
- this.sessionLogger.info({ agentId }, `Archiving agent ${agentId}`);
1379
1397
  const liveAgent = this.agentManager.getAgent(agentId);
1380
1398
  if (liveAgent) {
1381
1399
  await this.interruptAgentIfRunning(agentId);
@@ -1391,22 +1409,30 @@ export class Session {
1391
1409
  }
1392
1410
  if (this.agentUpdatesSubscription) {
1393
1411
  const payload = this.buildStoredAgentPayload(archivedRecord);
1394
- const project = await this.buildProjectPlacement(payload.cwd);
1395
- const matches = this.matchesAgentFilter({
1396
- agent: payload,
1397
- project,
1398
- filter: this.agentUpdatesSubscription.filter,
1399
- });
1400
- this.bufferOrEmitAgentUpdate(this.agentUpdatesSubscription, matches
1401
- ? {
1402
- kind: "upsert",
1412
+ const project = await this.buildProjectPlacementForCwd(payload.cwd);
1413
+ if (project) {
1414
+ const matches = this.matchesAgentFilter({
1403
1415
  agent: payload,
1404
1416
  project,
1405
- }
1406
- : {
1417
+ filter: this.agentUpdatesSubscription.filter,
1418
+ });
1419
+ this.bufferOrEmitAgentUpdate(this.agentUpdatesSubscription, matches
1420
+ ? {
1421
+ kind: "upsert",
1422
+ agent: payload,
1423
+ project,
1424
+ }
1425
+ : {
1426
+ kind: "remove",
1427
+ agentId,
1428
+ });
1429
+ }
1430
+ else {
1431
+ this.bufferOrEmitAgentUpdate(this.agentUpdatesSubscription, {
1407
1432
  kind: "remove",
1408
1433
  agentId,
1409
1434
  });
1435
+ }
1410
1436
  await this.emitWorkspaceUpdateForCwd(payload.cwd);
1411
1437
  }
1412
1438
  if (!archivedRecord.archivedAt) {
@@ -1477,26 +1503,10 @@ export class Session {
1477
1503
  return;
1478
1504
  }
1479
1505
  try {
1480
- const liveAgent = this.agentManager.getAgent(agentId);
1481
- if (liveAgent) {
1482
- if (normalizedName) {
1483
- await this.agentManager.setTitle(agentId, normalizedName);
1484
- }
1485
- if (normalizedLabels) {
1486
- await this.agentManager.setLabels(agentId, normalizedLabels);
1487
- }
1488
- }
1489
- else {
1490
- const existing = await this.agentStorage.get(agentId);
1491
- if (!existing) {
1492
- throw new Error(`Agent not found: ${agentId}`);
1493
- }
1494
- await this.agentStorage.upsert({
1495
- ...existing,
1496
- ...(normalizedName ? { title: normalizedName } : {}),
1497
- ...(normalizedLabels ? { labels: { ...existing.labels, ...normalizedLabels } } : {}),
1498
- });
1499
- }
1506
+ await this.agentManager.updateAgentMetadata(agentId, {
1507
+ ...(normalizedName ? { title: normalizedName } : {}),
1508
+ ...(normalizedLabels ? { labels: normalizedLabels } : {}),
1509
+ });
1500
1510
  this.emit({
1501
1511
  type: "update_agent_response",
1502
1512
  payload: { requestId, agentId, accepted: true, error: null },
@@ -1824,10 +1834,17 @@ export class Session {
1824
1834
  /**
1825
1835
  * Handle text message to agent (with optional image attachments)
1826
1836
  */
1827
- async handleSendAgentMessage(agentId, text, messageId, images, runOptions, options) {
1828
- this.sessionLogger.info({ agentId, textPreview: text.substring(0, 50), imageCount: images?.length ?? 0 }, `Sending text to agent ${agentId}${images && images.length > 0 ? ` with ${images.length} image attachment(s)` : ""}`);
1837
+ async handleSendAgentMessage(agentId, text, messageId, images, attachments, runOptions, options) {
1838
+ this.sessionLogger.info({
1839
+ agentId,
1840
+ textPreview: text.substring(0, 50),
1841
+ imageCount: images?.length ?? 0,
1842
+ attachmentCount: attachments?.length ?? 0,
1843
+ }, `Sending text to agent ${agentId}${images && images.length > 0 ? ` with ${images.length} image attachment(s)` : ""}${attachments && attachments.length > 0
1844
+ ? ` and ${attachments.length} structured attachment(s)`
1845
+ : ""}`);
1829
1846
  const promptText = options?.spokenInput ? wrapSpokenInput(text) : text;
1830
- const prompt = this.buildAgentPrompt(promptText, images);
1847
+ const prompt = this.buildAgentPrompt(promptText, images, attachments);
1831
1848
  try {
1832
1849
  await sendPromptToAgent({
1833
1850
  agentManager: this.agentManager,
@@ -1853,7 +1870,7 @@ export class Session {
1853
1870
  * Handle create agent request
1854
1871
  */
1855
1872
  async handleCreateAgentRequest(msg) {
1856
- const { config, worktreeName, requestId, initialPrompt, clientMessageId, outputSchema, git, images, labels, } = msg;
1873
+ const { config, worktreeName, requestId, initialPrompt, clientMessageId, outputSchema, git, images, attachments, labels, } = msg;
1857
1874
  this.sessionLogger.info({ cwd: config.cwd, provider: config.provider, worktreeName }, `Creating agent in ${config.cwd} (${config.provider})${worktreeName ? ` with worktree ${worktreeName}` : ""}`);
1858
1875
  try {
1859
1876
  const trimmedPrompt = initialPrompt?.trim();
@@ -1865,11 +1882,24 @@ export class Session {
1865
1882
  ...config,
1866
1883
  ...(provisionalTitle ? { title: provisionalTitle } : {}),
1867
1884
  };
1868
- const { sessionConfig, worktreeConfig } = await this.buildAgentSessionConfig(resolvedConfig, git, worktreeName, labels);
1869
- await this.ensureWorkspaceRegistered(sessionConfig.cwd);
1870
- const snapshot = await this.agentManager.createAgent(sessionConfig, undefined, { labels });
1885
+ const { sessionConfig, worktreeBootstrap } = await this.buildAgentSessionConfig(resolvedConfig, git, worktreeName, attachments);
1886
+ const resolvedWorkspace = msg.workspaceId
1887
+ ? await this.workspaceRegistry.get(msg.workspaceId)
1888
+ : ((await this.findWorkspaceByDirectory(sessionConfig.cwd)) ??
1889
+ (await this.findOrCreateWorkspaceForDirectory(sessionConfig.cwd)));
1890
+ if (!resolvedWorkspace) {
1891
+ throw new Error(`Workspace not found: ${msg.workspaceId}`);
1892
+ }
1893
+ const snapshot = await this.agentManager.createAgent({
1894
+ ...sessionConfig,
1895
+ cwd: resolvedWorkspace.cwd,
1896
+ }, undefined, {
1897
+ labels,
1898
+ workspaceId: resolvedWorkspace.workspaceId,
1899
+ initialPrompt: trimmedPrompt,
1900
+ });
1871
1901
  await this.forwardAgentUpdate(snapshot);
1872
- if (trimmedPrompt) {
1902
+ if (trimmedPrompt || (images?.length ?? 0) > 0 || (attachments?.length ?? 0) > 0) {
1873
1903
  scheduleAgentMetadataGeneration({
1874
1904
  agentManager: this.agentManager,
1875
1905
  agentId: snapshot.id,
@@ -1878,17 +1908,17 @@ export class Session {
1878
1908
  explicitTitle,
1879
1909
  paseoHome: this.paseoHome,
1880
1910
  logger: this.sessionLogger,
1911
+ deps: {
1912
+ workspaceGitService: this.workspaceGitService,
1913
+ },
1881
1914
  });
1882
- const started = await this.handleSendAgentMessage(snapshot.id, trimmedPrompt, resolveClientMessageId(clientMessageId), images, outputSchema ? { outputSchema } : undefined);
1915
+ const started = await this.handleSendAgentMessage(snapshot.id, trimmedPrompt || "", resolveClientMessageId(clientMessageId), images, attachments, outputSchema ? { outputSchema } : undefined);
1883
1916
  if (!started.ok) {
1884
1917
  throw new Error(started.error);
1885
1918
  }
1886
1919
  }
1887
1920
  if (requestId) {
1888
- const agentPayload = await this.getAgentPayloadById(snapshot.id);
1889
- if (!agentPayload) {
1890
- throw new Error(`Agent ${snapshot.id} not found after creation`);
1891
- }
1921
+ const agentPayload = await this.buildAgentPayload(snapshot);
1892
1922
  this.emit({
1893
1923
  type: "status",
1894
1924
  payload: {
@@ -1899,10 +1929,11 @@ export class Session {
1899
1929
  },
1900
1930
  });
1901
1931
  }
1902
- if (worktreeConfig) {
1932
+ if (worktreeBootstrap) {
1903
1933
  void runAsyncWorktreeBootstrap({
1904
1934
  agentId: snapshot.id,
1905
- worktree: worktreeConfig,
1935
+ worktree: worktreeBootstrap.worktree,
1936
+ shouldBootstrap: worktreeBootstrap.shouldBootstrap,
1906
1937
  terminalManager: this.terminalManager,
1907
1938
  appendTimelineItem: (item) => appendTimelineItemIfAgentKnown({
1908
1939
  agentManager: this.agentManager,
@@ -1920,6 +1951,7 @@ export class Session {
1920
1951
  this.sessionLogger.info({ agentId: snapshot.id, provider: snapshot.provider }, `Created agent ${snapshot.id} (${snapshot.provider})`);
1921
1952
  }
1922
1953
  catch (error) {
1954
+ const wireError = toWorktreeWireError(error);
1923
1955
  this.sessionLogger.error({ err: error }, "Failed to create agent");
1924
1956
  if (requestId) {
1925
1957
  this.emit({
@@ -1927,7 +1959,8 @@ export class Session {
1927
1959
  payload: {
1928
1960
  status: "agent_create_failed",
1929
1961
  requestId,
1930
- error: error?.message ?? String(error),
1962
+ error: wireError.message,
1963
+ errorCode: wireError.code,
1931
1964
  },
1932
1965
  });
1933
1966
  }
@@ -1937,7 +1970,7 @@ export class Session {
1937
1970
  id: uuidv4(),
1938
1971
  timestamp: new Date(),
1939
1972
  type: "error",
1940
- content: `Failed to create agent: ${error.message}`,
1973
+ content: `Failed to create agent: ${wireError.message}`,
1941
1974
  },
1942
1975
  });
1943
1976
  }
@@ -1966,10 +1999,7 @@ export class Session {
1966
1999
  await this.forwardAgentUpdate(snapshot);
1967
2000
  const timelineSize = this.agentManager.getTimeline(snapshot.id).length;
1968
2001
  if (requestId) {
1969
- const agentPayload = await this.getAgentPayloadById(snapshot.id);
1970
- if (!agentPayload) {
1971
- throw new Error(`Agent ${snapshot.id} not found after resume`);
1972
- }
2002
+ const agentPayload = await this.buildAgentPayload(snapshot);
1973
2003
  this.emit({
1974
2004
  type: "status",
1975
2005
  payload: {
@@ -2066,50 +2096,140 @@ export class Session {
2066
2096
  this.handleAgentRunError(agentId, error, "Failed to cancel running agent on request");
2067
2097
  }
2068
2098
  }
2069
- async buildAgentSessionConfig(config, gitOptions, legacyWorktreeName, _labels) {
2099
+ async buildAgentSessionConfig(config, gitOptions, legacyWorktreeName, attachments) {
2070
2100
  return buildWorktreeAgentSessionConfig({
2071
2101
  paseoHome: this.paseoHome,
2072
2102
  sessionLogger: this.sessionLogger,
2073
2103
  workspaceGitService: this.workspaceGitService,
2104
+ createPaseoWorktree: (input, serviceOptions) => this.createPaseoWorktree(input, serviceOptions),
2074
2105
  checkoutExistingBranch: (cwd, branch) => this.checkoutExistingBranch(cwd, branch),
2075
2106
  createBranchFromBase: (params) => this.createBranchFromBase(params),
2076
- }, config, gitOptions, legacyWorktreeName, _labels);
2107
+ github: this.github,
2108
+ }, config, gitOptions, legacyWorktreeName, attachments);
2077
2109
  }
2078
2110
  async handleListProviderModelsRequest(msg) {
2111
+ const cwd = resolveSnapshotCwd(msg.cwd ? expandTilde(msg.cwd) : undefined);
2079
2112
  const fetchedAt = new Date().toISOString();
2080
- try {
2081
- const models = await this.providerRegistry[msg.provider].fetchModels({
2082
- cwd: msg.cwd ? expandTilde(msg.cwd) : undefined,
2083
- });
2113
+ const manager = this.providerSnapshotManager;
2114
+ if (!manager) {
2115
+ try {
2116
+ const models = await this.providerRegistry[msg.provider].fetchModels({
2117
+ cwd,
2118
+ force: false,
2119
+ });
2120
+ this.emit({
2121
+ type: "list_provider_models_response",
2122
+ payload: {
2123
+ provider: msg.provider,
2124
+ models,
2125
+ error: null,
2126
+ fetchedAt,
2127
+ requestId: msg.requestId,
2128
+ },
2129
+ });
2130
+ }
2131
+ catch (error) {
2132
+ this.sessionLogger.error({ err: error, provider: msg.provider }, `Failed to list models for ${msg.provider}`);
2133
+ this.emit({
2134
+ type: "list_provider_models_response",
2135
+ payload: {
2136
+ provider: msg.provider,
2137
+ error: error?.message ?? String(error),
2138
+ fetchedAt,
2139
+ requestId: msg.requestId,
2140
+ },
2141
+ });
2142
+ }
2143
+ return;
2144
+ }
2145
+ const entry = await this.getProviderSnapshotEntryForRead(cwd, msg.provider);
2146
+ if (!entry) {
2084
2147
  this.emit({
2085
2148
  type: "list_provider_models_response",
2086
2149
  payload: {
2087
2150
  provider: msg.provider,
2088
- models,
2089
- error: null,
2151
+ error: `Unknown provider: ${msg.provider}`,
2090
2152
  fetchedAt,
2091
2153
  requestId: msg.requestId,
2092
2154
  },
2093
2155
  });
2156
+ return;
2094
2157
  }
2095
- catch (error) {
2096
- this.sessionLogger.error({ err: error, provider: msg.provider }, `Failed to list models for ${msg.provider}`);
2158
+ if (entry.status === "ready") {
2097
2159
  this.emit({
2098
2160
  type: "list_provider_models_response",
2099
2161
  payload: {
2100
2162
  provider: msg.provider,
2101
- error: error?.message ?? String(error),
2102
- fetchedAt,
2163
+ models: entry.models ?? [],
2164
+ error: null,
2165
+ fetchedAt: entry.fetchedAt ?? fetchedAt,
2103
2166
  requestId: msg.requestId,
2104
2167
  },
2105
2168
  });
2169
+ return;
2106
2170
  }
2171
+ const errorMessage = entry.status === "error"
2172
+ ? (entry.error ?? `Failed to list models for ${msg.provider}`)
2173
+ : `Provider ${msg.provider} is not available`;
2174
+ this.emit({
2175
+ type: "list_provider_models_response",
2176
+ payload: {
2177
+ provider: msg.provider,
2178
+ error: errorMessage,
2179
+ fetchedAt,
2180
+ requestId: msg.requestId,
2181
+ },
2182
+ });
2107
2183
  }
2108
2184
  async handleListProviderModesRequest(msg) {
2109
2185
  const fetchedAt = new Date().toISOString();
2186
+ const cwd = resolveSnapshotCwd(msg.cwd ? expandTilde(msg.cwd) : undefined);
2187
+ const manager = this.providerSnapshotManager;
2188
+ if (manager) {
2189
+ const entry = await this.getProviderSnapshotEntryForRead(cwd, msg.provider);
2190
+ if (!entry) {
2191
+ this.emit({
2192
+ type: "list_provider_modes_response",
2193
+ payload: {
2194
+ provider: msg.provider,
2195
+ error: `Unknown provider: ${msg.provider}`,
2196
+ fetchedAt,
2197
+ requestId: msg.requestId,
2198
+ },
2199
+ });
2200
+ return;
2201
+ }
2202
+ if (entry.status === "ready") {
2203
+ this.emit({
2204
+ type: "list_provider_modes_response",
2205
+ payload: {
2206
+ provider: msg.provider,
2207
+ modes: entry.modes ?? [],
2208
+ error: null,
2209
+ fetchedAt: entry.fetchedAt ?? fetchedAt,
2210
+ requestId: msg.requestId,
2211
+ },
2212
+ });
2213
+ return;
2214
+ }
2215
+ const errorMessage = entry.status === "error"
2216
+ ? (entry.error ?? `Failed to list modes for ${msg.provider}`)
2217
+ : `Provider ${msg.provider} is not available`;
2218
+ this.emit({
2219
+ type: "list_provider_modes_response",
2220
+ payload: {
2221
+ provider: msg.provider,
2222
+ error: errorMessage,
2223
+ fetchedAt,
2224
+ requestId: msg.requestId,
2225
+ },
2226
+ });
2227
+ return;
2228
+ }
2110
2229
  try {
2111
2230
  const modes = await this.providerRegistry[msg.provider].fetchModes({
2112
- cwd: msg.cwd ? expandTilde(msg.cwd) : undefined,
2231
+ cwd,
2232
+ force: false,
2113
2233
  });
2114
2234
  this.emit({
2115
2235
  type: "list_provider_modes_response",
@@ -2135,6 +2255,21 @@ export class Session {
2135
2255
  });
2136
2256
  }
2137
2257
  }
2258
+ async getProviderSnapshotEntryForRead(cwd, provider) {
2259
+ const manager = this.providerSnapshotManager;
2260
+ if (!manager) {
2261
+ return undefined;
2262
+ }
2263
+ const findEntry = () => manager.getSnapshot(cwd).find((candidate) => candidate.provider === provider);
2264
+ let entry = findEntry();
2265
+ if (!entry || entry.status === "loading") {
2266
+ // Awaits the in-flight warmup (deduped per-cwd) so old clients still get
2267
+ // a resolved answer rather than a loading placeholder.
2268
+ await manager.warmUpSnapshotForCwd({ cwd, providers: [provider] });
2269
+ entry = findEntry();
2270
+ }
2271
+ return entry;
2272
+ }
2138
2273
  buildDraftAgentSessionConfig(draftConfig) {
2139
2274
  return {
2140
2275
  provider: draftConfig.provider,
@@ -2177,9 +2312,7 @@ export class Session {
2177
2312
  async handleListAvailableProvidersRequest(msg) {
2178
2313
  const fetchedAt = new Date().toISOString();
2179
2314
  try {
2180
- let providers = await this.agentManager.listProviderAvailability();
2181
- // TODO: Remove once all app store clients are on >=0.1.45.
2182
- providers = providers.filter((p) => this.isProviderVisibleToClient(p.provider));
2315
+ const providers = (await this.agentManager.listProviderAvailability()).filter((provider) => this.isProviderVisibleToClient(provider.provider));
2183
2316
  this.emit({
2184
2317
  type: "list_available_providers_response",
2185
2318
  payload: {
@@ -2220,10 +2353,17 @@ export class Session {
2220
2353
  });
2221
2354
  }
2222
2355
  async handleRefreshProvidersSnapshotRequest(msg) {
2223
- await this.providerSnapshotManager?.refresh({
2224
- cwd: msg.cwd ? expandTilde(msg.cwd) : undefined,
2225
- providers: msg.providers,
2226
- });
2356
+ if (msg.cwd) {
2357
+ await this.providerSnapshotManager?.refreshSnapshotForCwd({
2358
+ cwd: expandTilde(msg.cwd),
2359
+ providers: msg.providers,
2360
+ });
2361
+ }
2362
+ else {
2363
+ await this.providerSnapshotManager?.refreshSettingsSnapshot({
2364
+ providers: msg.providers,
2365
+ });
2366
+ }
2227
2367
  this.emit({
2228
2368
  type: "refresh_providers_snapshot_response",
2229
2369
  payload: {
@@ -2276,7 +2416,10 @@ export class Session {
2276
2416
  return resolvedCandidate.startsWith(resolvedRoot + sep);
2277
2417
  }
2278
2418
  async generateCommitMessage(cwd) {
2279
- const diff = await getCheckoutDiff(cwd, { mode: "uncommitted", includeStructured: true }, { paseoHome: this.paseoHome });
2419
+ const diff = await this.workspaceGitService.getCheckoutDiff(cwd, {
2420
+ mode: "uncommitted",
2421
+ includeStructured: true,
2422
+ });
2280
2423
  const schema = z.object({
2281
2424
  message: z
2282
2425
  .string()
@@ -2331,11 +2474,11 @@ export class Session {
2331
2474
  }
2332
2475
  }
2333
2476
  async generatePullRequestText(cwd, baseRef) {
2334
- const diff = await getCheckoutDiff(cwd, {
2477
+ const diff = await this.workspaceGitService.getCheckoutDiff(cwd, {
2335
2478
  mode: "base",
2336
2479
  baseRef,
2337
2480
  includeStructured: true,
2338
- }, { paseoHome: this.paseoHome });
2481
+ });
2339
2482
  const schema = z.object({
2340
2483
  title: z.string().min(1).max(72),
2341
2484
  body: z.string().min(1),
@@ -2396,11 +2539,8 @@ export class Session {
2396
2539
  }
2397
2540
  async isWorkingTreeDirty(cwd) {
2398
2541
  try {
2399
- const { stdout } = await execAsync("git status --porcelain", {
2400
- cwd,
2401
- env: READ_ONLY_GIT_ENV,
2402
- });
2403
- return stdout.trim().length > 0;
2542
+ const snapshot = await this.workspaceGitService.getSnapshot(cwd);
2543
+ return snapshot.git.isDirty === true;
2404
2544
  }
2405
2545
  catch (error) {
2406
2546
  throw new Error(`Unable to inspect git status for ${cwd}: ${error.message}`);
@@ -2408,30 +2548,24 @@ export class Session {
2408
2548
  }
2409
2549
  async checkoutExistingBranch(cwd, branch) {
2410
2550
  this.assertSafeGitRef(branch, "branch");
2411
- try {
2412
- await execCommand("git", ["rev-parse", "--verify", branch], { cwd });
2413
- }
2414
- catch (error) {
2551
+ const resolution = await this.workspaceGitService.validateBranchRef(cwd, branch);
2552
+ if (resolution.kind === "not-found") {
2415
2553
  throw new Error(`Branch not found: ${branch}`);
2416
2554
  }
2417
- const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", {
2555
+ await this.ensureCleanWorkingTree(cwd);
2556
+ const result = await checkoutResolvedBranch({
2418
2557
  cwd,
2558
+ resolution,
2419
2559
  });
2420
- const current = stdout.trim();
2421
- if (current === branch) {
2422
- return;
2423
- }
2424
- await this.ensureCleanWorkingTree(cwd);
2425
- await execCommand("git", ["checkout", branch], { cwd });
2560
+ await this.notifyGitMutation(cwd, "switch-branch", { invalidateGithub: true });
2561
+ return result;
2426
2562
  }
2427
2563
  async createBranchFromBase(params) {
2428
2564
  const { cwd, baseBranch, newBranchName } = params;
2429
2565
  this.assertSafeGitRef(baseBranch, "base branch");
2430
2566
  this.assertSafeGitRef(newBranchName, "new branch");
2431
- try {
2432
- await execCommand("git", ["rev-parse", "--verify", baseBranch], { cwd });
2433
- }
2434
- catch (error) {
2567
+ const baseResolution = await this.workspaceGitService.validateBranchRef(cwd, baseBranch);
2568
+ if (baseResolution.kind === "not-found") {
2435
2569
  throw new Error(`Base branch not found: ${baseBranch}`);
2436
2570
  }
2437
2571
  const exists = await this.doesLocalBranchExist(cwd, newBranchName);
@@ -2442,17 +2576,21 @@ export class Session {
2442
2576
  await execCommand("git", ["checkout", "-b", newBranchName, baseBranch], {
2443
2577
  cwd,
2444
2578
  });
2579
+ await this.notifyGitMutation(cwd, "create-branch");
2445
2580
  }
2446
2581
  async doesLocalBranchExist(cwd, branch) {
2447
2582
  this.assertSafeGitRef(branch, "branch");
2583
+ return this.workspaceGitService.hasLocalBranch(cwd, branch);
2584
+ }
2585
+ async notifyGitMutation(cwd, reason, options) {
2586
+ if (options?.invalidateGithub) {
2587
+ this.github.invalidate({ cwd });
2588
+ }
2448
2589
  try {
2449
- await execCommand("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], {
2450
- cwd,
2451
- });
2452
- return true;
2590
+ await this.workspaceGitService.getSnapshot(cwd, { force: true, reason });
2453
2591
  }
2454
2592
  catch (error) {
2455
- return false;
2593
+ this.sessionLogger.warn({ err: error, cwd, reason }, "Failed to force-refresh workspace git snapshot after mutation");
2456
2594
  }
2457
2595
  }
2458
2596
  /**
@@ -2658,7 +2796,15 @@ export class Session {
2658
2796
  return;
2659
2797
  }
2660
2798
  if (!agent && draftConfig) {
2661
- const sessionConfig = this.buildDraftAgentSessionConfig(draftConfig);
2799
+ const sessionConfig = {
2800
+ provider: draftConfig.provider,
2801
+ cwd: expandTilde(draftConfig.cwd),
2802
+ ...(draftConfig.modeId ? { modeId: draftConfig.modeId } : {}),
2803
+ ...(draftConfig.model ? { model: draftConfig.model } : {}),
2804
+ ...(draftConfig.thinkingOptionId
2805
+ ? { thinkingOptionId: draftConfig.thinkingOptionId }
2806
+ : {}),
2807
+ };
2662
2808
  const commands = await this.agentManager.listDraftCommands(sessionConfig);
2663
2809
  this.emit({
2664
2810
  type: "list_commands_response",
@@ -2725,70 +2871,14 @@ export class Session {
2725
2871
  const { cwd, requestId } = msg;
2726
2872
  const resolvedCwd = expandTilde(cwd);
2727
2873
  try {
2728
- const status = await getCheckoutStatus(resolvedCwd, { paseoHome: this.paseoHome });
2729
- if (!status.isGit) {
2730
- this.emit({
2731
- type: "checkout_status_response",
2732
- payload: {
2733
- cwd,
2734
- isGit: false,
2735
- repoRoot: null,
2736
- currentBranch: null,
2737
- isDirty: null,
2738
- baseRef: null,
2739
- aheadBehind: null,
2740
- aheadOfOrigin: null,
2741
- behindOfOrigin: null,
2742
- hasRemote: false,
2743
- remoteUrl: null,
2744
- isPaseoOwnedWorktree: false,
2745
- error: null,
2746
- requestId,
2747
- },
2748
- });
2749
- return;
2750
- }
2751
- if (status.isPaseoOwnedWorktree) {
2752
- this.emit({
2753
- type: "checkout_status_response",
2754
- payload: {
2755
- cwd,
2756
- isGit: true,
2757
- repoRoot: status.repoRoot ?? null,
2758
- mainRepoRoot: status.mainRepoRoot,
2759
- currentBranch: status.currentBranch ?? null,
2760
- isDirty: status.isDirty ?? null,
2761
- baseRef: status.baseRef,
2762
- aheadBehind: status.aheadBehind ?? null,
2763
- aheadOfOrigin: status.aheadOfOrigin ?? null,
2764
- behindOfOrigin: status.behindOfOrigin ?? null,
2765
- hasRemote: status.hasRemote,
2766
- remoteUrl: status.remoteUrl,
2767
- isPaseoOwnedWorktree: true,
2768
- error: null,
2769
- requestId,
2770
- },
2771
- });
2772
- return;
2773
- }
2874
+ const snapshot = await this.workspaceGitService.getSnapshot(resolvedCwd);
2774
2875
  this.emit({
2775
2876
  type: "checkout_status_response",
2776
- payload: {
2877
+ payload: this.buildCheckoutStatusPayloadFromSnapshot({
2777
2878
  cwd,
2778
- isGit: true,
2779
- repoRoot: status.repoRoot ?? null,
2780
- currentBranch: status.currentBranch ?? null,
2781
- isDirty: status.isDirty ?? null,
2782
- baseRef: status.baseRef ?? null,
2783
- aheadBehind: status.aheadBehind ?? null,
2784
- aheadOfOrigin: status.aheadOfOrigin ?? null,
2785
- behindOfOrigin: status.behindOfOrigin ?? null,
2786
- hasRemote: status.hasRemote,
2787
- remoteUrl: status.remoteUrl,
2788
- isPaseoOwnedWorktree: false,
2789
- error: null,
2790
2879
  requestId,
2791
- },
2880
+ snapshot,
2881
+ }),
2792
2882
  });
2793
2883
  }
2794
2884
  catch (error) {
@@ -2818,55 +2908,76 @@ export class Session {
2818
2908
  try {
2819
2909
  const resolvedCwd = expandTilde(cwd);
2820
2910
  this.assertSafeGitRef(branchName, "branch");
2821
- // Try local branch first
2822
- try {
2823
- await execCommand("git", ["rev-parse", "--verify", branchName], {
2824
- cwd: resolvedCwd,
2825
- env: READ_ONLY_GIT_ENV,
2826
- });
2827
- this.emit({
2828
- type: "validate_branch_response",
2829
- payload: {
2830
- exists: true,
2831
- resolvedRef: branchName,
2832
- isRemote: false,
2833
- error: null,
2834
- requestId,
2835
- },
2836
- });
2837
- return;
2838
- }
2839
- catch {
2840
- // Local branch doesn't exist, try remote
2841
- }
2842
- // Try remote branch (origin/{branchName})
2843
- try {
2844
- await execCommand("git", ["rev-parse", "--verify", `origin/${branchName}`], {
2845
- cwd: resolvedCwd,
2846
- env: READ_ONLY_GIT_ENV,
2847
- });
2848
- this.emit({
2849
- type: "validate_branch_response",
2850
- payload: {
2851
- exists: true,
2852
- resolvedRef: `origin/${branchName}`,
2853
- isRemote: true,
2854
- error: null,
2855
- requestId,
2856
- },
2857
- });
2858
- return;
2859
- }
2860
- catch {
2861
- // Remote branch doesn't exist either
2911
+ const resolution = await this.workspaceGitService.validateBranchRef(resolvedCwd, branchName);
2912
+ switch (resolution.kind) {
2913
+ case "local":
2914
+ this.emit({
2915
+ type: "validate_branch_response",
2916
+ payload: {
2917
+ exists: true,
2918
+ resolvedRef: resolution.name,
2919
+ isRemote: false,
2920
+ error: null,
2921
+ requestId,
2922
+ },
2923
+ });
2924
+ return;
2925
+ case "remote-only":
2926
+ this.emit({
2927
+ type: "validate_branch_response",
2928
+ payload: {
2929
+ exists: true,
2930
+ resolvedRef: resolution.remoteRef,
2931
+ isRemote: true,
2932
+ error: null,
2933
+ requestId,
2934
+ },
2935
+ });
2936
+ return;
2937
+ case "not-found":
2938
+ this.emit({
2939
+ type: "validate_branch_response",
2940
+ payload: {
2941
+ exists: false,
2942
+ resolvedRef: null,
2943
+ isRemote: false,
2944
+ error: null,
2945
+ requestId,
2946
+ },
2947
+ });
2948
+ return;
2949
+ default: {
2950
+ const exhaustiveCheck = resolution;
2951
+ throw new Error(`Unhandled branch resolution: ${exhaustiveCheck}`);
2952
+ }
2862
2953
  }
2863
- // Branch not found anywhere
2954
+ }
2955
+ catch (error) {
2864
2956
  this.emit({
2865
2957
  type: "validate_branch_response",
2866
2958
  payload: {
2867
2959
  exists: false,
2868
2960
  resolvedRef: null,
2869
2961
  isRemote: false,
2962
+ error: error instanceof Error ? error.message : String(error),
2963
+ requestId,
2964
+ },
2965
+ });
2966
+ }
2967
+ }
2968
+ async handleBranchSuggestionsRequest(msg) {
2969
+ const { cwd, query, limit, requestId } = msg;
2970
+ try {
2971
+ const resolvedCwd = expandTilde(cwd);
2972
+ const branchDetails = await this.workspaceGitService.suggestBranchesForCwd(resolvedCwd, {
2973
+ query,
2974
+ limit,
2975
+ });
2976
+ this.emit({
2977
+ type: "branch_suggestions_response",
2978
+ payload: {
2979
+ branches: branchDetails.map((branch) => branch.name),
2980
+ branchDetails,
2870
2981
  error: null,
2871
2982
  requestId,
2872
2983
  },
@@ -2874,26 +2985,31 @@ export class Session {
2874
2985
  }
2875
2986
  catch (error) {
2876
2987
  this.emit({
2877
- type: "validate_branch_response",
2988
+ type: "branch_suggestions_response",
2878
2989
  payload: {
2879
- exists: false,
2880
- resolvedRef: null,
2881
- isRemote: false,
2990
+ branches: [],
2991
+ branchDetails: [],
2882
2992
  error: error instanceof Error ? error.message : String(error),
2883
2993
  requestId,
2884
2994
  },
2885
2995
  });
2886
2996
  }
2887
2997
  }
2888
- async handleBranchSuggestionsRequest(msg) {
2889
- const { cwd, query, limit, requestId } = msg;
2998
+ async handleGitHubSearchRequest(msg) {
2999
+ const { cwd, query, limit, kinds, requestId } = msg;
2890
3000
  try {
2891
3001
  const resolvedCwd = expandTilde(cwd);
2892
- const branches = await listBranchSuggestions(resolvedCwd, { query, limit });
3002
+ const result = await this.github.searchIssuesAndPrs({
3003
+ cwd: resolvedCwd,
3004
+ query,
3005
+ limit,
3006
+ kinds,
3007
+ });
2893
3008
  this.emit({
2894
- type: "branch_suggestions_response",
3009
+ type: "github_search_response",
2895
3010
  payload: {
2896
- branches,
3011
+ items: result.items,
3012
+ githubFeaturesEnabled: result.githubFeaturesEnabled,
2897
3013
  error: null,
2898
3014
  requestId,
2899
3015
  },
@@ -2901,9 +3017,10 @@ export class Session {
2901
3017
  }
2902
3018
  catch (error) {
2903
3019
  this.emit({
2904
- type: "branch_suggestions_response",
3020
+ type: "github_search_response",
2905
3021
  payload: {
2906
- branches: [],
3022
+ items: [],
3023
+ githubFeaturesEnabled: true,
2907
3024
  error: error instanceof Error ? error.message : String(error),
2908
3025
  requestId,
2909
3026
  },
@@ -2952,24 +3069,97 @@ export class Session {
2952
3069
  });
2953
3070
  }
2954
3071
  }
3072
+ closeWorkspaceGitWatchTarget(target) {
3073
+ if (target.debounceTimer) {
3074
+ clearTimeout(target.debounceTimer);
3075
+ target.debounceTimer = null;
3076
+ }
3077
+ for (const watcher of target.watchers) {
3078
+ try {
3079
+ watcher.close();
3080
+ }
3081
+ catch {
3082
+ // Ignore watcher close errors
3083
+ }
3084
+ }
3085
+ target.watchers.length = 0;
3086
+ }
3087
+ async removeWorkspaceGitWatchTarget(cwd) {
3088
+ const normalizedCwd = normalizePersistedWorkspaceId(cwd);
3089
+ const target = this.workspaceGitWatchTargets.get(normalizedCwd);
3090
+ if (target) {
3091
+ this.closeWorkspaceGitWatchTarget(target);
3092
+ this.workspaceGitWatchTargets.delete(normalizedCwd);
3093
+ }
3094
+ }
2955
3095
  removeWorkspaceGitSubscription(cwd) {
2956
- const workspaceId = normalizePersistedWorkspaceId(cwd);
2957
- this.workspaceGitSubscriptions.get(workspaceId)?.();
2958
- this.workspaceGitSubscriptions.delete(workspaceId);
3096
+ const normalizedCwd = normalizePersistedWorkspaceId(cwd);
3097
+ const target = this.workspaceGitWatchTargets.get(normalizedCwd);
3098
+ if (target) {
3099
+ const unsubscribeFetch = this.workspaceGitFetchSubscriptions.get(normalizedCwd);
3100
+ unsubscribeFetch?.();
3101
+ this.workspaceGitFetchSubscriptions.delete(normalizedCwd);
3102
+ this.closeWorkspaceGitWatchTarget(target);
3103
+ this.workspaceGitWatchTargets.delete(normalizedCwd);
3104
+ }
3105
+ this.workspaceGitSubscriptions.get(normalizedCwd)?.();
3106
+ this.workspaceGitSubscriptions.delete(normalizedCwd);
3107
+ }
3108
+ workspaceGitDescriptorFingerprint(workspace) {
3109
+ if (!workspace) {
3110
+ return WORKSPACE_GIT_WATCH_REMOVED_FINGERPRINT;
3111
+ }
3112
+ return JSON.stringify([
3113
+ workspace.name,
3114
+ workspace.diffStat ? [workspace.diffStat.additions, workspace.diffStat.deletions] : null,
3115
+ ]);
3116
+ }
3117
+ shouldSkipWorkspaceGitWatchUpdate(workspaceId, workspace) {
3118
+ const target = this.workspaceGitWatchTargets.get(workspaceId);
3119
+ if (!target) {
3120
+ return false;
3121
+ }
3122
+ const nextFingerprint = this.workspaceGitDescriptorFingerprint(workspace);
3123
+ if (target.latestFingerprint === nextFingerprint) {
3124
+ return true;
3125
+ }
3126
+ target.latestFingerprint = nextFingerprint;
3127
+ return false;
3128
+ }
3129
+ rememberWorkspaceGitWatchFingerprint(workspaceId, workspace) {
3130
+ const target = this.workspaceGitWatchTargets.get(workspaceId);
3131
+ if (!target) {
3132
+ return;
3133
+ }
3134
+ target.latestFingerprint = this.workspaceGitDescriptorFingerprint(workspace);
3135
+ target.lastBranchName = workspace?.name ?? null;
3136
+ }
3137
+ async primeWorkspaceGitWatchFingerprints(workspaces) {
3138
+ for (const workspace of workspaces) {
3139
+ const persistedWorkspace = await this.workspaceRegistry.get(workspace.id);
3140
+ if (!persistedWorkspace) {
3141
+ continue;
3142
+ }
3143
+ await this.syncWorkspaceGitWatchTarget(persistedWorkspace.cwd, {
3144
+ isGit: workspace.projectKind === "git",
3145
+ });
3146
+ this.rememberWorkspaceGitWatchFingerprint(persistedWorkspace.cwd, workspace);
3147
+ }
2959
3148
  }
2960
3149
  async syncWorkspaceGitWatchTarget(cwd, options) {
2961
- const workspaceId = normalizePersistedWorkspaceId(cwd);
3150
+ const normalizedCwd = normalizePersistedWorkspaceId(cwd);
2962
3151
  if (!options.isGit) {
2963
- this.removeWorkspaceGitSubscription(workspaceId);
3152
+ this.removeWorkspaceGitSubscription(normalizedCwd);
2964
3153
  return;
2965
3154
  }
2966
- if (this.workspaceGitSubscriptions.has(workspaceId)) {
3155
+ if (this.workspaceGitSubscriptions.has(normalizedCwd)) {
2967
3156
  return;
2968
3157
  }
2969
- const subscription = await this.workspaceGitService.subscribe({ cwd: workspaceId }, () => {
2970
- void this.emitWorkspaceUpdateForCwd(workspaceId);
3158
+ const subscription = await this.workspaceGitService.subscribe({ cwd: normalizedCwd }, (snapshot) => {
3159
+ void this.emitWorkspaceUpdateForCwd(normalizedCwd);
3160
+ this.emitCheckoutStatusUpdate(normalizedCwd, snapshot);
2971
3161
  });
2972
- this.workspaceGitSubscriptions.set(workspaceId, subscription.unsubscribe);
3162
+ this.workspaceGitSubscriptions.set(normalizedCwd, subscription.unsubscribe);
2973
3163
  }
2974
3164
  async handleSubscribeCheckoutDiffRequest(msg) {
2975
3165
  const cwd = expandTilde(msg.cwd);
@@ -2998,10 +3188,108 @@ export class Session {
2998
3188
  this.checkoutDiffSubscriptions.get(msg.subscriptionId)?.();
2999
3189
  this.checkoutDiffSubscriptions.delete(msg.subscriptionId);
3000
3190
  }
3191
+ buildCheckoutStatusPayloadFromSnapshot({ cwd, requestId, snapshot, }) {
3192
+ if (!snapshot.git.isGit) {
3193
+ return {
3194
+ cwd,
3195
+ isGit: false,
3196
+ repoRoot: null,
3197
+ currentBranch: null,
3198
+ isDirty: null,
3199
+ baseRef: null,
3200
+ aheadBehind: null,
3201
+ aheadOfOrigin: null,
3202
+ behindOfOrigin: null,
3203
+ hasRemote: false,
3204
+ remoteUrl: null,
3205
+ isPaseoOwnedWorktree: false,
3206
+ error: null,
3207
+ requestId,
3208
+ };
3209
+ }
3210
+ if (snapshot.git.repoRoot === null || snapshot.git.isDirty === null) {
3211
+ throw new Error("Workspace git snapshot is missing required checkout status fields");
3212
+ }
3213
+ if (snapshot.git.isPaseoOwnedWorktree) {
3214
+ if (snapshot.git.mainRepoRoot === null || snapshot.git.baseRef === null) {
3215
+ throw new Error("Workspace git snapshot is missing required worktree status fields");
3216
+ }
3217
+ return {
3218
+ cwd,
3219
+ isGit: true,
3220
+ repoRoot: snapshot.git.repoRoot,
3221
+ mainRepoRoot: snapshot.git.mainRepoRoot,
3222
+ currentBranch: snapshot.git.currentBranch ?? null,
3223
+ isDirty: snapshot.git.isDirty,
3224
+ baseRef: snapshot.git.baseRef,
3225
+ aheadBehind: snapshot.git.aheadBehind ?? null,
3226
+ aheadOfOrigin: snapshot.git.aheadOfOrigin ?? null,
3227
+ behindOfOrigin: snapshot.git.behindOfOrigin ?? null,
3228
+ hasRemote: snapshot.git.hasRemote,
3229
+ remoteUrl: snapshot.git.remoteUrl,
3230
+ isPaseoOwnedWorktree: true,
3231
+ error: null,
3232
+ requestId,
3233
+ };
3234
+ }
3235
+ return {
3236
+ cwd,
3237
+ isGit: true,
3238
+ repoRoot: snapshot.git.repoRoot,
3239
+ currentBranch: snapshot.git.currentBranch ?? null,
3240
+ isDirty: snapshot.git.isDirty,
3241
+ baseRef: snapshot.git.baseRef ?? null,
3242
+ aheadBehind: snapshot.git.aheadBehind ?? null,
3243
+ aheadOfOrigin: snapshot.git.aheadOfOrigin ?? null,
3244
+ behindOfOrigin: snapshot.git.behindOfOrigin ?? null,
3245
+ hasRemote: snapshot.git.hasRemote,
3246
+ remoteUrl: snapshot.git.remoteUrl,
3247
+ isPaseoOwnedWorktree: false,
3248
+ error: null,
3249
+ requestId,
3250
+ };
3251
+ }
3252
+ buildCheckoutPrStatusPayloadFromSnapshot({ cwd, requestId, snapshot, }) {
3253
+ return {
3254
+ cwd,
3255
+ status: normalizeCheckoutPrStatusPayload(snapshot.github.pullRequest),
3256
+ githubFeaturesEnabled: snapshot.github.featuresEnabled,
3257
+ error: snapshot.github.error
3258
+ ? {
3259
+ code: "UNKNOWN",
3260
+ message: snapshot.github.error.message,
3261
+ }
3262
+ : null,
3263
+ requestId,
3264
+ };
3265
+ }
3266
+ emitCheckoutStatusUpdate(cwd, snapshot) {
3267
+ try {
3268
+ const requestId = `subscription:${cwd}`;
3269
+ this.emit({
3270
+ type: "checkout_status_update",
3271
+ payload: {
3272
+ ...this.buildCheckoutStatusPayloadFromSnapshot({
3273
+ cwd,
3274
+ requestId,
3275
+ snapshot,
3276
+ }),
3277
+ prStatus: this.buildCheckoutPrStatusPayloadFromSnapshot({
3278
+ cwd,
3279
+ requestId,
3280
+ snapshot,
3281
+ }),
3282
+ },
3283
+ });
3284
+ }
3285
+ catch (error) {
3286
+ this.sessionLogger.warn({ err: error, cwd }, "Failed to emit workspace checkout status update");
3287
+ }
3288
+ }
3001
3289
  async handleCheckoutSwitchBranchRequest(msg) {
3002
3290
  const { cwd, branch, requestId } = msg;
3003
3291
  try {
3004
- await this.checkoutExistingBranch(cwd, branch);
3292
+ const checkoutResult = await this.checkoutExistingBranch(cwd, branch);
3005
3293
  this.checkoutDiffManager.scheduleRefreshForCwd(cwd);
3006
3294
  // Push a workspace_update immediately so the sidebar/header reflect
3007
3295
  // the new branch name without waiting for the background git watcher.
@@ -3012,6 +3300,7 @@ export class Session {
3012
3300
  cwd,
3013
3301
  success: true,
3014
3302
  branch,
3303
+ source: checkoutResult.source,
3015
3304
  error: null,
3016
3305
  requestId,
3017
3306
  },
@@ -3038,6 +3327,7 @@ export class Session {
3038
3327
  ? `${Session.PASEO_STASH_PREFIX} ${branchLabel}`
3039
3328
  : `${Session.PASEO_STASH_PREFIX} unnamed`;
3040
3329
  await execCommand("git", ["stash", "push", "--include-untracked", "-m", message], { cwd });
3330
+ await this.notifyGitMutation(cwd, "stash-push");
3041
3331
  this.checkoutDiffManager.scheduleRefreshForCwd(cwd);
3042
3332
  this.emit({
3043
3333
  type: "stash_save_response",
@@ -3055,6 +3345,7 @@ export class Session {
3055
3345
  const { cwd, stashIndex, requestId } = msg;
3056
3346
  try {
3057
3347
  await execCommand("git", ["stash", "pop", `stash@{${stashIndex}}`], { cwd });
3348
+ await this.notifyGitMutation(cwd, "stash-pop");
3058
3349
  this.checkoutDiffManager.scheduleRefreshForCwd(cwd);
3059
3350
  this.emit({
3060
3351
  type: "stash_pop_response",
@@ -3072,31 +3363,7 @@ export class Session {
3072
3363
  const { cwd, requestId } = msg;
3073
3364
  const paseoOnly = msg.paseoOnly !== false;
3074
3365
  try {
3075
- const { stdout } = await execAsync("git stash list --format=%gd%x00%s", {
3076
- cwd,
3077
- env: READ_ONLY_GIT_ENV,
3078
- });
3079
- const lines = stdout.trim().split("\n").filter(Boolean);
3080
- const entries = [];
3081
- for (const line of lines) {
3082
- const sepIdx = line.indexOf("\0");
3083
- if (sepIdx < 0)
3084
- continue;
3085
- const refPart = line.slice(0, sepIdx);
3086
- const subject = line.slice(sepIdx + 1);
3087
- const indexMatch = refPart.match(/\{(\d+)\}/);
3088
- if (!indexMatch)
3089
- continue;
3090
- const index = Number(indexMatch[1]);
3091
- const prefixIdx = subject.indexOf(Session.PASEO_STASH_PREFIX);
3092
- const isPaseo = prefixIdx >= 0;
3093
- const branch = isPaseo
3094
- ? subject.slice(prefixIdx + Session.PASEO_STASH_PREFIX.length).trim() || null
3095
- : null;
3096
- if (paseoOnly && !isPaseo)
3097
- continue;
3098
- entries.push({ index, message: subject, branch, isPaseo });
3099
- }
3366
+ const entries = await this.workspaceGitService.listStashes(cwd, { paseoOnly });
3100
3367
  this.emit({
3101
3368
  type: "stash_list_response",
3102
3369
  payload: { cwd, entries, error: null, requestId },
@@ -3123,6 +3390,7 @@ export class Session {
3123
3390
  message,
3124
3391
  addAll: msg.addAll ?? true,
3125
3392
  });
3393
+ await this.notifyGitMutation(cwd, "commit-changes");
3126
3394
  this.checkoutDiffManager.scheduleRefreshForCwd(cwd);
3127
3395
  this.emit({
3128
3396
  type: "checkout_commit_response",
@@ -3149,43 +3417,30 @@ export class Session {
3149
3417
  async handleCheckoutMergeRequest(msg) {
3150
3418
  const { cwd, requestId } = msg;
3151
3419
  try {
3152
- const status = await getCheckoutStatus(cwd, { paseoHome: this.paseoHome });
3153
- if (!status.isGit) {
3154
- try {
3155
- await execAsync("git rev-parse --is-inside-work-tree", {
3156
- cwd,
3157
- env: READ_ONLY_GIT_ENV,
3158
- });
3159
- }
3160
- catch (error) {
3161
- const details = typeof error?.stderr === "string"
3162
- ? String(error.stderr).trim()
3163
- : error instanceof Error
3164
- ? error.message
3165
- : String(error);
3166
- throw new Error(`Not a git repository: ${cwd}\n${details}`.trim());
3167
- }
3420
+ const snapshot = await this.workspaceGitService.getSnapshot(cwd);
3421
+ if (!snapshot.git.isGit) {
3422
+ throw new Error(`Not a git repository: ${cwd}`);
3168
3423
  }
3169
3424
  if (msg.requireCleanTarget) {
3170
- const { stdout } = await execAsync("git status --porcelain", {
3171
- cwd,
3172
- env: READ_ONLY_GIT_ENV,
3173
- });
3174
- if (stdout.trim().length > 0) {
3425
+ if (snapshot.git.isDirty) {
3175
3426
  throw new Error("Working directory has uncommitted changes.");
3176
3427
  }
3177
3428
  }
3178
- let baseRef = msg.baseRef ?? (status.isGit ? status.baseRef : null);
3429
+ let baseRef = msg.baseRef ?? snapshot.git.baseRef;
3179
3430
  if (!baseRef) {
3180
3431
  throw new Error("Base branch is required for merge");
3181
3432
  }
3182
3433
  if (baseRef.startsWith("origin/")) {
3183
3434
  baseRef = baseRef.slice("origin/".length);
3184
3435
  }
3185
- await mergeToBase(cwd, {
3436
+ const mutatedCwd = await mergeToBase(cwd, {
3186
3437
  baseRef,
3187
3438
  mode: msg.strategy === "squash" ? "squash" : "merge",
3188
3439
  }, { paseoHome: this.paseoHome });
3440
+ await Promise.all([
3441
+ this.notifyGitMutation(mutatedCwd, "merge-to-base", { invalidateGithub: true }),
3442
+ ...(mutatedCwd !== cwd ? [this.notifyGitMutation(cwd, "merge-to-base")] : []),
3443
+ ]);
3189
3444
  this.checkoutDiffManager.scheduleRefreshForCwd(cwd);
3190
3445
  this.emit({
3191
3446
  type: "checkout_merge_response",
@@ -3213,11 +3468,8 @@ export class Session {
3213
3468
  const { cwd, requestId } = msg;
3214
3469
  try {
3215
3470
  if (msg.requireCleanTarget ?? true) {
3216
- const { stdout } = await execAsync("git status --porcelain", {
3217
- cwd,
3218
- env: READ_ONLY_GIT_ENV,
3219
- });
3220
- if (stdout.trim().length > 0) {
3471
+ const snapshot = await this.workspaceGitService.getSnapshot(cwd);
3472
+ if (snapshot.git.isDirty) {
3221
3473
  throw new Error("Working directory has uncommitted changes.");
3222
3474
  }
3223
3475
  }
@@ -3225,6 +3477,7 @@ export class Session {
3225
3477
  baseRef: msg.baseRef,
3226
3478
  requireCleanTarget: msg.requireCleanTarget ?? true,
3227
3479
  });
3480
+ await this.notifyGitMutation(cwd, "merge-from-base", { invalidateGithub: true });
3228
3481
  this.checkoutDiffManager.scheduleRefreshForCwd(cwd);
3229
3482
  this.emit({
3230
3483
  type: "checkout_merge_from_base_response",
@@ -3252,6 +3505,7 @@ export class Session {
3252
3505
  const { cwd, requestId } = msg;
3253
3506
  try {
3254
3507
  await pullCurrentBranch(cwd);
3508
+ await this.notifyGitMutation(cwd, "pull", { invalidateGithub: true });
3255
3509
  this.checkoutDiffManager.scheduleRefreshForCwd(cwd);
3256
3510
  this.emit({
3257
3511
  type: "checkout_pull_response",
@@ -3279,6 +3533,7 @@ export class Session {
3279
3533
  const { cwd, requestId } = msg;
3280
3534
  try {
3281
3535
  await pushCurrentBranch(cwd);
3536
+ await this.notifyGitMutation(cwd, "push", { invalidateGithub: true });
3282
3537
  this.emit({
3283
3538
  type: "checkout_push_response",
3284
3539
  payload: {
@@ -3317,7 +3572,8 @@ export class Session {
3317
3572
  title,
3318
3573
  body,
3319
3574
  base: msg.baseRef,
3320
- });
3575
+ }, this.github, this.workspaceGitService);
3576
+ await this.notifyGitMutation(cwd, "create-pr", { invalidateGithub: true });
3321
3577
  this.emit({
3322
3578
  type: "checkout_pr_create_response",
3323
3579
  payload: {
@@ -3350,7 +3606,7 @@ export class Session {
3350
3606
  type: "checkout_pr_status_response",
3351
3607
  payload: {
3352
3608
  cwd,
3353
- status: snapshot.github.pullRequest,
3609
+ status: normalizeCheckoutPrStatusPayload(snapshot.github.pullRequest),
3354
3610
  githubFeaturesEnabled: snapshot.github.featuresEnabled,
3355
3611
  error: snapshot.github.error
3356
3612
  ? {
@@ -3375,22 +3631,108 @@ export class Session {
3375
3631
  });
3376
3632
  }
3377
3633
  }
3634
+ async handlePullRequestTimelineRequest(msg) {
3635
+ const { cwd, prNumber, repoOwner, repoName, requestId } = msg;
3636
+ if (!isValidPullRequestTimelineIdentity({ prNumber, repoOwner, repoName })) {
3637
+ this.emit({
3638
+ type: "pull_request_timeline_response",
3639
+ payload: {
3640
+ cwd,
3641
+ prNumber,
3642
+ items: [],
3643
+ truncated: false,
3644
+ error: {
3645
+ kind: "unknown",
3646
+ message: "Pull request timeline request has invalid PR identity",
3647
+ },
3648
+ requestId,
3649
+ githubFeaturesEnabled: true,
3650
+ },
3651
+ });
3652
+ return;
3653
+ }
3654
+ const githubFeaturesEnabled = await this.github.isAuthenticated({ cwd });
3655
+ if (!githubFeaturesEnabled) {
3656
+ this.emit({
3657
+ type: "pull_request_timeline_response",
3658
+ payload: {
3659
+ cwd,
3660
+ prNumber,
3661
+ items: [],
3662
+ truncated: false,
3663
+ error: {
3664
+ kind: "unknown",
3665
+ message: "GitHub CLI is unavailable or not authenticated",
3666
+ },
3667
+ requestId,
3668
+ githubFeaturesEnabled: false,
3669
+ },
3670
+ });
3671
+ return;
3672
+ }
3673
+ try {
3674
+ const timeline = await this.github.getPullRequestTimeline({
3675
+ cwd,
3676
+ prNumber,
3677
+ repoOwner,
3678
+ repoName,
3679
+ });
3680
+ this.emit({
3681
+ type: "pull_request_timeline_response",
3682
+ payload: {
3683
+ cwd,
3684
+ prNumber: timeline.prNumber,
3685
+ items: timeline.items.map(toPullRequestTimelinePayloadItem),
3686
+ truncated: timeline.truncated,
3687
+ error: timeline.error,
3688
+ requestId,
3689
+ githubFeaturesEnabled: true,
3690
+ },
3691
+ });
3692
+ }
3693
+ catch (error) {
3694
+ this.emit({
3695
+ type: "pull_request_timeline_response",
3696
+ payload: {
3697
+ cwd,
3698
+ prNumber,
3699
+ items: [],
3700
+ truncated: false,
3701
+ error: {
3702
+ kind: "unknown",
3703
+ message: error instanceof Error ? error.message : String(error),
3704
+ },
3705
+ requestId,
3706
+ githubFeaturesEnabled: true,
3707
+ },
3708
+ });
3709
+ }
3710
+ }
3378
3711
  async handlePaseoWorktreeListRequest(msg) {
3379
3712
  return handleWorktreeListRequest({
3380
3713
  emit: (message) => this.emit(message),
3381
3714
  paseoHome: this.paseoHome,
3715
+ workspaceGitService: this.workspaceGitService,
3382
3716
  }, msg);
3383
3717
  }
3384
3718
  async handlePaseoWorktreeArchiveRequest(msg) {
3385
3719
  return handleWorktreeArchiveRequest({
3386
3720
  paseoHome: this.paseoHome,
3721
+ github: this.github,
3722
+ workspaceGitService: this.workspaceGitService,
3387
3723
  agentManager: this.agentManager,
3388
3724
  agentStorage: this.agentStorage,
3389
- archiveWorkspaceRecord: (workspaceId) => this.archiveWorkspaceRecord(workspaceId),
3725
+ archiveWorkspaceRecord: async (workspaceDirectory) => {
3726
+ const workspace = await this.findWorkspaceByDirectory(workspaceDirectory);
3727
+ if (workspace) {
3728
+ await this.archiveWorkspaceRecord(workspace.workspaceId);
3729
+ }
3730
+ },
3390
3731
  emit: (message) => this.emit(message),
3391
3732
  emitWorkspaceUpdatesForCwds: (cwds) => this.emitWorkspaceUpdatesForCwds(cwds),
3392
3733
  isPathWithinRoot: (rootPath, candidatePath) => this.isPathWithinRoot(rootPath, candidatePath),
3393
3734
  killTerminalsUnderPath: (rootPath) => this.killTerminalsUnderPath(rootPath),
3735
+ sessionLogger: this.sessionLogger,
3394
3736
  }, msg);
3395
3737
  }
3396
3738
  /**
@@ -3578,6 +3920,7 @@ export class Session {
3578
3920
  .filter((record) => !liveIds.has(record.id) && !record.internal)
3579
3921
  .map((record) => this.buildStoredAgentPayload(record));
3580
3922
  let agents = [...liveAgents, ...persistedAgents];
3923
+ agents = agents.filter((agent) => this.isProviderVisibleToClient(agent.provider));
3581
3924
  // Filter by labels if filter provided
3582
3925
  if (filter?.labels) {
3583
3926
  const filterLabels = filter.labels;
@@ -3633,13 +3976,15 @@ export class Session {
3633
3976
  async getAgentPayloadById(agentId) {
3634
3977
  const live = this.agentManager.getAgent(agentId);
3635
3978
  if (live) {
3636
- return await this.buildAgentPayload(live);
3979
+ const payload = await this.buildAgentPayload(live);
3980
+ return this.isProviderVisibleToClient(payload.provider) ? payload : null;
3637
3981
  }
3638
3982
  const record = await this.agentStorage.get(agentId);
3639
3983
  if (!record || record.internal) {
3640
3984
  return null;
3641
3985
  }
3642
- return this.buildStoredAgentPayload(record);
3986
+ const payload = this.buildStoredAgentPayload(record);
3987
+ return this.isProviderVisibleToClient(payload.provider) ? payload : null;
3643
3988
  }
3644
3989
  normalizeFetchAgentsSort(sort) {
3645
3990
  const fallback = [{ key: "updated_at", direction: "desc" }];
@@ -3795,19 +4140,51 @@ export class Session {
3795
4140
  }
3796
4141
  return agent.id.localeCompare(cursor.id);
3797
4142
  }
4143
+ async buildActiveProjectPlacementsByWorkspaceCwd() {
4144
+ const [persistedWorkspaces, persistedProjects] = await Promise.all([
4145
+ this.workspaceRegistry.list(),
4146
+ this.projectRegistry.list(),
4147
+ ]);
4148
+ const activeProjects = new Map(persistedProjects
4149
+ .filter((project) => !project.archivedAt)
4150
+ .map((project) => [project.projectId, project]));
4151
+ const placementsByCwd = new Map();
4152
+ for (const workspace of persistedWorkspaces) {
4153
+ if (workspace.archivedAt) {
4154
+ continue;
4155
+ }
4156
+ const project = activeProjects.get(workspace.projectId);
4157
+ if (!project) {
4158
+ continue;
4159
+ }
4160
+ placementsByCwd.set(normalizePersistedWorkspaceId(workspace.cwd), await this.buildProjectPlacementForWorkspace(workspace, project));
4161
+ }
4162
+ return placementsByCwd;
4163
+ }
3798
4164
  async listFetchAgentsEntries(request) {
3799
- const filter = request.filter;
4165
+ const filter = request.type === "fetch_agent_history_request" &&
4166
+ request.filter?.includeArchived === undefined
4167
+ ? { ...request.filter, includeArchived: true }
4168
+ : request.filter;
4169
+ const scope = request.type === "fetch_agents_request" ? request.scope : undefined;
3800
4170
  const sort = this.normalizeFetchAgentsSort(request.sort);
3801
- const agents = await this.listAgentPayloads({
4171
+ let agents = await this.listAgentPayloads({
3802
4172
  labels: filter?.labels,
3803
4173
  });
4174
+ const activePlacementsByCwd = scope === "active" ? await this.buildActiveProjectPlacementsByWorkspaceCwd() : null;
4175
+ if (activePlacementsByCwd) {
4176
+ agents = agents.filter((agent) => !agent.archivedAt && activePlacementsByCwd.has(normalizePersistedWorkspaceId(agent.cwd)));
4177
+ }
3804
4178
  const placementByCwd = new Map();
3805
4179
  const getPlacement = (cwd) => {
4180
+ if (activePlacementsByCwd) {
4181
+ return Promise.resolve(activePlacementsByCwd.get(normalizePersistedWorkspaceId(cwd)) ?? null);
4182
+ }
3806
4183
  const existing = placementByCwd.get(cwd);
3807
4184
  if (existing) {
3808
4185
  return existing;
3809
4186
  }
3810
- const placementPromise = this.buildProjectPlacement(cwd);
4187
+ const placementPromise = this.buildProjectPlacementForCwd(cwd);
3811
4188
  placementByCwd.set(cwd, placementPromise);
3812
4189
  return placementPromise;
3813
4190
  };
@@ -3823,11 +4200,14 @@ export class Session {
3823
4200
  const batchSize = 25;
3824
4201
  for (let start = 0; start < candidates.length && matchedEntries.length <= limit; start += batchSize) {
3825
4202
  const batch = candidates.slice(start, start + batchSize);
3826
- const batchEntries = await Promise.all(batch.map(async (agent) => ({
3827
- agent,
3828
- project: await getPlacement(agent.cwd),
3829
- })));
4203
+ const batchEntries = await Promise.all(batch.map(async (agent) => {
4204
+ const project = await getPlacement(agent.cwd);
4205
+ return project ? { agent, project } : null;
4206
+ }));
3830
4207
  for (const entry of batchEntries) {
4208
+ if (!entry) {
4209
+ continue;
4210
+ }
3831
4211
  if (!this.matchesAgentFilter({
3832
4212
  agent: entry.agent,
3833
4213
  project: entry.project,
@@ -3873,17 +4253,34 @@ export class Session {
3873
4253
  }
3874
4254
  async describeWorkspaceRecord(workspace, projectRecord) {
3875
4255
  const resolvedProjectRecord = projectRecord ?? (await this.projectRegistry.get(workspace.projectId));
4256
+ let diffStat = null;
4257
+ const snapshot = this.workspaceGitService.peekSnapshot(workspace.cwd);
4258
+ if (snapshot?.git.diffStat) {
4259
+ diffStat = snapshot.git.diffStat;
4260
+ }
3876
4261
  return {
3877
4262
  id: workspace.workspaceId,
3878
4263
  projectId: workspace.projectId,
3879
- projectDisplayName: resolvedProjectRecord?.displayName ?? workspace.projectId,
4264
+ projectDisplayName: resolvedProjectRecord?.displayName ?? String(workspace.projectId),
3880
4265
  projectRootPath: resolvedProjectRecord?.rootPath ?? workspace.cwd,
3881
- projectKind: resolvedProjectRecord?.kind ?? "non_git",
4266
+ workspaceDirectory: workspace.cwd,
4267
+ projectKind: (resolvedProjectRecord?.kind ?? "directory") === "git" ? "git" : "non_git",
3882
4268
  workspaceKind: workspace.kind,
3883
4269
  name: workspace.displayName,
3884
4270
  status: "done",
3885
4271
  activityAt: null,
3886
- diffStat: null,
4272
+ diffStat,
4273
+ scripts: this.scriptRouteStore && this.scriptRuntimeStore
4274
+ ? buildWorkspaceScriptPayloads({
4275
+ workspaceId: workspace.workspaceId,
4276
+ workspaceDirectory: workspace.cwd,
4277
+ routeStore: this.scriptRouteStore,
4278
+ runtimeStore: this.scriptRuntimeStore,
4279
+ daemonPort: this.getDaemonTcpPort?.() ?? null,
4280
+ gitMetadata: this.resolveWorkspaceScriptGitMetadata(workspace.cwd),
4281
+ resolveHealth: this.resolveScriptHealth ?? undefined,
4282
+ })
4283
+ : [],
3887
4284
  };
3888
4285
  }
3889
4286
  buildWorkspaceGitRuntimePayload(snapshot) {
@@ -3905,17 +4302,12 @@ export class Session {
3905
4302
  featuresEnabled: snapshot.github.featuresEnabled,
3906
4303
  pullRequest: snapshot.github.pullRequest,
3907
4304
  error: snapshot.github.error,
3908
- refreshedAt: snapshot.github.refreshedAt,
3909
4305
  };
3910
4306
  }
3911
4307
  async describeWorkspaceRecordWithGitData(workspace, projectRecord) {
3912
4308
  const base = await this.describeWorkspaceRecord(workspace, projectRecord);
3913
- let snapshot;
3914
- try {
3915
- snapshot = await this.workspaceGitService.getSnapshot(workspace.cwd);
3916
- }
3917
- catch (error) {
3918
- this.sessionLogger.warn({ err: error, cwd: workspace.cwd }, "Failed to load git snapshot for workspace");
4309
+ const snapshot = this.workspaceGitService.peekSnapshot(workspace.cwd);
4310
+ if (!snapshot) {
3919
4311
  return base;
3920
4312
  }
3921
4313
  const checkout = checkoutLiteFromGitSnapshot(workspace.cwd, snapshot.git);
@@ -3924,10 +4316,37 @@ export class Session {
3924
4316
  ...base,
3925
4317
  name: displayName,
3926
4318
  diffStat: snapshot.git.diffStat ?? null,
3927
- gitRuntime: this.buildWorkspaceGitRuntimePayload(snapshot),
4319
+ gitRuntime: this.buildWorkspaceGitRuntimePayload(snapshot) ?? undefined,
3928
4320
  githubRuntime: this.buildWorkspaceGitHubRuntimePayload(snapshot),
3929
4321
  };
3930
4322
  }
4323
+ async describeCreatedWorktreeWorkspace(result) {
4324
+ const projectRecord = await this.projectRegistry.get(result.workspace.projectId);
4325
+ return {
4326
+ id: result.workspace.workspaceId,
4327
+ projectId: result.workspace.projectId,
4328
+ projectDisplayName: projectRecord?.displayName ?? String(result.workspace.projectId),
4329
+ projectRootPath: projectRecord?.rootPath ?? result.repoRoot,
4330
+ workspaceDirectory: result.workspace.cwd,
4331
+ projectKind: "git",
4332
+ workspaceKind: result.workspace.kind,
4333
+ name: result.worktree.branchName || result.workspace.displayName,
4334
+ status: "done",
4335
+ activityAt: null,
4336
+ diffStat: { additions: 0, deletions: 0 },
4337
+ scripts: [],
4338
+ gitRuntime: {
4339
+ currentBranch: result.worktree.branchName || null,
4340
+ remoteUrl: null,
4341
+ isPaseoOwnedWorktree: true,
4342
+ isDirty: false,
4343
+ aheadBehind: null,
4344
+ aheadOfOrigin: null,
4345
+ behindOfOrigin: null,
4346
+ },
4347
+ githubRuntime: null,
4348
+ };
4349
+ }
3931
4350
  async buildWorkspaceDescriptor(input) {
3932
4351
  if (input.includeGitData && input.projectRecord?.kind === "git") {
3933
4352
  return this.describeWorkspaceRecordWithGitData(input.workspace, input.projectRecord);
@@ -3940,14 +4359,14 @@ export class Session {
3940
4359
  this.workspaceRegistry.list(),
3941
4360
  this.projectRegistry.list(),
3942
4361
  ]);
3943
- const activeRecords = persistedWorkspaces.filter((workspace) => !workspace.archivedAt);
3944
4362
  const activeProjects = new Map(persistedProjects
3945
4363
  .filter((project) => !project.archivedAt)
3946
4364
  .map((project) => [project.projectId, project]));
4365
+ const archivedProjectIds = new Set(persistedProjects.filter((project) => project.archivedAt).map((project) => project.projectId));
4366
+ const activeRecords = persistedWorkspaces.filter((workspace) => !workspace.archivedAt && !archivedProjectIds.has(workspace.projectId));
3947
4367
  const descriptorsByWorkspaceId = new Map();
3948
- const workspaceIds = options.workspaceIds
3949
- ? new Set(Array.from(options.workspaceIds, (workspaceId) => normalizePersistedWorkspaceId(workspaceId)))
3950
- : null;
4368
+ const workspaceIds = options.workspaceIds ? new Set(options.workspaceIds) : null;
4369
+ const workspaceIdsByDirectory = new Map(activeRecords.map((workspace) => [normalizePersistedWorkspaceId(workspace.cwd), workspace.workspaceId]));
3951
4370
  for (const workspace of activeRecords) {
3952
4371
  if (workspaceIds && !workspaceIds.has(workspace.workspaceId)) {
3953
4372
  continue;
@@ -3963,7 +4382,13 @@ export class Session {
3963
4382
  if (agent.archivedAt) {
3964
4383
  continue;
3965
4384
  }
3966
- const workspaceId = this.resolveRegisteredWorkspaceIdForCwd(agent.cwd, activeRecords);
4385
+ if (!this.isProviderVisibleToClient(agent.provider)) {
4386
+ continue;
4387
+ }
4388
+ const workspaceId = workspaceIdsByDirectory.get(normalizePersistedWorkspaceId(agent.cwd));
4389
+ if (workspaceId === undefined) {
4390
+ continue;
4391
+ }
3967
4392
  const existing = descriptorsByWorkspaceId.get(workspaceId);
3968
4393
  if (!existing) {
3969
4394
  continue;
@@ -3977,19 +4402,17 @@ export class Session {
3977
4402
  }
3978
4403
  resolveRegisteredWorkspaceIdForCwd(cwd, workspaces) {
3979
4404
  const normalizedCwd = normalizePersistedWorkspaceId(cwd);
3980
- const exact = workspaces.find((workspace) => workspace.workspaceId === normalizedCwd);
4405
+ const exact = workspaces.find((workspace) => workspace.cwd === normalizedCwd);
3981
4406
  if (exact) {
3982
4407
  return exact.workspaceId;
3983
4408
  }
3984
4409
  let bestMatch = null;
3985
4410
  for (const workspace of workspaces) {
3986
- const prefix = workspace.workspaceId.endsWith(sep)
3987
- ? workspace.workspaceId
3988
- : `${workspace.workspaceId}${sep}`;
4411
+ const prefix = workspace.cwd.endsWith(sep) ? workspace.cwd : `${workspace.cwd}${sep}`;
3989
4412
  if (!normalizedCwd.startsWith(prefix)) {
3990
4413
  continue;
3991
4414
  }
3992
- if (!bestMatch || workspace.workspaceId.length > bestMatch.workspaceId.length) {
4415
+ if (!bestMatch || workspace.cwd.length > bestMatch.cwd.length) {
3993
4416
  bestMatch = workspace;
3994
4417
  }
3995
4418
  }
@@ -4095,7 +4518,7 @@ export class Session {
4095
4518
  return {
4096
4519
  sort: cursorSort,
4097
4520
  values: payload.values,
4098
- id: payload.id,
4521
+ id: String(payload.id),
4099
4522
  };
4100
4523
  }
4101
4524
  compareWorkspaceWithCursor(workspace, cursor, sort) {
@@ -4121,13 +4544,13 @@ export class Session {
4121
4544
  }
4122
4545
  }
4123
4546
  if (filter.idPrefix && filter.idPrefix.trim().length > 0) {
4124
- if (!workspace.id.startsWith(filter.idPrefix.trim())) {
4547
+ if (!String(workspace.id).startsWith(filter.idPrefix.trim())) {
4125
4548
  return false;
4126
4549
  }
4127
4550
  }
4128
4551
  if (filter.query && filter.query.trim().length > 0) {
4129
4552
  const query = filter.query.trim().toLocaleLowerCase();
4130
- const haystacks = [workspace.name, workspace.projectId, workspace.id];
4553
+ const haystacks = [workspace.name, String(workspace.projectId), String(workspace.id)];
4131
4554
  if (!haystacks.some((value) => value.toLocaleLowerCase().includes(query))) {
4132
4555
  return false;
4133
4556
  }
@@ -4213,32 +4636,95 @@ export class Session {
4213
4636
  });
4214
4637
  }
4215
4638
  }
4216
- async ensureWorkspaceRegistered(cwd) {
4217
- return (await this.reconcileWorkspaceRecord(cwd)).workspace;
4218
- }
4219
- async registerPendingWorktreeWorkspace(options) {
4220
- return registerPendingWorktreeWorkspaceSession({
4221
- buildPersistedProjectRecord: (input) => this.buildPersistedProjectRecord(input),
4222
- buildPersistedWorkspaceRecord: (input) => this.buildPersistedWorkspaceRecord(input),
4223
- buildProjectPlacement: (cwd) => this.buildProjectPlacement(cwd),
4639
+ async findOrCreateWorkspaceForDirectory(cwd) {
4640
+ const normalizedCwd = await this.resolveWorkspaceDirectory(cwd);
4641
+ const existingWorkspace = await this.findWorkspaceByDirectory(normalizedCwd);
4642
+ if (existingWorkspace) {
4643
+ return this.ensureWorkspaceRecordUnarchived(existingWorkspace);
4644
+ }
4645
+ const placement = await buildProjectPlacementForCwdStandalone({
4646
+ cwd: normalizedCwd,
4647
+ workspaceGitService: this.workspaceGitService,
4648
+ });
4649
+ const workspaceId = deriveWorkspaceId(normalizedCwd, placement.checkout);
4650
+ const timestamp = new Date().toISOString();
4651
+ const projectRecord = createPersistedProjectRecord({
4652
+ projectId: placement.projectKey,
4653
+ rootPath: deriveProjectRootPath({ cwd: normalizedCwd, checkout: placement.checkout }),
4654
+ kind: deriveProjectKind(placement.checkout),
4655
+ displayName: placement.projectName,
4656
+ createdAt: timestamp,
4657
+ updatedAt: timestamp,
4658
+ });
4659
+ await this.projectRegistry.upsert(projectRecord);
4660
+ const workspaceRecord = createPersistedWorkspaceRecord({
4661
+ workspaceId,
4662
+ projectId: placement.projectKey,
4663
+ cwd: normalizedCwd,
4664
+ kind: deriveWorkspaceKind(placement.checkout),
4665
+ displayName: deriveWorkspaceDisplayName({
4666
+ cwd: normalizedCwd,
4667
+ checkout: placement.checkout,
4668
+ }),
4669
+ createdAt: timestamp,
4670
+ updatedAt: timestamp,
4671
+ });
4672
+ await this.workspaceRegistry.upsert(workspaceRecord);
4673
+ return workspaceRecord;
4674
+ }
4675
+ async ensureWorkspaceRecordUnarchived(workspace) {
4676
+ const project = await this.projectRegistry.get(workspace.projectId);
4677
+ if (!workspace.archivedAt && (!project || !project.archivedAt)) {
4678
+ return workspace;
4679
+ }
4680
+ const timestamp = new Date().toISOString();
4681
+ let unarchivedWorkspace = workspace;
4682
+ if (workspace.archivedAt) {
4683
+ unarchivedWorkspace = { ...workspace, archivedAt: null, updatedAt: timestamp };
4684
+ await this.workspaceRegistry.upsert(unarchivedWorkspace);
4685
+ }
4686
+ if (project?.archivedAt) {
4687
+ await this.projectRegistry.upsert({
4688
+ ...project,
4689
+ archivedAt: null,
4690
+ updatedAt: timestamp,
4691
+ });
4692
+ }
4693
+ return unarchivedWorkspace;
4694
+ }
4695
+ async createPaseoWorktree(input, options) {
4696
+ const coreDeps = createWorktreeCoreDeps(this.github);
4697
+ const result = await createPaseoWorktree(input, {
4698
+ ...coreDeps,
4699
+ ...(options?.resolveDefaultBranch
4700
+ ? { resolveDefaultBranch: options.resolveDefaultBranch }
4701
+ : {}),
4224
4702
  projectRegistry: this.projectRegistry,
4225
4703
  workspaceRegistry: this.workspaceRegistry,
4226
- archiveProjectRecordIfEmpty: (projectId, archivedAt) => this.archiveProjectRecordIfEmpty(projectId, archivedAt),
4227
- }, options);
4704
+ workspaceGitService: this.workspaceGitService,
4705
+ });
4706
+ void Promise.all([
4707
+ this.notifyGitMutation(input.cwd, "create-worktree"),
4708
+ this.notifyGitMutation(result.worktree.worktreePath, "create-worktree"),
4709
+ ]).catch((error) => {
4710
+ this.sessionLogger.warn({ err: error, cwd: input.cwd, worktreePath: result.worktree.worktreePath }, "Failed to warm git snapshots after creating worktree");
4711
+ });
4712
+ return result;
4228
4713
  }
4229
4714
  async archiveWorkspaceRecord(workspaceId, archivedAt) {
4230
- const existing = await this.workspaceRegistry.get(workspaceId);
4231
- if (!existing || existing.archivedAt) {
4715
+ const existingWorkspace = await archivePersistedWorkspaceRecord({
4716
+ workspaceId,
4717
+ archivedAt,
4718
+ workspaceRegistry: this.workspaceRegistry,
4719
+ projectRegistry: this.projectRegistry,
4720
+ });
4721
+ if (!existingWorkspace) {
4232
4722
  this.removeWorkspaceGitSubscription(workspaceId);
4233
4723
  return;
4234
4724
  }
4235
- const nextArchivedAt = archivedAt ?? new Date().toISOString();
4236
- await this.workspaceRegistry.archive(workspaceId, nextArchivedAt);
4725
+ await this.removeWorkspaceGitWatchTarget(existingWorkspace.cwd);
4726
+ this.scriptRuntimeStore?.removeForWorkspace(existingWorkspace.cwd);
4237
4727
  this.removeWorkspaceGitSubscription(workspaceId);
4238
- const siblingWorkspaces = (await this.workspaceRegistry.list()).filter((workspace) => workspace.projectId === existing.projectId && !workspace.archivedAt);
4239
- if (siblingWorkspaces.length === 0) {
4240
- await this.projectRegistry.archive(existing.projectId, nextArchivedAt);
4241
- }
4242
4728
  }
4243
4729
  async reconcileAndEmitWorkspaceUpdates() {
4244
4730
  if (!this.workspaceUpdatesSubscription) {
@@ -4257,12 +4743,48 @@ export class Session {
4257
4743
  this.sessionLogger.error({ err: error }, "Background workspace reconciliation failed");
4258
4744
  }
4259
4745
  }
4746
+ async reconcileActiveWorkspaceRecords() {
4747
+ const service = new WorkspaceReconciliationService({
4748
+ projectRegistry: this.projectRegistry,
4749
+ workspaceRegistry: this.workspaceRegistry,
4750
+ logger: this.sessionLogger,
4751
+ workspaceGitService: this.workspaceGitService,
4752
+ });
4753
+ const result = await service.runOnce();
4754
+ const changedWorkspaceIds = new Set();
4755
+ const changedProjectIds = new Set();
4756
+ for (const change of result.changesApplied) {
4757
+ switch (change.kind) {
4758
+ case "workspace_archived":
4759
+ await this.removeWorkspaceGitWatchTarget(change.directory);
4760
+ this.scriptRuntimeStore?.removeForWorkspace(change.directory);
4761
+ this.removeWorkspaceGitSubscription(change.workspaceId);
4762
+ changedWorkspaceIds.add(change.workspaceId);
4763
+ break;
4764
+ case "workspace_updated":
4765
+ changedWorkspaceIds.add(change.workspaceId);
4766
+ break;
4767
+ case "project_archived":
4768
+ case "project_updated":
4769
+ changedProjectIds.add(change.projectId);
4770
+ break;
4771
+ }
4772
+ }
4773
+ if (changedProjectIds.size > 0) {
4774
+ for (const workspace of await this.workspaceRegistry.list()) {
4775
+ if (changedProjectIds.has(workspace.projectId)) {
4776
+ changedWorkspaceIds.add(workspace.workspaceId);
4777
+ }
4778
+ }
4779
+ }
4780
+ return changedWorkspaceIds;
4781
+ }
4260
4782
  async emitWorkspaceUpdatesForWorkspaceIds(workspaceIds, options) {
4261
4783
  const subscription = this.workspaceUpdatesSubscription;
4262
4784
  if (!subscription) {
4263
4785
  return;
4264
4786
  }
4265
- const uniqueWorkspaceIds = new Set(Array.from(workspaceIds, (workspaceId) => normalizePersistedWorkspaceId(workspaceId)));
4787
+ const uniqueWorkspaceIds = new Set(Array.from(workspaceIds));
4266
4788
  if (uniqueWorkspaceIds.size === 0) {
4267
4789
  return;
4268
4790
  }
@@ -4275,6 +4797,18 @@ export class Session {
4275
4797
  const nextWorkspace = workspace && this.matchesWorkspaceFilter({ workspace, filter: subscription.filter })
4276
4798
  ? workspace
4277
4799
  : null;
4800
+ if (options?.dedupeGitState &&
4801
+ this.shouldSkipWorkspaceGitWatchUpdate(workspaceId, nextWorkspace)) {
4802
+ continue;
4803
+ }
4804
+ const watchTarget = this.workspaceGitWatchTargets.get(workspaceId);
4805
+ if (watchTarget && this.onBranchChanged) {
4806
+ const newBranchName = nextWorkspace?.name ?? null;
4807
+ if (newBranchName !== watchTarget.lastBranchName) {
4808
+ this.onBranchChanged(workspaceId, watchTarget.lastBranchName, newBranchName);
4809
+ }
4810
+ }
4811
+ this.rememberWorkspaceGitWatchFingerprint(workspaceId, nextWorkspace);
4278
4812
  if (!nextWorkspace) {
4279
4813
  subscription.lastEmittedByWorkspaceId.delete(workspaceId);
4280
4814
  this.bufferOrEmitWorkspaceUpdate(subscription, {
@@ -4300,15 +4834,15 @@ export class Session {
4300
4834
  }
4301
4835
  }
4302
4836
  async emitWorkspaceUpdateForCwd(cwd, options) {
4303
- const activeWorkspaces = (await this.workspaceRegistry.list()).filter((workspace) => !workspace.archivedAt);
4304
- const workspaceId = this.resolveRegisteredWorkspaceIdForCwd(cwd, activeWorkspaces);
4837
+ const workspaces = await this.workspaceRegistry.list();
4838
+ const workspaceId = this.resolveRegisteredWorkspaceIdForCwd(cwd, workspaces);
4305
4839
  await this.emitWorkspaceUpdatesForWorkspaceIds([workspaceId], options);
4306
4840
  }
4307
4841
  async emitWorkspaceUpdatesForCwds(cwds) {
4308
- const activeWorkspaces = (await this.workspaceRegistry.list()).filter((workspace) => !workspace.archivedAt);
4842
+ const workspaces = await this.workspaceRegistry.list();
4309
4843
  const uniqueWorkspaceIds = new Set();
4310
4844
  for (const cwd of cwds) {
4311
- uniqueWorkspaceIds.add(this.resolveRegisteredWorkspaceIdForCwd(cwd, activeWorkspaces));
4845
+ uniqueWorkspaceIds.add(this.resolveRegisteredWorkspaceIdForCwd(cwd, workspaces));
4312
4846
  }
4313
4847
  await this.emitWorkspaceUpdatesForWorkspaceIds(uniqueWorkspaceIds);
4314
4848
  }
@@ -4329,8 +4863,6 @@ export class Session {
4329
4863
  };
4330
4864
  }
4331
4865
  const payload = await this.listFetchAgentsEntries(request);
4332
- // TODO: Remove once all app store clients are on >=0.1.45.
4333
- payload.entries = payload.entries.filter((entry) => this.isProviderVisibleToClient(entry.agent.provider));
4334
4866
  const snapshotUpdatedAtByAgentId = new Map();
4335
4867
  for (const entry of payload.entries) {
4336
4868
  const parsedUpdatedAt = Date.parse(entry.agent.updatedAt);
@@ -4368,6 +4900,32 @@ export class Session {
4368
4900
  });
4369
4901
  }
4370
4902
  }
4903
+ async handleFetchAgentHistory(request) {
4904
+ try {
4905
+ const payload = await this.listFetchAgentsEntries(request);
4906
+ this.emit({
4907
+ type: "fetch_agent_history_response",
4908
+ payload: {
4909
+ requestId: request.requestId,
4910
+ ...payload,
4911
+ },
4912
+ });
4913
+ }
4914
+ catch (error) {
4915
+ const code = error instanceof SessionRequestError ? error.code : "fetch_agent_history_failed";
4916
+ const message = error instanceof Error ? error.message : "Failed to fetch agent history";
4917
+ this.sessionLogger.error({ err: error }, "Failed to handle fetch_agent_history_request");
4918
+ this.emit({
4919
+ type: "rpc_error",
4920
+ payload: {
4921
+ requestId: request.requestId,
4922
+ requestType: request.type,
4923
+ error: message,
4924
+ code,
4925
+ },
4926
+ });
4927
+ }
4928
+ }
4371
4929
  async handleFetchWorkspacesRequest(request) {
4372
4930
  const requestedSubscriptionId = request.subscribe?.subscriptionId?.trim();
4373
4931
  const subscriptionId = request.subscribe
@@ -4393,6 +4951,7 @@ export class Session {
4393
4951
  };
4394
4952
  }
4395
4953
  const payload = await this.listFetchWorkspacesEntries(request);
4954
+ await this.primeWorkspaceGitWatchFingerprints(payload.entries);
4396
4955
  this.sessionLogger.debug({
4397
4956
  requestId: request.requestId,
4398
4957
  subscriptionId,
@@ -4441,10 +5000,8 @@ export class Session {
4441
5000
  }
4442
5001
  async handleOpenProjectRequest(request) {
4443
5002
  try {
4444
- const workspace = await this.ensureWorkspaceRegistered(request.cwd);
4445
- await this.emitWorkspaceUpdateForCwd(workspace.cwd, {
4446
- skipReconcile: true,
4447
- });
5003
+ const workspace = await this.findOrCreateWorkspaceForDirectory(request.cwd);
5004
+ await this.emitWorkspaceUpdateForCwd(workspace.cwd);
4448
5005
  const descriptor = await this.describeWorkspaceRecordWithGitData(workspace);
4449
5006
  this.emit({
4450
5007
  type: "open_project_response",
@@ -4468,12 +5025,105 @@ export class Session {
4468
5025
  });
4469
5026
  }
4470
5027
  }
5028
+ buildWorkspaceScriptPayloadSnapshot(workspaceId, workspaceDirectory) {
5029
+ if (!this.scriptRouteStore || !this.scriptRuntimeStore) {
5030
+ return [];
5031
+ }
5032
+ return buildWorkspaceScriptPayloads({
5033
+ workspaceId,
5034
+ workspaceDirectory,
5035
+ routeStore: this.scriptRouteStore,
5036
+ runtimeStore: this.scriptRuntimeStore,
5037
+ daemonPort: this.getDaemonTcpPort?.() ?? null,
5038
+ gitMetadata: this.resolveWorkspaceScriptGitMetadata(workspaceDirectory),
5039
+ resolveHealth: this.resolveScriptHealth ?? undefined,
5040
+ });
5041
+ }
5042
+ resolveWorkspaceScriptGitMetadata(workspaceDirectory) {
5043
+ const snapshot = this.workspaceGitService.peekSnapshot(workspaceDirectory);
5044
+ if (!snapshot) {
5045
+ return undefined;
5046
+ }
5047
+ return {
5048
+ projectSlug: deriveProjectSlug(workspaceDirectory, snapshot.git.isGit ? snapshot.git.remoteUrl : null),
5049
+ currentBranch: snapshot.git.currentBranch,
5050
+ };
5051
+ }
5052
+ emitWorkspaceScriptStatusUpdate(workspaceId, workspaceDirectory) {
5053
+ this.emit({
5054
+ type: "script_status_update",
5055
+ payload: {
5056
+ workspaceId,
5057
+ scripts: this.buildWorkspaceScriptPayloadSnapshot(workspaceId, workspaceDirectory),
5058
+ },
5059
+ });
5060
+ }
5061
+ async resolveAvailableEditorTargets() {
5062
+ return listAvailableEditorTargets();
5063
+ }
4471
5064
  async getAvailableEditorTargets() {
4472
- return this.filterEditorsForClient(await listAvailableEditorTargets());
5065
+ return this.filterEditorsForClient(await this.getMemoizedAvailableEditorTargets());
4473
5066
  }
4474
5067
  async openEditorTarget(options) {
4475
5068
  await openInEditorTarget(options);
4476
5069
  }
5070
+ async handleStartWorkspaceScriptRequest(request) {
5071
+ try {
5072
+ if (!this.terminalManager || !this.scriptRouteStore || !this.scriptRuntimeStore) {
5073
+ throw new Error("Workspace scripts are not available on this daemon");
5074
+ }
5075
+ const workspace = await this.workspaceRegistry.get(request.workspaceId);
5076
+ if (!workspace) {
5077
+ throw new Error(`Workspace not found: ${request.workspaceId}`);
5078
+ }
5079
+ const gitMetadata = await this.workspaceGitService.getWorkspaceGitMetadata(workspace.cwd);
5080
+ const serviceResult = await spawnWorkspaceScript({
5081
+ repoRoot: workspace.cwd,
5082
+ workspaceId: workspace.workspaceId,
5083
+ projectSlug: gitMetadata.projectSlug,
5084
+ branchName: gitMetadata.currentBranch,
5085
+ scriptName: request.scriptName,
5086
+ daemonPort: this.getDaemonTcpPort?.() ?? null,
5087
+ daemonListenHost: this.getDaemonTcpHost?.() ?? null,
5088
+ routeStore: this.scriptRouteStore,
5089
+ runtimeStore: this.scriptRuntimeStore,
5090
+ terminalManager: this.terminalManager,
5091
+ logger: this.sessionLogger,
5092
+ onLifecycleChanged: () => {
5093
+ this.emitWorkspaceScriptStatusUpdate(workspace.workspaceId, workspace.cwd);
5094
+ },
5095
+ });
5096
+ this.emitWorkspaceScriptStatusUpdate(workspace.workspaceId, workspace.cwd);
5097
+ this.emit({
5098
+ type: "start_workspace_script_response",
5099
+ payload: {
5100
+ requestId: request.requestId,
5101
+ workspaceId: request.workspaceId,
5102
+ scriptName: request.scriptName,
5103
+ terminalId: serviceResult.terminalId,
5104
+ error: null,
5105
+ },
5106
+ });
5107
+ }
5108
+ catch (error) {
5109
+ const message = error instanceof Error ? error.message : "Failed to start workspace script";
5110
+ this.sessionLogger.error({
5111
+ err: error,
5112
+ workspaceId: request.workspaceId,
5113
+ scriptName: request.scriptName,
5114
+ }, "Failed to start workspace script");
5115
+ this.emit({
5116
+ type: "start_workspace_script_response",
5117
+ payload: {
5118
+ requestId: request.requestId,
5119
+ workspaceId: request.workspaceId,
5120
+ scriptName: request.scriptName,
5121
+ terminalId: null,
5122
+ error: message,
5123
+ },
5124
+ });
5125
+ }
5126
+ }
4477
5127
  async handleListAvailableEditorsRequest(request) {
4478
5128
  try {
4479
5129
  const editors = await this.getAvailableEditorTargets();
@@ -4530,11 +5180,10 @@ export class Session {
4530
5180
  async handleCreatePaseoWorktreeRequest(request) {
4531
5181
  return handleCreateWorktreeRequest({
4532
5182
  paseoHome: this.paseoHome,
4533
- workspaceGitService: this.workspaceGitService,
4534
- describeWorkspaceRecord: (workspace) => this.describeWorkspaceRecordWithGitData(workspace),
5183
+ describeWorkspaceRecord: (result) => this.describeCreatedWorktreeWorkspace(result),
4535
5184
  emit: (message) => this.emit(message),
4536
- registerPendingWorktreeWorkspace: (options) => this.registerPendingWorktreeWorkspace(options),
4537
- syncWorkspaceGitWatchTarget: (cwd, syncOptions) => this.syncWorkspaceGitWatchTarget(cwd, syncOptions),
5185
+ createPaseoWorktree: (input) => this.createPaseoWorktree(input),
5186
+ warmWorkspaceGitData: (workspace) => this.warmWorkspaceGitDataForWorkspace(workspace),
4538
5187
  sessionLogger: this.sessionLogger,
4539
5188
  runWorktreeSetupInBackground: (options) => this.runWorktreeSetupInBackground(options),
4540
5189
  }, request);
@@ -4542,11 +5191,29 @@ export class Session {
4542
5191
  async runWorktreeSetupInBackground(options) {
4543
5192
  return runWorktreeSetupInBackgroundSession({
4544
5193
  paseoHome: this.paseoHome,
4545
- emitWorkspaceUpdateForCwd: (cwd) => this.emitWorkspaceUpdateForCwd(cwd),
5194
+ emitWorkspaceUpdateForCwd: (cwd, emitOptions) => this.emitWorkspaceUpdateForCwd(cwd, emitOptions),
5195
+ cacheWorkspaceSetupSnapshot: (workspaceId, snapshot) => {
5196
+ this.workspaceSetupSnapshots.set(workspaceId, snapshot);
5197
+ },
5198
+ emit: (message) => this.emit(message),
4546
5199
  sessionLogger: this.sessionLogger,
4547
5200
  terminalManager: this.terminalManager,
5201
+ archiveWorkspaceRecord: (workspaceId) => this.archiveWorkspaceRecord(workspaceId),
5202
+ scriptRouteStore: this.scriptRouteStore,
5203
+ scriptRuntimeStore: this.scriptRuntimeStore,
5204
+ getDaemonTcpPort: this.getDaemonTcpPort,
5205
+ getDaemonTcpHost: this.getDaemonTcpHost,
5206
+ onScriptsChanged: (workspaceId, workspaceDirectory) => {
5207
+ this.emitWorkspaceScriptStatusUpdate(workspaceId, workspaceDirectory);
5208
+ },
4548
5209
  }, options);
4549
5210
  }
5211
+ async handleWorkspaceSetupStatusRequest(request) {
5212
+ return handleWorkspaceSetupStatusRequestMessage({
5213
+ emit: (message) => this.emit(message),
5214
+ workspaceSetupSnapshots: this.workspaceSetupSnapshots,
5215
+ }, request);
5216
+ }
4550
5217
  async handleArchiveWorkspaceRequest(request) {
4551
5218
  try {
4552
5219
  const existing = await this.workspaceRegistry.get(request.workspaceId);
@@ -4557,7 +5224,7 @@ export class Session {
4557
5224
  throw new Error("Use worktree archive for Paseo worktrees");
4558
5225
  }
4559
5226
  const archivedAt = new Date().toISOString();
4560
- await this.archiveWorkspaceRecord(request.workspaceId, archivedAt);
5227
+ await this.archiveWorkspaceRecord(existing.workspaceId, archivedAt);
4561
5228
  await this.emitWorkspaceUpdateForCwd(existing.cwd);
4562
5229
  this.emit({
4563
5230
  type: "archive_workspace_response",
@@ -4605,7 +5272,7 @@ export class Session {
4605
5272
  });
4606
5273
  return;
4607
5274
  }
4608
- const project = await this.buildProjectPlacement(agent.cwd);
5275
+ const project = await this.buildProjectPlacementForCwd(agent.cwd);
4609
5276
  this.emit({
4610
5277
  type: "fetch_agent_response",
4611
5278
  payload: { requestId, agent, project, error: null },
@@ -4764,7 +5431,7 @@ export class Session {
4764
5431
  }
4765
5432
  try {
4766
5433
  const agentId = resolved.agentId;
4767
- const prompt = this.buildAgentPrompt(msg.text, msg.images);
5434
+ const prompt = this.buildAgentPrompt(msg.text, msg.images, msg.attachments);
4768
5435
  this.sessionLogger.trace({ agentId, messageId: msg.messageId, textPrefix: msg.text.slice(0, 80) }, "send_agent_message_request: dispatching shared sendPromptToAgent");
4769
5436
  try {
4770
5437
  await sendPromptToAgent({
@@ -5232,9 +5899,7 @@ export class Session {
5232
5899
  await this.flushPendingAudioSegments("no active voice agent");
5233
5900
  return;
5234
5901
  }
5235
- await this.handleSendAgentMessage(agentId, result.text, undefined, undefined, undefined, {
5236
- spokenInput: true,
5237
- });
5902
+ await this.handleSendAgentMessage(agentId, result.text, undefined, undefined, undefined, undefined, { spokenInput: true });
5238
5903
  await this.flushPendingAudioSegments("transcription complete");
5239
5904
  }
5240
5905
  registerVoiceBridgeForAgent(agentId) {
@@ -5492,9 +6157,9 @@ export class Session {
5492
6157
  }
5493
6158
  this.workspaceGitSubscriptions.clear();
5494
6159
  }
5495
- // ============================================================================
6160
+ // ----------------------------------------------------------------------------
5496
6161
  // Terminal Handlers
5497
- // ============================================================================
6162
+ // ----------------------------------------------------------------------------
5498
6163
  ensureTerminalExitSubscription(terminal) {
5499
6164
  if (this.terminalExitSubscriptions.has(terminal.id)) {
5500
6165
  return;
@@ -5933,15 +6598,27 @@ export class Session {
5933
6598
  },
5934
6599
  });
5935
6600
  }
6601
+ filterStandaloneTerminals(terminals) {
6602
+ return terminals;
6603
+ }
6604
+ toTerminalInfo(terminal) {
6605
+ const title = terminal.getTitle();
6606
+ return {
6607
+ id: terminal.id,
6608
+ name: terminal.name,
6609
+ ...(title ? { title } : {}),
6610
+ };
6611
+ }
5936
6612
  handleTerminalsChanged(event) {
5937
6613
  if (!this.subscribedTerminalDirectories.has(event.cwd)) {
5938
6614
  return;
5939
6615
  }
5940
6616
  this.emitTerminalsChangedSnapshot({
5941
6617
  cwd: event.cwd,
5942
- terminals: event.terminals.map((terminal) => ({
6618
+ terminals: this.filterStandaloneTerminals(event.terminals).map((terminal) => ({
5943
6619
  id: terminal.id,
5944
6620
  name: terminal.name,
6621
+ ...(terminal.title ? { title: terminal.title } : {}),
5945
6622
  })),
5946
6623
  });
5947
6624
  }
@@ -5957,7 +6634,7 @@ export class Session {
5957
6634
  return;
5958
6635
  }
5959
6636
  try {
5960
- const terminals = await this.terminalManager.getTerminals(cwd);
6637
+ const terminals = this.filterStandaloneTerminals(await this.terminalManager.getTerminals(cwd));
5961
6638
  for (const terminal of terminals) {
5962
6639
  this.ensureTerminalExitSubscription(terminal);
5963
6640
  }
@@ -5966,10 +6643,7 @@ export class Session {
5966
6643
  }
5967
6644
  this.emitTerminalsChangedSnapshot({
5968
6645
  cwd,
5969
- terminals: terminals.map((terminal) => ({
5970
- id: terminal.id,
5971
- name: terminal.name,
5972
- })),
6646
+ terminals: terminals.map((terminal) => this.toTerminalInfo(terminal)),
5973
6647
  });
5974
6648
  }
5975
6649
  catch (error) {
@@ -5989,9 +6663,9 @@ export class Session {
5989
6663
  return;
5990
6664
  }
5991
6665
  try {
5992
- const terminals = typeof msg.cwd === "string"
6666
+ const terminals = this.filterStandaloneTerminals(typeof msg.cwd === "string"
5993
6667
  ? await this.terminalManager.getTerminals(msg.cwd)
5994
- : await this.getAllTerminalSessions();
6668
+ : await this.getAllTerminalSessions());
5995
6669
  for (const terminal of terminals) {
5996
6670
  this.ensureTerminalExitSubscription(terminal);
5997
6671
  }
@@ -5999,7 +6673,7 @@ export class Session {
5999
6673
  type: "list_terminals_response",
6000
6674
  payload: {
6001
6675
  ...(msg.cwd ? { cwd: msg.cwd } : {}),
6002
- terminals: terminals.map((t) => ({ id: t.id, name: t.name })),
6676
+ terminals: terminals.map((terminal) => this.toTerminalInfo(terminal)),
6003
6677
  requestId: msg.requestId,
6004
6678
  },
6005
6679
  });
@@ -6037,15 +6711,33 @@ export class Session {
6037
6711
  return;
6038
6712
  }
6039
6713
  try {
6714
+ if (msg.agentId) {
6715
+ this.emit({
6716
+ type: "create_terminal_response",
6717
+ payload: {
6718
+ terminal: null,
6719
+ error: `Agent-backed terminals are no longer supported for agent ${msg.agentId}`,
6720
+ requestId: msg.requestId,
6721
+ },
6722
+ });
6723
+ return;
6724
+ }
6040
6725
  const session = await this.terminalManager.createTerminal({
6041
6726
  cwd: msg.cwd,
6042
6727
  name: msg.name,
6728
+ command: msg.command,
6729
+ args: msg.args,
6043
6730
  });
6044
6731
  this.ensureTerminalExitSubscription(session);
6045
6732
  this.emit({
6046
6733
  type: "create_terminal_response",
6047
6734
  payload: {
6048
- terminal: { id: session.id, name: session.name, cwd: session.cwd },
6735
+ terminal: {
6736
+ id: session.id,
6737
+ name: session.name,
6738
+ cwd: session.cwd,
6739
+ ...(session.getTitle() ? { title: session.getTitle() } : {}),
6740
+ },
6049
6741
  error: null,
6050
6742
  requestId: msg.requestId,
6051
6743
  },
@@ -6147,6 +6839,7 @@ export class Session {
6147
6839
  return killWorktreeTerminalsUnderPath({
6148
6840
  isPathWithinRoot: (pathRoot, candidatePath) => this.isPathWithinRoot(pathRoot, candidatePath),
6149
6841
  killTrackedTerminal: (terminalId, options) => this.killTrackedTerminal(terminalId, options),
6842
+ detachTerminalStream: (terminalId, options) => void this.detachTerminalStream(terminalId, options),
6150
6843
  sessionLogger: this.sessionLogger,
6151
6844
  terminalManager: this.terminalManager,
6152
6845
  }, rootPath);
@@ -6253,6 +6946,19 @@ export class Session {
6253
6946
  slot,
6254
6947
  unsubscribe: () => { },
6255
6948
  needsSnapshot: true,
6949
+ outputCoalescer: new TerminalOutputCoalescer({
6950
+ timers: { setTimeout, clearTimeout },
6951
+ onFlush: ({ payload }) => {
6952
+ if (this.activeTerminalStreams.get(slot) !== activeStream) {
6953
+ return;
6954
+ }
6955
+ this.emitBinary(encodeTerminalStreamFrame({
6956
+ opcode: TerminalStreamOpcode.Output,
6957
+ slot,
6958
+ payload,
6959
+ }));
6960
+ },
6961
+ }),
6256
6962
  };
6257
6963
  this.activeTerminalStreams.set(slot, activeStream);
6258
6964
  this.terminalIdToSlot.set(terminal.id, slot);
@@ -6261,17 +6967,18 @@ export class Session {
6261
6967
  return;
6262
6968
  }
6263
6969
  if (message.type === "snapshot") {
6970
+ activeStream.outputCoalescer.flush();
6971
+ activeStream.needsSnapshot = true;
6264
6972
  this.trySendTerminalSnapshot(activeStream);
6265
6973
  return;
6266
6974
  }
6975
+ if (message.type === "titleChange") {
6976
+ return;
6977
+ }
6267
6978
  if (activeStream.needsSnapshot || message.data.length === 0) {
6268
6979
  return;
6269
6980
  }
6270
- this.emitBinary(encodeTerminalStreamFrame({
6271
- opcode: TerminalStreamOpcode.Output,
6272
- slot,
6273
- payload: new Uint8Array(Buffer.from(message.data, "utf8")),
6274
- }));
6981
+ activeStream.outputCoalescer.handle(message.data);
6275
6982
  });
6276
6983
  return slot;
6277
6984
  }
@@ -6285,6 +6992,7 @@ export class Session {
6285
6992
  this.detachTerminalStream(activeStream.terminalId, { emitExit: true });
6286
6993
  return;
6287
6994
  }
6995
+ activeStream.outputCoalescer.flush();
6288
6996
  activeStream.needsSnapshot = false;
6289
6997
  this.emitBinary(encodeTerminalStreamFrame({
6290
6998
  opcode: TerminalStreamOpcode.Snapshot,
@@ -6313,6 +7021,7 @@ export class Session {
6313
7021
  this.terminalIdToSlot.delete(terminalId);
6314
7022
  return false;
6315
7023
  }
7024
+ activeStream.outputCoalescer.flush();
6316
7025
  this.activeTerminalStreams.delete(slot);
6317
7026
  this.terminalIdToSlot.delete(terminalId);
6318
7027
  try {
@@ -6341,4 +7050,37 @@ export class Session {
6341
7050
  // Stash handlers
6342
7051
  // ---------------------------------------------------------------------------
6343
7052
  Session.PASEO_STASH_PREFIX = "paseo-auto-stash:";
7053
+ export function normalizeCheckoutPrStatusPayload(status) {
7054
+ if (!status) {
7055
+ return null;
7056
+ }
7057
+ return {
7058
+ number: status.number,
7059
+ url: status.url,
7060
+ title: status.title,
7061
+ state: status.state,
7062
+ repoOwner: status.repoOwner,
7063
+ repoName: status.repoName,
7064
+ baseRefName: status.baseRefName,
7065
+ headRefName: status.headRefName,
7066
+ isMerged: status.isMerged,
7067
+ isDraft: status.isDraft ?? false,
7068
+ checks: status.checks ?? [],
7069
+ checksStatus: status.checksStatus,
7070
+ reviewDecision: status.reviewDecision,
7071
+ };
7072
+ }
7073
+ function isValidPullRequestTimelineIdentity(options) {
7074
+ if (!Number.isInteger(options.prNumber) || options.prNumber <= 0) {
7075
+ return false;
7076
+ }
7077
+ return isValidGitHubRepoSegment(options.repoOwner) && isValidGitHubRepoSegment(options.repoName);
7078
+ }
7079
+ function isValidGitHubRepoSegment(value) {
7080
+ return /^[A-Za-z0-9._-]+$/.test(value);
7081
+ }
7082
+ function toPullRequestTimelinePayloadItem(item) {
7083
+ const { authorUrl: _authorUrl, ...payload } = item;
7084
+ return payload;
7085
+ }
6344
7086
  //# sourceMappingURL=session.js.map