pi-ui-extend 1.0.42 → 1.0.46
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/README.md +41 -12
- package/dist/app/app.d.ts +10 -0
- package/dist/app/app.js +132 -8
- package/dist/app/cli/install.js +2 -2
- package/dist/app/commands/command-controller.d.ts +4 -0
- package/dist/app/commands/command-controller.js +11 -0
- package/dist/app/commands/command-git-actions.d.ts +25 -0
- package/dist/app/commands/command-git-actions.js +381 -0
- package/dist/app/commands/command-host.d.ts +3 -0
- package/dist/app/commands/command-host.js +5 -2
- package/dist/app/commands/command-model-actions.d.ts +1 -0
- package/dist/app/commands/command-model-actions.js +51 -34
- package/dist/app/commands/command-navigation-actions.d.ts +1 -0
- package/dist/app/commands/command-navigation-actions.js +16 -3
- package/dist/app/commands/command-registry.d.ts +2 -0
- package/dist/app/commands/command-registry.js +14 -0
- package/dist/app/commands/reload-context-inventory.js +12 -1
- package/dist/app/input/autocomplete-controller.js +61 -34
- package/dist/app/input/input-action-controller.d.ts +2 -0
- package/dist/app/input/input-action-controller.js +8 -1
- package/dist/app/input/input-controller.d.ts +2 -1
- package/dist/app/input/input-controller.js +7 -2
- package/dist/app/input/prompt-enhancer-controller.js +53 -35
- package/dist/app/input/voice-controller.d.ts +41 -45
- package/dist/app/input/voice-controller.js +351 -419
- package/dist/app/model/model-usage-controller.js +17 -7
- package/dist/app/model/model-usage-status.d.ts +4 -1
- package/dist/app/model/model-usage-status.js +270 -46
- package/dist/app/popup/menu-items-controller.d.ts +13 -3
- package/dist/app/popup/menu-items-controller.js +37 -21
- package/dist/app/popup/popup-action-controller.d.ts +12 -2
- package/dist/app/popup/popup-action-controller.js +77 -24
- package/dist/app/popup/popup-menu-controller.d.ts +36 -14
- package/dist/app/popup/popup-menu-controller.js +239 -69
- package/dist/app/rendering/dcp-stats.d.ts +9 -4
- package/dist/app/rendering/dcp-stats.js +40 -425
- package/dist/app/rendering/popup-menu-renderer.d.ts +3 -5
- package/dist/app/rendering/popup-menu-renderer.js +42 -37
- package/dist/app/rendering/render-controller.js +23 -2
- package/dist/app/rendering/status-line-renderer.d.ts +5 -0
- package/dist/app/rendering/status-line-renderer.js +41 -5
- package/dist/app/rendering/tab-line-renderer.js +26 -20
- package/dist/app/runtime.d.ts +12 -1
- package/dist/app/runtime.js +120 -12
- package/dist/app/screen/mouse-controller.d.ts +2 -0
- package/dist/app/screen/mouse-controller.js +17 -7
- package/dist/app/screen/status-controller.d.ts +4 -0
- package/dist/app/screen/status-controller.js +5 -0
- package/dist/app/session/lazy-session-manager.js +34 -0
- package/dist/app/session/session-event-controller.d.ts +1 -0
- package/dist/app/session/session-event-controller.js +10 -1
- package/dist/app/session/session-history.d.ts +1 -0
- package/dist/app/session/session-history.js +12 -1
- package/dist/app/session/session-lifecycle-controller.d.ts +6 -1
- package/dist/app/session/session-lifecycle-controller.js +10 -1
- package/dist/app/session/tabs-controller.d.ts +25 -1
- package/dist/app/session/tabs-controller.js +442 -153
- package/dist/app/subagents/subagents-widget-controller.d.ts +1 -0
- package/dist/app/subagents/subagents-widget-controller.js +6 -0
- package/dist/app/types.d.ts +11 -1
- package/dist/app/workspace/workspace-actions-controller.js +10 -2
- package/dist/app/workspace/workspace-undo.d.ts +1 -0
- package/dist/app/workspace/workspace-undo.js +1 -0
- package/dist/bundled-extensions/question/index.js +9 -1
- package/dist/bundled-extensions/question/remote.d.ts +4 -0
- package/dist/bundled-extensions/question/remote.js +33 -0
- package/dist/bundled-extensions/telegram-connector/bot.d.ts +43 -0
- package/dist/bundled-extensions/telegram-connector/bot.js +166 -0
- package/dist/bundled-extensions/telegram-connector/config.d.ts +8 -0
- package/dist/bundled-extensions/telegram-connector/config.js +87 -0
- package/dist/bundled-extensions/telegram-connector/coordinator.d.ts +66 -0
- package/dist/bundled-extensions/telegram-connector/coordinator.js +413 -0
- package/dist/bundled-extensions/telegram-connector/index.d.ts +3 -0
- package/dist/bundled-extensions/telegram-connector/index.js +195 -0
- package/dist/bundled-extensions/terminal-bell/index.d.ts +0 -8
- package/dist/bundled-extensions/terminal-bell/index.js +0 -76
- package/dist/bundled-extensions/workspace-undo/index.d.ts +26 -0
- package/dist/bundled-extensions/workspace-undo/index.js +191 -0
- package/dist/config.d.ts +21 -2
- package/dist/config.js +159 -32
- package/dist/default-pix-config.js +20 -10
- package/dist/schemas/pi-tools-suite-schema.d.ts +8 -4
- package/dist/schemas/pi-tools-suite-schema.js +11 -5
- package/dist/schemas/pix-schema.d.ts +12 -2
- package/dist/schemas/pix-schema.js +37 -9
- package/dist/schemas/tasks-schema.js +1 -1
- package/docs/concurrency.md +3 -1
- package/docs/desktop-mvp.md +10 -5
- package/docs/desktop-task-manager.md +81 -28
- package/docs/release.md +30 -6
- package/external/pi-tools-suite/README.md +165 -39
- package/external/pi-tools-suite/docs/session-recovery.md +7 -0
- package/external/pi-tools-suite/docs/subagent-model-pools.md +12 -5
- package/external/pi-tools-suite/docs/ui-qa-subagent.md +440 -0
- package/external/pi-tools-suite/package.json +6 -1
- package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +2 -1
- package/external/pi-tools-suite/src/antigravity-auth/constants.ts +9 -3
- package/external/pi-tools-suite/src/antigravity-auth/headers.ts +41 -3
- package/external/pi-tools-suite/src/antigravity-auth/models.ts +94 -21
- package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +5 -17
- package/external/pi-tools-suite/src/antigravity-auth/payload.ts +72 -7
- package/external/pi-tools-suite/src/antigravity-auth/stream.ts +13 -1
- package/external/pi-tools-suite/src/async-subagents/agents/implement.md +1 -1
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/browser.mjs +346 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/desktop.mjs +1038 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/tui.mjs +759 -0
- package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/scripts/browser-qa-runner.mjs +32 -23
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/chrome-devtools/chrome-devtools-provider.mjs +895 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/linux/linux-atspi.py +494 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/macos/macos-accessibility.swift +1085 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/native-terminal/bridge-client.mjs +50 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/native-terminal/native-terminal-host.mjs +501 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/windows/windows-uia.ps1 +454 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-auth.md +114 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-chrome-devtools.md +91 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-playwright.md +145 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser.md +82 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-linux.md +26 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-macos.md +29 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-windows.md +23 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop.md +54 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui-native-terminal.md +59 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui-pty.md +37 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui.md +60 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/scripts/ui-qa-runner.mjs +535 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa.md +75 -0
- package/external/pi-tools-suite/src/async-subagents/commands.ts +1 -1
- package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +1 -1
- package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +0 -2
- package/external/pi-tools-suite/src/async-subagents/core/browser-qa.ts +16 -2
- package/external/pi-tools-suite/src/async-subagents/core/config.ts +40 -17
- package/external/pi-tools-suite/src/async-subagents/core/model-selection.ts +2 -1
- package/external/pi-tools-suite/src/async-subagents/core/registry.ts +158 -33
- package/external/pi-tools-suite/src/async-subagents/core/routing.ts +7 -4
- package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +34 -24
- package/external/pi-tools-suite/src/async-subagents/core/ultrawork-auto.ts +51 -45
- package/external/pi-tools-suite/src/async-subagents/lib.ts +9 -1
- package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +0 -1
- package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +2 -2
- package/external/pi-tools-suite/src/coding-discipline/index.ts +90 -59
- package/external/pi-tools-suite/src/config.ts +14 -0
- package/external/pi-tools-suite/src/context-gateway/accounting-log.ts +282 -0
- package/external/pi-tools-suite/src/context-gateway/config.ts +66 -1
- package/external/pi-tools-suite/src/context-gateway/efficiency.ts +414 -0
- package/external/pi-tools-suite/src/context-gateway/enforcement.ts +123 -3
- package/external/pi-tools-suite/src/context-gateway/index.ts +137 -4
- package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +5 -5
- package/external/pi-tools-suite/src/context-gateway/telemetry.ts +2 -2
- package/external/pi-tools-suite/src/context-gateway/types.ts +8 -1
- package/external/pi-tools-suite/src/dcp/auto-compress-budget.ts +39 -4
- package/external/pi-tools-suite/src/dcp/auto-compress.ts +41 -14
- package/external/pi-tools-suite/src/dcp/commands.ts +14 -156
- package/external/pi-tools-suite/src/dcp/compress-tool.ts +8 -2
- package/external/pi-tools-suite/src/dcp/compression-progress.ts +22 -0
- package/external/pi-tools-suite/src/dcp/config.ts +58 -4
- package/external/pi-tools-suite/src/dcp/conversation-index.ts +36 -9
- package/external/pi-tools-suite/src/dcp/diagnostics.ts +41 -0
- package/external/pi-tools-suite/src/dcp/fresh-tool-results.ts +38 -0
- package/external/pi-tools-suite/src/dcp/index.ts +338 -66
- package/external/pi-tools-suite/src/dcp/journal.ts +126 -4
- package/external/pi-tools-suite/src/dcp/progress-controller.ts +2 -1
- package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +92 -2
- package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +36 -33
- package/external/pi-tools-suite/src/dcp/pruner.ts +1 -0
- package/external/pi-tools-suite/src/dcp/routine-pressure.ts +86 -0
- package/external/pi-tools-suite/src/dcp/state.ts +9 -0
- package/external/pi-tools-suite/src/dcp/statistics.d.ts +8 -0
- package/external/pi-tools-suite/src/dcp/statistics.js +156 -0
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +68 -16
- package/external/pi-tools-suite/src/lib/project.ts +36 -1
- package/external/pi-tools-suite/src/model-tools/index.ts +10 -7
- package/external/pi-tools-suite/src/repo-discovery/index.ts +304 -4
- package/external/pi-tools-suite/src/resource-registry/index.ts +282 -21
- package/external/pi-tools-suite/src/session-recovery/index.ts +17 -0
- package/external/pi-tools-suite/src/todo/index.ts +21 -0
- package/external/pi-tools-suite/src/todo/todo.ts +19 -1
- package/external/pi-tools-suite/src/tool-descriptions.ts +44 -15
- package/package.json +9 -10
- package/schemas/pi-tools-suite.json +44 -12
- package/schemas/pix.json +109 -15
- package/schemas/tasks.json +1 -2
- package/skills/simplify/SKILL.md +33 -5
- package/external/pi-tools-suite/docs/browser-qa-subagent.md +0 -196
- package/external/pi-tools-suite/src/async-subagents/agents/browser-qa.md +0 -598
- package/skills/playwright-cli/SKILL.md +0 -420
- package/skills/playwright-cli/references/element-attributes.md +0 -23
- package/skills/playwright-cli/references/playwright-tests.md +0 -50
- package/skills/playwright-cli/references/request-mocking.md +0 -87
- package/skills/playwright-cli/references/running-code.md +0 -241
- package/skills/playwright-cli/references/session-management.md +0 -273
- package/skills/playwright-cli/references/spec-driven-testing.md +0 -311
- package/skills/playwright-cli/references/storage-state.md +0 -290
- package/skills/playwright-cli/references/test-generation.md +0 -142
- package/skills/playwright-cli/references/tracing.md +0 -154
- package/skills/playwright-cli/references/video-recording.md +0 -147
- package/skills/spec-wiki/SKILL.md +0 -55
- package/skills/spec-wiki/evals/evals.json +0 -132
- package/skills/spec-wiki/evals/files/bootstrap/architecture/rfc-007.md +0 -14
- package/skills/spec-wiki/evals/files/bootstrap/docs/how-to-pay.md +0 -4
- package/skills/spec-wiki/evals/files/bootstrap/docs/overview.md +0 -9
- package/skills/spec-wiki/evals/files/bootstrap/requirements/payment-behavior.md +0 -17
- package/skills/spec-wiki/evals/files/bootstrap/src/payments.ts +0 -12
- package/skills/spec-wiki/evals/files/bootstrap/tests/payments.test.ts +0 -11
- package/skills/spec-wiki/evals/files/focused/src/session.ts +0 -5
- package/skills/spec-wiki/evals/files/focused/tests/session.test.ts +0 -17
- package/skills/spec-wiki/evals/files/maintenance/low-signal/incoming/capture.txt +0 -2
- package/skills/spec-wiki/evals/files/maintenance/low-signal/src/capture.ts +0 -3
- package/skills/spec-wiki/evals/files/maintenance/move/docs/old-job-contract.md +0 -18
- package/skills/spec-wiki/evals/files/maintenance/move/src/job.ts +0 -5
- package/skills/spec-wiki/evals/files/maintenance/new-relation/docs/session-refresh.md +0 -24
- package/skills/spec-wiki/evals/files/maintenance/new-relation/incoming/refresh-worker.ts +0 -7
- package/skills/spec-wiki/evals/files/maintenance/new-relation/src/session.ts +0 -7
- package/skills/spec-wiki/evals/files/maintenance/no-impact/docs/auth-contract.md +0 -17
- package/skills/spec-wiki/evals/files/maintenance/no-impact/incoming/telemetry-format.ts +0 -3
- package/skills/spec-wiki/evals/files/maintenance/no-impact/src/auth.ts +0 -3
- package/skills/spec-wiki/evals/files/query/docs/how-to-run-jobs.md +0 -4
- package/skills/spec-wiki/evals/files/query/docs/spec-overview.md +0 -9
- package/skills/spec-wiki/evals/files/query/features/jobs/cancellation-contract.md +0 -15
- package/skills/spec-wiki/evals/files/query/features/jobs/retry-spec.md +0 -6
- package/skills/spec-wiki/evals/maintenance_regression.py +0 -311
- package/skills/spec-wiki/evals/retrieval-eval.md +0 -37
- package/skills/spec-wiki/evals/retrieval-evals.json +0 -37
- package/skills/spec-wiki/evals/trigger-evals.json +0 -94
- package/skills/spec-wiki/references/bootstrap.md +0 -100
- package/skills/spec-wiki/references/focused-spec.md +0 -117
- package/skills/spec-wiki/references/index-schema.md +0 -330
- package/skills/spec-wiki/references/maintenance.md +0 -134
- package/skills/spec-wiki/references/query.md +0 -74
- package/skills/spec-wiki/scripts/spec_wiki.py +0 -2076
- /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/examples/qa-auth.example.jsonc +0 -0
- /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/examples/qa-flow.example.jsonc +0 -0
- /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/vendor/fflate.LICENSE +0 -0
- /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/vendor/fflate.mjs +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { existsSync, promises as fs } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import { basename, dirname, isAbsolute, join, relative } from "node:path";
|
|
4
|
+
import { basename, dirname, isAbsolute, join, relative, sep } from "node:path";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
6
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
6
7
|
import { applyEdits, modify } from "jsonc-parser";
|
|
7
8
|
|
|
@@ -19,8 +20,10 @@ const REGISTRY_AGENTS_DIR = "agents";
|
|
|
19
20
|
const REGISTRY_PROJECTS_DIR = "projects";
|
|
20
21
|
const SKILL_FILE = "SKILL.md";
|
|
21
22
|
const PROJECT_TASKS_FILE = "tasks.jsonc";
|
|
23
|
+
const PROJECT_TASK_ATTACHMENTS_DIR = "task-attachments";
|
|
22
24
|
const PROJECT_PLANS_DIR = "plans";
|
|
23
25
|
const PROJECT_TODO_FILE = "TODO.md";
|
|
26
|
+
const REGISTRY_TASK_ATTACHMENT_SCHEME = "pix-task-attachment:";
|
|
24
27
|
const PROVENANCE_FILE = "registry.json";
|
|
25
28
|
const PROVENANCE_VERSION = 1;
|
|
26
29
|
const SYSTEM_CUSTOM_MESSAGE_TYPE = "pix-system";
|
|
@@ -240,6 +243,10 @@ function projectArtifactLocalPath(cwd: string, artifact: ProjectArtifact): strin
|
|
|
240
243
|
}
|
|
241
244
|
}
|
|
242
245
|
|
|
246
|
+
function projectTaskAttachmentsLocalPath(cwd: string): string {
|
|
247
|
+
return join(cwd, PROJECT_DIR, PROJECT_TASK_ATTACHMENTS_DIR);
|
|
248
|
+
}
|
|
249
|
+
|
|
243
250
|
function projectArtifactRelativePath(projectKey: string, artifact: ProjectArtifact): string {
|
|
244
251
|
validateName(projectKey);
|
|
245
252
|
switch (artifact) {
|
|
@@ -249,6 +256,18 @@ function projectArtifactRelativePath(projectKey: string, artifact: ProjectArtifa
|
|
|
249
256
|
}
|
|
250
257
|
}
|
|
251
258
|
|
|
259
|
+
function projectTaskAttachmentsRelativePath(projectKey: string): string {
|
|
260
|
+
validateName(projectKey);
|
|
261
|
+
return `${REGISTRY_PROJECTS_DIR}/${projectKey}/${PROJECT_TASK_ATTACHMENTS_DIR}`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function projectArtifactRelativePaths(projectKey: string, artifact: ProjectArtifact): string[] {
|
|
265
|
+
const primary = projectArtifactRelativePath(projectKey, artifact);
|
|
266
|
+
return artifact === "tasks"
|
|
267
|
+
? [primary, projectTaskAttachmentsRelativePath(projectKey)]
|
|
268
|
+
: [primary];
|
|
269
|
+
}
|
|
270
|
+
|
|
252
271
|
function projectArtifactDisplayName(artifact: ProjectArtifact): string {
|
|
253
272
|
switch (artifact) {
|
|
254
273
|
case "tasks": return PROJECT_TASKS_FILE;
|
|
@@ -261,6 +280,10 @@ function projectArtifactRegistryPath(cacheDir: string, projectKey: string, artif
|
|
|
261
280
|
return join(cacheDir, ...projectArtifactRelativePath(projectKey, artifact).split("/"));
|
|
262
281
|
}
|
|
263
282
|
|
|
283
|
+
function projectTaskAttachmentsRegistryPath(cacheDir: string, projectKey: string): string {
|
|
284
|
+
return join(cacheDir, ...projectTaskAttachmentsRelativePath(projectKey).split("/"));
|
|
285
|
+
}
|
|
286
|
+
|
|
264
287
|
function canonicalGitRemote(remote: string): string | undefined {
|
|
265
288
|
const trimmed = remote.trim();
|
|
266
289
|
if (!trimmed) return undefined;
|
|
@@ -510,6 +533,22 @@ async function pathRevision(pi: ExtensionAPI, runtime: RegistryRuntime, rel: str
|
|
|
510
533
|
return revision || undefined;
|
|
511
534
|
}
|
|
512
535
|
|
|
536
|
+
async function projectArtifactRevision(
|
|
537
|
+
pi: ExtensionAPI,
|
|
538
|
+
runtime: RegistryRuntime,
|
|
539
|
+
projectKey: string,
|
|
540
|
+
artifact: ProjectArtifact,
|
|
541
|
+
): Promise<string | undefined> {
|
|
542
|
+
const result = await runGit(
|
|
543
|
+
pi,
|
|
544
|
+
runtime.cacheDir,
|
|
545
|
+
["log", "-1", "--format=%H", "--", ...projectArtifactRelativePaths(projectKey, artifact)],
|
|
546
|
+
{ allowFailure: true },
|
|
547
|
+
);
|
|
548
|
+
const revision = result.stdout.trim();
|
|
549
|
+
return revision || undefined;
|
|
550
|
+
}
|
|
551
|
+
|
|
513
552
|
async function resourceRevision(pi: ExtensionAPI, runtime: RegistryRuntime, type: ResourceType, name: string): Promise<string | undefined> {
|
|
514
553
|
return pathRevision(pi, runtime, registryResourceRelativePath(type, name));
|
|
515
554
|
}
|
|
@@ -666,6 +705,226 @@ async function hashPath(path: string): Promise<string> {
|
|
|
666
705
|
return hash.digest("hex");
|
|
667
706
|
}
|
|
668
707
|
|
|
708
|
+
type TaskBundle = {
|
|
709
|
+
source: string;
|
|
710
|
+
attachments: Map<string, string>;
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
function pathIsWithin(parent: string, child: string): boolean {
|
|
714
|
+
const rel = relative(parent, child);
|
|
715
|
+
return rel === "" || (!isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep}`));
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function registryTaskAttachmentMarker(name: string): string {
|
|
719
|
+
return `[Pix attachment: ${REGISTRY_TASK_ATTACHMENT_SCHEME}${encodeURIComponent(name)}]`;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function registryTaskAttachmentName(encoded: string): string {
|
|
723
|
+
let name: string;
|
|
724
|
+
try {
|
|
725
|
+
name = decodeURIComponent(encoded);
|
|
726
|
+
} catch {
|
|
727
|
+
throw new Error(`Invalid registry task attachment name: ${encoded}`);
|
|
728
|
+
}
|
|
729
|
+
if (!name || name === "." || name === ".." || name.includes("/") || name.includes("\\") || basename(name) !== name) {
|
|
730
|
+
throw new Error(`Invalid registry task attachment name: ${encoded}`);
|
|
731
|
+
}
|
|
732
|
+
return name;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
async function readLocalTaskBundle(cwd: string): Promise<TaskBundle> {
|
|
736
|
+
const tasksPath = projectArtifactLocalPath(cwd, "tasks");
|
|
737
|
+
const source = await fs.readFile(tasksPath, "utf8");
|
|
738
|
+
const attachmentsRoot = projectTaskAttachmentsLocalPath(cwd);
|
|
739
|
+
const attachments = new Map<string, string>();
|
|
740
|
+
const replacements = new Map<string, string>();
|
|
741
|
+
const rootExists = await pathExists(attachmentsRoot);
|
|
742
|
+
let canonicalRoot: string | undefined;
|
|
743
|
+
if (rootExists) {
|
|
744
|
+
const rootStat = await fs.lstat(attachmentsRoot);
|
|
745
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
746
|
+
throw new Error(`Project task attachment storage must be a regular directory: ${attachmentsRoot}`);
|
|
747
|
+
}
|
|
748
|
+
const [canonicalCwd, resolvedRoot] = await Promise.all([
|
|
749
|
+
fs.realpath(cwd),
|
|
750
|
+
fs.realpath(attachmentsRoot),
|
|
751
|
+
]);
|
|
752
|
+
if (!pathIsWithin(canonicalCwd, resolvedRoot)) {
|
|
753
|
+
throw new Error(`Project task attachment storage resolves outside the project: ${attachmentsRoot}`);
|
|
754
|
+
}
|
|
755
|
+
canonicalRoot = resolvedRoot;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
for (const match of source.matchAll(/\[Pix attachment: (file:\/\/[^\]]+)\]/g)) {
|
|
759
|
+
const marker = match[0];
|
|
760
|
+
const uri = match[1];
|
|
761
|
+
if (!uri || replacements.has(marker)) continue;
|
|
762
|
+
let candidate: string;
|
|
763
|
+
try {
|
|
764
|
+
candidate = fileURLToPath(uri);
|
|
765
|
+
} catch {
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
if (!canonicalRoot) continue;
|
|
769
|
+
const lexicalProjectAttachment = pathIsWithin(attachmentsRoot, candidate);
|
|
770
|
+
|
|
771
|
+
let canonicalFile: string;
|
|
772
|
+
try {
|
|
773
|
+
canonicalFile = await fs.realpath(candidate);
|
|
774
|
+
} catch (error) {
|
|
775
|
+
if (lexicalProjectAttachment) {
|
|
776
|
+
throw new Error(`Task attachment is missing: ${candidate}. ${error instanceof Error ? error.message : String(error)}`);
|
|
777
|
+
}
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
if (!pathIsWithin(canonicalRoot, canonicalFile)) {
|
|
781
|
+
if (lexicalProjectAttachment) throw new Error(`Task attachment resolves outside project task storage: ${candidate}`);
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
const stat = await fs.lstat(candidate);
|
|
785
|
+
if (stat.isSymbolicLink() || !stat.isFile()) throw new Error(`Task attachment is not a regular file: ${candidate}`);
|
|
786
|
+
const name = basename(canonicalFile);
|
|
787
|
+
const previous = attachments.get(name);
|
|
788
|
+
if (previous && previous !== canonicalFile) {
|
|
789
|
+
throw new Error(`Task attachments contain duplicate file name "${name}".`);
|
|
790
|
+
}
|
|
791
|
+
attachments.set(name, canonicalFile);
|
|
792
|
+
replacements.set(marker, registryTaskAttachmentMarker(name));
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
let normalizedSource = source;
|
|
796
|
+
for (const [marker, portableMarker] of replacements) {
|
|
797
|
+
normalizedSource = normalizedSource.split(marker).join(portableMarker);
|
|
798
|
+
}
|
|
799
|
+
return { source: normalizedSource, attachments };
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
async function readRemoteTaskBundle(runtime: RegistryRuntime, projectKey: string): Promise<TaskBundle> {
|
|
803
|
+
const tasksPath = projectArtifactRegistryPath(runtime.cacheDir, projectKey, "tasks");
|
|
804
|
+
const source = await fs.readFile(tasksPath, "utf8");
|
|
805
|
+
const attachmentsRoot = projectTaskAttachmentsRegistryPath(runtime.cacheDir, projectKey);
|
|
806
|
+
const attachments = new Map<string, string>();
|
|
807
|
+
for (const match of source.matchAll(/\[Pix attachment: pix-task-attachment:([^\]]+)\]/g)) {
|
|
808
|
+
const encoded = match[1];
|
|
809
|
+
if (!encoded) continue;
|
|
810
|
+
const name = registryTaskAttachmentName(encoded);
|
|
811
|
+
if (attachments.has(name)) continue;
|
|
812
|
+
const filePath = join(attachmentsRoot, name);
|
|
813
|
+
let stat: import("node:fs").Stats;
|
|
814
|
+
try {
|
|
815
|
+
stat = await fs.lstat(filePath);
|
|
816
|
+
} catch (error) {
|
|
817
|
+
throw new Error(`Registry task attachment is missing: ${name}. ${error instanceof Error ? error.message : String(error)}`);
|
|
818
|
+
}
|
|
819
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
820
|
+
throw new Error(`Registry task attachment is not a regular file: ${name}`);
|
|
821
|
+
}
|
|
822
|
+
attachments.set(name, filePath);
|
|
823
|
+
}
|
|
824
|
+
return { source, attachments };
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
async function hashTaskBundle(bundle: TaskBundle): Promise<string> {
|
|
828
|
+
const hash = createHash("sha256");
|
|
829
|
+
hash.update("tasks-bundle\0");
|
|
830
|
+
hash.update(bundle.source);
|
|
831
|
+
for (const name of [...bundle.attachments.keys()].sort()) {
|
|
832
|
+
hash.update(`\0attachment\0${name}\0`);
|
|
833
|
+
hash.update(await fs.readFile(bundle.attachments.get(name)!));
|
|
834
|
+
}
|
|
835
|
+
return hash.digest("hex");
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
async function hashProjectArtifactLocal(cwd: string, artifact: ProjectArtifact): Promise<string> {
|
|
839
|
+
return artifact === "tasks"
|
|
840
|
+
? hashTaskBundle(await readLocalTaskBundle(cwd))
|
|
841
|
+
: hashPath(projectArtifactLocalPath(cwd, artifact));
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
async function hashProjectArtifactRemote(
|
|
845
|
+
runtime: RegistryRuntime,
|
|
846
|
+
projectKey: string,
|
|
847
|
+
artifact: ProjectArtifact,
|
|
848
|
+
): Promise<string> {
|
|
849
|
+
return artifact === "tasks"
|
|
850
|
+
? hashTaskBundle(await readRemoteTaskBundle(runtime, projectKey))
|
|
851
|
+
: hashPath(projectArtifactRegistryPath(runtime.cacheDir, projectKey, artifact));
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
async function replaceRemoteTaskBundle(cwd: string, runtime: RegistryRuntime, projectKey: string): Promise<void> {
|
|
855
|
+
const bundle = await readLocalTaskBundle(cwd);
|
|
856
|
+
const tasksPath = projectArtifactRegistryPath(runtime.cacheDir, projectKey, "tasks");
|
|
857
|
+
const attachmentsPath = projectTaskAttachmentsRegistryPath(runtime.cacheDir, projectKey);
|
|
858
|
+
await fs.rm(tasksPath, { recursive: true, force: true });
|
|
859
|
+
await fs.rm(attachmentsPath, { recursive: true, force: true });
|
|
860
|
+
await fs.mkdir(dirname(tasksPath), { recursive: true });
|
|
861
|
+
await fs.writeFile(tasksPath, bundle.source, "utf8");
|
|
862
|
+
if (bundle.attachments.size === 0) return;
|
|
863
|
+
await fs.mkdir(attachmentsPath, { recursive: true });
|
|
864
|
+
for (const name of [...bundle.attachments.keys()].sort()) {
|
|
865
|
+
await fs.copyFile(bundle.attachments.get(name)!, join(attachmentsPath, name));
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
async function replaceLocalTaskBundle(cwd: string, runtime: RegistryRuntime, projectKey: string): Promise<void> {
|
|
870
|
+
const bundle = await readRemoteTaskBundle(runtime, projectKey);
|
|
871
|
+
const tasksPath = projectArtifactLocalPath(cwd, "tasks");
|
|
872
|
+
const attachmentsPath = projectTaskAttachmentsLocalPath(cwd);
|
|
873
|
+
await fs.mkdir(dirname(tasksPath), { recursive: true });
|
|
874
|
+
|
|
875
|
+
let materializedSource = bundle.source;
|
|
876
|
+
for (const name of bundle.attachments.keys()) {
|
|
877
|
+
materializedSource = materializedSource
|
|
878
|
+
.split(registryTaskAttachmentMarker(name))
|
|
879
|
+
.join(`[Pix attachment: ${pathToFileURL(join(attachmentsPath, name)).href}]`);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const stamp = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
883
|
+
const stagedTasks = join(dirname(tasksPath), `.tasks.registry-${stamp}.tmp`);
|
|
884
|
+
const stagedAttachments = join(dirname(tasksPath), `.task-attachments.registry-${stamp}.tmp`);
|
|
885
|
+
const backupTasks = join(dirname(tasksPath), `.tasks.registry-${stamp}.bak`);
|
|
886
|
+
const backupAttachments = join(dirname(tasksPath), `.task-attachments.registry-${stamp}.bak`);
|
|
887
|
+
let tasksBackedUp = false;
|
|
888
|
+
let attachmentsBackedUp = false;
|
|
889
|
+
let tasksInstalled = false;
|
|
890
|
+
let attachmentsInstalled = false;
|
|
891
|
+
try {
|
|
892
|
+
await fs.writeFile(stagedTasks, materializedSource, "utf8");
|
|
893
|
+
if (bundle.attachments.size > 0) {
|
|
894
|
+
await fs.mkdir(stagedAttachments, { recursive: true });
|
|
895
|
+
for (const name of [...bundle.attachments.keys()].sort()) {
|
|
896
|
+
await fs.copyFile(bundle.attachments.get(name)!, join(stagedAttachments, name));
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
if (await pathExists(tasksPath)) {
|
|
901
|
+
await fs.rename(tasksPath, backupTasks);
|
|
902
|
+
tasksBackedUp = true;
|
|
903
|
+
}
|
|
904
|
+
if (await pathExists(attachmentsPath)) {
|
|
905
|
+
await fs.rename(attachmentsPath, backupAttachments);
|
|
906
|
+
attachmentsBackedUp = true;
|
|
907
|
+
}
|
|
908
|
+
if (bundle.attachments.size > 0) {
|
|
909
|
+
await fs.rename(stagedAttachments, attachmentsPath);
|
|
910
|
+
attachmentsInstalled = true;
|
|
911
|
+
}
|
|
912
|
+
await fs.rename(stagedTasks, tasksPath);
|
|
913
|
+
tasksInstalled = true;
|
|
914
|
+
} catch (error) {
|
|
915
|
+
if (tasksInstalled) await fs.rm(tasksPath, { recursive: true, force: true }).catch(() => undefined);
|
|
916
|
+
if (attachmentsInstalled) await fs.rm(attachmentsPath, { recursive: true, force: true }).catch(() => undefined);
|
|
917
|
+
if (tasksBackedUp) await fs.rename(backupTasks, tasksPath).catch(() => undefined);
|
|
918
|
+
if (attachmentsBackedUp) await fs.rename(backupAttachments, attachmentsPath).catch(() => undefined);
|
|
919
|
+
throw error;
|
|
920
|
+
} finally {
|
|
921
|
+
await fs.rm(stagedTasks, { recursive: true, force: true }).catch(() => undefined);
|
|
922
|
+
await fs.rm(stagedAttachments, { recursive: true, force: true }).catch(() => undefined);
|
|
923
|
+
}
|
|
924
|
+
await fs.rm(backupTasks, { recursive: true, force: true }).catch(() => undefined);
|
|
925
|
+
await fs.rm(backupAttachments, { recursive: true, force: true }).catch(() => undefined);
|
|
926
|
+
}
|
|
927
|
+
|
|
669
928
|
async function readProvenance(project: ProjectContext): Promise<Provenance> {
|
|
670
929
|
try {
|
|
671
930
|
const parsed = JSON.parse(await fs.readFile(provenancePath(project), "utf8")) as Partial<Provenance>;
|
|
@@ -872,10 +1131,9 @@ async function collectProjectStatuses(
|
|
|
872
1131
|
statuses.push({ artifact, kind: "removed-remote", localExists, remoteExists, provenance: tracked });
|
|
873
1132
|
continue;
|
|
874
1133
|
}
|
|
875
|
-
const rel = projectArtifactRelativePath(projectKey, artifact);
|
|
876
1134
|
const [localHash, remoteRevision] = await Promise.all([
|
|
877
|
-
|
|
878
|
-
|
|
1135
|
+
hashProjectArtifactLocal(cwd, artifact),
|
|
1136
|
+
projectArtifactRevision(pi, runtime, projectKey, artifact),
|
|
879
1137
|
]);
|
|
880
1138
|
const localChanged = localHash !== tracked.hash;
|
|
881
1139
|
const remoteChanged = remoteRevision !== tracked.revision;
|
|
@@ -1518,18 +1776,21 @@ async function pushProjectState(pi: ExtensionAPI, ctx: ExtensionCommandContext,
|
|
|
1518
1776
|
for (const { artifact, deleteRemote } of targets) {
|
|
1519
1777
|
const localPath = projectArtifactLocalPath(ctx.cwd, artifact);
|
|
1520
1778
|
const remotePath = projectArtifactRegistryPath(runtime.cacheDir, projectKey, artifact);
|
|
1521
|
-
const rel = projectArtifactRelativePath(projectKey, artifact);
|
|
1522
1779
|
const tracked = provenance.projectResources[artifact];
|
|
1523
1780
|
if (tracked && (tracked.remote !== runtime.remote || tracked.branch !== runtime.branch || tracked.projectKey !== projectKey)) {
|
|
1524
1781
|
throw new Error(`${artifact} state is tracked under another registry/branch/project key. Run /${COMMAND} status before pushing.`);
|
|
1525
1782
|
}
|
|
1526
1783
|
const remoteExists = await pathExists(remotePath);
|
|
1527
|
-
const remoteRevision = remoteExists
|
|
1784
|
+
const remoteRevision = remoteExists
|
|
1785
|
+
? await projectArtifactRevision(pi, runtime, projectKey, artifact)
|
|
1786
|
+
: undefined;
|
|
1528
1787
|
if (tracked && remoteRevision && remoteRevision !== tracked.revision) {
|
|
1529
1788
|
throw new Error(`${artifact} state changed in the registry since revision ${tracked.revision.slice(0, 8)}. Pull or resolve it before pushing.`);
|
|
1530
1789
|
}
|
|
1531
|
-
const localHash = await
|
|
1532
|
-
if (!tracked && remoteExists && (deleteRemote || await
|
|
1790
|
+
const localHash = await hashProjectArtifactLocal(ctx.cwd, artifact);
|
|
1791
|
+
if (!tracked && remoteExists && (deleteRemote || await hashProjectArtifactRemote(runtime, projectKey, artifact) !== localHash)) {
|
|
1792
|
+
overwriteConflicts.push(artifact);
|
|
1793
|
+
}
|
|
1533
1794
|
localHashes.set(artifact, localHash);
|
|
1534
1795
|
}
|
|
1535
1796
|
|
|
@@ -1543,18 +1804,18 @@ async function pushProjectState(pi: ExtensionAPI, ctx: ExtensionCommandContext,
|
|
|
1543
1804
|
if (!confirmed) throw new Error(`Project state push cancelled because remote state already exists for: ${names}.`);
|
|
1544
1805
|
}
|
|
1545
1806
|
|
|
1546
|
-
const rels: string[] = [];
|
|
1547
1807
|
for (const { artifact, deleteRemote } of targets) {
|
|
1548
1808
|
const localPath = projectArtifactLocalPath(ctx.cwd, artifact);
|
|
1549
1809
|
const remotePath = projectArtifactRegistryPath(runtime.cacheDir, projectKey, artifact);
|
|
1550
1810
|
if (deleteRemote) await fs.rm(remotePath, { recursive: true, force: true });
|
|
1811
|
+
else if (artifact === "tasks") await replaceRemoteTaskBundle(ctx.cwd, runtime, projectKey);
|
|
1551
1812
|
else await replaceResource(localPath, remotePath);
|
|
1552
|
-
rels.push(projectArtifactRelativePath(projectKey, artifact));
|
|
1553
1813
|
}
|
|
1554
|
-
|
|
1555
|
-
|
|
1814
|
+
const projectRel = `${REGISTRY_PROJECTS_DIR}/${projectKey}`;
|
|
1815
|
+
await runGit(pi, runtime.cacheDir, ["add", "-A", "--", projectRel]);
|
|
1816
|
+
const changed = (await runGit(pi, runtime.cacheDir, ["status", "--porcelain", "--", projectRel])).stdout.trim();
|
|
1556
1817
|
if (changed) {
|
|
1557
|
-
await runGit(pi, runtime.cacheDir, ["commit", "-m", `Sync project state ${projectKey}`, "--",
|
|
1818
|
+
await runGit(pi, runtime.cacheDir, ["commit", "-m", `Sync project state ${projectKey}`, "--", projectRel]);
|
|
1558
1819
|
try {
|
|
1559
1820
|
await runGit(pi, runtime.cacheDir, ["push", "-u", "origin", runtime.branch], { timeout: 180_000 });
|
|
1560
1821
|
} catch (error) {
|
|
@@ -1568,8 +1829,7 @@ async function pushProjectState(pi: ExtensionAPI, ctx: ExtensionCommandContext,
|
|
|
1568
1829
|
removedArtifacts.push(artifact);
|
|
1569
1830
|
continue;
|
|
1570
1831
|
}
|
|
1571
|
-
const
|
|
1572
|
-
const revision = await pathRevision(pi, runtime, rel);
|
|
1832
|
+
const revision = await projectArtifactRevision(pi, runtime, projectKey, artifact);
|
|
1573
1833
|
if (!revision) throw new Error(`Cannot determine registry revision for project ${artifact} state.`);
|
|
1574
1834
|
await recordProjectProvenance(ctx, runtime, projectKey, artifact, revision, localHashes.get(artifact)!);
|
|
1575
1835
|
}
|
|
@@ -1599,16 +1859,15 @@ async function pullProjectState(pi: ExtensionAPI, ctx: ExtensionCommandContext,
|
|
|
1599
1859
|
for (const artifact of targets) {
|
|
1600
1860
|
const localPath = projectArtifactLocalPath(ctx.cwd, artifact);
|
|
1601
1861
|
const remotePath = projectArtifactRegistryPath(runtime.cacheDir, projectKey, artifact);
|
|
1602
|
-
const rel = projectArtifactRelativePath(projectKey, artifact);
|
|
1603
1862
|
const tracked = provenance.projectResources[artifact];
|
|
1604
1863
|
if (tracked && (tracked.remote !== runtime.remote || tracked.branch !== runtime.branch || tracked.projectKey !== projectKey)) {
|
|
1605
1864
|
throw new Error(`${artifact} state is tracked under another registry/branch/project key. Run /${COMMAND} status before pulling.`);
|
|
1606
1865
|
}
|
|
1607
|
-
const remoteRevision = await
|
|
1866
|
+
const remoteRevision = await projectArtifactRevision(pi, runtime, projectKey, artifact);
|
|
1608
1867
|
if (!remoteRevision) throw new Error(`Cannot determine registry revision for project ${artifact} state.`);
|
|
1609
|
-
const remoteHash = await
|
|
1868
|
+
const remoteHash = await hashProjectArtifactRemote(runtime, projectKey, artifact);
|
|
1610
1869
|
if (await pathExists(localPath)) {
|
|
1611
|
-
const localHash = await
|
|
1870
|
+
const localHash = await hashProjectArtifactLocal(ctx.cwd, artifact);
|
|
1612
1871
|
if (tracked && localHash !== tracked.hash) {
|
|
1613
1872
|
throw new Error(`Project ${artifact} state has local changes. Push or resolve them before pulling.`);
|
|
1614
1873
|
}
|
|
@@ -1630,9 +1889,11 @@ async function pullProjectState(pi: ExtensionAPI, ctx: ExtensionCommandContext,
|
|
|
1630
1889
|
for (const artifact of targets) {
|
|
1631
1890
|
const localPath = projectArtifactLocalPath(ctx.cwd, artifact);
|
|
1632
1891
|
const remotePath = projectArtifactRegistryPath(runtime.cacheDir, projectKey, artifact);
|
|
1633
|
-
await
|
|
1892
|
+
if (artifact === "tasks") await replaceLocalTaskBundle(ctx.cwd, runtime, projectKey);
|
|
1893
|
+
else await replaceResource(remotePath, localPath);
|
|
1634
1894
|
const item = metadata.get(artifact)!;
|
|
1635
|
-
await
|
|
1895
|
+
const localHash = await hashProjectArtifactLocal(ctx.cwd, artifact);
|
|
1896
|
+
await recordProjectProvenance(ctx, runtime, projectKey, artifact, item.remoteRevision, localHash);
|
|
1636
1897
|
}
|
|
1637
1898
|
notify(ctx, `Pulled ${targets.join(" + ")} for ${projectKey}. Reloading project state…`);
|
|
1638
1899
|
await reloadAfterResourceChange(ctx);
|
|
@@ -201,6 +201,14 @@ function toolResultFrom(entry: EntryLike): UnknownRecord | undefined {
|
|
|
201
201
|
return message?.role === "toolResult" ? message : undefined;
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
function recoverableGatewayDetails(message: UnknownRecord): UnknownRecord | undefined {
|
|
205
|
+
const toolName = typeof message.toolName === "string" ? message.toolName.toLowerCase() : "";
|
|
206
|
+
if (toolName !== "web_search" && toolName !== "web_fetch") return undefined;
|
|
207
|
+
if (!isRecord(message.details) || !isRecord(message.details.contextGateway)) return undefined;
|
|
208
|
+
if (message.details.contextGateway.representation !== "web-recoverable-compact") return undefined;
|
|
209
|
+
return message.details;
|
|
210
|
+
}
|
|
211
|
+
|
|
204
212
|
function summaryFrom(entry: EntryLike): string {
|
|
205
213
|
if ((entry.type === "compaction" || entry.type === "branch_summary") && typeof entry.summary === "string") {
|
|
206
214
|
return entry.summary;
|
|
@@ -218,6 +226,11 @@ function entryText(entry: EntryLike): string {
|
|
|
218
226
|
for (const call of toolCallsFrom(entry)) {
|
|
219
227
|
chunks.push(call.name, serializeJson(call.arguments));
|
|
220
228
|
}
|
|
229
|
+
if (message.role === "toolResult") {
|
|
230
|
+
const toolName = typeof message.toolName === "string" ? message.toolName : "unknown";
|
|
231
|
+
const callId = typeof message.toolCallId === "string" ? message.toolCallId : "unknown";
|
|
232
|
+
chunks.push(`tool_result ${toolName}#${callId}`);
|
|
233
|
+
}
|
|
221
234
|
} else if (entry.type === "custom_message") {
|
|
222
235
|
const text = textFromContent(entry.content);
|
|
223
236
|
if (text) chunks.push(text);
|
|
@@ -416,6 +429,10 @@ function renderEntryFull(entry: EntryLike): string {
|
|
|
416
429
|
const toolName = typeof message.toolName === "string" ? message.toolName : "unknown";
|
|
417
430
|
const callId = typeof message.toolCallId === "string" ? message.toolCallId : "unknown";
|
|
418
431
|
lines.push(`tool_result ${toolName}#${callId}${message.isError === true ? " error" : ""}`);
|
|
432
|
+
const recoverableDetails = recoverableGatewayDetails(message);
|
|
433
|
+
if (recoverableDetails) {
|
|
434
|
+
lines.push("recoverable_raw_details:", serializeJson(recoverableDetails));
|
|
435
|
+
}
|
|
419
436
|
}
|
|
420
437
|
} else if (entry.type === "compaction") {
|
|
421
438
|
lines.push(summaryFrom(entry));
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { loadPiToolsSuiteConfig, type TodoThinkingLevel as ConfigTodoThinkingLevel } from "../config.js";
|
|
3
3
|
import { isAgentBusyRaceError } from "../context-usage.js";
|
|
4
|
+
import { hasAvailableIndexedProjectRoot } from "../lib/project.js";
|
|
4
5
|
import { autoClearCompletedTodos } from "./state/auto-clear.js";
|
|
5
6
|
import { loadPersistedPlan, syncPersistedPlan } from "./state/persistence.js";
|
|
6
7
|
import { replayFromBranch } from "./state/replay.js";
|
|
@@ -32,6 +33,25 @@ const TODO_NUDGE_IDLE_RETRY_DELAY_MS = 100;
|
|
|
32
33
|
const TODO_NUDGE_MAX_IDLE_ATTEMPTS = 40;
|
|
33
34
|
const ASK_USER_TOOL_NAMES = new Set(["ask_user", "ask_user_question", "question"]);
|
|
34
35
|
const TODO_THINKING_RESTORE_METADATA_KEY = "__piTodoRestoreThinking";
|
|
36
|
+
const REPO_KNOWLEDGE_FINALIZATION_REMINDER =
|
|
37
|
+
"📚 Before completing the final todo: if behavior/contracts changed, reconcile affected specs and repo knowledge; skip for mechanical changes.";
|
|
38
|
+
|
|
39
|
+
function completesTodo(info: { action: string; params: TaskMutationParams }): boolean {
|
|
40
|
+
if (info.action === "update") return info.params.status === "completed";
|
|
41
|
+
if (info.action !== "batch_update") return false;
|
|
42
|
+
return (info.params.items ?? []).some((item) => item.status === "completed");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function repoKnowledgeFinalizationReminder(
|
|
46
|
+
state: ReturnType<typeof getState>,
|
|
47
|
+
ctx: ExtensionContext,
|
|
48
|
+
info: { action: string; params: TaskMutationParams },
|
|
49
|
+
): string | undefined {
|
|
50
|
+
if (!hasAvailableIndexedProjectRoot(ctx.cwd)) return undefined;
|
|
51
|
+
if (!completesTodo(info)) return undefined;
|
|
52
|
+
const remainingActive = selectVisibleTasks(state).filter((task) => ACTIVE_STATUSES.has(task.status));
|
|
53
|
+
return remainingActive.length === 1 ? REPO_KNOWLEDGE_FINALIZATION_REMINDER : undefined;
|
|
54
|
+
}
|
|
35
55
|
|
|
36
56
|
function isStaleExtensionContextError(error: unknown): boolean {
|
|
37
57
|
return error instanceof Error && /ctx is stale|stale ctx|stale after session replacement|stale after.*reload/i.test(error.message);
|
|
@@ -241,6 +261,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
241
261
|
registerTodoTool(pi, {
|
|
242
262
|
...thinkingPrompt,
|
|
243
263
|
...(availableThinkingLevels ? { parameters: todoParamsSchemaForThinkingLevels(availableThinkingLevels) } : {}),
|
|
264
|
+
resultReminder: repoKnowledgeFinalizationReminder,
|
|
244
265
|
prepareMutation: (state, ctx, info) => {
|
|
245
266
|
if (!todoThinkingEnabled) return info.params;
|
|
246
267
|
if (info.action === "create" || info.action === "update") return prepareTodoThinkingMutation(state, ctx, info.params);
|
|
@@ -84,6 +84,11 @@ interface TodoToolHooks {
|
|
|
84
84
|
ctx: ExtensionContext,
|
|
85
85
|
info: { action: TaskAction; params: TaskMutationParams },
|
|
86
86
|
) => TaskMutationParams | Promise<TaskMutationParams>;
|
|
87
|
+
resultReminder?: (
|
|
88
|
+
state: ReturnType<typeof getState>,
|
|
89
|
+
ctx: ExtensionContext,
|
|
90
|
+
info: { action: TaskAction; params: TaskMutationParams },
|
|
91
|
+
) => string | undefined | Promise<string | undefined>;
|
|
87
92
|
afterCommit?: (
|
|
88
93
|
state: ReturnType<typeof getState>,
|
|
89
94
|
ctx: ExtensionContext,
|
|
@@ -448,7 +453,20 @@ export function registerTodoTool(pi: ExtensionAPI, hooks: TodoToolRegistrationOp
|
|
|
448
453
|
params: preparedParams,
|
|
449
454
|
committedState: autoClear.state,
|
|
450
455
|
});
|
|
451
|
-
|
|
456
|
+
let toolResult = buildToolResult(params.action, preparedParams, autoClear.state, result.op);
|
|
457
|
+
const resultReminder = await hooks.resultReminder?.(result.state, _ctx as ExtensionContext, {
|
|
458
|
+
action: params.action,
|
|
459
|
+
params: preparedParams,
|
|
460
|
+
});
|
|
461
|
+
if (resultReminder?.trim()) {
|
|
462
|
+
toolResult = {
|
|
463
|
+
...toolResult,
|
|
464
|
+
content: [{
|
|
465
|
+
type: "text" as const,
|
|
466
|
+
text: `${toolResult.content[0]?.text ?? ""}\n\n${resultReminder.trim()}`.trim(),
|
|
467
|
+
}],
|
|
468
|
+
};
|
|
469
|
+
}
|
|
452
470
|
if (!autoClear.cleared) return toolResult;
|
|
453
471
|
return {
|
|
454
472
|
...toolResult,
|
|
@@ -17,6 +17,8 @@ export type RepoDiscoveryToolDescription = ToolDescription & Required<Pick<ToolD
|
|
|
17
17
|
targetDescription?: string;
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
+
export type RepoKnowledgeToolDescription = ToolDescription & Required<Pick<ToolDescription, "promptSnippet" | "promptGuidelines">>;
|
|
21
|
+
|
|
20
22
|
export type ToolDescriptionSetOptions = {
|
|
21
23
|
repoDiscovery?: boolean;
|
|
22
24
|
};
|
|
@@ -69,9 +71,9 @@ export function asyncSubagentToolDescriptions(options: ToolDescriptionSetOptions
|
|
|
69
71
|
name: "subagents",
|
|
70
72
|
label: "Subagents",
|
|
71
73
|
description: [
|
|
72
|
-
"For every real-
|
|
73
|
-
"If
|
|
74
|
-
"After
|
|
74
|
+
"For every real user-interface QA request (browser, terminal/TUI, or desktop GUI), immediately spawn subagentType='ui-qa' before checking files, URLs, servers, launch commands, or other prerequisites; the QA sub-agent owns feasibility checks and blocked reports, so the parent must not substitute static checks for requested UI QA.",
|
|
75
|
+
"If ui-qa browser testing reports that credentials are required, it must identify the generated project-local template and explicitly ask the user to fill it; the parent relays that request without reading or editing the credential file.",
|
|
76
|
+
"After UI testing, ui-qa must return clickable links for every available screenshot, terminal capture, video, trace, or other retained evidence; the parent must preserve those links in its user-facing report.",
|
|
75
77
|
SUBAGENT_DELEGATION_GUIDANCE,
|
|
76
78
|
"Presets declare available models; each agent's ordered models selects the first usable model in that pool. AGENTS_PRESET or /subagent-preset session <name> selects the current session pool. Bundled pools live with agent files, and projects can override/add pools in .pi/agents/presets.jsonc. Do not override the model merely to choose a role.",
|
|
77
79
|
SUBAGENT_TYPE_SELECTION_GUIDANCE,
|
|
@@ -82,18 +84,18 @@ export function asyncSubagentToolDescriptions(options: ToolDescriptionSetOptions
|
|
|
82
84
|
"Results are compact with artifact links. Agents run isolated pi processes with extensions disabled to prevent recursive spawning; spawn/task timeoutSeconds can shorten the default 30m watchdog, project concurrency queues excess agents, and retry backoff/fallback models/Antigravity account rotation are config-driven.",
|
|
83
85
|
].join(" "),
|
|
84
86
|
promptSnippet:
|
|
85
|
-
"For every
|
|
86
|
-
"Give
|
|
87
|
+
"For every real UI QA, UI bug reproduction, or user-facing fix-verification request across browser, terminal/TUI, or desktop GUI, immediately spawn subagentType='ui-qa' even for a single track and before inspecting files or checking prerequisites. The ui-qa sub-agent must discover the actual target/control path and report missing prerequisites; do not preflight, perform, or substitute requested UI QA in the parent agent. " +
|
|
88
|
+
"Give ui-qa a concise acceptance brief: the known target URL/app/command, user-visible flow, expected observable result, and required artifacts. Do not prescribe repository files, searches, commands, server setup, or mock/synthetic substitutes; unknown setup belongs to the QA sub-agent's discovery. " +
|
|
87
89
|
"For other work, use subagents action='spawn' for economical execution or context isolation, including one bounded sequential task or explicit delegate/parallelize/split work requests. " +
|
|
88
90
|
SUBAGENT_TYPE_SELECTION_GUIDANCE + " Avoid trivial reads/edits and do not call status/wait immediately after spawn just for progress. " +
|
|
89
91
|
(repoDiscovery
|
|
90
92
|
? "For one semantic code-discovery question, use repo_search; for independent tracks/hypotheses/review axes, delegate even when repo_* tools exist. Read result only after completion when findings are needed."
|
|
91
93
|
: "For one focused code-discovery question, use direct read/grep. Without repo_* tools, delegate bounded research tracks for broad discovery rather than flooding parent context. Read result only after completion when findings are needed."),
|
|
92
94
|
promptGuidelines: [
|
|
93
|
-
"Treat every real
|
|
94
|
-
"Keep the
|
|
95
|
-
"When
|
|
96
|
-
"After
|
|
95
|
+
"Treat every real UI QA request as a mandatory delegation trigger and an explicit exception to the large/parallel threshold: immediately spawn with `subagentType: \"ui-qa\"` before checking prerequisites. The QA sub-agent owns target discovery, feasibility checks, UI automation, evidence, and blocked reports; the parent must not inspect the project first or substitute non-UI checks.",
|
|
96
|
+
"Keep the ui-qa task payload at the user-visible acceptance level: known target URL/app/command, actions to perform, expected observable outcome, and requested evidence. Do not turn it into a repository investigation plan, name internal files or commands, dictate setup, or invent a mock/synthetic target. Leave unknown prerequisites to the QA sub-agent.",
|
|
97
|
+
"When ui-qa browser testing reports missing credentials, relay its explicit request and generated `.pi/qa_auth.jsonc` template path; never inspect, populate, or edit that credential file in the parent.",
|
|
98
|
+
"After ui-qa completes a test, preserve its clickable screenshot, terminal capture, video, trace, and other evidence links in the final user-facing response whenever those artifacts exist.",
|
|
97
99
|
SUBAGENT_DELEGATION_GUIDANCE,
|
|
98
100
|
repoDiscovery
|
|
99
101
|
? "For one discovery question, use repo_search; spawn for independent tracks/hypotheses/review axes, and do not let repo_* availability suppress delegation."
|
|
@@ -221,7 +223,24 @@ export const REPO_DISCOVERY_TOOLS: RepoDiscoveryToolDescription[] = [
|
|
|
221
223
|
},
|
|
222
224
|
];
|
|
223
225
|
|
|
224
|
-
export const
|
|
226
|
+
export const REPO_KNOWLEDGE_TOOL_DESCRIPTION: RepoKnowledgeToolDescription = {
|
|
227
|
+
name: "repo_knowledge",
|
|
228
|
+
label: "Repo Knowledge",
|
|
229
|
+
description:
|
|
230
|
+
"Project behavioral knowledge/spec maintenance backed by idx. Query contracts with context/search/show/status, review task-scoped impact after material behavior changes, discover new/moved docs, and explicitly record/relate/verify metadata only after semantic evidence review. This tool is exposed only when idx is available and the project is indexed.",
|
|
231
|
+
promptSnippet:
|
|
232
|
+
"Use repo_knowledge for project behavior/contracts and their maintenance. For a behavior/contract question that also needs implementation/tests/freshness, prefer action=context; use action=search when only the relevant specs/contracts are needed. Before a material behavior change, find the current primary contract; after implementation, run action=impact on this task's changed paths. Update the existing spec, or create a focused new spec with Edit/Write/apply_patch when no suitable contract exists, then record/relate and verify only after checking code/tests. Skip this lifecycle for mechanical/non-behavioral edits.",
|
|
233
|
+
promptGuidelines: [
|
|
234
|
+
"Primary specs/contracts are authoritative; catalog/search summaries, embeddings, graph proximity and semantic candidates are routing evidence only. record is classification, not verification; non-fresh status requires review before presenting behavior as current.",
|
|
235
|
+
"For material externally visible or project-contract behavior changes: query existing knowledge before/while implementing, keep the authoritative spec aligned in the same task, create a focused primary spec when no suitable contract exists, then run task-scoped impact and reconcile uncovered/new/moved docs before finishing.",
|
|
236
|
+
"Never persist a relation from similarity alone and never add one just to make coverage non-empty. A reviewed no-impact outcome is valid. verify only after reading the primary source and checking relevant implementation/tests; changed code never auto-rewrites spec semantics.",
|
|
237
|
+
],
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
export const REPO_DISCOVERY_TOOL_NAMES = [
|
|
241
|
+
...REPO_DISCOVERY_TOOLS.map((tool) => tool.name),
|
|
242
|
+
REPO_KNOWLEDGE_TOOL_DESCRIPTION.name,
|
|
243
|
+
];
|
|
225
244
|
|
|
226
245
|
export const TODO_TOOL_DESCRIPTION: ToolDescription = {
|
|
227
246
|
name: "todo",
|
|
@@ -332,12 +351,16 @@ export function claudeAliasToolDescriptions(options: ToolDescriptionSetOptions |
|
|
|
332
351
|
Edit: {
|
|
333
352
|
name: "Edit",
|
|
334
353
|
label: "Edit",
|
|
335
|
-
description:
|
|
354
|
+
description: repoDiscovery
|
|
355
|
+
? "Replace exact text in an existing file. Use for surgical edits; use Write only for whole-file replacement. After a material behavior change, do not finish until repo_knowledge impact reviews this task's changed paths and the authoritative spec is updated or created and verified; skip that lifecycle for mechanical/non-behavioral edits."
|
|
356
|
+
: "Replace exact text in an existing file. Use for surgical edits; use Write only for intentional whole-file replacement.",
|
|
336
357
|
},
|
|
337
358
|
Write: {
|
|
338
359
|
name: "Write",
|
|
339
360
|
label: "Write",
|
|
340
|
-
description:
|
|
361
|
+
description: repoDiscovery
|
|
362
|
+
? "Create or overwrite a file with complete contents. Use only for intentional whole-file writes. When creating/changing material project behavior, keep or create the primary spec in the same task and finish with task-scoped repo_knowledge impact + semantic verification; mechanical/non-behavioral writes do not require it."
|
|
363
|
+
: "Create or overwrite a file with complete contents. Use only when replacing the whole file is intended.",
|
|
341
364
|
},
|
|
342
365
|
Bash: {
|
|
343
366
|
name: "Bash",
|
|
@@ -364,7 +387,9 @@ export function claudeAliasToolDescriptions(options: ToolDescriptionSetOptions |
|
|
|
364
387
|
export const CLAUDE_ALIAS_TOOL_DESCRIPTIONS = claudeAliasToolDescriptions(false);
|
|
365
388
|
export const CLAUDE_ALIAS_TOOL_DESCRIPTIONS_WITH_REPO = claudeAliasToolDescriptions(true);
|
|
366
389
|
|
|
367
|
-
export
|
|
390
|
+
export function codexAliasToolDescriptions(options: ToolDescriptionSetOptions | boolean = false) {
|
|
391
|
+
const repoDiscovery = hasRepoDiscovery(options);
|
|
392
|
+
return {
|
|
368
393
|
shellCommand: {
|
|
369
394
|
name: "shell",
|
|
370
395
|
label: "shell",
|
|
@@ -373,7 +398,7 @@ export const CODEX_ALIAS_TOOL_DESCRIPTIONS = {
|
|
|
373
398
|
applyPatch: {
|
|
374
399
|
name: "apply_patch",
|
|
375
400
|
label: "apply_patch",
|
|
376
|
-
description: `Apply file edits with a relative-path patch or standard unified diff. Use for creating, updating, moving, or deleting files; keep each patch focused.
|
|
401
|
+
description: `Apply file edits with a relative-path patch or standard unified diff. Use for creating, updating, moving, or deleting files; keep each patch focused.${repoDiscovery ? " After a material behavior change, keep/create the authoritative primary spec in the same task and finish with task-scoped repo_knowledge impact plus semantic verification; skip this for mechanical/non-behavioral edits." : ""}
|
|
377
402
|
|
|
378
403
|
Begin-patch format:
|
|
379
404
|
*** Begin Patch
|
|
@@ -387,4 +412,8 @@ Sections: *** Add File (new lines start with +), *** Update File (optionally ***
|
|
|
387
412
|
|
|
388
413
|
Unified diff with ---/+++ headers is also supported. Paths must be workspace-relative, never absolute. Provide the complete patch in input.`,
|
|
389
414
|
},
|
|
390
|
-
} satisfies Record<string, ToolDescription>;
|
|
415
|
+
} satisfies Record<string, ToolDescription>;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export const CODEX_ALIAS_TOOL_DESCRIPTIONS = codexAliasToolDescriptions(false);
|
|
419
|
+
export const CODEX_ALIAS_TOOL_DESCRIPTIONS_WITH_REPO = codexAliasToolDescriptions(true);
|