@ilya-lesikov/pi-pi 0.1.0

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 (271) hide show
  1. package/3p/pi-ask-user/index.ts +1835 -0
  2. package/3p/pi-ask-user/package.json +50 -0
  3. package/3p/pi-ask-user/single-select-layout.ts +203 -0
  4. package/3p/pi-lsp/extensions/lsp/client.ts +542 -0
  5. package/3p/pi-lsp/extensions/lsp/config.ts +135 -0
  6. package/3p/pi-lsp/extensions/lsp/effects/command.ts +42 -0
  7. package/3p/pi-lsp/extensions/lsp/effects/filesystem.ts +50 -0
  8. package/3p/pi-lsp/extensions/lsp/effects/runtime.ts +21 -0
  9. package/3p/pi-lsp/extensions/lsp/errors.ts +113 -0
  10. package/3p/pi-lsp/extensions/lsp/formatting.ts +315 -0
  11. package/3p/pi-lsp/extensions/lsp/index.ts +231 -0
  12. package/3p/pi-lsp/extensions/lsp/protocol.ts +209 -0
  13. package/3p/pi-lsp/extensions/lsp/retry.ts +39 -0
  14. package/3p/pi-lsp/extensions/lsp/tools/programs.ts +293 -0
  15. package/3p/pi-lsp/extensions/lsp/tools.ts +89 -0
  16. package/3p/pi-lsp/extensions/lsp/types.ts +243 -0
  17. package/3p/pi-lsp/package.json +54 -0
  18. package/3p/pi-plannotator/AGENTS.md +583 -0
  19. package/3p/pi-plannotator/apps/pi-extension/README.md +228 -0
  20. package/3p/pi-plannotator/apps/pi-extension/assistant-message.ts +128 -0
  21. package/3p/pi-plannotator/apps/pi-extension/config.test.ts +166 -0
  22. package/3p/pi-plannotator/apps/pi-extension/config.ts +318 -0
  23. package/3p/pi-plannotator/apps/pi-extension/current-pi-session.ts +147 -0
  24. package/3p/pi-plannotator/apps/pi-extension/index.ts +1279 -0
  25. package/3p/pi-plannotator/apps/pi-extension/package.json +56 -0
  26. package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.test.ts +13 -0
  27. package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.ts +616 -0
  28. package/3p/pi-plannotator/apps/pi-extension/plannotator-events.ts +342 -0
  29. package/3p/pi-plannotator/apps/pi-extension/plannotator.json +12 -0
  30. package/3p/pi-plannotator/apps/pi-extension/server/agent-jobs.ts +515 -0
  31. package/3p/pi-plannotator/apps/pi-extension/server/ai-runtime.ts +169 -0
  32. package/3p/pi-plannotator/apps/pi-extension/server/annotations.ts +85 -0
  33. package/3p/pi-plannotator/apps/pi-extension/server/external-annotations.ts +189 -0
  34. package/3p/pi-plannotator/apps/pi-extension/server/handlers.ts +210 -0
  35. package/3p/pi-plannotator/apps/pi-extension/server/helpers.ts +78 -0
  36. package/3p/pi-plannotator/apps/pi-extension/server/ide.ts +46 -0
  37. package/3p/pi-plannotator/apps/pi-extension/server/integrations.ts +195 -0
  38. package/3p/pi-plannotator/apps/pi-extension/server/network.test.ts +158 -0
  39. package/3p/pi-plannotator/apps/pi-extension/server/network.ts +268 -0
  40. package/3p/pi-plannotator/apps/pi-extension/server/pr.ts +126 -0
  41. package/3p/pi-plannotator/apps/pi-extension/server/project.ts +64 -0
  42. package/3p/pi-plannotator/apps/pi-extension/server/reference.ts +362 -0
  43. package/3p/pi-plannotator/apps/pi-extension/server/serverAnnotate.ts +199 -0
  44. package/3p/pi-plannotator/apps/pi-extension/server/serverPlan.ts +505 -0
  45. package/3p/pi-plannotator/apps/pi-extension/server/serverReview.ts +1229 -0
  46. package/3p/pi-plannotator/apps/pi-extension/server/vcs.ts +121 -0
  47. package/3p/pi-plannotator/apps/pi-extension/server.test.ts +966 -0
  48. package/3p/pi-plannotator/apps/pi-extension/server.ts +44 -0
  49. package/3p/pi-plannotator/apps/pi-extension/tool-scope.test.ts +88 -0
  50. package/3p/pi-plannotator/apps/pi-extension/tool-scope.ts +39 -0
  51. package/3p/pi-plannotator/apps/pi-extension/tsconfig.json +16 -0
  52. package/3p/pi-plannotator/apps/pi-extension/vendor.sh +48 -0
  53. package/3p/pi-plannotator/package.json +53 -0
  54. package/3p/pi-plannotator/packages/ai/ai.test.ts +1407 -0
  55. package/3p/pi-plannotator/packages/ai/base-session.ts +94 -0
  56. package/3p/pi-plannotator/packages/ai/context.ts +250 -0
  57. package/3p/pi-plannotator/packages/ai/endpoints.ts +326 -0
  58. package/3p/pi-plannotator/packages/ai/index.ts +105 -0
  59. package/3p/pi-plannotator/packages/ai/package.json +21 -0
  60. package/3p/pi-plannotator/packages/ai/provider.ts +103 -0
  61. package/3p/pi-plannotator/packages/ai/providers/claude-agent-sdk.ts +447 -0
  62. package/3p/pi-plannotator/packages/ai/providers/codex-sdk.ts +430 -0
  63. package/3p/pi-plannotator/packages/ai/providers/command-path.ts +115 -0
  64. package/3p/pi-plannotator/packages/ai/providers/opencode-sdk.ts +546 -0
  65. package/3p/pi-plannotator/packages/ai/providers/pi-events.ts +110 -0
  66. package/3p/pi-plannotator/packages/ai/providers/pi-sdk-node.ts +425 -0
  67. package/3p/pi-plannotator/packages/ai/providers/pi-sdk.ts +469 -0
  68. package/3p/pi-plannotator/packages/ai/session-manager.ts +195 -0
  69. package/3p/pi-plannotator/packages/ai/tsconfig.json +15 -0
  70. package/3p/pi-plannotator/packages/ai/types.ts +379 -0
  71. package/3p/pi-plannotator/packages/server/agent-jobs.ts +538 -0
  72. package/3p/pi-plannotator/packages/server/agent-review-message.test.ts +135 -0
  73. package/3p/pi-plannotator/packages/server/agent-review-message.ts +243 -0
  74. package/3p/pi-plannotator/packages/server/ai-runtime.ts +108 -0
  75. package/3p/pi-plannotator/packages/server/annotate.ts +401 -0
  76. package/3p/pi-plannotator/packages/server/browser.test.ts +87 -0
  77. package/3p/pi-plannotator/packages/server/browser.ts +245 -0
  78. package/3p/pi-plannotator/packages/server/claude-review.ts +352 -0
  79. package/3p/pi-plannotator/packages/server/code-nav.ts +73 -0
  80. package/3p/pi-plannotator/packages/server/codex-review-schema.json +41 -0
  81. package/3p/pi-plannotator/packages/server/codex-review.ts +330 -0
  82. package/3p/pi-plannotator/packages/server/config.ts +8 -0
  83. package/3p/pi-plannotator/packages/server/draft.ts +1 -0
  84. package/3p/pi-plannotator/packages/server/editor-annotations.ts +76 -0
  85. package/3p/pi-plannotator/packages/server/external-annotations.test.ts +18 -0
  86. package/3p/pi-plannotator/packages/server/external-annotations.ts +207 -0
  87. package/3p/pi-plannotator/packages/server/git.ts +141 -0
  88. package/3p/pi-plannotator/packages/server/goal-setup.test.ts +55 -0
  89. package/3p/pi-plannotator/packages/server/goal-setup.ts +248 -0
  90. package/3p/pi-plannotator/packages/server/ide.ts +43 -0
  91. package/3p/pi-plannotator/packages/server/image.test.ts +63 -0
  92. package/3p/pi-plannotator/packages/server/image.ts +66 -0
  93. package/3p/pi-plannotator/packages/server/index.ts +648 -0
  94. package/3p/pi-plannotator/packages/server/integrations.test.ts +172 -0
  95. package/3p/pi-plannotator/packages/server/integrations.ts +214 -0
  96. package/3p/pi-plannotator/packages/server/jj.test.ts +69 -0
  97. package/3p/pi-plannotator/packages/server/jj.ts +86 -0
  98. package/3p/pi-plannotator/packages/server/p4.ts +417 -0
  99. package/3p/pi-plannotator/packages/server/package.json +40 -0
  100. package/3p/pi-plannotator/packages/server/path-utils.ts +18 -0
  101. package/3p/pi-plannotator/packages/server/pr.ts +146 -0
  102. package/3p/pi-plannotator/packages/server/project.test.ts +115 -0
  103. package/3p/pi-plannotator/packages/server/project.ts +45 -0
  104. package/3p/pi-plannotator/packages/server/reference-handlers.ts +404 -0
  105. package/3p/pi-plannotator/packages/server/remote.test.ts +150 -0
  106. package/3p/pi-plannotator/packages/server/remote.ts +74 -0
  107. package/3p/pi-plannotator/packages/server/repo.ts +80 -0
  108. package/3p/pi-plannotator/packages/server/resolve-file.test.ts +310 -0
  109. package/3p/pi-plannotator/packages/server/review-workspace.test.ts +1032 -0
  110. package/3p/pi-plannotator/packages/server/review-workspace.ts +48 -0
  111. package/3p/pi-plannotator/packages/server/review.ts +1318 -0
  112. package/3p/pi-plannotator/packages/server/sessions.ts +111 -0
  113. package/3p/pi-plannotator/packages/server/share-url.ts +53 -0
  114. package/3p/pi-plannotator/packages/server/shared-handlers.test.ts +43 -0
  115. package/3p/pi-plannotator/packages/server/shared-handlers.ts +177 -0
  116. package/3p/pi-plannotator/packages/server/storage.test.ts +176 -0
  117. package/3p/pi-plannotator/packages/server/storage.ts +17 -0
  118. package/3p/pi-plannotator/packages/server/tour/tour-review.test.ts +146 -0
  119. package/3p/pi-plannotator/packages/server/tour/tour-review.ts +604 -0
  120. package/3p/pi-plannotator/packages/server/tsconfig.json +15 -0
  121. package/3p/pi-plannotator/packages/server/vcs.test.ts +48 -0
  122. package/3p/pi-plannotator/packages/server/vcs.ts +80 -0
  123. package/3p/pi-plannotator/packages/shared/agent-jobs.ts +132 -0
  124. package/3p/pi-plannotator/packages/shared/agents.ts +53 -0
  125. package/3p/pi-plannotator/packages/shared/annotate-args.test.ts +386 -0
  126. package/3p/pi-plannotator/packages/shared/annotate-args.ts +107 -0
  127. package/3p/pi-plannotator/packages/shared/at-reference.test.ts +99 -0
  128. package/3p/pi-plannotator/packages/shared/at-reference.ts +52 -0
  129. package/3p/pi-plannotator/packages/shared/checklist.ts +52 -0
  130. package/3p/pi-plannotator/packages/shared/code-file.test.ts +112 -0
  131. package/3p/pi-plannotator/packages/shared/code-file.ts +41 -0
  132. package/3p/pi-plannotator/packages/shared/code-nav.test.ts +515 -0
  133. package/3p/pi-plannotator/packages/shared/code-nav.ts +436 -0
  134. package/3p/pi-plannotator/packages/shared/compress.ts +51 -0
  135. package/3p/pi-plannotator/packages/shared/config.ts +262 -0
  136. package/3p/pi-plannotator/packages/shared/crypto.test.ts +172 -0
  137. package/3p/pi-plannotator/packages/shared/crypto.ts +97 -0
  138. package/3p/pi-plannotator/packages/shared/data-dir.ts +42 -0
  139. package/3p/pi-plannotator/packages/shared/diff-paths.test.ts +30 -0
  140. package/3p/pi-plannotator/packages/shared/diff-paths.ts +137 -0
  141. package/3p/pi-plannotator/packages/shared/draft.ts +64 -0
  142. package/3p/pi-plannotator/packages/shared/external-annotation.ts +397 -0
  143. package/3p/pi-plannotator/packages/shared/extract-code-paths.test.ts +59 -0
  144. package/3p/pi-plannotator/packages/shared/extract-code-paths.ts +66 -0
  145. package/3p/pi-plannotator/packages/shared/favicon.ts +5 -0
  146. package/3p/pi-plannotator/packages/shared/feedback-templates.test.ts +65 -0
  147. package/3p/pi-plannotator/packages/shared/feedback-templates.ts +29 -0
  148. package/3p/pi-plannotator/packages/shared/goal-setup.test.ts +231 -0
  149. package/3p/pi-plannotator/packages/shared/goal-setup.ts +336 -0
  150. package/3p/pi-plannotator/packages/shared/html-to-markdown.test.ts +62 -0
  151. package/3p/pi-plannotator/packages/shared/html-to-markdown.ts +32 -0
  152. package/3p/pi-plannotator/packages/shared/improvement-hooks.test.ts +135 -0
  153. package/3p/pi-plannotator/packages/shared/improvement-hooks.ts +115 -0
  154. package/3p/pi-plannotator/packages/shared/integrations-common.ts +243 -0
  155. package/3p/pi-plannotator/packages/shared/jj-core.test.ts +236 -0
  156. package/3p/pi-plannotator/packages/shared/jj-core.ts +433 -0
  157. package/3p/pi-plannotator/packages/shared/package.json +55 -0
  158. package/3p/pi-plannotator/packages/shared/pfm-reminder.test.ts +88 -0
  159. package/3p/pi-plannotator/packages/shared/pfm-reminder.ts +80 -0
  160. package/3p/pi-plannotator/packages/shared/pr-github.ts +661 -0
  161. package/3p/pi-plannotator/packages/shared/pr-gitlab.test.ts +202 -0
  162. package/3p/pi-plannotator/packages/shared/pr-gitlab.ts +620 -0
  163. package/3p/pi-plannotator/packages/shared/pr-provider.test.ts +266 -0
  164. package/3p/pi-plannotator/packages/shared/pr-provider.ts +123 -0
  165. package/3p/pi-plannotator/packages/shared/pr-stack.test.ts +104 -0
  166. package/3p/pi-plannotator/packages/shared/pr-stack.ts +194 -0
  167. package/3p/pi-plannotator/packages/shared/pr-types.ts +326 -0
  168. package/3p/pi-plannotator/packages/shared/project.ts +71 -0
  169. package/3p/pi-plannotator/packages/shared/prompts-integration.test.ts +421 -0
  170. package/3p/pi-plannotator/packages/shared/prompts.test.ts +504 -0
  171. package/3p/pi-plannotator/packages/shared/prompts.ts +247 -0
  172. package/3p/pi-plannotator/packages/shared/reference-common.ts +87 -0
  173. package/3p/pi-plannotator/packages/shared/repo.ts +71 -0
  174. package/3p/pi-plannotator/packages/shared/resolve-file.test.ts +113 -0
  175. package/3p/pi-plannotator/packages/shared/resolve-file.ts +509 -0
  176. package/3p/pi-plannotator/packages/shared/review-args.test.ts +64 -0
  177. package/3p/pi-plannotator/packages/shared/review-args.ts +85 -0
  178. package/3p/pi-plannotator/packages/shared/review-core.test.ts +286 -0
  179. package/3p/pi-plannotator/packages/shared/review-core.ts +895 -0
  180. package/3p/pi-plannotator/packages/shared/review-workspace-node.ts +230 -0
  181. package/3p/pi-plannotator/packages/shared/review-workspace.ts +436 -0
  182. package/3p/pi-plannotator/packages/shared/semantic-diff-types.ts +76 -0
  183. package/3p/pi-plannotator/packages/shared/semantic-diff.test.ts +322 -0
  184. package/3p/pi-plannotator/packages/shared/semantic-diff.ts +520 -0
  185. package/3p/pi-plannotator/packages/shared/storage.ts +378 -0
  186. package/3p/pi-plannotator/packages/shared/tour.ts +61 -0
  187. package/3p/pi-plannotator/packages/shared/tsconfig.json +15 -0
  188. package/3p/pi-plannotator/packages/shared/types.ts +29 -0
  189. package/3p/pi-plannotator/packages/shared/url-to-markdown.test.ts +177 -0
  190. package/3p/pi-plannotator/packages/shared/url-to-markdown.ts +351 -0
  191. package/3p/pi-plannotator/packages/shared/vcs-core.test.ts +332 -0
  192. package/3p/pi-plannotator/packages/shared/vcs-core.ts +482 -0
  193. package/3p/pi-plannotator/packages/shared/worktree-pool.test.ts +162 -0
  194. package/3p/pi-plannotator/packages/shared/worktree-pool.ts +103 -0
  195. package/3p/pi-plannotator/packages/shared/worktree.ts +119 -0
  196. package/3p/pi-subagents/package.json +50 -0
  197. package/3p/pi-subagents/src/agent-manager.ts +413 -0
  198. package/3p/pi-subagents/src/agent-runner.ts +502 -0
  199. package/3p/pi-subagents/src/agent-types.ts +248 -0
  200. package/3p/pi-subagents/src/context.ts +58 -0
  201. package/3p/pi-subagents/src/cross-extension-rpc.ts +135 -0
  202. package/3p/pi-subagents/src/custom-agents.ts +137 -0
  203. package/3p/pi-subagents/src/default-agents.ts +144 -0
  204. package/3p/pi-subagents/src/env.ts +33 -0
  205. package/3p/pi-subagents/src/group-join.ts +141 -0
  206. package/3p/pi-subagents/src/index.ts +1811 -0
  207. package/3p/pi-subagents/src/invocation-config.ts +40 -0
  208. package/3p/pi-subagents/src/memory.ts +165 -0
  209. package/3p/pi-subagents/src/model-resolver.ts +81 -0
  210. package/3p/pi-subagents/src/output-file.ts +77 -0
  211. package/3p/pi-subagents/src/prompts.ts +85 -0
  212. package/3p/pi-subagents/src/skill-loader.ts +79 -0
  213. package/3p/pi-subagents/src/types.ts +111 -0
  214. package/3p/pi-subagents/src/ui/agent-widget.ts +496 -0
  215. package/3p/pi-subagents/src/ui/conversation-viewer.ts +250 -0
  216. package/3p/pi-subagents/src/worktree.ts +162 -0
  217. package/3p/pi-tasks/package.json +52 -0
  218. package/3p/pi-tasks/src/auto-clear.ts +91 -0
  219. package/3p/pi-tasks/src/index.ts +1150 -0
  220. package/3p/pi-tasks/src/process-tracker.ts +140 -0
  221. package/3p/pi-tasks/src/reminder-cadence.ts +90 -0
  222. package/3p/pi-tasks/src/task-store.ts +324 -0
  223. package/3p/pi-tasks/src/tasks-config.ts +27 -0
  224. package/3p/pi-tasks/src/types.ts +40 -0
  225. package/3p/pi-tasks/src/ui/settings-menu.ts +152 -0
  226. package/3p/pi-tasks/src/ui/task-widget.ts +296 -0
  227. package/AGENTS.md +28 -0
  228. package/LICENSE +201 -0
  229. package/extensions/orchestrator/agents/brainstorm-reviewer.ts +77 -0
  230. package/extensions/orchestrator/agents/code-reviewer.ts +89 -0
  231. package/extensions/orchestrator/agents/explore.ts +33 -0
  232. package/extensions/orchestrator/agents/librarian.ts +43 -0
  233. package/extensions/orchestrator/agents/plan-reviewer.ts +75 -0
  234. package/extensions/orchestrator/agents/planner.ts +61 -0
  235. package/extensions/orchestrator/agents/registry.ts +156 -0
  236. package/extensions/orchestrator/agents/task.ts +50 -0
  237. package/extensions/orchestrator/agents/tool-routing.ts +84 -0
  238. package/extensions/orchestrator/ast-search.ts +85 -0
  239. package/extensions/orchestrator/cbm.ts +330 -0
  240. package/extensions/orchestrator/command-handlers.test.ts +163 -0
  241. package/extensions/orchestrator/command-handlers.ts +116 -0
  242. package/extensions/orchestrator/commands.test.ts +81 -0
  243. package/extensions/orchestrator/commands.ts +75 -0
  244. package/extensions/orchestrator/config.test.ts +221 -0
  245. package/extensions/orchestrator/config.ts +230 -0
  246. package/extensions/orchestrator/context.test.ts +293 -0
  247. package/extensions/orchestrator/context.ts +206 -0
  248. package/extensions/orchestrator/event-handlers.test.ts +190 -0
  249. package/extensions/orchestrator/event-handlers.ts +1416 -0
  250. package/extensions/orchestrator/exa.ts +104 -0
  251. package/extensions/orchestrator/flant-infra.ts +486 -0
  252. package/extensions/orchestrator/index.ts +80 -0
  253. package/extensions/orchestrator/integration.test.ts +1214 -0
  254. package/extensions/orchestrator/orchestrator.test.ts +252 -0
  255. package/extensions/orchestrator/orchestrator.ts +538 -0
  256. package/extensions/orchestrator/phases/brainstorm.test.ts +15 -0
  257. package/extensions/orchestrator/phases/brainstorm.ts +273 -0
  258. package/extensions/orchestrator/phases/implementation.ts +40 -0
  259. package/extensions/orchestrator/phases/machine.test.ts +293 -0
  260. package/extensions/orchestrator/phases/machine.ts +209 -0
  261. package/extensions/orchestrator/phases/planning.ts +255 -0
  262. package/extensions/orchestrator/phases/review.ts +193 -0
  263. package/extensions/orchestrator/plannotator.ts +56 -0
  264. package/extensions/orchestrator/pp-menu.ts +866 -0
  265. package/extensions/orchestrator/state.test.ts +343 -0
  266. package/extensions/orchestrator/state.ts +237 -0
  267. package/extensions/orchestrator/validate-artifacts.test.ts +88 -0
  268. package/extensions/orchestrator/validate-artifacts.ts +272 -0
  269. package/extensions/orchestrator/vendor.d.ts +26 -0
  270. package/package.json +73 -0
  271. package/scripts/postinstall.sh +18 -0
