@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.
- package/3p/pi-ask-user/index.ts +1835 -0
- package/3p/pi-ask-user/package.json +50 -0
- package/3p/pi-ask-user/single-select-layout.ts +203 -0
- package/3p/pi-lsp/extensions/lsp/client.ts +542 -0
- package/3p/pi-lsp/extensions/lsp/config.ts +135 -0
- package/3p/pi-lsp/extensions/lsp/effects/command.ts +42 -0
- package/3p/pi-lsp/extensions/lsp/effects/filesystem.ts +50 -0
- package/3p/pi-lsp/extensions/lsp/effects/runtime.ts +21 -0
- package/3p/pi-lsp/extensions/lsp/errors.ts +113 -0
- package/3p/pi-lsp/extensions/lsp/formatting.ts +315 -0
- package/3p/pi-lsp/extensions/lsp/index.ts +231 -0
- package/3p/pi-lsp/extensions/lsp/protocol.ts +209 -0
- package/3p/pi-lsp/extensions/lsp/retry.ts +39 -0
- package/3p/pi-lsp/extensions/lsp/tools/programs.ts +293 -0
- package/3p/pi-lsp/extensions/lsp/tools.ts +89 -0
- package/3p/pi-lsp/extensions/lsp/types.ts +243 -0
- package/3p/pi-lsp/package.json +54 -0
- package/3p/pi-plannotator/AGENTS.md +583 -0
- package/3p/pi-plannotator/apps/pi-extension/README.md +228 -0
- package/3p/pi-plannotator/apps/pi-extension/assistant-message.ts +128 -0
- package/3p/pi-plannotator/apps/pi-extension/config.test.ts +166 -0
- package/3p/pi-plannotator/apps/pi-extension/config.ts +318 -0
- package/3p/pi-plannotator/apps/pi-extension/current-pi-session.ts +147 -0
- package/3p/pi-plannotator/apps/pi-extension/index.ts +1279 -0
- package/3p/pi-plannotator/apps/pi-extension/package.json +56 -0
- package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.test.ts +13 -0
- package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.ts +616 -0
- package/3p/pi-plannotator/apps/pi-extension/plannotator-events.ts +342 -0
- package/3p/pi-plannotator/apps/pi-extension/plannotator.json +12 -0
- package/3p/pi-plannotator/apps/pi-extension/server/agent-jobs.ts +515 -0
- package/3p/pi-plannotator/apps/pi-extension/server/ai-runtime.ts +169 -0
- package/3p/pi-plannotator/apps/pi-extension/server/annotations.ts +85 -0
- package/3p/pi-plannotator/apps/pi-extension/server/external-annotations.ts +189 -0
- package/3p/pi-plannotator/apps/pi-extension/server/handlers.ts +210 -0
- package/3p/pi-plannotator/apps/pi-extension/server/helpers.ts +78 -0
- package/3p/pi-plannotator/apps/pi-extension/server/ide.ts +46 -0
- package/3p/pi-plannotator/apps/pi-extension/server/integrations.ts +195 -0
- package/3p/pi-plannotator/apps/pi-extension/server/network.test.ts +158 -0
- package/3p/pi-plannotator/apps/pi-extension/server/network.ts +268 -0
- package/3p/pi-plannotator/apps/pi-extension/server/pr.ts +126 -0
- package/3p/pi-plannotator/apps/pi-extension/server/project.ts +64 -0
- package/3p/pi-plannotator/apps/pi-extension/server/reference.ts +362 -0
- package/3p/pi-plannotator/apps/pi-extension/server/serverAnnotate.ts +199 -0
- package/3p/pi-plannotator/apps/pi-extension/server/serverPlan.ts +505 -0
- package/3p/pi-plannotator/apps/pi-extension/server/serverReview.ts +1229 -0
- package/3p/pi-plannotator/apps/pi-extension/server/vcs.ts +121 -0
- package/3p/pi-plannotator/apps/pi-extension/server.test.ts +966 -0
- package/3p/pi-plannotator/apps/pi-extension/server.ts +44 -0
- package/3p/pi-plannotator/apps/pi-extension/tool-scope.test.ts +88 -0
- package/3p/pi-plannotator/apps/pi-extension/tool-scope.ts +39 -0
- package/3p/pi-plannotator/apps/pi-extension/tsconfig.json +16 -0
- package/3p/pi-plannotator/apps/pi-extension/vendor.sh +48 -0
- package/3p/pi-plannotator/package.json +53 -0
- package/3p/pi-plannotator/packages/ai/ai.test.ts +1407 -0
- package/3p/pi-plannotator/packages/ai/base-session.ts +94 -0
- package/3p/pi-plannotator/packages/ai/context.ts +250 -0
- package/3p/pi-plannotator/packages/ai/endpoints.ts +326 -0
- package/3p/pi-plannotator/packages/ai/index.ts +105 -0
- package/3p/pi-plannotator/packages/ai/package.json +21 -0
- package/3p/pi-plannotator/packages/ai/provider.ts +103 -0
- package/3p/pi-plannotator/packages/ai/providers/claude-agent-sdk.ts +447 -0
- package/3p/pi-plannotator/packages/ai/providers/codex-sdk.ts +430 -0
- package/3p/pi-plannotator/packages/ai/providers/command-path.ts +115 -0
- package/3p/pi-plannotator/packages/ai/providers/opencode-sdk.ts +546 -0
- package/3p/pi-plannotator/packages/ai/providers/pi-events.ts +110 -0
- package/3p/pi-plannotator/packages/ai/providers/pi-sdk-node.ts +425 -0
- package/3p/pi-plannotator/packages/ai/providers/pi-sdk.ts +469 -0
- package/3p/pi-plannotator/packages/ai/session-manager.ts +195 -0
- package/3p/pi-plannotator/packages/ai/tsconfig.json +15 -0
- package/3p/pi-plannotator/packages/ai/types.ts +379 -0
- package/3p/pi-plannotator/packages/server/agent-jobs.ts +538 -0
- package/3p/pi-plannotator/packages/server/agent-review-message.test.ts +135 -0
- package/3p/pi-plannotator/packages/server/agent-review-message.ts +243 -0
- package/3p/pi-plannotator/packages/server/ai-runtime.ts +108 -0
- package/3p/pi-plannotator/packages/server/annotate.ts +401 -0
- package/3p/pi-plannotator/packages/server/browser.test.ts +87 -0
- package/3p/pi-plannotator/packages/server/browser.ts +245 -0
- package/3p/pi-plannotator/packages/server/claude-review.ts +352 -0
- package/3p/pi-plannotator/packages/server/code-nav.ts +73 -0
- package/3p/pi-plannotator/packages/server/codex-review-schema.json +41 -0
- package/3p/pi-plannotator/packages/server/codex-review.ts +330 -0
- package/3p/pi-plannotator/packages/server/config.ts +8 -0
- package/3p/pi-plannotator/packages/server/draft.ts +1 -0
- package/3p/pi-plannotator/packages/server/editor-annotations.ts +76 -0
- package/3p/pi-plannotator/packages/server/external-annotations.test.ts +18 -0
- package/3p/pi-plannotator/packages/server/external-annotations.ts +207 -0
- package/3p/pi-plannotator/packages/server/git.ts +141 -0
- package/3p/pi-plannotator/packages/server/goal-setup.test.ts +55 -0
- package/3p/pi-plannotator/packages/server/goal-setup.ts +248 -0
- package/3p/pi-plannotator/packages/server/ide.ts +43 -0
- package/3p/pi-plannotator/packages/server/image.test.ts +63 -0
- package/3p/pi-plannotator/packages/server/image.ts +66 -0
- package/3p/pi-plannotator/packages/server/index.ts +648 -0
- package/3p/pi-plannotator/packages/server/integrations.test.ts +172 -0
- package/3p/pi-plannotator/packages/server/integrations.ts +214 -0
- package/3p/pi-plannotator/packages/server/jj.test.ts +69 -0
- package/3p/pi-plannotator/packages/server/jj.ts +86 -0
- package/3p/pi-plannotator/packages/server/p4.ts +417 -0
- package/3p/pi-plannotator/packages/server/package.json +40 -0
- package/3p/pi-plannotator/packages/server/path-utils.ts +18 -0
- package/3p/pi-plannotator/packages/server/pr.ts +146 -0
- package/3p/pi-plannotator/packages/server/project.test.ts +115 -0
- package/3p/pi-plannotator/packages/server/project.ts +45 -0
- package/3p/pi-plannotator/packages/server/reference-handlers.ts +404 -0
- package/3p/pi-plannotator/packages/server/remote.test.ts +150 -0
- package/3p/pi-plannotator/packages/server/remote.ts +74 -0
- package/3p/pi-plannotator/packages/server/repo.ts +80 -0
- package/3p/pi-plannotator/packages/server/resolve-file.test.ts +310 -0
- package/3p/pi-plannotator/packages/server/review-workspace.test.ts +1032 -0
- package/3p/pi-plannotator/packages/server/review-workspace.ts +48 -0
- package/3p/pi-plannotator/packages/server/review.ts +1318 -0
- package/3p/pi-plannotator/packages/server/sessions.ts +111 -0
- package/3p/pi-plannotator/packages/server/share-url.ts +53 -0
- package/3p/pi-plannotator/packages/server/shared-handlers.test.ts +43 -0
- package/3p/pi-plannotator/packages/server/shared-handlers.ts +177 -0
- package/3p/pi-plannotator/packages/server/storage.test.ts +176 -0
- package/3p/pi-plannotator/packages/server/storage.ts +17 -0
- package/3p/pi-plannotator/packages/server/tour/tour-review.test.ts +146 -0
- package/3p/pi-plannotator/packages/server/tour/tour-review.ts +604 -0
- package/3p/pi-plannotator/packages/server/tsconfig.json +15 -0
- package/3p/pi-plannotator/packages/server/vcs.test.ts +48 -0
- package/3p/pi-plannotator/packages/server/vcs.ts +80 -0
- package/3p/pi-plannotator/packages/shared/agent-jobs.ts +132 -0
- package/3p/pi-plannotator/packages/shared/agents.ts +53 -0
- package/3p/pi-plannotator/packages/shared/annotate-args.test.ts +386 -0
- package/3p/pi-plannotator/packages/shared/annotate-args.ts +107 -0
- package/3p/pi-plannotator/packages/shared/at-reference.test.ts +99 -0
- package/3p/pi-plannotator/packages/shared/at-reference.ts +52 -0
- package/3p/pi-plannotator/packages/shared/checklist.ts +52 -0
- package/3p/pi-plannotator/packages/shared/code-file.test.ts +112 -0
- package/3p/pi-plannotator/packages/shared/code-file.ts +41 -0
- package/3p/pi-plannotator/packages/shared/code-nav.test.ts +515 -0
- package/3p/pi-plannotator/packages/shared/code-nav.ts +436 -0
- package/3p/pi-plannotator/packages/shared/compress.ts +51 -0
- package/3p/pi-plannotator/packages/shared/config.ts +262 -0
- package/3p/pi-plannotator/packages/shared/crypto.test.ts +172 -0
- package/3p/pi-plannotator/packages/shared/crypto.ts +97 -0
- package/3p/pi-plannotator/packages/shared/data-dir.ts +42 -0
- package/3p/pi-plannotator/packages/shared/diff-paths.test.ts +30 -0
- package/3p/pi-plannotator/packages/shared/diff-paths.ts +137 -0
- package/3p/pi-plannotator/packages/shared/draft.ts +64 -0
- package/3p/pi-plannotator/packages/shared/external-annotation.ts +397 -0
- package/3p/pi-plannotator/packages/shared/extract-code-paths.test.ts +59 -0
- package/3p/pi-plannotator/packages/shared/extract-code-paths.ts +66 -0
- package/3p/pi-plannotator/packages/shared/favicon.ts +5 -0
- package/3p/pi-plannotator/packages/shared/feedback-templates.test.ts +65 -0
- package/3p/pi-plannotator/packages/shared/feedback-templates.ts +29 -0
- package/3p/pi-plannotator/packages/shared/goal-setup.test.ts +231 -0
- package/3p/pi-plannotator/packages/shared/goal-setup.ts +336 -0
- package/3p/pi-plannotator/packages/shared/html-to-markdown.test.ts +62 -0
- package/3p/pi-plannotator/packages/shared/html-to-markdown.ts +32 -0
- package/3p/pi-plannotator/packages/shared/improvement-hooks.test.ts +135 -0
- package/3p/pi-plannotator/packages/shared/improvement-hooks.ts +115 -0
- package/3p/pi-plannotator/packages/shared/integrations-common.ts +243 -0
- package/3p/pi-plannotator/packages/shared/jj-core.test.ts +236 -0
- package/3p/pi-plannotator/packages/shared/jj-core.ts +433 -0
- package/3p/pi-plannotator/packages/shared/package.json +55 -0
- package/3p/pi-plannotator/packages/shared/pfm-reminder.test.ts +88 -0
- package/3p/pi-plannotator/packages/shared/pfm-reminder.ts +80 -0
- package/3p/pi-plannotator/packages/shared/pr-github.ts +661 -0
- package/3p/pi-plannotator/packages/shared/pr-gitlab.test.ts +202 -0
- package/3p/pi-plannotator/packages/shared/pr-gitlab.ts +620 -0
- package/3p/pi-plannotator/packages/shared/pr-provider.test.ts +266 -0
- package/3p/pi-plannotator/packages/shared/pr-provider.ts +123 -0
- package/3p/pi-plannotator/packages/shared/pr-stack.test.ts +104 -0
- package/3p/pi-plannotator/packages/shared/pr-stack.ts +194 -0
- package/3p/pi-plannotator/packages/shared/pr-types.ts +326 -0
- package/3p/pi-plannotator/packages/shared/project.ts +71 -0
- package/3p/pi-plannotator/packages/shared/prompts-integration.test.ts +421 -0
- package/3p/pi-plannotator/packages/shared/prompts.test.ts +504 -0
- package/3p/pi-plannotator/packages/shared/prompts.ts +247 -0
- package/3p/pi-plannotator/packages/shared/reference-common.ts +87 -0
- package/3p/pi-plannotator/packages/shared/repo.ts +71 -0
- package/3p/pi-plannotator/packages/shared/resolve-file.test.ts +113 -0
- package/3p/pi-plannotator/packages/shared/resolve-file.ts +509 -0
- package/3p/pi-plannotator/packages/shared/review-args.test.ts +64 -0
- package/3p/pi-plannotator/packages/shared/review-args.ts +85 -0
- package/3p/pi-plannotator/packages/shared/review-core.test.ts +286 -0
- package/3p/pi-plannotator/packages/shared/review-core.ts +895 -0
- package/3p/pi-plannotator/packages/shared/review-workspace-node.ts +230 -0
- package/3p/pi-plannotator/packages/shared/review-workspace.ts +436 -0
- package/3p/pi-plannotator/packages/shared/semantic-diff-types.ts +76 -0
- package/3p/pi-plannotator/packages/shared/semantic-diff.test.ts +322 -0
- package/3p/pi-plannotator/packages/shared/semantic-diff.ts +520 -0
- package/3p/pi-plannotator/packages/shared/storage.ts +378 -0
- package/3p/pi-plannotator/packages/shared/tour.ts +61 -0
- package/3p/pi-plannotator/packages/shared/tsconfig.json +15 -0
- package/3p/pi-plannotator/packages/shared/types.ts +29 -0
- package/3p/pi-plannotator/packages/shared/url-to-markdown.test.ts +177 -0
- package/3p/pi-plannotator/packages/shared/url-to-markdown.ts +351 -0
- package/3p/pi-plannotator/packages/shared/vcs-core.test.ts +332 -0
- package/3p/pi-plannotator/packages/shared/vcs-core.ts +482 -0
- package/3p/pi-plannotator/packages/shared/worktree-pool.test.ts +162 -0
- package/3p/pi-plannotator/packages/shared/worktree-pool.ts +103 -0
- package/3p/pi-plannotator/packages/shared/worktree.ts +119 -0
- package/3p/pi-subagents/package.json +50 -0
- package/3p/pi-subagents/src/agent-manager.ts +413 -0
- package/3p/pi-subagents/src/agent-runner.ts +502 -0
- package/3p/pi-subagents/src/agent-types.ts +248 -0
- package/3p/pi-subagents/src/context.ts +58 -0
- package/3p/pi-subagents/src/cross-extension-rpc.ts +135 -0
- package/3p/pi-subagents/src/custom-agents.ts +137 -0
- package/3p/pi-subagents/src/default-agents.ts +144 -0
- package/3p/pi-subagents/src/env.ts +33 -0
- package/3p/pi-subagents/src/group-join.ts +141 -0
- package/3p/pi-subagents/src/index.ts +1811 -0
- package/3p/pi-subagents/src/invocation-config.ts +40 -0
- package/3p/pi-subagents/src/memory.ts +165 -0
- package/3p/pi-subagents/src/model-resolver.ts +81 -0
- package/3p/pi-subagents/src/output-file.ts +77 -0
- package/3p/pi-subagents/src/prompts.ts +85 -0
- package/3p/pi-subagents/src/skill-loader.ts +79 -0
- package/3p/pi-subagents/src/types.ts +111 -0
- package/3p/pi-subagents/src/ui/agent-widget.ts +496 -0
- package/3p/pi-subagents/src/ui/conversation-viewer.ts +250 -0
- package/3p/pi-subagents/src/worktree.ts +162 -0
- package/3p/pi-tasks/package.json +52 -0
- package/3p/pi-tasks/src/auto-clear.ts +91 -0
- package/3p/pi-tasks/src/index.ts +1150 -0
- package/3p/pi-tasks/src/process-tracker.ts +140 -0
- package/3p/pi-tasks/src/reminder-cadence.ts +90 -0
- package/3p/pi-tasks/src/task-store.ts +324 -0
- package/3p/pi-tasks/src/tasks-config.ts +27 -0
- package/3p/pi-tasks/src/types.ts +40 -0
- package/3p/pi-tasks/src/ui/settings-menu.ts +152 -0
- package/3p/pi-tasks/src/ui/task-widget.ts +296 -0
- package/AGENTS.md +28 -0
- package/LICENSE +201 -0
- package/extensions/orchestrator/agents/brainstorm-reviewer.ts +77 -0
- package/extensions/orchestrator/agents/code-reviewer.ts +89 -0
- package/extensions/orchestrator/agents/explore.ts +33 -0
- package/extensions/orchestrator/agents/librarian.ts +43 -0
- package/extensions/orchestrator/agents/plan-reviewer.ts +75 -0
- package/extensions/orchestrator/agents/planner.ts +61 -0
- package/extensions/orchestrator/agents/registry.ts +156 -0
- package/extensions/orchestrator/agents/task.ts +50 -0
- package/extensions/orchestrator/agents/tool-routing.ts +84 -0
- package/extensions/orchestrator/ast-search.ts +85 -0
- package/extensions/orchestrator/cbm.ts +330 -0
- package/extensions/orchestrator/command-handlers.test.ts +163 -0
- package/extensions/orchestrator/command-handlers.ts +116 -0
- package/extensions/orchestrator/commands.test.ts +81 -0
- package/extensions/orchestrator/commands.ts +75 -0
- package/extensions/orchestrator/config.test.ts +221 -0
- package/extensions/orchestrator/config.ts +230 -0
- package/extensions/orchestrator/context.test.ts +293 -0
- package/extensions/orchestrator/context.ts +206 -0
- package/extensions/orchestrator/event-handlers.test.ts +190 -0
- package/extensions/orchestrator/event-handlers.ts +1416 -0
- package/extensions/orchestrator/exa.ts +104 -0
- package/extensions/orchestrator/flant-infra.ts +486 -0
- package/extensions/orchestrator/index.ts +80 -0
- package/extensions/orchestrator/integration.test.ts +1214 -0
- package/extensions/orchestrator/orchestrator.test.ts +252 -0
- package/extensions/orchestrator/orchestrator.ts +538 -0
- package/extensions/orchestrator/phases/brainstorm.test.ts +15 -0
- package/extensions/orchestrator/phases/brainstorm.ts +273 -0
- package/extensions/orchestrator/phases/implementation.ts +40 -0
- package/extensions/orchestrator/phases/machine.test.ts +293 -0
- package/extensions/orchestrator/phases/machine.ts +209 -0
- package/extensions/orchestrator/phases/planning.ts +255 -0
- package/extensions/orchestrator/phases/review.ts +193 -0
- package/extensions/orchestrator/plannotator.ts +56 -0
- package/extensions/orchestrator/pp-menu.ts +866 -0
- package/extensions/orchestrator/state.test.ts +343 -0
- package/extensions/orchestrator/state.ts +237 -0
- package/extensions/orchestrator/validate-artifacts.test.ts +88 -0
- package/extensions/orchestrator/validate-artifacts.ts +272 -0
- package/extensions/orchestrator/vendor.d.ts +26 -0
- package/package.json +73 -0
- package/scripts/postinstall.sh +18 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
export interface DiffPathPair {
|
|
2
|
+
oldPath?: string;
|
|
3
|
+
newPath?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function unquoteGitPath(value: string): string {
|
|
7
|
+
if (!value.startsWith('"') || !value.endsWith('"')) return value;
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(value) as string;
|
|
10
|
+
} catch {
|
|
11
|
+
return value.slice(1, -1)
|
|
12
|
+
.replace(/\\"/g, '"')
|
|
13
|
+
.replace(/\\\\/g, "\\")
|
|
14
|
+
.replace(/\\t/g, "\t")
|
|
15
|
+
.replace(/\\n/g, "\n");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function quoteGitPath(value: string): string {
|
|
20
|
+
if (!/[\s"\\]/.test(value)) return value;
|
|
21
|
+
return JSON.stringify(value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function stripUnquotedPathMetadata(token: string): string {
|
|
25
|
+
if (token.startsWith('"')) return token;
|
|
26
|
+
const tabIndex = token.indexOf("\t");
|
|
27
|
+
return tabIndex === -1 ? token : token.slice(0, tabIndex);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function parsePatchPathToken(token: string, side: "a" | "b"): string | null {
|
|
31
|
+
const pathToken = stripUnquotedPathMetadata(token);
|
|
32
|
+
if (pathToken === "/dev/null") return "/dev/null";
|
|
33
|
+
const unquoted = unquoteGitPath(pathToken);
|
|
34
|
+
const prefix = `${side}/`;
|
|
35
|
+
return unquoted.startsWith(prefix) ? unquoted.slice(prefix.length) : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function scanHeaderToken(input: string): { token: string; rest: string } | null {
|
|
39
|
+
const trimmed = input.trimStart();
|
|
40
|
+
if (!trimmed) return null;
|
|
41
|
+
|
|
42
|
+
if (trimmed.startsWith('"')) {
|
|
43
|
+
let escaped = false;
|
|
44
|
+
for (let i = 1; i < trimmed.length; i += 1) {
|
|
45
|
+
const char = trimmed[i];
|
|
46
|
+
if (escaped) {
|
|
47
|
+
escaped = false;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (char === "\\") {
|
|
51
|
+
escaped = true;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (char === '"') {
|
|
55
|
+
return { token: trimmed.slice(0, i + 1), rest: trimmed.slice(i + 1) };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const space = trimmed.indexOf(" ");
|
|
62
|
+
if (space === -1) return { token: trimmed, rest: "" };
|
|
63
|
+
return { token: trimmed.slice(0, space), rest: trimmed.slice(space + 1) };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function parseDiffGitHeader(header: string): DiffPathPair {
|
|
67
|
+
const prefix = "diff --git ";
|
|
68
|
+
if (!header.startsWith(prefix)) return {};
|
|
69
|
+
|
|
70
|
+
const rest = header.slice(prefix.length);
|
|
71
|
+
if (rest.trimStart().startsWith('"')) {
|
|
72
|
+
const first = scanHeaderToken(rest);
|
|
73
|
+
const second = first ? scanHeaderToken(first.rest) : null;
|
|
74
|
+
if (first && second) {
|
|
75
|
+
const oldPath = parsePatchPathToken(first.token, "a");
|
|
76
|
+
const newPath = parsePatchPathToken(second.token, "b");
|
|
77
|
+
return {
|
|
78
|
+
oldPath: oldPath && oldPath !== "/dev/null" ? oldPath : undefined,
|
|
79
|
+
newPath: newPath && newPath !== "/dev/null" ? newPath : undefined,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const match = header.match(/^diff --git a\/(.+) b\/(.+)$/);
|
|
85
|
+
if (!match) return {};
|
|
86
|
+
return { oldPath: match[1], newPath: match[2] };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function formatPatchPathToken(side: "a" | "b", filePath: string): string {
|
|
90
|
+
if (filePath === "/dev/null") return filePath;
|
|
91
|
+
return quoteGitPath(`${side}/${filePath}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function parseDiffMetadataPathToken(token: string): string {
|
|
95
|
+
if (token === "/dev/null") return token;
|
|
96
|
+
return unquoteGitPath(token);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function formatDiffMetadataPathToken(filePath: string): string {
|
|
100
|
+
if (filePath === "/dev/null") return filePath;
|
|
101
|
+
return quoteGitPath(filePath);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function parseDiffFilePathLines(lines: string[]): DiffPathPair {
|
|
105
|
+
let oldPath: string | undefined;
|
|
106
|
+
let newPath: string | undefined;
|
|
107
|
+
|
|
108
|
+
for (const line of lines) {
|
|
109
|
+
if (line.startsWith("@@ ") || line === "GIT binary patch") break;
|
|
110
|
+
if (line.startsWith("--- ")) {
|
|
111
|
+
const parsed = parsePatchPathToken(line.slice(4), "a");
|
|
112
|
+
if (parsed && parsed !== "/dev/null") oldPath = parsed;
|
|
113
|
+
} else if (line.startsWith("+++ ")) {
|
|
114
|
+
const parsed = parsePatchPathToken(line.slice(4), "b");
|
|
115
|
+
if (parsed && parsed !== "/dev/null") newPath = parsed;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { oldPath, newPath };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function parseDiffMetadataPathLines(lines: string[]): DiffPathPair {
|
|
123
|
+
let oldPath: string | undefined;
|
|
124
|
+
let newPath: string | undefined;
|
|
125
|
+
|
|
126
|
+
for (const line of lines) {
|
|
127
|
+
if (line.startsWith("rename from ") || line.startsWith("copy from ")) {
|
|
128
|
+
const parsed = parseDiffMetadataPathToken(line.slice(line.indexOf(" from ") + " from ".length));
|
|
129
|
+
if (parsed !== "/dev/null") oldPath = parsed;
|
|
130
|
+
} else if (line.startsWith("rename to ") || line.startsWith("copy to ")) {
|
|
131
|
+
const parsed = parseDiffMetadataPathToken(line.slice(line.indexOf(" to ") + " to ".length));
|
|
132
|
+
if (parsed !== "/dev/null") newPath = parsed;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { oldPath, newPath };
|
|
137
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Draft Storage
|
|
3
|
+
*
|
|
4
|
+
* Persists annotation drafts to ~/.plannotator/drafts/ so they survive
|
|
5
|
+
* server crashes. Each draft is keyed by a content hash of the plan/diff
|
|
6
|
+
* it was created against.
|
|
7
|
+
*
|
|
8
|
+
* Runtime-agnostic: uses only node:fs, node:path, node:os, node:crypto.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
import { mkdirSync, writeFileSync, readFileSync, unlinkSync, existsSync } from "fs";
|
|
13
|
+
import { createHash } from "crypto";
|
|
14
|
+
import { getPlannotatorDataDir } from "./data-dir";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get the drafts directory, creating it if needed.
|
|
18
|
+
*/
|
|
19
|
+
export function getDraftDir(): string {
|
|
20
|
+
const dir = join(getPlannotatorDataDir(), "drafts");
|
|
21
|
+
mkdirSync(dir, { recursive: true });
|
|
22
|
+
return dir;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Generate a stable key from content using truncated SHA-256.
|
|
27
|
+
* Same content always produces the same key across server restarts.
|
|
28
|
+
*/
|
|
29
|
+
export function contentHash(content: string): string {
|
|
30
|
+
return createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Save a draft to disk.
|
|
35
|
+
*/
|
|
36
|
+
export function saveDraft(key: string, data: object): void {
|
|
37
|
+
const dir = getDraftDir();
|
|
38
|
+
writeFileSync(join(dir, `${key}.json`), JSON.stringify(data), "utf-8");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Load a draft from disk. Returns null if not found.
|
|
43
|
+
*/
|
|
44
|
+
export function loadDraft(key: string): object | null {
|
|
45
|
+
const filePath = join(getDraftDir(), `${key}.json`);
|
|
46
|
+
try {
|
|
47
|
+
if (!existsSync(filePath)) return null;
|
|
48
|
+
return JSON.parse(readFileSync(filePath, "utf-8"));
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Delete a draft from disk. No-op if not found.
|
|
56
|
+
*/
|
|
57
|
+
export function deleteDraft(key: string): void {
|
|
58
|
+
const filePath = join(getDraftDir(), `${key}.json`);
|
|
59
|
+
try {
|
|
60
|
+
if (existsSync(filePath)) unlinkSync(filePath);
|
|
61
|
+
} catch {
|
|
62
|
+
// Ignore delete failures
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External Annotations — shared types, store logic, and SSE helpers.
|
|
3
|
+
*
|
|
4
|
+
* Runtime-agnostic: no node:fs, no node:http, no Bun APIs.
|
|
5
|
+
* Both the Bun server handler and Pi server handler import this module
|
|
6
|
+
* and wrap it with their respective HTTP transport layers.
|
|
7
|
+
*
|
|
8
|
+
* The store is generic — plan servers store Annotation objects,
|
|
9
|
+
* review servers store CodeAnnotation objects. The mode-specific
|
|
10
|
+
* input transformers handle validation and field assignment.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Types
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
/** Constraint for any annotation type the store can hold. */
|
|
18
|
+
export type StorableAnnotation = { id: string; source?: string };
|
|
19
|
+
|
|
20
|
+
export type ExternalAnnotationEvent<T = unknown> =
|
|
21
|
+
| { type: "snapshot"; annotations: T[] }
|
|
22
|
+
| { type: "add"; annotations: T[] }
|
|
23
|
+
| { type: "remove"; ids: string[] }
|
|
24
|
+
| { type: "clear"; source?: string }
|
|
25
|
+
| { type: "update"; id: string; annotation: T };
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// SSE helpers
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
/** Heartbeat comment to keep SSE connections alive (sent every 30s). */
|
|
32
|
+
export const HEARTBEAT_COMMENT = ":\n\n";
|
|
33
|
+
|
|
34
|
+
/** Interval in ms between heartbeat comments. */
|
|
35
|
+
export const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
36
|
+
|
|
37
|
+
/** Encode an event as an SSE `data:` line. */
|
|
38
|
+
export function serializeSSEEvent<T>(event: ExternalAnnotationEvent<T>): string {
|
|
39
|
+
return `data: ${JSON.stringify(event)}\n\n`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Input validation — shared helpers
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
export interface ParseError {
|
|
47
|
+
error: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Unwrap a POST body into an array of raw input objects.
|
|
52
|
+
*
|
|
53
|
+
* Accepts either:
|
|
54
|
+
* - A single annotation object: `{ source: "...", ... }`
|
|
55
|
+
* - A batch wrapper: `{ annotations: [{ source: "...", ... }, ...] }`
|
|
56
|
+
*/
|
|
57
|
+
function unwrapBody(body: unknown): Record<string, unknown>[] | ParseError {
|
|
58
|
+
if (!body || typeof body !== "object") {
|
|
59
|
+
return { error: "Request body must be a JSON object" };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const obj = body as Record<string, unknown>;
|
|
63
|
+
|
|
64
|
+
// Batch format: { annotations: [...] }
|
|
65
|
+
if (Array.isArray(obj.annotations)) {
|
|
66
|
+
if (obj.annotations.length === 0) {
|
|
67
|
+
return { error: "annotations array must not be empty" };
|
|
68
|
+
}
|
|
69
|
+
const items: Record<string, unknown>[] = [];
|
|
70
|
+
for (let i = 0; i < obj.annotations.length; i++) {
|
|
71
|
+
const item = obj.annotations[i];
|
|
72
|
+
if (!item || typeof item !== "object") {
|
|
73
|
+
return { error: `annotations[${i}] must be an object` };
|
|
74
|
+
}
|
|
75
|
+
items.push(item as Record<string, unknown>);
|
|
76
|
+
}
|
|
77
|
+
return items;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Single format: { source: "...", ... }
|
|
81
|
+
if (typeof obj.source === "string") {
|
|
82
|
+
return [obj as Record<string, unknown>];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { error: 'Missing required "source" field or "annotations" array' };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function requireString(obj: Record<string, unknown>, field: string, index: number): string | ParseError {
|
|
89
|
+
const val = obj[field];
|
|
90
|
+
if (typeof val !== "string" || val.length === 0) {
|
|
91
|
+
return { error: `annotations[${index}] missing required "${field}" field` };
|
|
92
|
+
}
|
|
93
|
+
return val;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Plan mode transformer — produces Annotation objects
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
/** The Annotation type shape for plan mode (mirrors packages/ui/types.ts). */
|
|
101
|
+
interface PlanAnnotation {
|
|
102
|
+
id: string;
|
|
103
|
+
blockId: string;
|
|
104
|
+
startOffset: number;
|
|
105
|
+
endOffset: number;
|
|
106
|
+
type: string; // AnnotationType value
|
|
107
|
+
text?: string;
|
|
108
|
+
originalText: string;
|
|
109
|
+
createdA: number;
|
|
110
|
+
author?: string;
|
|
111
|
+
source?: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const VALID_PLAN_TYPES = ["DELETION", "COMMENT", "GLOBAL_COMMENT"];
|
|
115
|
+
|
|
116
|
+
export function transformPlanInput(
|
|
117
|
+
body: unknown,
|
|
118
|
+
): { annotations: PlanAnnotation[] } | ParseError {
|
|
119
|
+
const items = unwrapBody(body);
|
|
120
|
+
if ("error" in items) return items;
|
|
121
|
+
|
|
122
|
+
const annotations: PlanAnnotation[] = [];
|
|
123
|
+
for (let i = 0; i < items.length; i++) {
|
|
124
|
+
const obj = items[i];
|
|
125
|
+
|
|
126
|
+
const source = requireString(obj, "source", i);
|
|
127
|
+
if (typeof source !== "string") return source;
|
|
128
|
+
|
|
129
|
+
// Must have text content
|
|
130
|
+
if (typeof obj.text !== "string" || obj.text.length === 0) {
|
|
131
|
+
return { error: `annotations[${i}] missing required "text" field` };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Validate type if provided, default to GLOBAL_COMMENT
|
|
135
|
+
const type = typeof obj.type === "string" ? obj.type : "GLOBAL_COMMENT";
|
|
136
|
+
if (!VALID_PLAN_TYPES.includes(type)) {
|
|
137
|
+
return {
|
|
138
|
+
error: `annotations[${i}] invalid type "${type}". Must be one of: ${VALID_PLAN_TYPES.join(", ")}`,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// DELETION requires originalText (the text to remove)
|
|
143
|
+
if (type === "DELETION" && (typeof obj.originalText !== "string" || obj.originalText.length === 0)) {
|
|
144
|
+
return { error: `annotations[${i}] DELETION type requires non-empty "originalText" field` };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// COMMENT requires originalText so the renderer can pin it to a phrase.
|
|
148
|
+
// External agents that want sidebar-only feedback should use GLOBAL_COMMENT
|
|
149
|
+
// instead — without a phrase to anchor to, a COMMENT renders as an empty
|
|
150
|
+
// quote bubble in the sidebar and exports as `Feedback on: ""`.
|
|
151
|
+
if (type === "COMMENT" && (typeof obj.originalText !== "string" || obj.originalText.length === 0)) {
|
|
152
|
+
return {
|
|
153
|
+
error: `annotations[${i}] COMMENT requires non-empty "originalText" field. Use GLOBAL_COMMENT for sidebar-only feedback.`,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
annotations.push({
|
|
158
|
+
id: crypto.randomUUID(),
|
|
159
|
+
blockId: "external",
|
|
160
|
+
startOffset: 0,
|
|
161
|
+
endOffset: 0,
|
|
162
|
+
type,
|
|
163
|
+
text: String(obj.text),
|
|
164
|
+
originalText: typeof obj.originalText === "string" ? obj.originalText : "",
|
|
165
|
+
createdA: Date.now(),
|
|
166
|
+
author: typeof obj.author === "string" ? obj.author : undefined,
|
|
167
|
+
source,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return { annotations };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// Review mode transformer — produces CodeAnnotation objects
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
/** The CodeAnnotation type shape for review mode (mirrors packages/ui/types.ts). */
|
|
179
|
+
interface ReviewAnnotation {
|
|
180
|
+
id: string;
|
|
181
|
+
type: string; // CodeAnnotationType value
|
|
182
|
+
scope?: string;
|
|
183
|
+
filePath: string;
|
|
184
|
+
lineStart: number;
|
|
185
|
+
lineEnd: number;
|
|
186
|
+
side: string;
|
|
187
|
+
text?: string;
|
|
188
|
+
suggestedCode?: string;
|
|
189
|
+
originalCode?: string;
|
|
190
|
+
createdAt: number;
|
|
191
|
+
author?: string;
|
|
192
|
+
source?: string;
|
|
193
|
+
// Agent review metadata (optional — only set by Claude review findings)
|
|
194
|
+
severity?: string; // "important" | "nit" | "pre_existing"
|
|
195
|
+
reasoning?: string; // Validation chain explaining how the issue was confirmed
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const VALID_REVIEW_TYPES = ["comment", "suggestion", "concern"];
|
|
199
|
+
const VALID_SIDES = ["old", "new"];
|
|
200
|
+
const VALID_SCOPES = ["line", "file"];
|
|
201
|
+
|
|
202
|
+
export function transformReviewInput(
|
|
203
|
+
body: unknown,
|
|
204
|
+
): { annotations: ReviewAnnotation[] } | ParseError {
|
|
205
|
+
const items = unwrapBody(body);
|
|
206
|
+
if ("error" in items) return items;
|
|
207
|
+
|
|
208
|
+
const annotations: ReviewAnnotation[] = [];
|
|
209
|
+
for (let i = 0; i < items.length; i++) {
|
|
210
|
+
const obj = items[i];
|
|
211
|
+
|
|
212
|
+
const source = requireString(obj, "source", i);
|
|
213
|
+
if (typeof source !== "string") return source;
|
|
214
|
+
|
|
215
|
+
const filePath = requireString(obj, "filePath", i);
|
|
216
|
+
if (typeof filePath !== "string") return filePath;
|
|
217
|
+
|
|
218
|
+
if (typeof obj.lineStart !== "number") {
|
|
219
|
+
return { error: `annotations[${i}] missing required "lineStart" field` };
|
|
220
|
+
}
|
|
221
|
+
if (typeof obj.lineEnd !== "number") {
|
|
222
|
+
return { error: `annotations[${i}] missing required "lineEnd" field` };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// side: optional, defaults to "new"
|
|
226
|
+
const side = typeof obj.side === "string" ? obj.side : "new";
|
|
227
|
+
if (!VALID_SIDES.includes(side)) {
|
|
228
|
+
return {
|
|
229
|
+
error: `annotations[${i}] invalid side "${side}". Must be one of: ${VALID_SIDES.join(", ")}`,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// type: optional, defaults to "comment"
|
|
234
|
+
const type = typeof obj.type === "string" ? obj.type : "comment";
|
|
235
|
+
if (!VALID_REVIEW_TYPES.includes(type)) {
|
|
236
|
+
return {
|
|
237
|
+
error: `annotations[${i}] invalid type "${type}". Must be one of: ${VALID_REVIEW_TYPES.join(", ")}`,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// scope: optional, defaults to "line"
|
|
242
|
+
const scope = typeof obj.scope === "string" ? obj.scope : "line";
|
|
243
|
+
if (!VALID_SCOPES.includes(scope)) {
|
|
244
|
+
return {
|
|
245
|
+
error: `annotations[${i}] invalid scope "${scope}". Must be one of: ${VALID_SCOPES.join(", ")}`,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Must have at least text or suggestedCode
|
|
250
|
+
if (typeof obj.text !== "string" && typeof obj.suggestedCode !== "string") {
|
|
251
|
+
return {
|
|
252
|
+
error: `annotations[${i}] must have at least one of: text, suggestedCode`,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
annotations.push({
|
|
257
|
+
id: crypto.randomUUID(),
|
|
258
|
+
type,
|
|
259
|
+
scope,
|
|
260
|
+
filePath,
|
|
261
|
+
lineStart: obj.lineStart,
|
|
262
|
+
lineEnd: obj.lineEnd,
|
|
263
|
+
side,
|
|
264
|
+
text: typeof obj.text === "string" ? obj.text : undefined,
|
|
265
|
+
suggestedCode: typeof obj.suggestedCode === "string" ? obj.suggestedCode : undefined,
|
|
266
|
+
originalCode: typeof obj.originalCode === "string" ? obj.originalCode : undefined,
|
|
267
|
+
createdAt: Date.now(),
|
|
268
|
+
author: typeof obj.author === "string" ? obj.author : undefined,
|
|
269
|
+
source,
|
|
270
|
+
// Agent review metadata (optional — only set by Claude review findings)
|
|
271
|
+
...(typeof obj.severity === "string" && { severity: obj.severity }),
|
|
272
|
+
...(typeof obj.reasoning === "string" && { reasoning: obj.reasoning }),
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return { annotations };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
// Annotation Store (generic)
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
type MutationListener<T> = (event: ExternalAnnotationEvent<T>) => void;
|
|
284
|
+
|
|
285
|
+
export interface AnnotationStore<T extends StorableAnnotation> {
|
|
286
|
+
/** Add fully-formed annotations. Returns the added annotations. */
|
|
287
|
+
add(items: T[]): T[];
|
|
288
|
+
/** Remove an annotation by ID. Returns true if found. */
|
|
289
|
+
remove(id: string): boolean;
|
|
290
|
+
/** Remove all annotations from a specific source. Returns count removed. */
|
|
291
|
+
clearBySource(source: string): number;
|
|
292
|
+
/** Update an annotation by ID. Returns the updated annotation, or null if not found. */
|
|
293
|
+
update(id: string, fields: Partial<T>): T | null;
|
|
294
|
+
/** Remove all annotations. Returns count removed. */
|
|
295
|
+
clearAll(): number;
|
|
296
|
+
/** Get all annotations (snapshot). */
|
|
297
|
+
getAll(): T[];
|
|
298
|
+
/** Monotonic version counter — incremented on every mutation. */
|
|
299
|
+
readonly version: number;
|
|
300
|
+
/** Register a listener for mutation events. Returns unsubscribe function. */
|
|
301
|
+
onMutation(listener: MutationListener<T>): () => void;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Create an in-memory annotation store.
|
|
306
|
+
*
|
|
307
|
+
* The store is runtime-agnostic — it holds data and emits events.
|
|
308
|
+
* HTTP transport (SSE broadcasting, request parsing) is handled by
|
|
309
|
+
* the server-specific adapter (Bun or Pi).
|
|
310
|
+
*/
|
|
311
|
+
export function createAnnotationStore<T extends StorableAnnotation>(): AnnotationStore<T> {
|
|
312
|
+
const annotations: T[] = [];
|
|
313
|
+
const listeners = new Set<MutationListener<T>>();
|
|
314
|
+
let version = 0;
|
|
315
|
+
|
|
316
|
+
function emit(event: ExternalAnnotationEvent<T>): void {
|
|
317
|
+
for (const listener of listeners) {
|
|
318
|
+
try {
|
|
319
|
+
listener(event);
|
|
320
|
+
} catch {
|
|
321
|
+
// Don't let a failing listener break the store
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
add(items) {
|
|
328
|
+
if (items.length > 0) {
|
|
329
|
+
for (const item of items) {
|
|
330
|
+
annotations.push(item);
|
|
331
|
+
}
|
|
332
|
+
version++;
|
|
333
|
+
emit({ type: "add", annotations: items });
|
|
334
|
+
}
|
|
335
|
+
return items;
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
remove(id) {
|
|
339
|
+
const idx = annotations.findIndex((a) => a.id === id);
|
|
340
|
+
if (idx === -1) return false;
|
|
341
|
+
annotations.splice(idx, 1);
|
|
342
|
+
version++;
|
|
343
|
+
emit({ type: "remove", ids: [id] });
|
|
344
|
+
return true;
|
|
345
|
+
},
|
|
346
|
+
|
|
347
|
+
update(id, fields) {
|
|
348
|
+
const idx = annotations.findIndex((a) => a.id === id);
|
|
349
|
+
if (idx === -1) return null;
|
|
350
|
+
const merged = { ...annotations[idx], ...fields, id } as T;
|
|
351
|
+
annotations[idx] = merged;
|
|
352
|
+
version++;
|
|
353
|
+
emit({ type: "update", id, annotation: merged });
|
|
354
|
+
return merged;
|
|
355
|
+
},
|
|
356
|
+
|
|
357
|
+
clearBySource(source) {
|
|
358
|
+
const before = annotations.length;
|
|
359
|
+
for (let i = annotations.length - 1; i >= 0; i--) {
|
|
360
|
+
if (annotations[i].source === source) {
|
|
361
|
+
annotations.splice(i, 1);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
const removed = before - annotations.length;
|
|
365
|
+
if (removed > 0) {
|
|
366
|
+
version++;
|
|
367
|
+
emit({ type: "clear", source });
|
|
368
|
+
}
|
|
369
|
+
return removed;
|
|
370
|
+
},
|
|
371
|
+
|
|
372
|
+
clearAll() {
|
|
373
|
+
const count = annotations.length;
|
|
374
|
+
if (count > 0) {
|
|
375
|
+
annotations.length = 0;
|
|
376
|
+
version++;
|
|
377
|
+
emit({ type: "clear" });
|
|
378
|
+
}
|
|
379
|
+
return count;
|
|
380
|
+
},
|
|
381
|
+
|
|
382
|
+
getAll() {
|
|
383
|
+
return [...annotations];
|
|
384
|
+
},
|
|
385
|
+
|
|
386
|
+
get version() {
|
|
387
|
+
return version;
|
|
388
|
+
},
|
|
389
|
+
|
|
390
|
+
onMutation(listener) {
|
|
391
|
+
listeners.add(listener);
|
|
392
|
+
return () => {
|
|
393
|
+
listeners.delete(listener);
|
|
394
|
+
};
|
|
395
|
+
},
|
|
396
|
+
};
|
|
397
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { extractCandidateCodePaths } from "./extract-code-paths";
|
|
3
|
+
|
|
4
|
+
describe("extractCandidateCodePaths", () => {
|
|
5
|
+
test("extracts backtick code-file paths", () => {
|
|
6
|
+
const md = "Open `packages/editor/App.tsx` to see the code.";
|
|
7
|
+
expect(extractCandidateCodePaths(md)).toEqual(["packages/editor/App.tsx"]);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("extracts bare-prose paths", () => {
|
|
11
|
+
const md = "see editor/App.tsx and review-editor/App.tsx";
|
|
12
|
+
const out = extractCandidateCodePaths(md);
|
|
13
|
+
expect(out).toContain("editor/App.tsx");
|
|
14
|
+
expect(out).toContain("review-editor/App.tsx");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("dedupes repeated references", () => {
|
|
18
|
+
const md = "`src/foo.ts` and src/foo.ts again";
|
|
19
|
+
expect(extractCandidateCodePaths(md)).toEqual(["src/foo.ts"]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("strips line anchors", () => {
|
|
23
|
+
const md = "see `src/foo.ts#L42`";
|
|
24
|
+
expect(extractCandidateCodePaths(md)).toEqual(["src/foo.ts"]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("rejects shell brace expansion", () => {
|
|
28
|
+
const md = "files in packages/ui/{a,b}.ts";
|
|
29
|
+
expect(extractCandidateCodePaths(md)).toEqual([]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("ignores fenced code blocks", () => {
|
|
33
|
+
const md = "```ts\nimport foo from 'src/foo.ts';\n```";
|
|
34
|
+
expect(extractCandidateCodePaths(md)).toEqual([]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("ignores HTML comments", () => {
|
|
38
|
+
const md = "<!-- src/foo.ts is a placeholder -->";
|
|
39
|
+
expect(extractCandidateCodePaths(md)).toEqual([]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("URLs do not produce path-shaped substrings", () => {
|
|
43
|
+
const md = "see https://github.com/foo/bar.ts in the docs";
|
|
44
|
+
expect(extractCandidateCodePaths(md)).toEqual([]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("URL on same line as a real path keeps the path", () => {
|
|
48
|
+
const md = "https://github.com/example.com docs and editor/App.tsx";
|
|
49
|
+
const out = extractCandidateCodePaths(md);
|
|
50
|
+
expect(out).toContain("editor/App.tsx");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("URLs containing parens or brackets do not leak path-shaped substrings", () => {
|
|
54
|
+
// Wikipedia-style URL ends in `).ts` — earlier extractor erroneously
|
|
55
|
+
// stopped its URL match at `(`, leaving `bar).ts` as a path candidate.
|
|
56
|
+
const md = "see https://en.wikipedia.org/wiki/Foo_(bar).ts in the docs";
|
|
57
|
+
expect(extractCandidateCodePaths(md)).toEqual([]);
|
|
58
|
+
});
|
|
59
|
+
});
|