@gajae-code/coding-agent 0.11.11 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -3
- package/dist/types/cli/read-cli.d.ts +1 -0
- package/dist/types/commands/deep-interview.d.ts +9 -0
- package/dist/types/commands/harness.d.ts +22 -0
- package/dist/types/commands/read.d.ts +6 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/provider-auth-health.d.ts +14 -0
- package/dist/types/config/provider-ranking.d.ts +59 -0
- package/dist/types/config/settings-schema.d.ts +29 -1
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/extensibility/extensions/types.d.ts +22 -2
- package/dist/types/extensibility/shared-events.d.ts +6 -0
- package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
- package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
- package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
- package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +52 -1
- package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
- package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
- package/dist/types/gjc-runtime/team-store.d.ts +12 -0
- package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +7 -0
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
- package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
- package/dist/types/main.d.ts +1 -1
- package/dist/types/memories/index.d.ts +2 -0
- package/dist/types/modes/acp/acp-agent.d.ts +8 -0
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
- package/dist/types/modes/components/read-tool-group.d.ts +4 -4
- package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/controllers/event-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
- package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +5 -0
- package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
- package/dist/types/sdk/bus/index.d.ts +15 -1
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
- package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/query/handlers.d.ts +5 -0
- package/dist/types/session/agent-session.d.ts +44 -10
- package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
- package/dist/types/session/session-manager.d.ts +25 -13
- package/dist/types/session/streaming-output.d.ts +60 -5
- package/dist/types/skill-state/active-state.d.ts +0 -4
- package/dist/types/skill-state/workflow-hud.d.ts +3 -0
- package/dist/types/tools/browser/launch.d.ts +7 -0
- package/dist/types/tools/fetch.d.ts +17 -1
- package/dist/types/tools/output-meta.d.ts +21 -1
- package/dist/types/tools/read-internals.d.ts +12 -0
- package/dist/types/tools/read.d.ts +45 -1
- package/dist/types/tools/resource-gc.d.ts +4 -0
- package/dist/types/tools/tool-result.d.ts +7 -1
- package/package.json +9 -13
- package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
- package/scripts/capture-sticky-viewport-showcase.ts +292 -0
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
- package/scripts/verify-sticky-viewport-showcase.ts +611 -0
- package/src/capability/index.ts +8 -0
- package/src/cli/read-cli.ts +2 -1
- package/src/cli.ts +1 -0
- package/src/commands/deep-interview.ts +29 -9
- package/src/commands/harness.ts +35 -12
- package/src/commands/read.ts +11 -2
- package/src/commands/team.ts +1 -1
- package/src/config/model-registry.ts +21 -2
- package/src/config/provider-auth-health.ts +42 -0
- package/src/config/provider-ranking.ts +119 -0
- package/src/config/settings-schema.ts +23 -1
- package/src/config/settings.ts +246 -108
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +229 -82
- package/src/defaults/gjc/skills/ralplan/SKILL.md +58 -25
- package/src/defaults/gjc/skills/team/SKILL.md +1 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +25 -15
- package/src/eval/py/executor.ts +7 -2
- package/src/extensibility/extensions/types.ts +28 -4
- package/src/extensibility/shared-events.ts +5 -0
- package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -82
- package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
- package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
- package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
- package/src/gjc-runtime/deep-interview-state.ts +12 -943
- package/src/gjc-runtime/gc-render.ts +1 -0
- package/src/gjc-runtime/gc-runtime.ts +11 -1
- package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
- package/src/gjc-runtime/ralplan-runtime.ts +742 -85
- package/src/gjc-runtime/state-runtime.ts +2 -1
- package/src/gjc-runtime/state-writer.ts +70 -222
- package/src/gjc-runtime/team-launch.ts +34 -5
- package/src/gjc-runtime/team-runtime.ts +970 -33
- package/src/gjc-runtime/team-store.ts +32 -0
- package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
- package/src/gjc-runtime/team-workers.ts +1 -0
- package/src/gjc-runtime/tmux-owner-isolation.ts +18 -0
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +13 -7
- package/src/gjc-runtime/workflow-manifest.generated.json +97 -350
- package/src/gjc-runtime/workflow-manifest.ts +34 -179
- package/src/hooks/native-skill-hook.ts +17 -4
- package/src/hooks/skill-state.ts +1 -23
- package/src/internal-urls/docs-index.generated.ts +8 -9
- package/src/internal-urls/local-root-gc.ts +127 -0
- package/src/main.ts +9 -5
- package/src/memories/index.ts +10 -0
- package/src/modes/DESIGN.md +47 -0
- package/src/modes/acp/acp-agent.ts +43 -7
- package/src/modes/acp/acp-event-mapper.ts +14 -7
- package/src/modes/acp/acp-mode.ts +39 -3
- package/src/modes/components/assistant-message.ts +123 -22
- package/src/modes/components/irc-sidebar.ts +176 -62
- package/src/modes/components/model-selector.ts +61 -4
- package/src/modes/components/oauth-selector.ts +42 -12
- package/src/modes/components/read-tool-group.ts +82 -14
- package/src/modes/components/session-observer-overlay.ts +187 -32
- package/src/modes/components/settings-selector.ts +60 -18
- package/src/modes/components/tool-execution.ts +138 -27
- package/src/modes/controllers/event-controller.ts +109 -27
- package/src/modes/controllers/extension-ui-controller.ts +19 -2
- package/src/modes/controllers/input-controller.ts +18 -3
- package/src/modes/controllers/selector-controller.ts +83 -37
- package/src/modes/interactive-mode.ts +62 -3
- package/src/modes/irc-observation-ledger.ts +71 -15
- package/src/modes/runtime-init.ts +19 -2
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +94 -1
- package/src/prompts/agents/architect.md +9 -1
- package/src/prompts/agents/critic.md +9 -0
- package/src/prompts/system/system-prompt.md +0 -1
- package/src/prompts/tools/read.md +4 -2
- package/src/runtime-mcp/smithery-auth.ts +17 -2
- package/src/runtime-mcp/smithery-connect.ts +11 -1
- package/src/sdk/broker/lifecycle.ts +25 -6
- package/src/sdk/bus/control-drain-lease.ts +104 -0
- package/src/sdk/bus/index.ts +341 -38
- package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
- package/src/sdk/bus/prompt-reconciliation.ts +2 -1
- package/src/sdk/bus/reconciliation-store.ts +224 -0
- package/src/sdk/host/control/dispatch.ts +5 -1
- package/src/sdk/host/control/operations.ts +1 -1
- package/src/sdk/host/host.ts +23 -1
- package/src/sdk/host/query/handlers.ts +55 -0
- package/src/sdk/mcp/server.ts +46 -18
- package/src/sdk/protocol/operation-inventory.generated.json +29 -0
- package/src/sdk/protocol/operation-registry.ts +7 -2
- package/src/sdk/session.ts +13 -1
- package/src/session/agent-session.ts +466 -50
- package/src/session/memory-guard-checkpoint-participant.ts +131 -0
- package/src/session/session-manager.ts +764 -17
- package/src/session/streaming-output.ts +262 -43
- package/src/setup/provider-onboarding.ts +19 -4
- package/src/skill-state/active-state.ts +0 -10
- package/src/skill-state/workflow-hud.ts +25 -2
- package/src/skill-state/workflow-mutation-guard.ts +65 -7
- package/src/tools/ask.ts +89 -69
- package/src/tools/browser/launch.ts +41 -6
- package/src/tools/fetch.ts +114 -23
- package/src/tools/output-meta.ts +176 -9
- package/src/tools/path-utils.ts +5 -1
- package/src/tools/read-internals.ts +44 -0
- package/src/tools/read.ts +1542 -310
- package/src/tools/resource-gc.ts +182 -18
- package/src/tools/tool-result.ts +14 -1
- package/src/web/search/providers/codex.ts +1 -1
- package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
- package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
- package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
- package/src/gjc-runtime/deep-interview-draft.ts +0 -843
- package/src/gjc-runtime/deep-interview-payload.ts +0 -262
- package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { isEnoent } from "@gajae-code/utils";
|
|
5
|
+
import type { GcCollectResult, GcContext, GcError, GcRecord, GcStoreAdapter } from "../gjc-runtime/gc-runtime";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* GC for `local://` session roots.
|
|
9
|
+
*
|
|
10
|
+
* `resolveLocalRoot` gives every session its own `<tmp>/gjc-local/<session-id>`
|
|
11
|
+
* directory, and the first `local://` resolution seeds it with a migration
|
|
12
|
+
* marker. Nothing ever removes those directories, so a machine accumulates one
|
|
13
|
+
* per session indefinitely — and the overwhelming majority only ever hold the
|
|
14
|
+
* marker, because most sessions never write a `local://` file.
|
|
15
|
+
*
|
|
16
|
+
* Only marker-only directories are eligible, and only after a grace period.
|
|
17
|
+
* That keeps the rule provably safe without needing a session-liveness oracle:
|
|
18
|
+
* a directory holding real content is never touched, and a session that just
|
|
19
|
+
* started is still inside its grace window.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const LOCAL_ROOT_PARENT = "gjc-local";
|
|
23
|
+
const LEGACY_MIGRATION_MARKER = ".gjc-local-legacy-migrated-v1";
|
|
24
|
+
|
|
25
|
+
/** Directories younger than this are left alone so a live session is never raced. */
|
|
26
|
+
const GRACE_MS = 24 * 60 * 60 * 1000;
|
|
27
|
+
|
|
28
|
+
/** Bound the scan so a pathological directory cannot stall `gjc gc`. */
|
|
29
|
+
const MAX_ENTRIES = 20_000;
|
|
30
|
+
|
|
31
|
+
function localRootParent(env: NodeJS.ProcessEnv): string {
|
|
32
|
+
return path.join(env.TMPDIR?.trim() || os.tmpdir(), LOCAL_ROOT_PARENT);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A root is prunable only when the marker is the single thing inside it. */
|
|
36
|
+
async function classify(dir: string, now: number): Promise<{ status: string; stale: boolean; reason: string } | null> {
|
|
37
|
+
let entries: string[];
|
|
38
|
+
try {
|
|
39
|
+
entries = await fs.readdir(dir);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (isEnoent(error)) return null;
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const extra = entries.filter(name => name !== LEGACY_MIGRATION_MARKER);
|
|
46
|
+
if (extra.length > 0)
|
|
47
|
+
return {
|
|
48
|
+
status: "in_use",
|
|
49
|
+
stale: false,
|
|
50
|
+
reason: `Holds ${extra.length} entr${extra.length === 1 ? "y" : "ies"} beyond the marker`,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const stat = await fs.stat(dir);
|
|
54
|
+
if (now - stat.mtimeMs < GRACE_MS) return { status: "recent", stale: false, reason: "Inside the grace window" };
|
|
55
|
+
return { status: "marker_only", stale: true, reason: "Only the migration marker, past the grace window" };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const localRootsGcAdapter: GcStoreAdapter = {
|
|
59
|
+
store: "local_roots",
|
|
60
|
+
async collect(ctx: GcContext): Promise<GcCollectResult> {
|
|
61
|
+
const records: GcRecord[] = [];
|
|
62
|
+
const errors: GcError[] = [];
|
|
63
|
+
const parent = localRootParent(ctx.env);
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
|
|
66
|
+
let dirents: string[];
|
|
67
|
+
try {
|
|
68
|
+
dirents = await fs.readdir(parent);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (isEnoent(error)) return { records, errors };
|
|
71
|
+
errors.push({ store: "local_roots", scope: parent, message: (error as Error).message });
|
|
72
|
+
return { records, errors };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (const name of dirents.slice(0, MAX_ENTRIES)) {
|
|
76
|
+
const dir = path.join(parent, name);
|
|
77
|
+
try {
|
|
78
|
+
const classified = await classify(dir, now);
|
|
79
|
+
if (!classified) continue;
|
|
80
|
+
records.push({
|
|
81
|
+
store: "local_roots",
|
|
82
|
+
id: name,
|
|
83
|
+
path: dir,
|
|
84
|
+
status: classified.status,
|
|
85
|
+
stale: classified.stale,
|
|
86
|
+
removable: classified.stale,
|
|
87
|
+
action: classified.stale ? "would_remove" : "none",
|
|
88
|
+
reason: classified.reason,
|
|
89
|
+
});
|
|
90
|
+
} catch (error) {
|
|
91
|
+
errors.push({ store: "local_roots", scope: dir, message: (error as Error).message });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (dirents.length > MAX_ENTRIES) {
|
|
96
|
+
errors.push({
|
|
97
|
+
store: "local_roots",
|
|
98
|
+
scope: parent,
|
|
99
|
+
message: `Scan truncated at ${MAX_ENTRIES} entries`,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { records, errors };
|
|
104
|
+
},
|
|
105
|
+
async prune(record: GcRecord) {
|
|
106
|
+
const dir = record.path;
|
|
107
|
+
if (!dir) return { removed: false, error: "Missing local root path" };
|
|
108
|
+
|
|
109
|
+
// Re-validate immediately before removing: the session may have written a
|
|
110
|
+
// file between collect and prune.
|
|
111
|
+
let classified: Awaited<ReturnType<typeof classify>>;
|
|
112
|
+
try {
|
|
113
|
+
classified = await classify(dir, Date.now());
|
|
114
|
+
} catch (error) {
|
|
115
|
+
return { removed: false, error: (error as Error).message };
|
|
116
|
+
}
|
|
117
|
+
if (!classified) return { removed: false, skipped: "Local root disappeared" };
|
|
118
|
+
if (!classified.stale) return { removed: false, skipped: `Local root no longer eligible (${classified.status})` };
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
122
|
+
return { removed: true };
|
|
123
|
+
} catch (error) {
|
|
124
|
+
return { removed: false, error: (error as Error).message };
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
};
|
package/src/main.ts
CHANGED
|
@@ -698,7 +698,7 @@ async function promptForkSession(session: SessionInfo): Promise<boolean> {
|
|
|
698
698
|
}
|
|
699
699
|
}
|
|
700
700
|
|
|
701
|
-
async function getChangelogForDisplay(parsed: Args): Promise<string | undefined> {
|
|
701
|
+
export async function getChangelogForDisplay(parsed: Args): Promise<string | undefined> {
|
|
702
702
|
if (parsed.continue || parsed.resume) {
|
|
703
703
|
return undefined;
|
|
704
704
|
}
|
|
@@ -710,15 +710,19 @@ async function getChangelogForDisplay(parsed: Args): Promise<string | undefined>
|
|
|
710
710
|
|
|
711
711
|
const lastVersion = settings.get("lastChangelogVersion");
|
|
712
712
|
if (!lastVersion) {
|
|
713
|
-
settings.
|
|
714
|
-
|
|
713
|
+
if (settings.canWriteDurableConfig()) {
|
|
714
|
+
settings.set("lastChangelogVersion", VERSION);
|
|
715
|
+
await flushChangelogVersion();
|
|
716
|
+
}
|
|
715
717
|
return getInstalledVersionChangelogEntry(entries, VERSION)?.content;
|
|
716
718
|
}
|
|
717
719
|
|
|
718
720
|
if (lastVersion !== VERSION) {
|
|
719
721
|
const newEntries = getNewEntries(entries, lastVersion);
|
|
720
|
-
settings.
|
|
721
|
-
|
|
722
|
+
if (settings.canWriteDurableConfig()) {
|
|
723
|
+
settings.set("lastChangelogVersion", VERSION);
|
|
724
|
+
await flushChangelogVersion();
|
|
725
|
+
}
|
|
722
726
|
if (newEntries.length > 0) {
|
|
723
727
|
return newEntries.map(e => e.content).join("\n\n");
|
|
724
728
|
}
|
package/src/memories/index.ts
CHANGED
|
@@ -976,6 +976,11 @@ function redactSecrets(input: string): string {
|
|
|
976
976
|
/(?:sk|pk|rk|tok|key|secret|token|password)[-_A-Za-z0-9]{12,}/g,
|
|
977
977
|
/[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}/g,
|
|
978
978
|
/(?:AKIA|ASIA)[A-Z0-9]{16}/g,
|
|
979
|
+
// GitHub tokens carry no keyword the first pattern recognizes, so without
|
|
980
|
+
// this they reached MEMORY.md and memory_summary.md verbatim — and the
|
|
981
|
+
// summary is injected into every later session. Same shapes the
|
|
982
|
+
// contribution-prep scrubber already covers.
|
|
983
|
+
/\b(?:gh[opsur]_[A-Za-z0-9_]{12,}|github_pat_[A-Za-z0-9_]{12,})\b/g,
|
|
979
984
|
];
|
|
980
985
|
for (const pattern of patterns) {
|
|
981
986
|
out = out.replace(pattern, "[REDACTED]");
|
|
@@ -983,6 +988,11 @@ function redactSecrets(input: string): string {
|
|
|
983
988
|
return out;
|
|
984
989
|
}
|
|
985
990
|
|
|
991
|
+
/** Secret scrubbing applied to every phase-2 consolidation output. Test seam. */
|
|
992
|
+
export function redactMemorySecretsForTesting(input: string): string {
|
|
993
|
+
return redactSecrets(input);
|
|
994
|
+
}
|
|
995
|
+
|
|
986
996
|
function sanitizeSkillName(name: string): string {
|
|
987
997
|
return name
|
|
988
998
|
.toLowerCase()
|
package/src/modes/DESIGN.md
CHANGED
|
@@ -261,3 +261,50 @@ record both manifest counts as 108 plus CJK review results.
|
|
|
261
261
|
|
|
262
262
|
No raw third-party design corpus, screenshot, or reference asset is stored by
|
|
263
263
|
this workflow.
|
|
264
|
+
|
|
265
|
+
## Sticky transcript viewport contract
|
|
266
|
+
|
|
267
|
+
Live mode remains in natural terminal flow. Entering manual history makes the
|
|
268
|
+
application-owned transcript lane scrollable while the status line and every
|
|
269
|
+
later direct composer child are a fixed suffix at the bottom. PageUp/PageDown
|
|
270
|
+
move by the transcript capacity; the wheel remains three rows. A focused editor
|
|
271
|
+
keeps focus, and ordinary editor input or paste follows live before processing
|
|
272
|
+
that input.
|
|
273
|
+
|
|
274
|
+
Manual output has one bounded boolean indication, exactly **`New output — type
|
|
275
|
+
to follow`**. It is not a count. New visible agent/tool/extension output may set
|
|
276
|
+
it while manual; reflow, transient chrome, reconciliation, and user input may
|
|
277
|
+
not. Following clears it only after a successful live repaint. Manual-era output
|
|
278
|
+
is authoritative in the application transcript but is never retroactively
|
|
279
|
+
replayed into native host scrollback; subsequent ordinary live output follows
|
|
280
|
+
normal host behavior.
|
|
281
|
+
|
|
282
|
+
The transcript is the only selectable coordinate space. Pinned status/composer
|
|
283
|
+
chrome, notices, blank rows, and overlays never enter selection or copied text;
|
|
284
|
+
CJK selection clamping remains directional and grapheme/cell aware. Under
|
|
285
|
+
constrained height the notice drops first, then decorative pet and low-priority
|
|
286
|
+
hooks. Focused editor/cursor, status, and normal hooks outrank those rows; zero
|
|
287
|
+
transcript capacity is valid and must not corrupt cursor geometry.
|
|
288
|
+
|
|
289
|
+
At narrow widths use ANSI-aware terminal-cell measurement. ASCII/no-color keeps
|
|
290
|
+
textual state without SGR. Korean, Japanese, Chinese, and mixed CJK/Latin prose
|
|
291
|
+
must wrap at semantic phrase boundaries, never through an action/status label or
|
|
292
|
+
short identifier; a semantic CJK break is a visual-QA failure.
|
|
293
|
+
|
|
294
|
+
### Sticky viewport deterministic visual QA
|
|
295
|
+
|
|
296
|
+
`test/fixtures/tui/sticky-viewport-showcase.ts` is a fixed-clock, no-network,
|
|
297
|
+
first-party harness that starts the production `TUI` over a `VirtualTerminal`.
|
|
298
|
+
It constructs transcript, status, hooks, and the real composer as children,
|
|
299
|
+
then drives the live/manual viewport path before capturing the terminal frame.
|
|
300
|
+
Capture with `bun packages/coding-agent/scripts/capture-sticky-viewport-showcase.ts
|
|
301
|
+
--out .gjc/qa/sticky-viewport-<run>` and verify with the paired `--root` script.
|
|
302
|
+
The immutable matrix has exactly 20 keys: `live-overflow`, `manual-history`,
|
|
303
|
+
`manual-new-output`, `multiline-editor-hooks-pet`, `capacity-many`,
|
|
304
|
+
`capacity-one`, `capacity-zero`, and `selection-boundary` at both 80x24 and
|
|
305
|
+
120x36 Unicode/color; plus `manual-new-output/80x24/ascii-no-color`,
|
|
306
|
+
`capacity-zero/48x10/ascii-no-color`,
|
|
307
|
+
`multiline-editor-hooks-pet/48x10/unicode-color`, and
|
|
308
|
+
`narrow-cjk/48x10/unicode-color`. Do not add or replace a manual-follow case.
|
|
309
|
+
|
|
310
|
+
Each key has only `terminal.txt`, ANSI-preserving `terminal-ansi.txt`, `terminal.html`, and `metadata.json`; the manifest records SHA-256 and byte length. Per-key metadata binds immutable font/render assumptions and the ANSI-aware wrapping/truncation policy. `VirtualTerminal` reconstructs ANSI from visible xterm cells, including cell padding, palette/RGB colors, attributes, and inverse video; plain text is always the stripped reconstruction. The verifier owns an independent literal 20-key oracle and fails closed unless stripped ANSI equals text, `terminal.html` equals the exported canonical `ansiToHtml(terminal-ansi.txt)` byte-for-byte (including its complete document envelope and global CSS), HTML independently preserves the ANSI style-run text, every retained row has the exact `Bun.stringWidth` cell width (including trailing spaces), and `ansi_mode` agrees with required Unicode color SGR or ASCII/no-color output. Every metadata entry has exact CJK phrase-boundary metadata: the narrow-CJK key has only the three canonical boundaries in order and every other key has `[]`. Manual captures prove successful production wheel and PageUp paths and retain observable historical transcript-row evidence. It validates exact payload paths (no duplicates or traversal), immutable source/output revisions, state/status/suffix order, notice cardinality, capacity, actual mouse-copied transcript-only selection, composer, CJK, and provenance invariants. `review-input.json` binds the exact manifest digest, capture author/executor identity, acceptance/design versions, required artifacts, narrow-CJK boundaries, and deterministic host matrix. `--require-independent-review` requires an attestation with an exact root key set; exact per-key result and artifact-check key sets; exact defect `{ description, accepted }` keys with a trimmed, nonblank description; canonical trimmed reviewer identity distinct from both bound identities; the independent-terminal-reviewer role; fixture revision; expected and observed counts of 20; exact checked keys; accepted per-key artifact-check/notes results; accepted artifact/CJK/host decisions; bound digest; and final `accept`. Any malformed, incomplete, or extra attestation content fails closed.
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
PROTOCOL_VERSION,
|
|
26
26
|
type PromptRequest,
|
|
27
27
|
type PromptResponse,
|
|
28
|
+
RequestError,
|
|
28
29
|
type ResumeSessionRequest,
|
|
29
30
|
type ResumeSessionResponse,
|
|
30
31
|
type SessionInfo,
|
|
@@ -532,6 +533,34 @@ export function acpPromptPayload(blocks: PromptRequest["prompt"]): {
|
|
|
532
533
|
return { text: text.join("\n"), images };
|
|
533
534
|
}
|
|
534
535
|
|
|
536
|
+
/**
|
|
537
|
+
* `AcpSdkAdapterError.code` is an internal string, but the SDK only derives a
|
|
538
|
+
* JSON-RPC code from a `RequestError`. Everything else collapses to an opaque
|
|
539
|
+
* `-32603 Internal error`, which hides the reason and defeats client-side
|
|
540
|
+
* recovery (an ACP client cannot see that it must authenticate). Map the codes
|
|
541
|
+
* that have a defined ACP/JSON-RPC counterpart onto a real `RequestError`.
|
|
542
|
+
*/
|
|
543
|
+
export function acpRequestFailure(error: unknown): unknown {
|
|
544
|
+
const code = typeof error === "object" && error !== null && "code" in error ? error.code : undefined;
|
|
545
|
+
if (typeof code !== "string") return error;
|
|
546
|
+
const message = error instanceof Error ? error.message : code;
|
|
547
|
+
switch (code) {
|
|
548
|
+
case "authentication_failed":
|
|
549
|
+
return RequestError.authRequired({ code, details: message }, message);
|
|
550
|
+
case "not_found":
|
|
551
|
+
return RequestError.resourceNotFound(message);
|
|
552
|
+
case "invalid_input":
|
|
553
|
+
case "unsupported":
|
|
554
|
+
case "unsupported_content":
|
|
555
|
+
return RequestError.invalidParams({ code, details: message }, message);
|
|
556
|
+
default:
|
|
557
|
+
// The remaining internal codes (conflict, unavailable, busy, …) have no ACP
|
|
558
|
+
// counterpart and stay -32603. Keep the discriminator in `data` so a client can
|
|
559
|
+
// branch on retry/reconnect instead of parsing an English message.
|
|
560
|
+
return RequestError.internalError({ code, details: message }, message);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
535
564
|
/** Registers a permission provider only when the ACP client requires prompts. */
|
|
536
565
|
export function acpProviderRegistrations(
|
|
537
566
|
capabilities: ClientCapabilities | undefined,
|
|
@@ -988,6 +1017,16 @@ export class AcpAgent implements Agent {
|
|
|
988
1017
|
}
|
|
989
1018
|
|
|
990
1019
|
async extMethod(method: string, params: JsonObject): Promise<JsonObject> {
|
|
1020
|
+
// An unrecognized extension method is a protocol failure, not an application
|
|
1021
|
+
// result: it must reach the client as JSON-RPC -32601 rather than a resolved
|
|
1022
|
+
// payload. Recognized `_gjc/*` methods keep their `{ok:false}` result contract.
|
|
1023
|
+
if (
|
|
1024
|
+
method !== "session/set_model" &&
|
|
1025
|
+
method !== "_gjc/sdk/global" &&
|
|
1026
|
+
method !== "_gjc/sdk/control" &&
|
|
1027
|
+
method !== "_gjc/sdk/query"
|
|
1028
|
+
)
|
|
1029
|
+
throw RequestError.methodNotFound(method);
|
|
991
1030
|
try {
|
|
992
1031
|
if (method === "session/set_model") {
|
|
993
1032
|
const sessionId = typeof params.sessionId === "string" ? params.sessionId : undefined;
|
|
@@ -1001,13 +1040,10 @@ export class AcpAgent implements Agent {
|
|
|
1001
1040
|
const result = await (await this.#brokerAdapter()).handle(method, params);
|
|
1002
1041
|
return object(result) ?? {};
|
|
1003
1042
|
}
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
return object(result) ?? {};
|
|
1009
|
-
}
|
|
1010
|
-
throw new AcpSdkAdapterError("method_not_found", `Unknown ACP ext method: ${method}`);
|
|
1043
|
+
const id = typeof params.sessionId === "string" ? params.sessionId : undefined;
|
|
1044
|
+
if (!id) throw new AcpSdkAdapterError("invalid_input", "sessionId is required.");
|
|
1045
|
+
const result = await this.#adapter(id).handle(method, params);
|
|
1046
|
+
return object(result) ?? {};
|
|
1011
1047
|
} catch (error) {
|
|
1012
1048
|
const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "internal";
|
|
1013
1049
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -198,7 +198,10 @@ export function mapAgentSessionEventToAcpSessionUpdates(
|
|
|
198
198
|
}
|
|
199
199
|
case "tool_execution_end": {
|
|
200
200
|
const args = getToolExecutionEndArgs(event, options);
|
|
201
|
-
const resultContent = [
|
|
201
|
+
const resultContent = [
|
|
202
|
+
...extractDiffToolCallContent(event.result, options.cwd),
|
|
203
|
+
...extractToolCallContent(event.result),
|
|
204
|
+
];
|
|
202
205
|
const content = mergeToolUpdateContent(buildToolStartContent(event.toolName, args), resultContent);
|
|
203
206
|
const update: SessionUpdate = {
|
|
204
207
|
sessionUpdate: "tool_call_update",
|
|
@@ -207,8 +210,9 @@ export function mapAgentSessionEventToAcpSessionUpdates(
|
|
|
207
210
|
rawOutput: event.result,
|
|
208
211
|
};
|
|
209
212
|
if (event.isError) {
|
|
213
|
+
// Failure is carried by `status`; `kind` stays the tool's real category so
|
|
214
|
+
// clients keep the icon/treatment established by the initial `tool_call`.
|
|
210
215
|
update.title = `Failed: ${buildToolTitle(event.toolName, args, undefined)}`;
|
|
211
|
-
update.kind = "other";
|
|
212
216
|
}
|
|
213
217
|
if (content.length > 0) {
|
|
214
218
|
update.content = content;
|
|
@@ -713,7 +717,7 @@ function extractToolLocationsFromResult(result: unknown, cwd?: string): ToolCall
|
|
|
713
717
|
}
|
|
714
718
|
|
|
715
719
|
/** Emit a `diff` ToolCallContent for each per-file edit result that carries oldText/newText. */
|
|
716
|
-
function extractDiffToolCallContent(result: unknown): ToolCallContent[] {
|
|
720
|
+
function extractDiffToolCallContent(result: unknown, cwd?: string): ToolCallContent[] {
|
|
717
721
|
if (typeof result !== "object" || result === null) return [];
|
|
718
722
|
const details = (result as { details?: unknown }).details;
|
|
719
723
|
if (typeof details !== "object" || details === null) return [];
|
|
@@ -721,13 +725,13 @@ function extractDiffToolCallContent(result: unknown): ToolCallContent[] {
|
|
|
721
725
|
const perFile = (details as { perFileResults?: unknown }).perFileResults;
|
|
722
726
|
const entries: unknown[] = Array.isArray(perFile) ? perFile : [details];
|
|
723
727
|
for (const entry of entries) {
|
|
724
|
-
const block = buildDiffContent(entry);
|
|
728
|
+
const block = buildDiffContent(entry, cwd);
|
|
725
729
|
if (block) blocks.push(block);
|
|
726
730
|
}
|
|
727
731
|
return blocks;
|
|
728
732
|
}
|
|
729
733
|
|
|
730
|
-
function buildDiffContent(entry: unknown): ToolCallContent | undefined {
|
|
734
|
+
function buildDiffContent(entry: unknown, cwd?: string): ToolCallContent | undefined {
|
|
731
735
|
if (typeof entry !== "object" || entry === null) return undefined;
|
|
732
736
|
const candidate = entry as { path?: unknown; oldText?: unknown; newText?: unknown; isError?: unknown };
|
|
733
737
|
if (candidate.isError === true) return undefined;
|
|
@@ -738,7 +742,8 @@ function buildDiffContent(entry: unknown): ToolCallContent | undefined {
|
|
|
738
742
|
if (oldText === undefined && newText === undefined) return undefined;
|
|
739
743
|
return {
|
|
740
744
|
type: "diff",
|
|
741
|
-
path,
|
|
745
|
+
// `Diff.path` is specified as an absolute path, same as ToolCallLocation.
|
|
746
|
+
path: toAcpLocationPath(path, cwd),
|
|
742
747
|
oldText: oldText ?? null,
|
|
743
748
|
newText: newText ?? "",
|
|
744
749
|
};
|
|
@@ -859,7 +864,9 @@ function toToolCallContent(value: unknown): ToolCallContent | undefined {
|
|
|
859
864
|
resourceLinkContent.mimeType = mimeType;
|
|
860
865
|
}
|
|
861
866
|
const size = extractNumberProperty<ResourceLinkLikeContent>(value, "size");
|
|
862
|
-
|
|
867
|
+
// `ResourceLink.size` is a byte count typed as int64; drop fractional, negative,
|
|
868
|
+
// or non-representable values rather than emitting a block clients must reject.
|
|
869
|
+
if (size !== undefined && Number.isSafeInteger(size) && size >= 0) {
|
|
863
870
|
resourceLinkContent.size = size;
|
|
864
871
|
}
|
|
865
872
|
return {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as stream from "node:stream";
|
|
2
|
-
import { AgentSideConnection, ndJsonStream, type Stream } from "@agentclientprotocol/sdk";
|
|
3
|
-
import { AcpAgent } from "./acp-agent";
|
|
2
|
+
import { type Agent, AgentSideConnection, ndJsonStream, type Stream } from "@agentclientprotocol/sdk";
|
|
3
|
+
import { AcpAgent, acpRequestFailure } from "./acp-agent";
|
|
4
4
|
import type { AcpStartupOptions } from "./startup-options";
|
|
5
5
|
|
|
6
6
|
export interface AcpModeOptions {
|
|
@@ -8,8 +8,44 @@ export interface AcpModeOptions {
|
|
|
8
8
|
startupOptions?: AcpStartupOptions;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Every agent method reports failures to the client, and the SDK only derives a
|
|
13
|
+
* JSON-RPC code from a `RequestError` — anything else collapses to an opaque
|
|
14
|
+
* `-32603 Internal error` with the reason buried in `data`. Translating once at the
|
|
15
|
+
* connection boundary keeps that contract in one place instead of asking each method
|
|
16
|
+
* to remember it.
|
|
17
|
+
*/
|
|
18
|
+
function withAcpRequestFailures(agent: AcpAgent): Agent {
|
|
19
|
+
const wrapped = new Map<string | symbol, (...args: unknown[]) => unknown>();
|
|
20
|
+
return new Proxy(agent, {
|
|
21
|
+
get(target, property) {
|
|
22
|
+
// Read against the target, not the proxy: `signal`/`closed` are getters that
|
|
23
|
+
// touch private fields, and the proxy carries no private-field brand.
|
|
24
|
+
const value = Reflect.get(target, property, target);
|
|
25
|
+
if (typeof value !== "function") return value;
|
|
26
|
+
const cached = wrapped.get(property);
|
|
27
|
+
if (cached) return cached;
|
|
28
|
+
// Cache per property so repeated reads keep a stable method identity.
|
|
29
|
+
const wrapper = (...args: unknown[]) => {
|
|
30
|
+
try {
|
|
31
|
+
const result = (value as (...a: unknown[]) => unknown).apply(target, args);
|
|
32
|
+
return result instanceof Promise
|
|
33
|
+
? result.catch((error: unknown) => {
|
|
34
|
+
throw acpRequestFailure(error);
|
|
35
|
+
})
|
|
36
|
+
: result;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
throw acpRequestFailure(error);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
wrapped.set(property, wrapper);
|
|
42
|
+
return wrapper;
|
|
43
|
+
},
|
|
44
|
+
}) as unknown as Agent;
|
|
45
|
+
}
|
|
46
|
+
|
|
11
47
|
export function createAcpConnection(transport: Stream, options: AcpModeOptions = {}): AgentSideConnection {
|
|
12
|
-
return new AgentSideConnection(conn => new AcpAgent(conn, options), transport);
|
|
48
|
+
return new AgentSideConnection(conn => withAcpRequestFailures(new AcpAgent(conn, options)), transport);
|
|
13
49
|
}
|
|
14
50
|
|
|
15
51
|
export async function runAcpMode(options: AcpModeOptions = {}): Promise<never> {
|
|
@@ -84,8 +84,10 @@ export class AssistantMessageComponent extends Container {
|
|
|
84
84
|
#lastMessage?: AssistantMessage;
|
|
85
85
|
#toolImagesByCallId = new Map<string, ImageContent[]>();
|
|
86
86
|
#usageInfo?: Usage;
|
|
87
|
-
#convertedKittyImages = new Map<string, ImageContent>();
|
|
88
|
-
#kittyConversionsInFlight = new
|
|
87
|
+
#convertedKittyImages = new Map<string, ImageContent & { source: string }>();
|
|
88
|
+
#kittyConversionsInFlight = new Map<string, string>();
|
|
89
|
+
#toolImageGenerations = new Map<string, number>();
|
|
90
|
+
|
|
89
91
|
#responseHeader = new Text(theme.bold(theme.fg("statusLineModel", "gajae")), 1, 0);
|
|
90
92
|
#contentBlocksCache = new WeakMap<object, { source: string; component: Component }>();
|
|
91
93
|
#lastStreaming = false;
|
|
@@ -94,11 +96,17 @@ export class AssistantMessageComponent extends Container {
|
|
|
94
96
|
#nextContentBlockKey = 0;
|
|
95
97
|
#reusableChildren = new WeakSet<Component>();
|
|
96
98
|
#viewportAnchorId?: string;
|
|
99
|
+
#visibleSignature = "";
|
|
100
|
+
#visibleSignatureInitialized = false;
|
|
101
|
+
#toolImagesRevision = 0;
|
|
102
|
+
#convertedToolImagesRevision = 0;
|
|
103
|
+
#disposed = false;
|
|
97
104
|
constructor(
|
|
98
105
|
message?: AssistantMessage,
|
|
99
106
|
private hideThinkingBlock = false,
|
|
100
107
|
private readonly onImageUpdate?: () => void,
|
|
101
108
|
viewportAnchorId?: string,
|
|
109
|
+
private readonly onVisibleMutation?: () => void,
|
|
102
110
|
) {
|
|
103
111
|
super();
|
|
104
112
|
this.#viewportAnchorId = viewportAnchorId;
|
|
@@ -109,6 +117,56 @@ export class AssistantMessageComponent extends Container {
|
|
|
109
117
|
if (message) {
|
|
110
118
|
this.updateContent(message);
|
|
111
119
|
}
|
|
120
|
+
this.#visibleSignature = this.#visibleContentSignature();
|
|
121
|
+
this.#visibleSignatureInitialized = true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
#visibleContentSignature(): string {
|
|
125
|
+
const message = this.#lastMessage;
|
|
126
|
+
const content: string[] = [];
|
|
127
|
+
let hasMarkdownContent = false;
|
|
128
|
+
for (const block of message?.content ?? []) {
|
|
129
|
+
if (block.type === "text" && block.text.trim()) {
|
|
130
|
+
content.push(`text:${block.text.trim()}`);
|
|
131
|
+
hasMarkdownContent = true;
|
|
132
|
+
} else if (block.type === "thinking" && block.thinking.trim()) {
|
|
133
|
+
content.push(
|
|
134
|
+
this.hideThinkingBlock
|
|
135
|
+
? "thinking"
|
|
136
|
+
: `thinking:${elideRunawayThinkingRepetition(block.thinking.trim())}`,
|
|
137
|
+
);
|
|
138
|
+
if (!this.hideThinkingBlock) hasMarkdownContent = true;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (hasMarkdownContent) {
|
|
142
|
+
content.unshift(this.#lastStreaming ? "streaming" : "final");
|
|
143
|
+
}
|
|
144
|
+
const hasToolCalls = message?.content.some(block => block.type === "toolCall") ?? false;
|
|
145
|
+
if (!hasToolCalls && message?.stopReason === "aborted" && !isSilentAbort(message.errorMessage)) {
|
|
146
|
+
content.push(`aborted:${message.errorMessage ?? ""}`);
|
|
147
|
+
} else if (!hasToolCalls && message?.stopReason === "error") {
|
|
148
|
+
content.push(`error:${message.errorMessage ?? ""}`);
|
|
149
|
+
} else if (
|
|
150
|
+
message?.errorMessage &&
|
|
151
|
+
!isSilentAbort(message.errorMessage) &&
|
|
152
|
+
message.stopReason !== "aborted" &&
|
|
153
|
+
message.stopReason !== "error"
|
|
154
|
+
) {
|
|
155
|
+
content.push(`message-error:${message.errorMessage}`);
|
|
156
|
+
}
|
|
157
|
+
if (settings.get("display.showTokenUsage") && this.#usageInfo) {
|
|
158
|
+
const usage = this.#usageInfo;
|
|
159
|
+
content.push(`usage:${usage.input}:${usage.cacheWrite}:${usage.output}:${usage.cacheRead}`);
|
|
160
|
+
}
|
|
161
|
+
content.push(`images:${this.#toolImagesRevision}:${this.#convertedToolImagesRevision}`);
|
|
162
|
+
return content.join("\u0000");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
#notifyVisibleMutation(): void {
|
|
166
|
+
const after = this.#visibleContentSignature();
|
|
167
|
+
if (after === this.#visibleSignature) return;
|
|
168
|
+
this.#visibleSignature = after;
|
|
169
|
+
if (this.#visibleSignatureInitialized) this.onVisibleMutation?.();
|
|
112
170
|
}
|
|
113
171
|
|
|
114
172
|
#contentAnchorSource(
|
|
@@ -126,22 +184,36 @@ export class AssistantMessageComponent extends Container {
|
|
|
126
184
|
}
|
|
127
185
|
|
|
128
186
|
setHideThinkingBlock(hide: boolean): void {
|
|
187
|
+
if (this.hideThinkingBlock === hide) return;
|
|
129
188
|
this.hideThinkingBlock = hide;
|
|
189
|
+
this.#notifyVisibleMutation();
|
|
130
190
|
}
|
|
131
191
|
|
|
132
192
|
setToolResultImages(toolCallId: string, images: ImageContent[]): void {
|
|
133
193
|
if (!toolCallId) return;
|
|
134
194
|
const validImages = images.filter(img => img.type === "image" && img.data && img.mimeType);
|
|
195
|
+
const previousImages = this.#toolImagesByCallId.get(toolCallId);
|
|
196
|
+
if (
|
|
197
|
+
(previousImages?.length ?? 0) === validImages.length &&
|
|
198
|
+
(previousImages?.every(
|
|
199
|
+
(image, index) =>
|
|
200
|
+
image.type === validImages[index]?.type &&
|
|
201
|
+
image.data === validImages[index]?.data &&
|
|
202
|
+
image.mimeType === validImages[index]?.mimeType,
|
|
203
|
+
) ??
|
|
204
|
+
true)
|
|
205
|
+
) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
135
208
|
for (const key of Array.from(this.#convertedKittyImages.keys())) {
|
|
136
|
-
if (key.startsWith(`${toolCallId}:`))
|
|
137
|
-
this.#convertedKittyImages.delete(key);
|
|
138
|
-
}
|
|
209
|
+
if (key.startsWith(`${toolCallId}:`)) this.#convertedKittyImages.delete(key);
|
|
139
210
|
}
|
|
140
|
-
for (const key of Array.from(this.#kittyConversionsInFlight)) {
|
|
141
|
-
if (key.startsWith(`${toolCallId}:`))
|
|
142
|
-
this.#kittyConversionsInFlight.delete(key);
|
|
143
|
-
}
|
|
211
|
+
for (const key of Array.from(this.#kittyConversionsInFlight.keys())) {
|
|
212
|
+
if (key.startsWith(`${toolCallId}:`)) this.#kittyConversionsInFlight.delete(key);
|
|
144
213
|
}
|
|
214
|
+
this.#toolImageGenerations.set(toolCallId, (this.#toolImageGenerations.get(toolCallId) ?? 0) + 1);
|
|
215
|
+
|
|
216
|
+
this.#toolImagesRevision += 1;
|
|
145
217
|
if (validImages.length === 0) {
|
|
146
218
|
this.#toolImagesByCallId.delete(toolCallId);
|
|
147
219
|
} else {
|
|
@@ -153,30 +225,49 @@ export class AssistantMessageComponent extends Container {
|
|
|
153
225
|
}
|
|
154
226
|
}
|
|
155
227
|
|
|
228
|
+
#toolImageSource(image: ImageContent): string {
|
|
229
|
+
return `${image.mimeType}\u0000${image.data}`;
|
|
230
|
+
}
|
|
231
|
+
|
|
156
232
|
#convertToolImagesForKitty(toolCallId: string, images: ImageContent[]): void {
|
|
157
233
|
if (TERMINAL.imageProtocol !== ImageProtocol.Kitty) return;
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (
|
|
234
|
+
const generation = this.#toolImageGenerations.get(toolCallId) ?? 0;
|
|
235
|
+
for (const [index, image] of images.entries()) {
|
|
236
|
+
if (image.mimeType === "image/png") continue;
|
|
161
237
|
const key = `${toolCallId}:${index}`;
|
|
162
|
-
|
|
163
|
-
|
|
238
|
+
const source = this.#toolImageSource(image);
|
|
239
|
+
if (
|
|
240
|
+
this.#convertedKittyImages.get(key)?.source === source ||
|
|
241
|
+
this.#kittyConversionsInFlight.get(key) === source
|
|
242
|
+
)
|
|
243
|
+
continue;
|
|
244
|
+
this.#kittyConversionsInFlight.set(key, source);
|
|
164
245
|
new Bun.Image(Buffer.from(image.data, "base64"))
|
|
165
246
|
.png()
|
|
166
247
|
.toBase64()
|
|
167
248
|
.then(data => {
|
|
249
|
+
const currentImage = this.#toolImagesByCallId.get(toolCallId)?.[index];
|
|
250
|
+
if (
|
|
251
|
+
this.#disposed ||
|
|
252
|
+
this.#toolImageGenerations.get(toolCallId) !== generation ||
|
|
253
|
+
this.#kittyConversionsInFlight.get(key) !== source ||
|
|
254
|
+
!currentImage ||
|
|
255
|
+
this.#toolImageSource(currentImage) !== source
|
|
256
|
+
)
|
|
257
|
+
return;
|
|
168
258
|
this.#kittyConversionsInFlight.delete(key);
|
|
169
|
-
this.#convertedKittyImages.set(key, {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
mimeType: "image/png",
|
|
173
|
-
});
|
|
174
|
-
if (this.#lastMessage) {
|
|
175
|
-
this.updateContent(this.#lastMessage, { streaming: this.#lastStreaming });
|
|
176
|
-
}
|
|
259
|
+
this.#convertedKittyImages.set(key, { type: "image", data, mimeType: "image/png", source });
|
|
260
|
+
this.#convertedToolImagesRevision += 1;
|
|
261
|
+
if (this.#lastMessage) this.updateContent(this.#lastMessage, { streaming: this.#lastStreaming });
|
|
177
262
|
this.onImageUpdate?.();
|
|
178
263
|
})
|
|
179
264
|
.catch(() => {
|
|
265
|
+
if (
|
|
266
|
+
this.#disposed ||
|
|
267
|
+
this.#toolImageGenerations.get(toolCallId) !== generation ||
|
|
268
|
+
this.#kittyConversionsInFlight.get(key) !== source
|
|
269
|
+
)
|
|
270
|
+
return;
|
|
180
271
|
this.#kittyConversionsInFlight.delete(key);
|
|
181
272
|
});
|
|
182
273
|
}
|
|
@@ -487,5 +578,15 @@ export class AssistantMessageComponent extends Container {
|
|
|
487
578
|
}
|
|
488
579
|
|
|
489
580
|
this.#reconcileChildren(descriptors);
|
|
581
|
+
this.#notifyVisibleMutation();
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
override dispose(): void {
|
|
585
|
+
if (this.#disposed) return;
|
|
586
|
+
this.#disposed = true;
|
|
587
|
+
this.#toolImageGenerations.clear();
|
|
588
|
+
this.#kittyConversionsInFlight.clear();
|
|
589
|
+
this.#convertedKittyImages.clear();
|
|
590
|
+
super.dispose();
|
|
490
591
|
}
|
|
491
592
|
}
|