@gajae-code/coding-agent 0.18.7 → 0.18.8
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/CHANGELOG.md +279 -1
- package/dist/types/async/job-manager.d.ts +1 -1
- package/dist/types/capability/mcp.d.ts +2 -0
- package/dist/types/capability/skill.d.ts +2 -0
- package/dist/types/capability/types.d.ts +2 -0
- package/dist/types/cli/public-command-errors.d.ts +3 -1
- package/dist/types/cli/setup-cli.d.ts +1 -1
- package/dist/types/commands/deep-interview.d.ts +3 -0
- package/dist/types/config/autorouting-tier-map.d.ts +348 -9
- package/dist/types/config/file-lock.d.ts +1 -0
- package/dist/types/config/model-bindings-applier.d.ts +2 -0
- package/dist/types/config/model-profile-activation.d.ts +11 -3
- package/dist/types/config/model-registry.d.ts +4 -9
- package/dist/types/config/skill-settings-defaults.d.ts +2 -0
- package/dist/types/cursor-delete-path.d.ts +1 -0
- package/dist/types/cursor-mcp-name.d.ts +8 -0
- package/dist/types/customization/inventory.d.ts +2 -0
- package/dist/types/debug/report-bundle-parent.d.ts +2 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +1 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.d.ts +31 -0
- package/dist/types/defaults/gjc-skills.generated.d.ts +5 -6
- package/dist/types/discovery/helpers.d.ts +6 -1
- package/dist/types/export/html/css-color.d.ts +2 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/extensions/types.d.ts +7 -0
- package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +14 -1
- package/dist/types/extensibility/plugins/marketplace/cache.d.ts +2 -0
- package/dist/types/extensibility/plugins/plugin-link-path.d.ts +2 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +3 -1
- package/dist/types/extensibility/skill-management.d.ts +3 -1
- package/dist/types/extensibility/skills.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-crystallize.d.ts +74 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +11 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +5 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +0 -5
- package/dist/types/gjc-runtime/state-argv.d.ts +1 -1
- package/dist/types/gjc-runtime/state-migrations.d.ts +8 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +95 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +11 -1
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -0
- package/dist/types/internal-urls/skill-path.d.ts +1 -0
- package/dist/types/lsp/workspace-path.d.ts +1 -0
- package/dist/types/modes/acp/acp-agent.d.ts +7 -0
- package/dist/types/modes/components/custom-editor.d.ts +6 -0
- package/dist/types/modes/components/customization/customization-dashboard.d.ts +1 -0
- package/dist/types/modes/components/extensions/state-manager.d.ts +4 -3
- package/dist/types/modes/controllers/input-controller.d.ts +9 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/utils/interrupt-ownership.d.ts +3 -0
- package/dist/types/modes/utils/keybinding-matchers.d.ts +1 -0
- package/dist/types/modes/utils/macos-option-forwarding.d.ts +17 -0
- package/dist/types/progress/collect-project-progress.d.ts +38 -0
- package/dist/types/progress/progress-contract.d.ts +154 -0
- package/dist/types/progress/progress-snapshot.d.ts +11 -0
- package/dist/types/progress/project-progress.d.ts +141 -0
- package/dist/types/progress/render-progress.d.ts +28 -0
- package/dist/types/runtime-mcp/oauth-public-url.d.ts +1 -0
- package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +1 -0
- package/dist/types/runtime-mcp/smithery-registry.d.ts +24 -0
- package/dist/types/runtime-mcp/types.d.ts +6 -0
- package/dist/types/sdk/acp/adapter.d.ts +1 -1
- package/dist/types/sdk/broker/broker-exit.d.ts +6 -2
- package/dist/types/sdk/broker/broker.d.ts +18 -1
- package/dist/types/sdk/broker/session-index.d.ts +12 -6
- package/dist/types/sdk/bus/index.d.ts +12 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -1
- package/dist/types/sdk/client/client.d.ts +2 -0
- package/dist/types/sdk/host/query/handlers.d.ts +3 -0
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/router/session-router.d.ts +2 -0
- package/dist/types/sdk/session-list.d.ts +1 -1
- package/dist/types/sdk/session.d.ts +8 -0
- package/dist/types/session/agent-session.d.ts +38 -9
- package/dist/types/session/artifacts.d.ts +2 -2
- package/dist/types/session/internal/managed-gc-retirement-codec.d.ts +32 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +79 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +26 -1
- package/dist/types/session/internal/managed-task-owner-cleanup.d.ts +62 -0
- package/dist/types/session/internal/task-artifact-owner-access.d.ts +2 -0
- package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +9 -1
- package/dist/types/session/session-manager.d.ts +31 -5
- package/dist/types/session/session-retirement.d.ts +31 -7
- package/dist/types/session/session-storage.d.ts +62 -2
- package/dist/types/session/task-artifact-owner-codec.d.ts +2 -0
- package/dist/types/setup/credential-auto-import.d.ts +1 -1
- package/dist/types/setup/credential-import.d.ts +6 -4
- package/dist/types/skill-state/active-state.d.ts +38 -0
- package/dist/types/slash-commands/helpers/progress-report.d.ts +8 -0
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/tools/bash-skill-urls.d.ts +1 -1
- package/dist/types/tools/index.d.ts +3 -0
- package/dist/types/tools/python.d.ts +9 -1
- package/dist/types/tools/recipe/recipe-guard.d.ts +7 -0
- package/dist/types/tools/restricted-cwd.d.ts +1 -0
- package/dist/types/tools/todo-write.d.ts +6 -0
- package/dist/types/utils/tools-checksum.d.ts +7 -0
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +21 -0
- package/scripts/verify-managed-empty-stop.ts +28 -0
- package/src/async/job-manager.ts +228 -122
- package/src/capability/index.ts +17 -0
- package/src/capability/mcp.ts +2 -0
- package/src/capability/skill.ts +2 -0
- package/src/capability/types.ts +2 -0
- package/src/cli/auth-broker-cli.ts +1 -1
- package/src/cli/customize-doctor.ts +15 -2
- package/src/cli/public-command-errors.ts +21 -1
- package/src/cli/setup-cli.ts +4 -4
- package/src/cli/skills-cli.ts +5 -0
- package/src/commands/deep-interview.ts +47 -8
- package/src/commands/sdk.ts +28 -8
- package/src/config/autorouting-tier-map.ts +117 -7
- package/src/config/file-lock.ts +307 -68
- package/src/config/mcp-schema.json +4 -0
- package/src/config/model-bindings-applier.ts +20 -0
- package/src/config/model-profile-activation.ts +240 -46
- package/src/config/model-registry.ts +47 -153
- package/src/config/skill-settings-defaults.ts +7 -0
- package/src/coordinator-mcp/policy.ts +3 -2
- package/src/cursor-delete-path.ts +10 -0
- package/src/cursor-mcp-name.ts +15 -0
- package/src/cursor.ts +13 -2
- package/src/customization/inventory.ts +3 -0
- package/src/debug/report-bundle-parent.ts +22 -0
- package/src/debug/report-bundle.ts +3 -0
- package/src/debug/system-info.ts +13 -1
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +67 -4
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.ts +80 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +105 -10
- package/src/defaults/gjc/skills/ralplan/SKILL.md +6 -4
- package/src/defaults/gjc-defaults.test.ts +18 -1
- package/src/defaults/gjc-defaults.ts +3 -0
- package/src/defaults/gjc-skills.generated.ts +58 -36
- package/src/discovery/agents.ts +6 -1
- package/src/discovery/builtin.ts +10 -0
- package/src/discovery/helpers.ts +159 -20
- package/src/discovery/mcp-json.ts +6 -0
- package/src/discovery/ssh.ts +1 -1
- package/src/discovery/substitute-plugin-root.ts +3 -1
- package/src/edit/file-read-cache.ts +1 -1
- package/src/eval/py/executor.ts +380 -120
- package/src/export/html/css-color.ts +9 -0
- package/src/export/html/index.ts +5 -4
- package/src/export/html/template.generated.ts +1 -1
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +7 -0
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +73 -8
- package/src/extensibility/plugins/manager.ts +3 -2
- package/src/extensibility/plugins/marketplace/cache.ts +15 -0
- package/src/extensibility/plugins/marketplace/manager.ts +11 -3
- package/src/extensibility/plugins/plugin-link-path.ts +12 -0
- package/src/extensibility/runtime-skill-discovery.ts +20 -2
- package/src/extensibility/skill-management.ts +6 -2
- package/src/extensibility/skills.ts +6 -1
- package/src/gjc-runtime/deep-interview-crystallize.ts +1814 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +20 -2
- package/src/gjc-runtime/deep-interview-runtime.ts +1415 -13
- package/src/gjc-runtime/deep-interview-stage.ts +98 -21
- package/src/gjc-runtime/gc-runtime.ts +206 -9
- package/src/gjc-runtime/launch-worktree.ts +14 -4
- package/src/gjc-runtime/ralplan-runtime.ts +447 -52
- package/src/gjc-runtime/state-argv.ts +2 -0
- package/src/gjc-runtime/state-migrations.ts +91 -29
- package/src/gjc-runtime/state-runtime.ts +3716 -135
- package/src/gjc-runtime/state-writer.ts +83 -8
- package/src/gjc-runtime/ultragoal-runtime.ts +16 -4
- package/src/gjc-runtime/workflow-manifest.generated.json +70 -42
- package/src/gjc-runtime/workflow-manifest.ts +54 -21
- package/src/harness-control-plane/operate.ts +12 -4
- package/src/harness-control-plane/owner.ts +5 -1
- package/src/hashline/parser.ts +48 -9
- package/src/hashline/recovery.ts +13 -4
- package/src/internal-urls/docs-index.generated.ts +18 -17
- package/src/internal-urls/skill-path.ts +19 -0
- package/src/internal-urls/skill-protocol.ts +2 -0
- package/src/lsp/edits.ts +6 -0
- package/src/lsp/workspace-path.ts +51 -0
- package/src/main.ts +195 -31
- package/src/modes/acp/acp-agent.ts +88 -4
- package/src/modes/components/custom-editor.ts +14 -0
- package/src/modes/components/customization/customization-dashboard.ts +12 -0
- package/src/modes/components/extensions/extension-dashboard.ts +2 -2
- package/src/modes/components/extensions/state-manager.ts +15 -5
- package/src/modes/components/model-selector.ts +1 -4
- package/src/modes/components/provider-onboarding-selector.ts +1 -1
- package/src/modes/components/status-line/segments.ts +4 -0
- package/src/modes/controllers/event-controller.ts +11 -4
- package/src/modes/controllers/input-controller.ts +74 -29
- package/src/modes/controllers/runtime-mcp-command-controller.ts +8 -3
- package/src/modes/controllers/selector-controller.ts +28 -15
- package/src/modes/interactive-mode.ts +4 -0
- package/src/modes/runtime-init.ts +4 -0
- package/src/modes/utils/abort-message.ts +3 -1
- package/src/modes/utils/interrupt-ownership.ts +12 -0
- package/src/modes/utils/keybinding-matchers.ts +4 -0
- package/src/modes/utils/macos-option-forwarding.ts +80 -0
- package/src/progress/collect-project-progress.ts +153 -0
- package/src/progress/progress-contract.ts +172 -0
- package/src/progress/progress-snapshot.ts +140 -0
- package/src/progress/project-progress.ts +446 -0
- package/src/progress/render-progress.ts +209 -0
- package/src/prompts/system/master-mode.md +1 -0
- package/src/prompts/tools/skill-discovery.md +1 -1
- package/src/runtime-mcp/config-writer.ts +10 -6
- package/src/runtime-mcp/config.ts +1 -0
- package/src/runtime-mcp/oauth-flow.ts +5 -0
- package/src/runtime-mcp/oauth-public-url.ts +8 -0
- package/src/runtime-mcp/plugin-network-boundary.ts +4 -0
- package/src/runtime-mcp/smithery-registry.ts +22 -3
- package/src/runtime-mcp/transports/http.ts +2 -2
- package/src/runtime-mcp/types.ts +6 -0
- package/src/sdk/acp/adapter.ts +93 -8
- package/src/sdk/broker/broker-exit.ts +52 -23
- package/src/sdk/broker/broker.ts +75 -6
- package/src/sdk/broker/ensure.ts +2 -0
- package/src/sdk/broker/lifecycle.ts +711 -66
- package/src/sdk/broker/session-index.ts +191 -81
- package/src/sdk/bus/chat-daemon-cli.ts +38 -1
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/index.ts +238 -43
- package/src/sdk/bus/telegram-daemon-contract.ts +6 -1
- package/src/sdk/cli/session-cli.ts +17 -3
- package/src/sdk/client/client.ts +28 -8
- package/src/sdk/host/query/handlers.ts +9 -1
- package/src/sdk/host/session-runtime.test.ts +2647 -294
- package/src/sdk/host/session-runtime.ts +426 -109
- package/src/sdk/host/surface-policy.ts +1 -0
- package/src/sdk/index.ts +8 -0
- package/src/sdk/lifecycle-session.ts +3 -1
- package/src/sdk/prompt-deadline-manager.ts +15 -0
- package/src/sdk/protocol/operation-inventory.generated.json +146 -0
- package/src/sdk/protocol/operation-registry.ts +9 -2
- package/src/sdk/router/session-router.ts +75 -12
- package/src/sdk/session-list.ts +1 -0
- package/src/sdk/session.ts +67 -38
- package/src/session/agent-session.ts +623 -224
- package/src/session/artifacts.ts +174 -70
- package/src/session/internal/managed-gc-retirement-codec.ts +319 -0
- package/src/session/internal/managed-session-scope.ts +3457 -462
- package/src/session/internal/managed-session-storage.ts +376 -61
- package/src/session/internal/managed-task-owner-cleanup.ts +473 -0
- package/src/session/internal/task-artifact-owner-access.ts +19 -3
- package/src/session/internal/task-artifact-owner-transcript.ts +547 -1
- package/src/session/session-manager.ts +939 -133
- package/src/session/session-retirement.ts +573 -14
- package/src/session/session-storage.ts +417 -14
- package/src/session/task-artifact-owner-codec.ts +71 -8
- package/src/session/task-artifact-owner-retirement.ts +21 -19
- package/src/setup/credential-auto-import.ts +12 -3
- package/src/setup/credential-import.ts +176 -5
- package/src/setup/paseo/announce.ts +6 -1
- package/src/skill-state/active-state.ts +132 -2
- package/src/slash-commands/builtin-registry.ts +39 -9
- package/src/slash-commands/helpers/progress-report.ts +23 -0
- package/src/stt/setup.ts +3 -5
- package/src/task/commands.ts +3 -1
- package/src/task/executor.ts +4 -0
- package/src/task/index.ts +6 -0
- package/src/tools/ask.ts +205 -6
- package/src/tools/ast-edit.ts +4 -0
- package/src/tools/bash-allowed-prefixes.ts +15 -0
- package/src/tools/bash-skill-urls.ts +56 -3
- package/src/tools/bash.ts +48 -79
- package/src/tools/descriptors.ts +5 -1
- package/src/tools/eval.ts +56 -36
- package/src/tools/image-gen.ts +1 -1
- package/src/tools/index.ts +3 -0
- package/src/tools/python.ts +187 -66
- package/src/tools/recipe/index.ts +9 -0
- package/src/tools/recipe/recipe-guard.ts +19 -0
- package/src/tools/restricted-cwd.ts +10 -0
- package/src/tools/skill-discovery.ts +5 -0
- package/src/tools/skill.ts +28 -1
- package/src/tools/todo-write.ts +10 -1
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/src/tools/write.ts +1 -0
- package/src/utils/tools-checksum.ts +34 -0
- package/src/utils/tools-manager.ts +24 -6
- package/src/web/search/providers/codex.ts +3 -2
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
export async function realSkillPath(baseDir: string, targetPath: string): Promise<string> {
|
|
5
|
+
const realBase = await fs.realpath(baseDir);
|
|
6
|
+
let realTarget: string;
|
|
7
|
+
try {
|
|
8
|
+
realTarget = await fs.realpath(targetPath);
|
|
9
|
+
} catch (error) {
|
|
10
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
11
|
+
throw new Error(`File not found: ${targetPath}`);
|
|
12
|
+
}
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
if (realTarget !== realBase && !realTarget.startsWith(realBase + path.sep)) {
|
|
16
|
+
throw new Error("Path traversal is not allowed");
|
|
17
|
+
}
|
|
18
|
+
return realTarget;
|
|
19
|
+
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import * as path from "node:path";
|
|
11
11
|
import { getActiveSkills } from "../extensibility/skills";
|
|
12
|
+
import { realSkillPath } from "./skill-path";
|
|
12
13
|
import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
|
|
13
14
|
|
|
14
15
|
function getContentType(filePath: string): InternalResource["contentType"] {
|
|
@@ -70,6 +71,7 @@ export class SkillProtocolHandler implements ProtocolHandler {
|
|
|
70
71
|
if (!resolvedPath.startsWith(resolvedBaseDir + path.sep) && resolvedPath !== resolvedBaseDir) {
|
|
71
72
|
throw new Error("Path traversal is not allowed");
|
|
72
73
|
}
|
|
74
|
+
targetPath = await realSkillPath(skill.baseDir, targetPath);
|
|
73
75
|
} else {
|
|
74
76
|
targetPath = skill.filePath;
|
|
75
77
|
}
|
package/src/lsp/edits.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
WorkspaceEdit,
|
|
14
14
|
} from "./types";
|
|
15
15
|
import { uriToFile } from "./utils";
|
|
16
|
+
import { assertInsideWorkspace } from "./workspace-path";
|
|
16
17
|
|
|
17
18
|
// =============================================================================
|
|
18
19
|
// Text Edit Application
|
|
@@ -133,6 +134,7 @@ export async function applyWorkspaceEdit(edit: WorkspaceEdit, cwd: string): Prom
|
|
|
133
134
|
const textEditsByUri = flattenWorkspaceTextEdits(edit);
|
|
134
135
|
for (const [uri, textEdits] of textEditsByUri) {
|
|
135
136
|
const filePath = uriToFile(uri);
|
|
137
|
+
await assertInsideWorkspace(cwd, filePath);
|
|
136
138
|
await applyTextEdits(filePath, textEdits);
|
|
137
139
|
applied.push(`Applied ${textEdits.length} edit(s) to ${formatPathRelativeToCwd(filePath, cwd)}`);
|
|
138
140
|
}
|
|
@@ -144,18 +146,22 @@ export async function applyWorkspaceEdit(edit: WorkspaceEdit, cwd: string): Prom
|
|
|
144
146
|
if (change.kind === "create") {
|
|
145
147
|
const createOp = change as CreateFile;
|
|
146
148
|
const filePath = uriToFile(createOp.uri);
|
|
149
|
+
await assertInsideWorkspace(cwd, filePath);
|
|
147
150
|
await Bun.write(filePath, "");
|
|
148
151
|
applied.push(`Created ${formatPathRelativeToCwd(filePath, cwd)}`);
|
|
149
152
|
} else if (change.kind === "rename") {
|
|
150
153
|
const renameOp = change as RenameFile;
|
|
151
154
|
const oldPath = uriToFile(renameOp.oldUri);
|
|
152
155
|
const newPath = uriToFile(renameOp.newUri);
|
|
156
|
+
await assertInsideWorkspace(cwd, oldPath);
|
|
157
|
+
await assertInsideWorkspace(cwd, newPath);
|
|
153
158
|
await fs.mkdir(path.dirname(newPath), { recursive: true });
|
|
154
159
|
await fs.rename(oldPath, newPath);
|
|
155
160
|
applied.push(`Renamed ${formatPathRelativeToCwd(oldPath, cwd)} → ${formatPathRelativeToCwd(newPath, cwd)}`);
|
|
156
161
|
} else if (change.kind === "delete") {
|
|
157
162
|
const deleteOp = change as DeleteFile;
|
|
158
163
|
const filePath = uriToFile(deleteOp.uri);
|
|
164
|
+
await assertInsideWorkspace(cwd, filePath);
|
|
159
165
|
await fs.rm(filePath, { recursive: true });
|
|
160
166
|
applied.push(`Deleted ${formatPathRelativeToCwd(filePath, cwd)}`);
|
|
161
167
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { ToolError } from "../tools/tool-errors";
|
|
4
|
+
|
|
5
|
+
function isEnoent(error: unknown): boolean {
|
|
6
|
+
return (error as NodeJS.ErrnoException).code === "ENOENT";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function escapes(root: string, candidate: string): boolean {
|
|
10
|
+
return candidate !== root && !candidate.startsWith(`${root}${path.sep}`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Resolve a missing path through the nearest existing ancestor. */
|
|
14
|
+
async function canonicalize(filePath: string): Promise<string> {
|
|
15
|
+
try {
|
|
16
|
+
return await fs.realpath(filePath);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
if (!isEnoent(error)) throw error;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const missing: string[] = [];
|
|
22
|
+
let cursor = filePath;
|
|
23
|
+
for (;;) {
|
|
24
|
+
let stat: Awaited<ReturnType<typeof fs.lstat>> | undefined;
|
|
25
|
+
try {
|
|
26
|
+
stat = await fs.lstat(cursor);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (!isEnoent(error)) throw error;
|
|
29
|
+
}
|
|
30
|
+
if (stat) {
|
|
31
|
+
try {
|
|
32
|
+
return path.join(await fs.realpath(cursor), ...missing);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (isEnoent(error)) throw new ToolError(`LSP edit escapes the workspace: ${filePath}`);
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const parent = path.dirname(cursor);
|
|
39
|
+
if (parent === cursor) throw new ToolError(`LSP edit escapes the workspace: ${filePath}`);
|
|
40
|
+
missing.unshift(path.basename(cursor));
|
|
41
|
+
cursor = parent;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function assertInsideWorkspace(cwd: string, filePath: string): Promise<void> {
|
|
46
|
+
const root = await fs.realpath(cwd);
|
|
47
|
+
const candidate = await canonicalize(filePath);
|
|
48
|
+
if (escapes(root, candidate)) {
|
|
49
|
+
throw new ToolError(`LSP edit escapes the workspace: ${filePath}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/main.ts
CHANGED
|
@@ -475,6 +475,11 @@ type StartupModelProfileArgs = {
|
|
|
475
475
|
preferCachedDefaultProfile?: boolean;
|
|
476
476
|
};
|
|
477
477
|
|
|
478
|
+
type StartupModelProfileFailure = {
|
|
479
|
+
profileName: string;
|
|
480
|
+
error: ModelProfileCredentialError | UnknownModelProfileError;
|
|
481
|
+
};
|
|
482
|
+
|
|
478
483
|
function staleDefaultProfileMessage(error: UnknownModelProfileError, profileCatalogRefreshUnavailable = false): string {
|
|
479
484
|
const refreshNotice = profileCatalogRefreshUnavailable
|
|
480
485
|
? " The online profile catalog could not be refreshed; continuing with the last accepted catalog."
|
|
@@ -488,6 +493,8 @@ async function applyStartupModelProfilesWithPolicy(
|
|
|
488
493
|
onUnknownDefault?: (error: UnknownModelProfileError, profileCatalogRefreshUnavailable: boolean) => void,
|
|
489
494
|
): Promise<void> {
|
|
490
495
|
let profileCatalogRefreshUnavailable = false;
|
|
496
|
+
let profilePassFailures: StartupModelProfileFailure[] = [];
|
|
497
|
+
let unhandledProfileName: string | undefined;
|
|
491
498
|
const applyProfile = async (
|
|
492
499
|
profileName: string,
|
|
493
500
|
persistDefault: boolean,
|
|
@@ -496,37 +503,66 @@ async function applyStartupModelProfilesWithPolicy(
|
|
|
496
503
|
tolerateCredentialError?: boolean;
|
|
497
504
|
tolerateUnknownDefault?: boolean;
|
|
498
505
|
runtimeBindingsOnly?: boolean;
|
|
506
|
+
userSelectionRevision?: number;
|
|
499
507
|
} = {},
|
|
500
508
|
): Promise<boolean> => {
|
|
501
|
-
|
|
509
|
+
const isCurrent =
|
|
510
|
+
options.userSelectionRevision === undefined
|
|
511
|
+
? undefined
|
|
512
|
+
: () => args.session.getUserModelSelectionRevision() === options.userSelectionRevision;
|
|
513
|
+
if (isCurrent && !isCurrent()) return false;
|
|
514
|
+
const apply = async (): Promise<boolean> => {
|
|
515
|
+
if (isCurrent && !isCurrent()) return false;
|
|
502
516
|
if (options.runtimeBindingsOnly) {
|
|
503
|
-
await applyModelProfileRuntimeBindings(
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
517
|
+
await applyModelProfileRuntimeBindings(
|
|
518
|
+
{
|
|
519
|
+
session: args.session,
|
|
520
|
+
modelRegistry: args.modelRegistry,
|
|
521
|
+
settings: args.settings,
|
|
522
|
+
profileName,
|
|
523
|
+
},
|
|
524
|
+
isCurrent,
|
|
525
|
+
);
|
|
509
526
|
} else {
|
|
510
527
|
await activateModelProfile(
|
|
511
528
|
{ session: args.session, modelRegistry: args.modelRegistry, settings: args.settings, profileName },
|
|
512
|
-
{
|
|
529
|
+
{
|
|
530
|
+
persistDefault,
|
|
531
|
+
thinkingLevelOverride: options.thinkingLevelOverride,
|
|
532
|
+
isCurrent,
|
|
533
|
+
},
|
|
513
534
|
);
|
|
514
535
|
}
|
|
515
536
|
return true;
|
|
537
|
+
};
|
|
538
|
+
try {
|
|
539
|
+
return isCurrent ? await args.session.withSdkControlMutation(apply) : await apply();
|
|
516
540
|
} catch (error) {
|
|
517
541
|
if (error instanceof ModelProfileCredentialError && (onCredentialError || options.tolerateCredentialError)) {
|
|
518
|
-
|
|
519
|
-
|
|
542
|
+
profilePassFailures.push({ profileName, error });
|
|
543
|
+
logger.warn("Startup model profile not applied: missing provider credentials", {
|
|
544
|
+
profile: profileName,
|
|
545
|
+
errorClass: error.name,
|
|
546
|
+
providers: error.providers,
|
|
547
|
+
role: error.role,
|
|
548
|
+
});
|
|
549
|
+
if (!onCredentialError) process.stderr.write(`${chalk.yellow(`Warning: ${error.message}`)}\n`);
|
|
520
550
|
return false;
|
|
521
551
|
}
|
|
522
552
|
if (error instanceof UnknownModelProfileError && options.tolerateUnknownDefault) {
|
|
523
|
-
|
|
524
|
-
|
|
553
|
+
profilePassFailures.push({ profileName, error });
|
|
554
|
+
logger.warn("Startup model profile not applied: unknown profile", {
|
|
555
|
+
profile: profileName,
|
|
556
|
+
errorClass: error.name,
|
|
557
|
+
catalogRefreshUnavailable: profileCatalogRefreshUnavailable,
|
|
558
|
+
});
|
|
559
|
+
if (!onUnknownDefault)
|
|
525
560
|
process.stderr.write(
|
|
526
561
|
`${chalk.yellow(`Warning: ${staleDefaultProfileMessage(error, profileCatalogRefreshUnavailable)}`)}\n`,
|
|
527
562
|
);
|
|
528
563
|
return false;
|
|
529
564
|
}
|
|
565
|
+
unhandledProfileName = profileName;
|
|
530
566
|
throw error;
|
|
531
567
|
}
|
|
532
568
|
};
|
|
@@ -546,9 +582,16 @@ async function applyStartupModelProfilesWithPolicy(
|
|
|
546
582
|
const preferCachedProfiles =
|
|
547
583
|
(args.preferCachedModels === true && args.parsedArgs.mpreset !== undefined) ||
|
|
548
584
|
(args.preferCachedDefaultProfile === true && defaultProfile !== undefined);
|
|
549
|
-
|
|
585
|
+
let modelSelectionChangedDuringRecovery = false;
|
|
586
|
+
const applyConfiguredProfiles = async (
|
|
587
|
+
allowMissingDefault: boolean,
|
|
588
|
+
profileNames?: ReadonlySet<string>,
|
|
589
|
+
userSelectionRevision?: number,
|
|
590
|
+
): Promise<boolean> => {
|
|
591
|
+
profilePassFailures = [];
|
|
592
|
+
unhandledProfileName = undefined;
|
|
550
593
|
let applied = true;
|
|
551
|
-
if (defaultProfile) {
|
|
594
|
+
if (defaultProfile && (!profileNames || profileNames.has(defaultProfile))) {
|
|
552
595
|
applied =
|
|
553
596
|
(await applyProfile(defaultProfile, false, {
|
|
554
597
|
thinkingLevelOverride: args.settings.has("defaultThinkingLevel")
|
|
@@ -556,37 +599,141 @@ async function applyStartupModelProfilesWithPolicy(
|
|
|
556
599
|
: undefined,
|
|
557
600
|
tolerateCredentialError: tolerateDefaultProfileFailure,
|
|
558
601
|
tolerateUnknownDefault:
|
|
559
|
-
|
|
602
|
+
onUnknownDefault !== undefined || (allowMissingDefault && tolerateDefaultProfileFailure),
|
|
560
603
|
runtimeBindingsOnly: args.session.hasRecoveredDefaultFallbackChain(),
|
|
604
|
+
userSelectionRevision,
|
|
561
605
|
})) && applied;
|
|
562
606
|
}
|
|
563
|
-
if (args.parsedArgs.mpreset) {
|
|
564
|
-
applied =
|
|
607
|
+
if (args.parsedArgs.mpreset && (!profileNames || profileNames.has(args.parsedArgs.mpreset))) {
|
|
608
|
+
applied =
|
|
609
|
+
(await applyProfile(args.parsedArgs.mpreset, args.parsedArgs.default === true, {
|
|
610
|
+
userSelectionRevision,
|
|
611
|
+
})) && applied;
|
|
565
612
|
}
|
|
566
613
|
return applied;
|
|
567
614
|
};
|
|
615
|
+
const profilesFromFirstFailure = (failedProfiles: ReadonlySet<string>): ReadonlySet<string> => {
|
|
616
|
+
const orderedProfiles = [defaultProfile, args.parsedArgs.mpreset].filter(
|
|
617
|
+
(name): name is string => name !== undefined,
|
|
618
|
+
);
|
|
619
|
+
const firstFailureIndex = orderedProfiles.findIndex(name => failedProfiles.has(name));
|
|
620
|
+
return firstFailureIndex === -1 ? failedProfiles : new Set(orderedProfiles.slice(firstFailureIndex));
|
|
621
|
+
};
|
|
622
|
+
const applyFailedProfiles = async (
|
|
623
|
+
failedProfiles: ReadonlySet<string>,
|
|
624
|
+
userSelectionRevision: number,
|
|
625
|
+
): Promise<boolean> => {
|
|
626
|
+
const applied = await applyConfiguredProfiles(true, failedProfiles, userSelectionRevision);
|
|
627
|
+
if (
|
|
628
|
+
applied &&
|
|
629
|
+
defaultProfile !== undefined &&
|
|
630
|
+
failedProfiles.has(defaultProfile) &&
|
|
631
|
+
args.parsedArgs.mpreset !== undefined &&
|
|
632
|
+
!failedProfiles.has(args.parsedArgs.mpreset)
|
|
633
|
+
) {
|
|
634
|
+
return applyProfile(args.parsedArgs.mpreset, args.parsedArgs.default === true, { userSelectionRevision });
|
|
635
|
+
}
|
|
636
|
+
return applied;
|
|
637
|
+
};
|
|
638
|
+
const refreshAuthAndCatalog = async (continueOnRefreshError: boolean): Promise<void> => {
|
|
639
|
+
try {
|
|
640
|
+
await args.modelRegistry.authStorage?.reload();
|
|
641
|
+
} catch (error) {
|
|
642
|
+
logger.warn("Failed to reload auth before startup model profile recovery", {
|
|
643
|
+
error: error instanceof Error ? error.message : String(error),
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
try {
|
|
647
|
+
await args.modelRegistry.refreshModelPresetProfilesFromRegistry();
|
|
648
|
+
} catch (error) {
|
|
649
|
+
if (!continueOnRefreshError && args.modelRegistry.getError()) throw error;
|
|
650
|
+
profileCatalogRefreshUnavailable = true;
|
|
651
|
+
logger.warn("Failed to refresh model profile catalog before startup recovery", {
|
|
652
|
+
error: error instanceof Error ? error.message : String(error),
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
try {
|
|
656
|
+
await args.modelRegistry.refresh("online-if-uncached", args.session.credentialSessionId);
|
|
657
|
+
} catch (error) {
|
|
658
|
+
if (!continueOnRefreshError) throw error;
|
|
659
|
+
logger.warn("Failed to refresh model catalog before startup profile recovery", {
|
|
660
|
+
error: error instanceof Error ? error.message : String(error),
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
const reportFinalProfileFailures = (markDefaultUnavailable = true): void => {
|
|
665
|
+
for (const failure of profilePassFailures) {
|
|
666
|
+
if (failure.error instanceof ModelProfileCredentialError) {
|
|
667
|
+
onCredentialError?.(failure.error);
|
|
668
|
+
} else {
|
|
669
|
+
onUnknownDefault?.(failure.error, profileCatalogRefreshUnavailable);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
if (
|
|
673
|
+
markDefaultUnavailable &&
|
|
674
|
+
(onCredentialError || onUnknownDefault) &&
|
|
675
|
+
args.parsedArgs.mpreset === undefined &&
|
|
676
|
+
args.parsedArgs.model === undefined
|
|
677
|
+
) {
|
|
678
|
+
const defaultFailure = profilePassFailures.find(failure => failure.profileName === defaultProfile);
|
|
679
|
+
if (defaultFailure) args.session.setUnavailableModelProfile(defaultFailure.profileName);
|
|
680
|
+
}
|
|
681
|
+
};
|
|
568
682
|
|
|
683
|
+
// Deferred --mpreset startup can overlap selector input; never reapply its
|
|
684
|
+
// captured profile after the user has selected a newer model.
|
|
569
685
|
if (preferCachedProfiles) {
|
|
570
686
|
let applied: boolean;
|
|
571
687
|
let refreshedOnline = false;
|
|
688
|
+
const selectionRevision = args.session.getUserModelSelectionRevision();
|
|
572
689
|
try {
|
|
573
|
-
applied = await applyConfiguredProfiles(false);
|
|
690
|
+
applied = await applyConfiguredProfiles(false, undefined, selectionRevision);
|
|
574
691
|
} catch (error) {
|
|
575
692
|
if (error instanceof ModelProfileCredentialError) throw error;
|
|
576
693
|
if (error instanceof UnknownModelProfileError) {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
if (args.modelRegistry.getError()) throw refreshError;
|
|
581
|
-
profileCatalogRefreshUnavailable = true;
|
|
582
|
-
logger.warn("Failed to refresh model profile catalog before startup recovery", {
|
|
583
|
-
error: refreshError instanceof Error ? refreshError.message : String(refreshError),
|
|
584
|
-
});
|
|
585
|
-
}
|
|
694
|
+
await refreshAuthAndCatalog(onCredentialError !== undefined || onUnknownDefault !== undefined);
|
|
695
|
+
} else {
|
|
696
|
+
await args.modelRegistry.refresh("online-if-uncached", args.session.credentialSessionId);
|
|
586
697
|
}
|
|
587
|
-
|
|
698
|
+
if (args.session.getUserModelSelectionRevision() !== selectionRevision) return;
|
|
588
699
|
refreshedOnline = true;
|
|
589
|
-
|
|
700
|
+
const failedProfiles = new Set(profilePassFailures.map(failure => failure.profileName));
|
|
701
|
+
if (unhandledProfileName) failedProfiles.add(unhandledProfileName);
|
|
702
|
+
const profilesToRetry = profilesFromFirstFailure(failedProfiles);
|
|
703
|
+
applied = await applyConfiguredProfiles(
|
|
704
|
+
true,
|
|
705
|
+
profilesToRetry.size > 0 ? profilesToRetry : undefined,
|
|
706
|
+
selectionRevision,
|
|
707
|
+
);
|
|
708
|
+
if (args.session.getUserModelSelectionRevision() !== selectionRevision) return;
|
|
709
|
+
}
|
|
710
|
+
if (args.session.getUserModelSelectionRevision() !== selectionRevision) {
|
|
711
|
+
modelSelectionChangedDuringRecovery = true;
|
|
712
|
+
applied = false;
|
|
713
|
+
}
|
|
714
|
+
if (
|
|
715
|
+
!modelSelectionChangedDuringRecovery &&
|
|
716
|
+
!applied &&
|
|
717
|
+
profilePassFailures.length > 0 &&
|
|
718
|
+
(onCredentialError !== undefined || onUnknownDefault !== undefined)
|
|
719
|
+
) {
|
|
720
|
+
if (!refreshedOnline) {
|
|
721
|
+
const failedProfiles = new Set(profilePassFailures.map(failure => failure.profileName));
|
|
722
|
+
await refreshAuthAndCatalog(true);
|
|
723
|
+
refreshedOnline = true;
|
|
724
|
+
if (args.session.getUserModelSelectionRevision() !== selectionRevision) {
|
|
725
|
+
modelSelectionChangedDuringRecovery = true;
|
|
726
|
+
applied = false;
|
|
727
|
+
} else {
|
|
728
|
+
applied = await applyFailedProfiles(failedProfiles, selectionRevision);
|
|
729
|
+
if (args.session.getUserModelSelectionRevision() !== selectionRevision) {
|
|
730
|
+
modelSelectionChangedDuringRecovery = true;
|
|
731
|
+
applied = false;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
if (applied) args.session.setUnavailableModelProfile(undefined);
|
|
736
|
+
else reportFinalProfileFailures(!modelSelectionChangedDuringRecovery);
|
|
590
737
|
}
|
|
591
738
|
if (applied && !refreshedOnline)
|
|
592
739
|
args.modelRegistry.refreshInBackground("online-if-uncached", args.session.credentialSessionId);
|
|
@@ -598,7 +745,7 @@ async function applyStartupModelProfilesWithPolicy(
|
|
|
598
745
|
}
|
|
599
746
|
|
|
600
747
|
// Explicit CLI --model/--thinking must win over any activated or skipped profile.
|
|
601
|
-
if (explicitModel) {
|
|
748
|
+
if (explicitModel && !modelSelectionChangedDuringRecovery) {
|
|
602
749
|
await args.session.setModelTemporary(explicitModel, args.startupThinkingLevel ?? args.parsedArgs.thinking, {
|
|
603
750
|
persistAsSessionDefault: true,
|
|
604
751
|
cause: "startup-override",
|
|
@@ -606,7 +753,7 @@ async function applyStartupModelProfilesWithPolicy(
|
|
|
606
753
|
const selector = `${explicitModel.provider}/${explicitModel.id}`;
|
|
607
754
|
args.session.setConfiguredModelChain("default", [selector], "startup-override", undefined, true);
|
|
608
755
|
args.session.seedDefaultFallbackResolution(0, []);
|
|
609
|
-
} else if (args.parsedArgs.thinking && args.session.model) {
|
|
756
|
+
} else if (!modelSelectionChangedDuringRecovery && args.parsedArgs.thinking && args.session.model) {
|
|
610
757
|
await args.session.setModelTemporary(args.session.model, args.parsedArgs.thinking, { cause: "startup-override" });
|
|
611
758
|
}
|
|
612
759
|
}
|
|
@@ -975,6 +1122,21 @@ export async function runInteractiveMode(
|
|
|
975
1122
|
);
|
|
976
1123
|
}
|
|
977
1124
|
|
|
1125
|
+
// Startup input bypasses the editor submit path, so seed the automatic title
|
|
1126
|
+
// here. Only text that `prompt` actually dispatches to a loaded skill is
|
|
1127
|
+
// skipped; unknown `/skill:` text is submitted as an ordinary prompt and is
|
|
1128
|
+
// titled like one. It must run before the prompt (the title gate skips
|
|
1129
|
+
// sessions that already hold a user message), and a title failure must never
|
|
1130
|
+
// drop the prompt.
|
|
1131
|
+
const maybeGenerateStartupTitle = (text: string, images?: readonly unknown[]): void => {
|
|
1132
|
+
try {
|
|
1133
|
+
if (session.resolvePromptSkillInvocation(text, images) || session.isLocallyHandledSlashCommand(text)) return;
|
|
1134
|
+
mode.maybeGenerateSessionTitle(text);
|
|
1135
|
+
} catch (error: unknown) {
|
|
1136
|
+
logger.warn("Startup session title generation failed", { error: String(error) });
|
|
1137
|
+
}
|
|
1138
|
+
};
|
|
1139
|
+
|
|
978
1140
|
const runStartupInputAndPromptLoop = async (): Promise<never> => {
|
|
979
1141
|
const hasStartupInput = initialMessage !== undefined || initialMessages.length > 0;
|
|
980
1142
|
if (!hasStartupInput && resumeAction === "continue-tail") {
|
|
@@ -988,6 +1150,7 @@ export async function runInteractiveMode(
|
|
|
988
1150
|
|
|
989
1151
|
if (initialMessage !== undefined) {
|
|
990
1152
|
try {
|
|
1153
|
+
maybeGenerateStartupTitle(initialMessage, initialImages);
|
|
991
1154
|
await session.prompt(initialMessage, { images: initialImages });
|
|
992
1155
|
} catch (error: unknown) {
|
|
993
1156
|
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
@@ -1004,6 +1167,7 @@ export async function runInteractiveMode(
|
|
|
1004
1167
|
});
|
|
1005
1168
|
if (slashResult === true) continue;
|
|
1006
1169
|
if (typeof slashResult === "string") text = slashResult;
|
|
1170
|
+
maybeGenerateStartupTitle(text);
|
|
1007
1171
|
await session.prompt(text);
|
|
1008
1172
|
} catch (error: unknown) {
|
|
1009
1173
|
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
@@ -38,6 +38,8 @@ import {
|
|
|
38
38
|
} from "@agentclientprotocol/sdk";
|
|
39
39
|
import { getAgentDir, logger, resolveEquivalentPath } from "@gajae-code/utils";
|
|
40
40
|
import packageJson from "../../../package.json" with { type: "json" };
|
|
41
|
+
import { PROJECT_PROGRESS_SNAPSHOT_SCHEMA, type ProjectProgressSnapshot } from "../../progress/progress-contract";
|
|
42
|
+
import { PROGRESS_COMMAND_ACP_DESCRIPTION, renderProgressSnapshot } from "../../progress/render-progress";
|
|
41
43
|
import {
|
|
42
44
|
ACP_SESSION_RECONNECT,
|
|
43
45
|
type AcpProviderRegistration,
|
|
@@ -258,6 +260,8 @@ type PendingAttachment = { epoch: number; task: Promise<void> };
|
|
|
258
260
|
|
|
259
261
|
type PromptAdmissionSettlement = { kind: "cancelled" } | { kind: "rejected"; error: AcpSdkAdapterError };
|
|
260
262
|
type PromptAdmissionReservation = {
|
|
263
|
+
/** Held by a model-free builtin (`/progress`) answered locally; no host turn exists to abort. */
|
|
264
|
+
localCommand?: true;
|
|
261
265
|
cancelled?: boolean;
|
|
262
266
|
settlement?: PromptAdmissionSettlement;
|
|
263
267
|
wake: () => void;
|
|
@@ -526,9 +530,12 @@ function pageItems(value: unknown): unknown[] {
|
|
|
526
530
|
return Array.isArray(page?.items) ? page.items : [];
|
|
527
531
|
}
|
|
528
532
|
|
|
533
|
+
/** Builtin `/progress`: answered locally from the read-only `session.progress` SDK query, never a model turn. */
|
|
534
|
+
const ACP_PROGRESS_COMMAND: AvailableCommand = { name: "progress", description: PROGRESS_COMMAND_ACP_DESCRIPTION };
|
|
535
|
+
|
|
529
536
|
/** Build the ACP command palette from the shared builtins and live SDK skill state. */
|
|
530
537
|
export function acpAvailableCommandsFromSkills(query: unknown): AvailableCommand[] {
|
|
531
|
-
const commands = new Map<string, AvailableCommand>();
|
|
538
|
+
const commands = new Map<string, AvailableCommand>([[ACP_PROGRESS_COMMAND.name, ACP_PROGRESS_COMMAND]]);
|
|
532
539
|
for (const item of pageItems(query)) {
|
|
533
540
|
const skill = object(item);
|
|
534
541
|
if (typeof skill?.name !== "string" || !skill.name) continue;
|
|
@@ -1508,6 +1515,22 @@ export function acpSkillInvocation(blocks: PromptRequest["prompt"]): { name: str
|
|
|
1508
1515
|
return { name: match[1], args: match[2]?.trim() ?? "" };
|
|
1509
1516
|
}
|
|
1510
1517
|
|
|
1518
|
+
/**
|
|
1519
|
+
* Recognize the builtin `/progress` command only when it is the complete, single text
|
|
1520
|
+
* prompt. Like the TUI (`allowArgs: false`), `/progress <args>` is not the command.
|
|
1521
|
+
*/
|
|
1522
|
+
export function isAcpProgressCommand(blocks: PromptRequest["prompt"]): boolean {
|
|
1523
|
+
return blocks.length === 1 && blocks[0]?.type === "text" && blocks[0].text.trim() === "/progress";
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
/** Render the `session.progress` query result as the plain-text `/progress` view. */
|
|
1527
|
+
export function acpProgressReportText(query: unknown): string {
|
|
1528
|
+
const snapshot = object(pageItems(query)[0]);
|
|
1529
|
+
if (snapshot?.schema !== PROJECT_PROGRESS_SNAPSHOT_SCHEMA)
|
|
1530
|
+
throw new AcpSdkAdapterError("unavailable", "SDK session.progress response omitted the progress snapshot.");
|
|
1531
|
+
return renderProgressSnapshot(snapshot as unknown as ProjectProgressSnapshot).join("\n");
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1511
1534
|
/** Convert every ACP prompt block the agent advertises without silently discarding context. */
|
|
1512
1535
|
export function acpPromptPayload(blocks: PromptRequest["prompt"]): {
|
|
1513
1536
|
text: string;
|
|
@@ -2398,6 +2421,38 @@ export class AcpAgent implements Agent {
|
|
|
2398
2421
|
}
|
|
2399
2422
|
}
|
|
2400
2423
|
|
|
2424
|
+
/**
|
|
2425
|
+
* Read-only builtin `/progress`: answered from the session.progress query instead of a
|
|
2426
|
+
* model turn. It owns prompt admission (`pendingPromptAdmission`) from the query until its
|
|
2427
|
+
* response is published, so a concurrent prompt is refused as `conflict` exactly as it is
|
|
2428
|
+
* while a model turn is active. `cancel` and session teardown settle that reservation
|
|
2429
|
+
* through the existing admission paths; this command observes it and stops publishing.
|
|
2430
|
+
*/
|
|
2431
|
+
async #runProgressCommand(sessionId: string, record: SessionRecord): Promise<PromptResponse> {
|
|
2432
|
+
const { promise: woken, resolve: wake } = Promise.withResolvers<void>();
|
|
2433
|
+
const reservation: PromptAdmissionReservation = { localCommand: true, wake, woken };
|
|
2434
|
+
// Replaces a follow-up reservation this prompt already consumed; `#submitPrompt` only
|
|
2435
|
+
// clears the reservation it installed, so ownership stays with this command.
|
|
2436
|
+
record.pendingPromptAdmission = reservation;
|
|
2437
|
+
try {
|
|
2438
|
+
const query = record.adapter.query("session.progress");
|
|
2439
|
+
void query.catch(() => undefined);
|
|
2440
|
+
const result = await Promise.race([query.then(value => ({ value })), woken.then(() => undefined)]);
|
|
2441
|
+
const settlement = reservation.settlement;
|
|
2442
|
+
if (settlement?.kind === "rejected") throw settlement.error;
|
|
2443
|
+
if (settlement?.kind === "cancelled" || reservation.cancelled || !result) return { stopReason: "cancelled" };
|
|
2444
|
+
const text = acpProgressReportText(result.value);
|
|
2445
|
+
await this.#publishSessionUpdate(
|
|
2446
|
+
sessionId,
|
|
2447
|
+
{ sessionId, update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text } } },
|
|
2448
|
+
record.adapter,
|
|
2449
|
+
);
|
|
2450
|
+
return { stopReason: "end_turn" };
|
|
2451
|
+
} finally {
|
|
2452
|
+
if (record.pendingPromptAdmission === reservation) record.pendingPromptAdmission = undefined;
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2401
2456
|
async #waitForPromptSettlement(
|
|
2402
2457
|
waiter: PromptWaiter,
|
|
2403
2458
|
admissionReservation: PromptAdmissionReservation,
|
|
@@ -2482,6 +2537,7 @@ export class AcpAgent implements Agent {
|
|
|
2482
2537
|
);
|
|
2483
2538
|
return { stopReason: "end_turn" };
|
|
2484
2539
|
}
|
|
2540
|
+
if (isAcpProgressCommand(params.prompt)) return await this.#runProgressCommand(params.sessionId, record);
|
|
2485
2541
|
// The SDK transport hard-caps a single request frame at 256 KiB and answers an
|
|
2486
2542
|
// oversize frame by closing the socket (CloseCode::Size, crates/gjc-sdk/src/server.rs),
|
|
2487
2543
|
// which surfaces to the client as an opaque `connection_closed` mid-turn. Reject
|
|
@@ -2969,6 +3025,7 @@ export class AcpAgent implements Agent {
|
|
|
2969
3025
|
// Record the client's intent before awaiting the SDK so a prompt that rejects
|
|
2970
3026
|
// mid-cancel (e.g. preflight `busy`) can still settle as `cancelled`.
|
|
2971
3027
|
record.cancelRequested = true;
|
|
3028
|
+
const localCommand = !record.activePrompt && record.pendingPromptAdmission?.localCommand === true;
|
|
2972
3029
|
this.#settlePendingPromptAdmission(record, { kind: "cancelled" });
|
|
2973
3030
|
// C04 terminal abort: an external client cancel stops the current turn
|
|
2974
3031
|
// (`scope:"turn"`, the default, matching the SDK `turn.abort` default and
|
|
@@ -2976,6 +3033,12 @@ export class AcpAgent implements Agent {
|
|
|
2976
3033
|
// subagents and background tasks stopped opts in with
|
|
2977
3034
|
// `_meta.gjc.abortScope: "owned"` (or `GJC_ACP_ABORT_SCOPE=owned`).
|
|
2978
3035
|
const scope = resolveAcpAbortScope(params._meta, process.env);
|
|
3036
|
+
// The current turn is a model-free builtin answered locally: settling its reservation
|
|
3037
|
+
// above cancels it, and the host has no turn to abort.
|
|
3038
|
+
if (localCommand && scope === "turn") {
|
|
3039
|
+
record.cancelRequested = false;
|
|
3040
|
+
return;
|
|
3041
|
+
}
|
|
2979
3042
|
const waiter = record.activePrompt;
|
|
2980
3043
|
const waiterWasUnacknowledged = waiter !== undefined && !waiter.acknowledged;
|
|
2981
3044
|
const waiterWasBeforeActivity = waiter !== undefined && !waiter.observedTurnActivity;
|
|
@@ -3051,9 +3114,11 @@ export class AcpAgent implements Agent {
|
|
|
3051
3114
|
// publication is advisory and must never gate the settlement above.
|
|
3052
3115
|
void this.#publishPromptPhaseIdle(params.sessionId, record.adapter);
|
|
3053
3116
|
} else {
|
|
3117
|
+
if (waiter && record.activePrompt === waiter && waiter.terminal && !waiter.settled)
|
|
3118
|
+
this.#settlePrompt(params.sessionId, record, waiter);
|
|
3054
3119
|
// The acknowledgement proves the run was aborted, not that its terminal was
|
|
3055
3120
|
// published. Arm the bounded settlement so the turn cannot outlive the cancel.
|
|
3056
|
-
this.#scheduleCancelSettlement(params.sessionId, record);
|
|
3121
|
+
else this.#scheduleCancelSettlement(params.sessionId, record);
|
|
3057
3122
|
}
|
|
3058
3123
|
waiter?.cancelAttemptResolve?.(true);
|
|
3059
3124
|
} catch (error) {
|
|
@@ -3069,6 +3134,9 @@ export class AcpAgent implements Agent {
|
|
|
3069
3134
|
!waiter.settled
|
|
3070
3135
|
) {
|
|
3071
3136
|
if (waiter.terminalReserved) {
|
|
3137
|
+
await record.frameTail;
|
|
3138
|
+
if (record.activePrompt === waiter && waiter.terminal && !waiter.settled)
|
|
3139
|
+
this.#settlePrompt(params.sessionId, record, waiter);
|
|
3072
3140
|
waiter.cancelAttemptResolve?.(true);
|
|
3073
3141
|
return;
|
|
3074
3142
|
}
|
|
@@ -3124,6 +3192,9 @@ export class AcpAgent implements Agent {
|
|
|
3124
3192
|
waiter.cancelAttempt = undefined;
|
|
3125
3193
|
waiter.cancelAttemptResolve = undefined;
|
|
3126
3194
|
}
|
|
3195
|
+
const cancelWaveSettled = !waiter || waiter.cancelAcknowledged || (waiter.pendingCancelAttempts ?? 0) <= 1;
|
|
3196
|
+
if (cancelWaveSettled && waiter && record.activePrompt === waiter && waiter.terminal && !waiter.settled)
|
|
3197
|
+
this.#settlePrompt(params.sessionId, record, waiter);
|
|
3127
3198
|
throw cancellationError;
|
|
3128
3199
|
} finally {
|
|
3129
3200
|
if (waiter) {
|
|
@@ -4885,7 +4956,18 @@ export class AcpAgent implements Agent {
|
|
|
4885
4956
|
// Failure diagnostics are useful but advisory. Settle before any mapped
|
|
4886
4957
|
// session update can await a backpressured client transport; otherwise an
|
|
4887
4958
|
// already-decided failure can still lose to the inactivity watchdog.
|
|
4888
|
-
|
|
4959
|
+
// A terminal that races an in-flight cancel must not settle the public prompt before
|
|
4960
|
+
// the cancel request proves its outcome. Otherwise a follow-up prompt can be admitted
|
|
4961
|
+
// between the stale terminal settlement and the abort acknowledgement, leaving the
|
|
4962
|
+
// cancelled background request reported as `end_turn`.
|
|
4963
|
+
const cancelSettlementPending =
|
|
4964
|
+
activePrompt.cancelAttempt !== undefined &&
|
|
4965
|
+
!activePrompt.cancelAcknowledged &&
|
|
4966
|
+
record.statusRecovery !== activePrompt &&
|
|
4967
|
+
!this.#ownsUncertainAbort(record, activePrompt);
|
|
4968
|
+
if (!cancelSettlementPending) {
|
|
4969
|
+
this.#settlePrompt(id, record, activePrompt);
|
|
4970
|
+
}
|
|
4889
4971
|
}
|
|
4890
4972
|
if (!isTerminal && derivedCorrelation === undefined) return;
|
|
4891
4973
|
if (!isTerminal && hasCorrelation(correlation)) {
|
|
@@ -5274,7 +5356,9 @@ export class AcpAgent implements Agent {
|
|
|
5274
5356
|
const { outcome } = waiter.terminal;
|
|
5275
5357
|
this.#rememberSettledPromptCorrelation(id, record, waiter.correlation);
|
|
5276
5358
|
if (outcome.kind === "stopped") {
|
|
5277
|
-
|
|
5359
|
+
// Once the SDK has acknowledged a client cancel, the cancellation owns the
|
|
5360
|
+
// turn even if a stale terminal frame races in with another stopped reason.
|
|
5361
|
+
waiter.resolve({ stopReason: waiter.cancelAcknowledged ? "cancelled" : outcome.reason });
|
|
5278
5362
|
return;
|
|
5279
5363
|
}
|
|
5280
5364
|
// `phase` is the host's own claim, and a host that omits it leaves `terminalOutcome`
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Editor, type KeyId, matchesKey, parseKittySequence } from "@gajae-code/tui";
|
|
2
2
|
import { BracketedPasteHandler } from "@gajae-code/tui/bracketed-paste";
|
|
3
3
|
import { type AppKeybinding, KEYBINDINGS } from "../../config/keybindings";
|
|
4
|
+
import { macosOptionChordForComposedText } from "../utils/macos-option-forwarding";
|
|
4
5
|
|
|
5
6
|
type ConfigurableEditorAction = Extract<
|
|
6
7
|
AppKeybinding,
|
|
@@ -116,6 +117,12 @@ export class CustomEditor extends Editor {
|
|
|
116
117
|
onQueue?: () => void;
|
|
117
118
|
/** Called when Caps Lock is pressed. */
|
|
118
119
|
onCapsLock?: () => void;
|
|
120
|
+
/**
|
|
121
|
+
* Called when typed text is what macOS composes for a bound Option chord
|
|
122
|
+
* (for example `œ` for a bound `alt+q`), meaning the terminal sent Option as
|
|
123
|
+
* text. The text is still inserted; the callback only explains the miss.
|
|
124
|
+
*/
|
|
125
|
+
onUnforwardedOptionChord?: (key: KeyId, text: string) => void;
|
|
119
126
|
/** Called when PageUp/PageDown should scroll the transcript viewport instead of prompt history. */
|
|
120
127
|
onViewportPageScroll?: (direction: -1 | 1) => void;
|
|
121
128
|
/** Called before regular composer input should return the transcript viewport to the live bottom. */
|
|
@@ -473,6 +480,13 @@ export class CustomEditor extends Editor {
|
|
|
473
480
|
}
|
|
474
481
|
}
|
|
475
482
|
|
|
483
|
+
if (this.onUnforwardedOptionChord) {
|
|
484
|
+
const optionKey = macosOptionChordForComposedText(data);
|
|
485
|
+
if (optionKey && (this.hasActionKey(optionKey) || this.#customKeyHandlers.has(optionKey))) {
|
|
486
|
+
this.onUnforwardedOptionChord(optionKey, data);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
476
490
|
// Pass to parent for normal handling.
|
|
477
491
|
super.handleInput(data);
|
|
478
492
|
}
|