@@ -0,0 +1,230 @@
1
+ import { existsSync, readdirSync, type Dirent } from "node:fs";
2
+ import { basename, relative, resolve } from "node:path";
3
+
4
+ import {
5
+ formatDiffMetadataPathToken,
6
+ formatPatchPathToken,
7
+ parseDiffFilePathLines,
8
+ parseDiffGitHeader,
9
+ parseDiffMetadataPathLines,
10
+ parseDiffMetadataPathToken,
11
+ parsePatchPathToken,
12
+ } from "./diff-paths";
13
+ import { validateFilePath } from "./review-core";
14
+
15
+ const SKIP_DIRS = new Set([
16
+ ".git",
17
+ ".jj",
18
+ "node_modules",
19
+ ".turbo",
20
+ ".next",
21
+ "dist",
22
+ "build",
23
+ "coverage",
24
+ ]);
25
+
26
+ const VCS_MARKERS = [".jj", ".git"] as const;
27
+
28
+ export interface WorkspacePathEntry {
29
+ label: string;
30
+ }
31
+
32
+ export interface WorkspacePatchEntry {
33
+ label: string;
34
+ selected: boolean;
35
+ rawPatch: string;
36
+ gitRef?: string;
37
+ error?: string;
38
+ }
39
+
40
+ export interface WorkspacePathResolution<T extends WorkspacePathEntry> {
41
+ repo: T;
42
+ repoRelativePath: string;
43
+ }
44
+
45
+ export interface WorkspacePatchAggregate {
46
+ rawPatch: string;
47
+ gitRef: string;
48
+ errors: string[];
49
+ }
50
+
51
+ export function normalizeWorkspacePath(path: string): string {
52
+ return path.replace(/\\/g, "/").replace(/^\/+/, "");
53
+ }
54
+
55
+ function prefixRepoPath(label: string, filePath: string): string {
56
+ if (filePath === "/dev/null") return filePath;
57
+ const normalizedFilePath = normalizeWorkspacePath(filePath);
58
+ return `${normalizeWorkspacePath(label)}/${normalizedFilePath}`;
59
+ }
60
+
61
+ function rewritePatchLine(line: string, label: string): string {
62
+ if (line.startsWith("--- ")) {
63
+ const parsed = parsePatchPathToken(line.slice(4), "a");
64
+ if (parsed === "/dev/null") return line;
65
+ if (parsed) return `--- ${formatPatchPathToken("a", prefixRepoPath(label, parsed))}`;
66
+ return line;
67
+ }
68
+
69
+ if (line.startsWith("+++ ")) {
70
+ const parsed = parsePatchPathToken(line.slice(4), "b");
71
+ if (parsed === "/dev/null") return line;
72
+ if (parsed) return `+++ ${formatPatchPathToken("b", prefixRepoPath(label, parsed))}`;
73
+ return line;
74
+ }
75
+
76
+ if (line.startsWith("rename from ")) {
77
+ const parsed = parseDiffMetadataPathToken(line.slice("rename from ".length));
78
+ if (parsed === "/dev/null") return line;
79
+ return `rename from ${formatDiffMetadataPathToken(prefixRepoPath(label, parsed))}`;
80
+ }
81
+ if (line.startsWith("rename to ")) {
82
+ const parsed = parseDiffMetadataPathToken(line.slice("rename to ".length));
83
+ if (parsed === "/dev/null") return line;
84
+ return `rename to ${formatDiffMetadataPathToken(prefixRepoPath(label, parsed))}`;
85
+ }
86
+ if (line.startsWith("copy from ")) {
87
+ const parsed = parseDiffMetadataPathToken(line.slice("copy from ".length));
88
+ if (parsed === "/dev/null") return line;
89
+ return `copy from ${formatDiffMetadataPathToken(prefixRepoPath(label, parsed))}`;
90
+ }
91
+ if (line.startsWith("copy to ")) {
92
+ const parsed = parseDiffMetadataPathToken(line.slice("copy to ".length));
93
+ if (parsed === "/dev/null") return line;
94
+ return `copy to ${formatDiffMetadataPathToken(prefixRepoPath(label, parsed))}`;
95
+ }
96
+
97
+ return line;
98
+ }
99
+
100
+ function rewritePatchChunk(chunk: string, label: string): string {
101
+ const lines = chunk.split("\n");
102
+ const fromFileLines = parseDiffFilePathLines(lines);
103
+ const fromMetadata = parseDiffMetadataPathLines(lines);
104
+ const fromHeader = parseDiffGitHeader(lines[0] ?? "");
105
+ const oldPath = fromFileLines.oldPath ?? fromMetadata.oldPath ?? fromHeader.oldPath;
106
+ const newPath = fromFileLines.newPath ?? fromMetadata.newPath ?? fromHeader.newPath;
107
+ const headerOldPath = oldPath ?? newPath;
108
+ const headerNewPath = newPath ?? oldPath;
109
+
110
+ if (lines[0]?.startsWith("diff --git ") && headerOldPath && headerNewPath) {
111
+ const prefixedOld = prefixRepoPath(label, headerOldPath);
112
+ const prefixedNew = prefixRepoPath(label, headerNewPath);
113
+ lines[0] = `diff --git ${formatPatchPathToken("a", prefixedOld)} ${formatPatchPathToken("b", prefixedNew)}`;
114
+ }
115
+
116
+ return lines.map((line, index) => index === 0 ? line : rewritePatchLine(line, label)).join("\n");
117
+ }
118
+
119
+ export function prefixWorkspacePatchPaths(rawPatch: string, label: string): string {
120
+ if (!rawPatch.trim()) return rawPatch;
121
+ if (!rawPatch.includes("diff --git ")) {
122
+ return rawPatch
123
+ .split("\n")
124
+ .map((line) => rewritePatchLine(line, label))
125
+ .join("\n");
126
+ }
127
+
128
+ const chunks = rawPatch.split(/^diff --git /m);
129
+ const prefix = chunks.shift() ?? "";
130
+ return prefix + chunks.map((chunk) => rewritePatchChunk(`diff --git ${chunk}`, label)).join("");
131
+ }
132
+
133
+ export function resolveWorkspaceFilePath<T extends WorkspacePathEntry>(
134
+ repos: T[],
135
+ prefixedPath: string,
136
+ ): WorkspacePathResolution<T> | null {
137
+ const normalizedPath = normalizeWorkspacePath(prefixedPath);
138
+ validateFilePath(normalizedPath);
139
+
140
+ const sorted = [...repos].sort((a, b) => b.label.length - a.label.length);
141
+
142
+ for (const repo of sorted) {
143
+ const label = normalizeWorkspacePath(repo.label);
144
+ const prefix = `${label}/`;
145
+ if (normalizedPath.startsWith(prefix)) {
146
+ const repoRelativePath = normalizedPath.slice(prefix.length);
147
+ if (!repoRelativePath) return null;
148
+ return {
149
+ repo,
150
+ repoRelativePath,
151
+ };
152
+ }
153
+ }
154
+
155
+ return null;
156
+ }
157
+
158
+ function hasVcsMarker(dirPath: string): boolean {
159
+ return VCS_MARKERS.some((marker) => existsSync(resolve(dirPath, marker)));
160
+ }
161
+
162
+ function collectWorkspaceRepos(root: string, current: string, results: string[]): void {
163
+ let entries: Dirent[];
164
+ try {
165
+ entries = readdirSync(current, { withFileTypes: true });
166
+ } catch {
167
+ return;
168
+ }
169
+
170
+ if (current !== root && hasVcsMarker(current)) {
171
+ results.push(current);
172
+ return;
173
+ }
174
+
175
+ for (const entry of entries) {
176
+ if (!entry.isDirectory()) continue;
177
+ if (SKIP_DIRS.has(entry.name)) continue;
178
+ collectWorkspaceRepos(root, resolve(current, entry.name), results);
179
+ }
180
+ }
181
+
182
+ export function discoverWorkspaceRepoPaths(root: string): string[] {
183
+ const resolvedRoot = resolve(root);
184
+ const results: string[] = [];
185
+ collectWorkspaceRepos(resolvedRoot, resolvedRoot, results);
186
+ return results.sort();
187
+ }
188
+
189
+ function buildRepoLabel(root: string, cwd: string, used: Set<string>): string {
190
+ const rel = normalizeWorkspacePath(relative(root, cwd));
191
+ const preferred = rel && rel !== "" ? rel : basename(cwd);
192
+ if (!used.has(preferred)) {
193
+ used.add(preferred);
194
+ return preferred;
195
+ }
196
+
197
+ const fallback = normalizeWorkspacePath(basename(cwd));
198
+ if (!used.has(fallback)) {
199
+ used.add(fallback);
200
+ return fallback;
201
+ }
202
+
203
+ let counter = 2;
204
+ let next = `${fallback}-${counter}`;
205
+ while (used.has(next)) {
206
+ counter += 1;
207
+ next = `${fallback}-${counter}`;
208
+ }
209
+ used.add(next);
210
+ return next;
211
+ }
212
+
213
+ export function buildWorkspaceRepoLabels(root: string, repoPaths: string[]): string[] {
214
+ const resolvedRoot = resolve(root);
215
+ const usedLabels = new Set<string>();
216
+ return repoPaths.map((cwd) => buildRepoLabel(resolvedRoot, cwd, usedLabels));
217
+ }
218
+
219
+ export function aggregateWorkspacePatch(repos: WorkspacePatchEntry[]): WorkspacePatchAggregate {
220
+ const selected = repos.filter((repo) => repo.selected);
221
+ const trimmedPatches = selected
222
+ .map((repo) => repo.rawPatch)
223
+ .filter((patch) => patch.trim().length > 0)
224
+ .map((patch) => patch.replace(/\n+$/, ""));
225
+ return {
226
+ rawPatch: trimmedPatches.join("\n\n"),
227
+ gitRef: selected.map((repo) => repo.gitRef || repo.label).filter(Boolean).join(" | ") || "Workspace review",
228
+ errors: repos.flatMap((repo) => repo.error ? [`${repo.label}: ${repo.error}`] : []),
229
+ };
230
+ }
@@ -0,0 +1,436 @@
1
+ import { isAbsolute, relative, resolve } from "node:path";
2
+
3
+ import type { DiffOption, DiffResult, DiffType, GitContext, GitDiffOptions } from "./review-core";
4
+ import {
5
+ aggregateWorkspacePatch,
6
+ buildWorkspaceRepoLabels,
7
+ discoverWorkspaceRepoPaths,
8
+ normalizeWorkspacePath,
9
+ prefixWorkspacePatchPaths,
10
+ resolveWorkspaceFilePath,
11
+ } from "./review-workspace-node";
12
+
13
+ export type WorkspaceDiffType =
14
+ | "workspace-current"
15
+ | "workspace-staged"
16
+ | "workspace-unstaged"
17
+ | "workspace-last";
18
+
19
+ export type WorkspaceChildVcsType = "git" | "jj";
20
+
21
+ export interface WorkspaceRepoState {
22
+ id: string;
23
+ label: string;
24
+ cwd: string;
25
+ selected: boolean;
26
+ vcsType?: WorkspaceChildVcsType;
27
+ diffType?: DiffType;
28
+ gitContext?: GitContext;
29
+ diffOptions?: DiffOption[];
30
+ error?: string;
31
+ }
32
+
33
+ export interface WorkspaceRepoRuntimeState extends WorkspaceRepoState {
34
+ rawPatch: string;
35
+ gitRef: string;
36
+ }
37
+
38
+ export interface WorkspaceReviewState {
39
+ mode: "workspace";
40
+ root: string;
41
+ diffType: WorkspaceDiffType;
42
+ diffOptions: DiffOption[];
43
+ repos: WorkspaceRepoState[];
44
+ }
45
+
46
+ export interface WorkspaceReviewRuntime {
47
+ getVcsContext(cwd?: string): Promise<GitContext>;
48
+ runVcsDiff(
49
+ diffType: DiffType,
50
+ defaultBranch?: string,
51
+ cwd?: string,
52
+ options?: GitDiffOptions,
53
+ ): Promise<DiffResult>;
54
+ getVcsFileContentsForDiff(
55
+ diffType: DiffType,
56
+ defaultBranch: string,
57
+ filePath: string,
58
+ oldPath?: string,
59
+ cwd?: string,
60
+ ): Promise<{ oldContent: string | null; newContent: string | null }>;
61
+ canStageFiles(diffType: string, cwd?: string): Promise<boolean>;
62
+ stageFile(diffType: string, filePath: string, cwd?: string): Promise<void>;
63
+ unstageFile(diffType: string, filePath: string, cwd?: string): Promise<void>;
64
+ }
65
+
66
+ export interface WorkspaceReviewBuildOptions {
67
+ requestedDiffType?: DiffType | WorkspaceDiffType;
68
+ configuredDiffType?: DiffType;
69
+ hideWhitespace?: boolean;
70
+ }
71
+
72
+ export interface WorkspacePromptRepoContext {
73
+ label: string;
74
+ cwd: string;
75
+ changed: boolean;
76
+ vcsType?: WorkspaceChildVcsType;
77
+ gitRef?: string;
78
+ error?: string;
79
+ }
80
+
81
+ export interface WorkspaceReviewPromptContext {
82
+ root: string;
83
+ repos: WorkspacePromptRepoContext[];
84
+ }
85
+
86
+ export interface WorkspaceDiffSnapshot {
87
+ rawPatch: string;
88
+ gitRef: string;
89
+ error?: string;
90
+ }
91
+
92
+ const WORKSPACE_CURRENT: DiffOption = { id: "workspace-current", label: "Current changes" };
93
+ const WORKSPACE_STAGED: DiffOption = { id: "workspace-staged", label: "Staged changes" };
94
+ const WORKSPACE_UNSTAGED: DiffOption = { id: "workspace-unstaged", label: "Unstaged changes" };
95
+ const WORKSPACE_LAST: DiffOption = { id: "workspace-last", label: "Last change" };
96
+
97
+ const WORKSPACE_DIFF_TYPES = new Set<WorkspaceDiffType>([
98
+ "workspace-current",
99
+ "workspace-staged",
100
+ "workspace-unstaged",
101
+ "workspace-last",
102
+ ]);
103
+
104
+ function isWorkspaceDiffType(value: string | undefined): value is WorkspaceDiffType {
105
+ return !!value && WORKSPACE_DIFF_TYPES.has(value as WorkspaceDiffType);
106
+ }
107
+
108
+ function normalizeVcsType(value: string | undefined): WorkspaceChildVcsType | undefined {
109
+ return value === "git" || value === "jj" ? value : undefined;
110
+ }
111
+
112
+ export function mapWorkspaceModeToRepoDiffType(
113
+ workspaceDiffType: WorkspaceDiffType,
114
+ vcsType: WorkspaceChildVcsType | undefined,
115
+ ): DiffType | null {
116
+ if (vcsType === "jj") {
117
+ switch (workspaceDiffType) {
118
+ case "workspace-current":
119
+ return "jj-current";
120
+ case "workspace-last":
121
+ return "jj-last";
122
+ default:
123
+ return null;
124
+ }
125
+ }
126
+
127
+ if (vcsType === "git") {
128
+ switch (workspaceDiffType) {
129
+ case "workspace-current":
130
+ return "uncommitted";
131
+ case "workspace-staged":
132
+ return "staged";
133
+ case "workspace-unstaged":
134
+ return "unstaged";
135
+ case "workspace-last":
136
+ return "last-commit";
137
+ }
138
+ }
139
+
140
+ return null;
141
+ }
142
+
143
+ export function mapRepoDiffTypeToWorkspaceMode(
144
+ diffType: DiffType | WorkspaceDiffType | undefined,
145
+ ): WorkspaceDiffType | undefined {
146
+ if (isWorkspaceDiffType(diffType)) return diffType;
147
+ switch (diffType) {
148
+ case "uncommitted":
149
+ case "jj-current":
150
+ return "workspace-current";
151
+ case "staged":
152
+ return "workspace-staged";
153
+ case "unstaged":
154
+ return "workspace-unstaged";
155
+ case "last-commit":
156
+ case "jj-last":
157
+ return "workspace-last";
158
+ default:
159
+ return undefined;
160
+ }
161
+ }
162
+
163
+ export function resolveWorkspaceInitialDiffType(
164
+ repos: WorkspaceRepoRuntimeState[],
165
+ requested?: DiffType | WorkspaceDiffType,
166
+ configured?: DiffType,
167
+ ): WorkspaceDiffType {
168
+ for (const candidate of [
169
+ mapRepoDiffTypeToWorkspaceMode(requested),
170
+ mapRepoDiffTypeToWorkspaceMode(configured),
171
+ "workspace-current" as const,
172
+ ]) {
173
+ if (candidate && workspaceModeAvailable(repos, candidate)) return candidate;
174
+ }
175
+ return "workspace-current";
176
+ }
177
+
178
+ export function workspaceModeAvailable(
179
+ repos: WorkspaceRepoRuntimeState[],
180
+ diffType: WorkspaceDiffType,
181
+ ): boolean {
182
+ if (diffType === "workspace-staged" || diffType === "workspace-unstaged") {
183
+ const detectedRepos = repos.filter((repo) => repo.vcsType);
184
+ return detectedRepos.length > 0 && detectedRepos.every((repo) => repo.vcsType === "git");
185
+ }
186
+ return true;
187
+ }
188
+
189
+ export function getWorkspaceDiffOptions(repos: WorkspaceRepoRuntimeState[]): DiffOption[] {
190
+ const options = [WORKSPACE_CURRENT];
191
+ if (workspaceModeAvailable(repos, "workspace-staged")) {
192
+ options.push(WORKSPACE_STAGED, WORKSPACE_UNSTAGED);
193
+ }
194
+ options.push(WORKSPACE_LAST);
195
+ return options;
196
+ }
197
+
198
+ function aggregateRepos(repos: WorkspaceRepoRuntimeState[]): WorkspaceDiffSnapshot {
199
+ const aggregate = aggregateWorkspacePatch(repos);
200
+ return {
201
+ rawPatch: aggregate.rawPatch,
202
+ gitRef: aggregate.gitRef,
203
+ error: aggregate.errors.length > 0 ? aggregate.errors.join("\n") : undefined,
204
+ };
205
+ }
206
+
207
+ function normalizeAgentPath(root: string, repos: WorkspaceRepoRuntimeState[], filePath: string): string {
208
+ const normalized = normalizeWorkspacePath(filePath);
209
+ if (resolveWorkspaceFilePath(repos, normalized)) return normalized;
210
+
211
+ const sorted = [...repos].sort((a, b) => b.cwd.length - a.cwd.length);
212
+ for (const repo of sorted) {
213
+ const rel = normalizeWorkspacePath(relative(repo.cwd, filePath));
214
+ if (rel && !rel.startsWith("..") && !rel.startsWith("/")) {
215
+ return `${normalizeWorkspacePath(repo.label)}/${rel}`;
216
+ }
217
+ }
218
+
219
+ const rootRel = normalizeWorkspacePath(relative(root, filePath));
220
+ if (rootRel && !rootRel.startsWith("..") && !rootRel.startsWith("/")) {
221
+ if (resolveWorkspaceFilePath(repos, rootRel)) return rootRel;
222
+ }
223
+
224
+ const changedRepos = repos.filter((repo) => repo.selected && repo.rawPatch.trim());
225
+ if (!isAbsolute(filePath) && changedRepos.length === 1 && normalized && !normalized.startsWith("..")) {
226
+ return `${normalizeWorkspacePath(changedRepos[0].label)}/${normalized}`;
227
+ }
228
+
229
+ if (rootRel && !rootRel.startsWith("..") && !rootRel.startsWith("/")) return rootRel;
230
+ return normalized;
231
+ }
232
+
233
+ export class WorkspaceReviewSession implements WorkspaceReviewState {
234
+ readonly mode = "workspace" as const;
235
+ readonly root: string;
236
+ repos: WorkspaceRepoRuntimeState[];
237
+ diffType: WorkspaceDiffType;
238
+ diffOptions: DiffOption[];
239
+ rawPatch: string;
240
+ gitRef: string;
241
+ error?: string;
242
+ hideWhitespace: boolean;
243
+
244
+ private constructor(
245
+ private readonly runtime: WorkspaceReviewRuntime,
246
+ root: string,
247
+ repos: WorkspaceRepoRuntimeState[],
248
+ diffType: WorkspaceDiffType,
249
+ hideWhitespace: boolean,
250
+ ) {
251
+ this.root = resolve(root);
252
+ this.repos = repos;
253
+ this.diffType = diffType;
254
+ this.hideWhitespace = hideWhitespace;
255
+ this.diffOptions = getWorkspaceDiffOptions(repos);
256
+ const snapshot = aggregateRepos(repos);
257
+ this.rawPatch = snapshot.rawPatch;
258
+ this.gitRef = snapshot.gitRef;
259
+ this.error = snapshot.error;
260
+ }
261
+
262
+ static async create(
263
+ runtime: WorkspaceReviewRuntime,
264
+ root: string,
265
+ options: WorkspaceReviewBuildOptions = {},
266
+ ): Promise<WorkspaceReviewSession> {
267
+ const resolvedRoot = resolve(root);
268
+ const repoPaths = discoverWorkspaceRepoPaths(resolvedRoot);
269
+ const labels = buildWorkspaceRepoLabels(resolvedRoot, repoPaths);
270
+
271
+ const repos = await Promise.all(repoPaths.map(async (cwd, index) => {
272
+ const label = labels[index];
273
+ try {
274
+ const gitContext = await runtime.getVcsContext(cwd);
275
+ const vcsType = normalizeVcsType(gitContext.vcsType);
276
+ return {
277
+ id: `repo-${index + 1}`,
278
+ label,
279
+ cwd,
280
+ selected: false,
281
+ vcsType,
282
+ gitContext,
283
+ diffOptions: gitContext.diffOptions,
284
+ rawPatch: "",
285
+ gitRef: "",
286
+ } satisfies WorkspaceRepoRuntimeState;
287
+ } catch (error) {
288
+ return {
289
+ id: `repo-${index + 1}`,
290
+ label,
291
+ cwd,
292
+ selected: false,
293
+ rawPatch: "",
294
+ gitRef: "",
295
+ error: error instanceof Error ? error.message : String(error),
296
+ } satisfies WorkspaceRepoRuntimeState;
297
+ }
298
+ }));
299
+
300
+ const diffType = resolveWorkspaceInitialDiffType(
301
+ repos.filter((repo) => repo.vcsType),
302
+ options.requestedDiffType,
303
+ options.configuredDiffType,
304
+ );
305
+ const session = new WorkspaceReviewSession(
306
+ runtime,
307
+ resolvedRoot,
308
+ repos,
309
+ diffType,
310
+ options.hideWhitespace ?? false,
311
+ );
312
+ await session.rebuild({ diffType, hideWhitespace: options.hideWhitespace });
313
+ return session;
314
+ }
315
+
316
+ async rebuild(options: {
317
+ diffType?: DiffType | WorkspaceDiffType;
318
+ hideWhitespace?: boolean;
319
+ } = {}): Promise<WorkspaceDiffSnapshot> {
320
+ const requestedMode = mapRepoDiffTypeToWorkspaceMode(options.diffType) ?? this.diffType;
321
+ if (!workspaceModeAvailable(this.repos, requestedMode)) {
322
+ throw new Error(`Workspace diff mode is not available: ${requestedMode}`);
323
+ }
324
+
325
+ if (typeof options.hideWhitespace === "boolean") {
326
+ this.hideWhitespace = options.hideWhitespace;
327
+ }
328
+
329
+ const repos = await Promise.all(this.repos.map(async (repo) => {
330
+ if (!repo.vcsType || !repo.gitContext) {
331
+ return { ...repo, selected: false, rawPatch: "", gitRef: "" };
332
+ }
333
+
334
+ const repoDiffType = mapWorkspaceModeToRepoDiffType(requestedMode, repo.vcsType);
335
+ if (!repoDiffType) {
336
+ return {
337
+ ...repo,
338
+ selected: false,
339
+ diffType: undefined,
340
+ rawPatch: "",
341
+ gitRef: "",
342
+ error: `Workspace diff mode ${requestedMode} is not available for ${repo.vcsType}`,
343
+ };
344
+ }
345
+
346
+ try {
347
+ const diff = await this.runtime.runVcsDiff(repoDiffType, repo.gitContext.defaultBranch, repo.cwd, {
348
+ hideWhitespace: this.hideWhitespace,
349
+ });
350
+ return {
351
+ ...repo,
352
+ selected: !!diff.patch.trim(),
353
+ diffType: repoDiffType,
354
+ rawPatch: prefixWorkspacePatchPaths(diff.patch, repo.label),
355
+ gitRef: diff.label,
356
+ error: diff.error,
357
+ };
358
+ } catch (error) {
359
+ return {
360
+ ...repo,
361
+ selected: false,
362
+ diffType: repoDiffType,
363
+ rawPatch: "",
364
+ gitRef: "",
365
+ error: error instanceof Error ? error.message : String(error),
366
+ };
367
+ }
368
+ }));
369
+
370
+ this.repos = repos;
371
+ this.diffType = requestedMode;
372
+ this.diffOptions = getWorkspaceDiffOptions(repos);
373
+ const snapshot = aggregateRepos(repos);
374
+ this.rawPatch = snapshot.rawPatch;
375
+ this.gitRef = snapshot.gitRef;
376
+ this.error = snapshot.error;
377
+ return snapshot;
378
+ }
379
+
380
+ getPromptContext(): WorkspaceReviewPromptContext {
381
+ return {
382
+ root: this.root,
383
+ repos: this.repos
384
+ .filter((repo) => (repo.selected && repo.rawPatch.trim()) || repo.error)
385
+ .map((repo) => ({
386
+ label: repo.label,
387
+ cwd: repo.cwd,
388
+ changed: repo.selected && !!repo.rawPatch.trim(),
389
+ vcsType: repo.vcsType,
390
+ gitRef: repo.gitRef,
391
+ error: repo.error,
392
+ })),
393
+ };
394
+ }
395
+
396
+ normalizeAnnotationPath(filePath: string): string {
397
+ return normalizeAgentPath(this.root, this.repos, filePath);
398
+ }
399
+
400
+ async getFileContents(
401
+ filePath: string,
402
+ oldPath?: string,
403
+ ): Promise<{ oldContent: string | null; newContent: string | null }> {
404
+ const resolved = resolveWorkspaceFilePath(this.repos, filePath);
405
+ if (!resolved) throw new Error("File is not part of this workspace review");
406
+
407
+ const resolvedOld = oldPath ? resolveWorkspaceFilePath(this.repos, oldPath) : null;
408
+ if (oldPath && (!resolvedOld || resolvedOld.repo.id !== resolved.repo.id)) {
409
+ throw new Error("Old path is not part of the same workspace repository");
410
+ }
411
+
412
+ return this.runtime.getVcsFileContentsForDiff(
413
+ resolved.repo.diffType ?? mapWorkspaceModeToRepoDiffType(this.diffType, resolved.repo.vcsType) ?? "uncommitted",
414
+ resolved.repo.gitContext?.defaultBranch ?? "main",
415
+ resolved.repoRelativePath,
416
+ resolvedOld?.repoRelativePath,
417
+ resolved.repo.cwd,
418
+ );
419
+ }
420
+
421
+ async stageFile(filePath: string, undo?: boolean): Promise<void> {
422
+ const resolved = resolveWorkspaceFilePath(this.repos, filePath);
423
+ if (!resolved) throw new Error("File is not part of this workspace review");
424
+
425
+ const diffType = resolved.repo.diffType ?? mapWorkspaceModeToRepoDiffType(this.diffType, resolved.repo.vcsType) ?? "uncommitted";
426
+ if (!(await this.runtime.canStageFiles(diffType, resolved.repo.cwd))) {
427
+ throw new Error("Staging not available");
428
+ }
429
+
430
+ if (undo) {
431
+ await this.runtime.unstageFile(diffType, resolved.repoRelativePath, resolved.repo.cwd);
432
+ } else {
433
+ await this.runtime.stageFile(diffType, resolved.repoRelativePath, resolved.repo.cwd);
434
+ }
435
+ }
436
+ }