mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 08:03:20 +00:00
Merge remote-tracking branch 'origin/main' into mobile-rearch
pr-code-change-scope keeps the branch's streamed stdin helper (main adopted the same approach inline); the lockfile is regenerated over the merged manifest. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
@@ -14,8 +14,8 @@
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.19.14",
|
||||
"hono": "^4.12.27",
|
||||
"@hono/node-server": "^1.19.17",
|
||||
"hono": "^4.13.7",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.19.14",
|
||||
"hono": "^4.12.27",
|
||||
"@hono/node-server": "^1.19.17",
|
||||
"hono": "^4.13.7",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -15,13 +15,13 @@
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.19.14",
|
||||
"@hono/node-server": "^1.19.17",
|
||||
"@orca-cloud/relay-contract": "workspace:*",
|
||||
"hono": "^4.12.27",
|
||||
"hono": "^4.13.7",
|
||||
"jose": "^6.1.3",
|
||||
"pg": "^8.22.0",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"ws": "^8.18.3",
|
||||
"ws": "^8.21.3",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
Generated
+32
-32
@@ -24,14 +24,14 @@ importers:
|
||||
apps/relay:
|
||||
dependencies:
|
||||
'@hono/node-server':
|
||||
specifier: ^1.19.14
|
||||
version: 1.19.14(hono@4.12.27)
|
||||
specifier: ^1.19.17
|
||||
version: 1.19.17(hono@4.13.7)
|
||||
'@orca-cloud/relay-contract':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/relay-contract
|
||||
hono:
|
||||
specifier: ^4.12.27
|
||||
version: 4.12.27
|
||||
specifier: ^4.13.7
|
||||
version: 4.13.7
|
||||
jose:
|
||||
specifier: ^6.1.3
|
||||
version: 6.2.3
|
||||
@@ -42,8 +42,8 @@ importers:
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3
|
||||
ws:
|
||||
specifier: ^8.18.3
|
||||
version: 8.21.0
|
||||
specifier: ^8.21.3
|
||||
version: 8.21.3
|
||||
zod:
|
||||
specifier: ^3.25.76
|
||||
version: 3.25.76
|
||||
@@ -70,11 +70,11 @@ importers:
|
||||
apps/relay-fence-broker:
|
||||
dependencies:
|
||||
'@hono/node-server':
|
||||
specifier: ^1.19.14
|
||||
version: 1.19.14(hono@4.12.27)
|
||||
specifier: ^1.19.17
|
||||
version: 1.19.17(hono@4.13.7)
|
||||
hono:
|
||||
specifier: ^4.12.27
|
||||
version: 4.12.27
|
||||
specifier: ^4.13.7
|
||||
version: 4.13.7
|
||||
zod:
|
||||
specifier: ^3.25.76
|
||||
version: 3.25.76
|
||||
@@ -95,11 +95,11 @@ importers:
|
||||
apps/relay-ops:
|
||||
dependencies:
|
||||
'@hono/node-server':
|
||||
specifier: ^1.19.14
|
||||
version: 1.19.14(hono@4.12.27)
|
||||
specifier: ^1.19.17
|
||||
version: 1.19.17(hono@4.13.7)
|
||||
hono:
|
||||
specifier: ^4.12.27
|
||||
version: 4.12.27
|
||||
specifier: ^4.13.7
|
||||
version: 4.13.7
|
||||
zod:
|
||||
specifier: ^3.25.76
|
||||
version: 3.25.76
|
||||
@@ -300,8 +300,8 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@hono/node-server@1.19.14':
|
||||
resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==}
|
||||
'@hono/node-server@1.19.17':
|
||||
resolution: {integrity: sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ==}
|
||||
engines: {node: '>=18.14.1'}
|
||||
peerDependencies:
|
||||
hono: ^4
|
||||
@@ -507,8 +507,8 @@ packages:
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
hono@4.12.27:
|
||||
resolution: {integrity: sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==}
|
||||
hono@4.13.7:
|
||||
resolution: {integrity: sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ==}
|
||||
engines: {node: '>=16.9.0'}
|
||||
|
||||
jose@6.2.3:
|
||||
@@ -587,8 +587,8 @@ packages:
|
||||
magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
nanoid@3.3.13:
|
||||
resolution: {integrity: sha512-sPdqC6ByMVVGvF1ynvvMo0/o+oD1VX7DaHhijt1bFgjvBkHBib4t49GoNDhf2NDta4oeUNlaGbSt5K7qjZ955Q==}
|
||||
nanoid@3.3.18:
|
||||
resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
@@ -640,8 +640,8 @@ packages:
|
||||
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
postcss@8.5.15:
|
||||
resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
|
||||
postcss@8.5.28:
|
||||
resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
postgres-array@2.0.0:
|
||||
@@ -805,8 +805,8 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
hasBin: true
|
||||
|
||||
ws@8.21.0:
|
||||
resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==}
|
||||
ws@8.21.3:
|
||||
resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
@@ -920,9 +920,9 @@ snapshots:
|
||||
'@esbuild/win32-x64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@hono/node-server@1.19.14(hono@4.12.27)':
|
||||
'@hono/node-server@1.19.17(hono@4.13.7)':
|
||||
dependencies:
|
||||
hono: 4.12.27
|
||||
hono: 4.13.7
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||
|
||||
@@ -1109,7 +1109,7 @@ snapshots:
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
hono@4.12.27: {}
|
||||
hono@4.13.7: {}
|
||||
|
||||
jose@6.2.3: {}
|
||||
|
||||
@@ -1166,7 +1166,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
nanoid@3.3.13: {}
|
||||
nanoid@3.3.18: {}
|
||||
|
||||
obug@2.1.3: {}
|
||||
|
||||
@@ -1211,9 +1211,9 @@ snapshots:
|
||||
|
||||
picomatch@4.0.4: {}
|
||||
|
||||
postcss@8.5.15:
|
||||
postcss@8.5.28:
|
||||
dependencies:
|
||||
nanoid: 3.3.13
|
||||
nanoid: 3.3.18
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
@@ -1288,7 +1288,7 @@ snapshots:
|
||||
dependencies:
|
||||
lightningcss: 1.32.0
|
||||
picomatch: 4.0.4
|
||||
postcss: 8.5.15
|
||||
postcss: 8.5.28
|
||||
rolldown: 1.0.3
|
||||
tinyglobby: 0.2.17
|
||||
optionalDependencies:
|
||||
@@ -1329,7 +1329,7 @@ snapshots:
|
||||
siginfo: 2.0.0
|
||||
stackback: 0.0.2
|
||||
|
||||
ws@8.21.0: {}
|
||||
ws@8.21.3: {}
|
||||
|
||||
xtend@4.0.2: {}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1104,31 +1104,124 @@ index c9ec396ab66cb966d49aa63bed09cdf9cd6c4246..d4d5106d8dd723eceed87310c193ccfe
|
||||
}
|
||||
}
|
||||
diff --git a/src/common/SortedList.ts b/src/common/SortedList.ts
|
||||
index 8a10076e3963e33b4a7d1e4602333eb3f4772dc9..df0761c35907ddc48eb102ba181b0dac8e61f00d 100644
|
||||
index 8a10076e3963e33b4a7d1e4602333eb3f4772dc9..c6dcf18b762e3c56fe22e9c2d49b8e550d96f915 100644
|
||||
--- a/src/common/SortedList.ts
|
||||
+++ b/src/common/SortedList.ts
|
||||
@@ -87,6 +87,24 @@ export class SortedList<T> {
|
||||
if (key === undefined) {
|
||||
return false;
|
||||
@@ -22,7 +22,8 @@ export class SortedList<T> {
|
||||
private readonly _flushInsertedTask: InstanceType<typeof IdleTaskQueue>;
|
||||
private _isFlushingInserted = false;
|
||||
|
||||
- private readonly _deletedIndices: number[] = [];
|
||||
+ private readonly _deletedIndices = new Set<number>();
|
||||
+ private readonly _indicesByValue = new Map<T, number | number[]>();
|
||||
private readonly _flushDeletedTask: InstanceType<typeof IdleTaskQueue>;
|
||||
private _isFlushingDeleted = false;
|
||||
|
||||
@@ -36,10 +37,11 @@ export class SortedList<T> {
|
||||
|
||||
public clear(): void {
|
||||
this._array.length = 0;
|
||||
+ this._indicesByValue.clear();
|
||||
this._insertedValues.length = 0;
|
||||
this._flushInsertedTask.clear();
|
||||
this._isFlushingInserted = false;
|
||||
- this._deletedIndices.length = 0;
|
||||
+ this._deletedIndices.clear();
|
||||
this._flushDeletedTask.clear();
|
||||
this._isFlushingDeleted = false;
|
||||
}
|
||||
@@ -69,6 +71,7 @@ export class SortedList<T> {
|
||||
}
|
||||
+ if (this._deleteAtKey(value, key)) {
|
||||
+ return true;
|
||||
|
||||
this._array = newArray;
|
||||
+ this._rebuildIdentityIndex();
|
||||
this._insertedValues.length = 0;
|
||||
}
|
||||
|
||||
@@ -78,54 +81,60 @@ export class SortedList<T> {
|
||||
}
|
||||
}
|
||||
|
||||
+ private _rebuildIdentityIndex(): void {
|
||||
+ this._indicesByValue.clear();
|
||||
+ // Reverse indices let duplicate identities remove their first occurrence in O(1).
|
||||
+ for (let index = this._array.length - 1; index >= 0; index--) {
|
||||
+ const value = this._array[index];
|
||||
+ const indices = this._indicesByValue.get(value);
|
||||
+ if (indices === undefined) {
|
||||
+ this._indicesByValue.set(value, index);
|
||||
+ } else if (typeof indices === 'number') {
|
||||
+ this._indicesByValue.set(value, [indices, index]);
|
||||
+ } else {
|
||||
+ indices.push(index);
|
||||
+ }
|
||||
+ }
|
||||
+ // A pending deletion whose key mutated after `delete()` (disposing a marker
|
||||
+ // resets `line` to -1, and `line` is the sort key) leaves `_array` out of
|
||||
+ // order, so the binary search above can miss a value that is present.
|
||||
+ // Compacting those entries out restores the order; retry before reporting
|
||||
+ // the value absent, else its `onDecorationRemoved` never fires and the
|
||||
+ // decoration paints forever. Miss path only, so the common bulk delete
|
||||
+ // keeps its O(log n) search and deferred-compaction batching.
|
||||
+ if (this._deletedIndices.length === 0) {
|
||||
+ return false;
|
||||
+ }
|
||||
+ this._flushCleanupDeleted();
|
||||
+ return this._deleteAtKey(value, key);
|
||||
+ }
|
||||
+
|
||||
+ private _deleteAtKey(value: T, key: number): boolean {
|
||||
i = this._search(key);
|
||||
if (i === -1) {
|
||||
public delete(value: T): boolean {
|
||||
this._flushCleanupInserted();
|
||||
- if (this._array.length === 0) {
|
||||
+ // Marker disposal mutates the sort key before removal; identity stays stable.
|
||||
+ const indices = this._indicesByValue.get(value);
|
||||
+ if (indices === undefined) {
|
||||
return false;
|
||||
}
|
||||
- const key = this._getKey(value);
|
||||
- if (key === undefined) {
|
||||
+ const index = typeof indices === 'number' ? indices : indices.pop();
|
||||
+ if (index === undefined) {
|
||||
return false;
|
||||
}
|
||||
- i = this._search(key);
|
||||
- if (i === -1) {
|
||||
- return false;
|
||||
+ if (typeof indices === 'number' || indices.length === 0) {
|
||||
+ this._indicesByValue.delete(value);
|
||||
}
|
||||
- if (this._getKey(this._array[i]) !== key) {
|
||||
- return false;
|
||||
+ if (this._deletedIndices.size === 0) {
|
||||
+ this._flushDeletedTask.enqueue(() => this._flushDeleted());
|
||||
}
|
||||
- do {
|
||||
- if (this._array[i] === value) {
|
||||
- if (this._deletedIndices.length === 0) {
|
||||
- this._flushDeletedTask.enqueue(() => this._flushDeleted());
|
||||
- }
|
||||
- this._deletedIndices.push(i);
|
||||
- return true;
|
||||
- }
|
||||
- } while (++i < this._array.length && this._getKey(this._array[i]) === key);
|
||||
- return false;
|
||||
+ this._deletedIndices.add(index);
|
||||
+ return true;
|
||||
}
|
||||
|
||||
private _flushDeleted(): void {
|
||||
this._isFlushingDeleted = true;
|
||||
- const sortedDeletedIndices = this._deletedIndices.sort((a, b) => a - b);
|
||||
- let sortedDeletedIndicesIndex = 0;
|
||||
- const newArray = new Array(this._array.length - sortedDeletedIndices.length);
|
||||
+ const newArray = new Array(this._array.length - this._deletedIndices.size);
|
||||
let newArrayIndex = 0;
|
||||
for (let i = 0; i < this._array.length; i++) {
|
||||
- if (sortedDeletedIndices[sortedDeletedIndicesIndex] === i) {
|
||||
- sortedDeletedIndicesIndex++;
|
||||
- } else {
|
||||
+ if (!this._deletedIndices.has(i)) {
|
||||
newArray[newArrayIndex++] = this._array[i];
|
||||
}
|
||||
}
|
||||
this._array = newArray;
|
||||
- this._deletedIndices.length = 0;
|
||||
+ this._rebuildIdentityIndex();
|
||||
+ this._deletedIndices.clear();
|
||||
this._isFlushingDeleted = false;
|
||||
}
|
||||
|
||||
private _flushCleanupDeleted(): void {
|
||||
- if (!this._isFlushingDeleted && this._deletedIndices.length > 0) {
|
||||
+ if (!this._isFlushingDeleted && this._deletedIndices.size > 0) {
|
||||
this._flushDeletedTask.flush();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { spawn, spawnSync } from 'node:child_process'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { Readable } from 'node:stream'
|
||||
@@ -422,6 +422,54 @@ describe('per-job path classification', () => {
|
||||
expect(result.stdout).toContain('package=false\n')
|
||||
expect(result.stdout).toContain('test=true\n')
|
||||
})
|
||||
|
||||
// A long-lived PR whose base.sha has gone stale diffs thousands of files, so the writer
|
||||
// outruns one pipe buffer. A single fd-0 read then returns early, breaks the writer's pipe,
|
||||
// and still exits 0 -- emitting no pairs at all, which silently skips every lane.
|
||||
it('classifies a path that arrives after the first pipe buffer', async () => {
|
||||
const filler = Array.from(
|
||||
{ length: 12_000 },
|
||||
(_, index) => `docs/reference/generated-placeholder-${index}.md`
|
||||
)
|
||||
const input = `${[...filler, 'config/patches/xterm-upstream.json'].join('\n')}\n`
|
||||
expect(input.length).toBeGreaterThan(64 * 1024)
|
||||
|
||||
const child = spawn(process.execPath, ['config/scripts/pr-code-change-scope.mjs'], {
|
||||
cwd: projectDir,
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
})
|
||||
let stdout = ''
|
||||
let stderr = ''
|
||||
let brokePipe = false
|
||||
child.stdout.setEncoding('utf8')
|
||||
child.stderr.setEncoding('utf8')
|
||||
child.stdout.on('data', (chunk) => (stdout += chunk))
|
||||
child.stderr.on('data', (chunk) => (stderr += chunk))
|
||||
child.stdin.on('error', (error) => {
|
||||
brokePipe ||= error.code === 'EPIPE'
|
||||
})
|
||||
|
||||
const exitCode = await new Promise((resolvePromise) => {
|
||||
child.on('close', resolvePromise)
|
||||
let offset = 0
|
||||
const step = () => {
|
||||
if (offset >= input.length) {
|
||||
child.stdin.end()
|
||||
return
|
||||
}
|
||||
child.stdin.write(input.slice(offset, offset + 64 * 1024))
|
||||
offset += 64 * 1024
|
||||
setTimeout(step, 20)
|
||||
}
|
||||
step()
|
||||
})
|
||||
|
||||
expect(stderr).not.toContain('EAGAIN')
|
||||
expect(brokePipe).toBe(false)
|
||||
expect(exitCode, stderr).toBe(0)
|
||||
expect(stdout).toContain('should_run=true\n')
|
||||
expect(stdout).toContain('xterm_patch_sync=true\n')
|
||||
})
|
||||
})
|
||||
|
||||
describe('PR Checks skip wiring', () => {
|
||||
|
||||
Generated
+8
-8
@@ -2321,11 +2321,11 @@ packages:
|
||||
bindings@1.5.0:
|
||||
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
|
||||
|
||||
brace-expansion@1.1.16:
|
||||
resolution: {integrity: sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==}
|
||||
brace-expansion@1.1.18:
|
||||
resolution: {integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==}
|
||||
|
||||
brace-expansion@5.0.8:
|
||||
resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==}
|
||||
brace-expansion@5.0.9:
|
||||
resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
braces@3.0.3:
|
||||
@@ -6854,12 +6854,12 @@ snapshots:
|
||||
dependencies:
|
||||
file-uri-to-path: 1.0.0
|
||||
|
||||
brace-expansion@1.1.16:
|
||||
brace-expansion@1.1.18:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
concat-map: 0.0.1
|
||||
|
||||
brace-expansion@5.0.8:
|
||||
brace-expansion@5.0.9:
|
||||
dependencies:
|
||||
balanced-match: 4.0.4
|
||||
|
||||
@@ -8764,11 +8764,11 @@ snapshots:
|
||||
|
||||
minimatch@10.2.6:
|
||||
dependencies:
|
||||
brace-expansion: 5.0.8
|
||||
brace-expansion: 5.0.9
|
||||
|
||||
minimatch@3.1.5:
|
||||
dependencies:
|
||||
brace-expansion: 1.1.16
|
||||
brace-expansion: 1.1.18
|
||||
|
||||
minimist@1.2.8: {}
|
||||
|
||||
|
||||
Generated
+9
-9
@@ -3084,12 +3084,12 @@ packages:
|
||||
'@vitest/utils@4.1.11':
|
||||
resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==}
|
||||
|
||||
'@xmldom/xmldom@0.8.13':
|
||||
resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==}
|
||||
'@xmldom/xmldom@0.8.15':
|
||||
resolution: {integrity: sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
||||
'@xmldom/xmldom@0.9.10':
|
||||
resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==}
|
||||
'@xmldom/xmldom@0.9.12':
|
||||
resolution: {integrity: sha512-5AXjrcMClTryPe9LgZrygpB1lj7s0S9E0+W+AHaVKAVyHanafK86iPSvG5xHVSp/jC+VH1UXu0TAEmY279xH7A==}
|
||||
engines: {node: '>=14.6'}
|
||||
|
||||
'@xterm/addon-fit@0.12.0-beta.300':
|
||||
@@ -9080,13 +9080,13 @@ snapshots:
|
||||
|
||||
'@expo/plist@0.5.3':
|
||||
dependencies:
|
||||
'@xmldom/xmldom': 0.8.13
|
||||
'@xmldom/xmldom': 0.8.15
|
||||
base64-js: 1.5.1
|
||||
xmlbuilder: 15.1.1
|
||||
|
||||
'@expo/plist@0.5.4':
|
||||
dependencies:
|
||||
'@xmldom/xmldom': 0.8.13
|
||||
'@xmldom/xmldom': 0.8.15
|
||||
base64-js: 1.5.1
|
||||
xmlbuilder: 15.1.1
|
||||
|
||||
@@ -10588,9 +10588,9 @@ snapshots:
|
||||
convert-source-map: 2.0.0
|
||||
tinyrainbow: 3.1.0
|
||||
|
||||
'@xmldom/xmldom@0.8.13': {}
|
||||
'@xmldom/xmldom@0.8.15': {}
|
||||
|
||||
'@xmldom/xmldom@0.9.10': {}
|
||||
'@xmldom/xmldom@0.9.12': {}
|
||||
|
||||
'@xterm/addon-fit@0.12.0-beta.300(@xterm/xterm@6.1.0-beta.303)':
|
||||
dependencies:
|
||||
@@ -14469,7 +14469,7 @@ snapshots:
|
||||
|
||||
plist@3.1.1:
|
||||
dependencies:
|
||||
'@xmldom/xmldom': 0.9.10
|
||||
'@xmldom/xmldom': 0.9.12
|
||||
base64-js: 1.5.1
|
||||
xmlbuilder: 15.1.1
|
||||
|
||||
|
||||
+2
-2
@@ -184,7 +184,7 @@
|
||||
"proper-lockfile": "4.1.2",
|
||||
"psl": "1.15.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"react-i18next": "^17.0.8",
|
||||
"react-i18next": "17.0.13",
|
||||
"serve-sim": "^0.1.40",
|
||||
"sherpa-onnx": "1.12.37",
|
||||
"ssh2": "^1.17.0",
|
||||
@@ -245,7 +245,7 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"dompurify": "^3.4.13",
|
||||
"dompurify": "3.4.14",
|
||||
"electron": "^43.4.1",
|
||||
"electron-builder": "^26.15.3",
|
||||
"electron-builder-squirrel-windows": "^26.15.3",
|
||||
|
||||
Generated
+96
-102
@@ -106,7 +106,7 @@ settings:
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
overrides:
|
||||
monaco-editor>dompurify: 3.4.13
|
||||
monaco-editor>dompurify: 3.4.14
|
||||
|
||||
patchedDependencies:
|
||||
'@vscode/windows-process-tree@0.8.0': e66202cc623996d02040c93449eb9ae353fddadf426cb53202a59ee710ee6fe7
|
||||
@@ -114,7 +114,7 @@ patchedDependencies:
|
||||
'@xterm/addon-search@0.17.0-beta.300': eee5338dd2621ece46e79c61ec06766cd7fadaf79ffdb24e2a8ab68e97ef31f0
|
||||
'@xterm/addon-serialize@0.15.0-beta.300': 851eac3d75e6d8c013b9f4c053e61d824b23965cb19ecc28e335e05059f3a294
|
||||
'@xterm/addon-webgl@0.20.0-beta.299': 94687e89a0115e6e6aa102837f986debdc029c091527ee5eb4a4e17ceaf9473e
|
||||
'@xterm/xterm@6.1.0-beta.303': 98756bcedc402bcdb7c6ab7b015d2e59cd18e97b03a2c06a27e95bb3ba429d9d
|
||||
'@xterm/xterm@6.1.0-beta.303': 1f36ce689bc50c703ae09aeda0e064f107e18e4a5ecba19e746fa5edc4b02ef4
|
||||
lint-staged@16.4.0: 7333b3837f80a7fbd045964db6d76ba4fc118e49134bdbabb00585b6b7b60673
|
||||
node-pty@1.1.0: bac3a53fb15efc9b3b944fbe3c4718b5174a0b3bd6ead84e21975edad4bc6615
|
||||
|
||||
@@ -142,7 +142,7 @@ importers:
|
||||
version: 2.5.6
|
||||
'@xterm/addon-serialize':
|
||||
specifier: 0.15.0-beta.300
|
||||
version: 0.15.0-beta.300(patch_hash=851eac3d75e6d8c013b9f4c053e61d824b23965cb19ecc28e335e05059f3a294)(@xterm/xterm@6.1.0-beta.303(patch_hash=98756bcedc402bcdb7c6ab7b015d2e59cd18e97b03a2c06a27e95bb3ba429d9d))
|
||||
version: 0.15.0-beta.300(patch_hash=851eac3d75e6d8c013b9f4c053e61d824b23965cb19ecc28e335e05059f3a294)(@xterm/xterm@6.1.0-beta.303(patch_hash=1f36ce689bc50c703ae09aeda0e064f107e18e4a5ecba19e746fa5edc4b02ef4))
|
||||
'@xterm/headless':
|
||||
specifier: 6.1.0-beta.302
|
||||
version: 6.1.0-beta.302
|
||||
@@ -174,8 +174,8 @@ importers:
|
||||
specifier: ^1.5.4
|
||||
version: 1.5.4
|
||||
react-i18next:
|
||||
specifier: ^17.0.8
|
||||
version: 17.0.8(i18next@26.3.1(typescript@7.0.2))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2)
|
||||
specifier: 17.0.13
|
||||
version: 17.0.13(i18next@26.3.1(typescript@7.0.2))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2)
|
||||
serve-sim:
|
||||
specifier: ^0.1.40
|
||||
version: 0.1.40(typescript@7.0.2)
|
||||
@@ -320,25 +320,25 @@ importers:
|
||||
version: 5.2.0(rolldown-vite@7.3.1(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@types/node@25.9.5)(jiti@2.7.0)(yaml@2.8.4))
|
||||
'@xterm/addon-fit':
|
||||
specifier: 0.12.0-beta.300
|
||||
version: 0.12.0-beta.300(@xterm/xterm@6.1.0-beta.303(patch_hash=98756bcedc402bcdb7c6ab7b015d2e59cd18e97b03a2c06a27e95bb3ba429d9d))
|
||||
version: 0.12.0-beta.300(@xterm/xterm@6.1.0-beta.303(patch_hash=1f36ce689bc50c703ae09aeda0e064f107e18e4a5ecba19e746fa5edc4b02ef4))
|
||||
'@xterm/addon-ligatures':
|
||||
specifier: 0.11.0-beta.300
|
||||
version: 0.11.0-beta.300(patch_hash=47405b9994b5acf1b4e90b49250358c1ca03649854d59560e7732b72fe336920)(@xterm/xterm@6.1.0-beta.303(patch_hash=98756bcedc402bcdb7c6ab7b015d2e59cd18e97b03a2c06a27e95bb3ba429d9d))
|
||||
version: 0.11.0-beta.300(patch_hash=47405b9994b5acf1b4e90b49250358c1ca03649854d59560e7732b72fe336920)(@xterm/xterm@6.1.0-beta.303(patch_hash=1f36ce689bc50c703ae09aeda0e064f107e18e4a5ecba19e746fa5edc4b02ef4))
|
||||
'@xterm/addon-search':
|
||||
specifier: 0.17.0-beta.300
|
||||
version: 0.17.0-beta.300(patch_hash=eee5338dd2621ece46e79c61ec06766cd7fadaf79ffdb24e2a8ab68e97ef31f0)(@xterm/xterm@6.1.0-beta.303(patch_hash=98756bcedc402bcdb7c6ab7b015d2e59cd18e97b03a2c06a27e95bb3ba429d9d))
|
||||
version: 0.17.0-beta.300(patch_hash=eee5338dd2621ece46e79c61ec06766cd7fadaf79ffdb24e2a8ab68e97ef31f0)(@xterm/xterm@6.1.0-beta.303(patch_hash=1f36ce689bc50c703ae09aeda0e064f107e18e4a5ecba19e746fa5edc4b02ef4))
|
||||
'@xterm/addon-unicode11':
|
||||
specifier: 0.10.0-beta.300
|
||||
version: 0.10.0-beta.300(@xterm/xterm@6.1.0-beta.303(patch_hash=98756bcedc402bcdb7c6ab7b015d2e59cd18e97b03a2c06a27e95bb3ba429d9d))
|
||||
version: 0.10.0-beta.300(@xterm/xterm@6.1.0-beta.303(patch_hash=1f36ce689bc50c703ae09aeda0e064f107e18e4a5ecba19e746fa5edc4b02ef4))
|
||||
'@xterm/addon-web-links':
|
||||
specifier: 0.13.0-beta.300
|
||||
version: 0.13.0-beta.300(@xterm/xterm@6.1.0-beta.303(patch_hash=98756bcedc402bcdb7c6ab7b015d2e59cd18e97b03a2c06a27e95bb3ba429d9d))
|
||||
version: 0.13.0-beta.300(@xterm/xterm@6.1.0-beta.303(patch_hash=1f36ce689bc50c703ae09aeda0e064f107e18e4a5ecba19e746fa5edc4b02ef4))
|
||||
'@xterm/addon-webgl':
|
||||
specifier: 0.20.0-beta.299
|
||||
version: 0.20.0-beta.299(patch_hash=94687e89a0115e6e6aa102837f986debdc029c091527ee5eb4a4e17ceaf9473e)(@xterm/xterm@6.1.0-beta.303(patch_hash=98756bcedc402bcdb7c6ab7b015d2e59cd18e97b03a2c06a27e95bb3ba429d9d))
|
||||
version: 0.20.0-beta.299(patch_hash=94687e89a0115e6e6aa102837f986debdc029c091527ee5eb4a4e17ceaf9473e)(@xterm/xterm@6.1.0-beta.303(patch_hash=1f36ce689bc50c703ae09aeda0e064f107e18e4a5ecba19e746fa5edc4b02ef4))
|
||||
'@xterm/xterm':
|
||||
specifier: 6.1.0-beta.303
|
||||
version: 6.1.0-beta.303(patch_hash=98756bcedc402bcdb7c6ab7b015d2e59cd18e97b03a2c06a27e95bb3ba429d9d)
|
||||
version: 6.1.0-beta.303(patch_hash=1f36ce689bc50c703ae09aeda0e064f107e18e4a5ecba19e746fa5edc4b02ef4)
|
||||
buffer:
|
||||
specifier: 6.0.3
|
||||
version: 6.0.3
|
||||
@@ -352,8 +352,8 @@ importers:
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||
dompurify:
|
||||
specifier: ^3.4.13
|
||||
version: 3.4.13
|
||||
specifier: 3.4.14
|
||||
version: 3.4.14
|
||||
electron:
|
||||
specifier: ^43.4.1
|
||||
version: 43.4.1(supports-color@7.2.0)
|
||||
@@ -3519,8 +3519,8 @@ packages:
|
||||
'@vscode/windows-process-tree@0.8.0':
|
||||
resolution: {integrity: sha512-TI+h2GRwX+igD/YYJMQQVAFcsCNSg7Te2yYxQpKMzwto5RsJ8d2KKgOeur/p/6sAOQwZvopiTDOClyTHEn9MhQ==}
|
||||
|
||||
'@xmldom/xmldom@0.8.13':
|
||||
resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==}
|
||||
'@xmldom/xmldom@0.8.15':
|
||||
resolution: {integrity: sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
||||
'@xterm/addon-fit@0.12.0-beta.300':
|
||||
@@ -3683,8 +3683,8 @@ packages:
|
||||
base64-js@1.5.1:
|
||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||
|
||||
baseline-browser-mapping@2.10.27:
|
||||
resolution: {integrity: sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA==}
|
||||
baseline-browser-mapping@2.11.21:
|
||||
resolution: {integrity: sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
|
||||
@@ -3721,8 +3721,8 @@ packages:
|
||||
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
browserslist@4.28.2:
|
||||
resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==}
|
||||
browserslist@4.28.9:
|
||||
resolution: {integrity: sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==}
|
||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||
hasBin: true
|
||||
|
||||
@@ -3788,8 +3788,8 @@ packages:
|
||||
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
caniuse-lite@1.0.30001791:
|
||||
resolution: {integrity: sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==}
|
||||
caniuse-lite@1.0.30001810:
|
||||
resolution: {integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==}
|
||||
|
||||
ccount@2.0.1:
|
||||
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
||||
@@ -4270,8 +4270,8 @@ packages:
|
||||
dom-accessibility-api@0.6.3:
|
||||
resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==}
|
||||
|
||||
dompurify@3.4.13:
|
||||
resolution: {integrity: sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==}
|
||||
dompurify@3.4.14:
|
||||
resolution: {integrity: sha512-dVoH9z+MY+C9IilgGCk3YfFqjLi3fChm2OiKJMzh6axrJ5qwxqWaZamgmHrpv22CN/KdbZJuGEGgfQoL00LTdg==}
|
||||
|
||||
dotenv-expand@11.0.7:
|
||||
resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==}
|
||||
@@ -4315,8 +4315,8 @@ packages:
|
||||
electron-publish@26.15.3:
|
||||
resolution: {integrity: sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q==}
|
||||
|
||||
electron-to-chromium@1.5.351:
|
||||
resolution: {integrity: sha512-9D7Iqx8RImSvCnOsj86rCH6eQjZFQoM04Jn6HnZVM0Nu/G58/gmKYQ1d12MZTbjQbQSTGI8nwEy07ErsA2slLA==}
|
||||
electron-to-chromium@1.5.422:
|
||||
resolution: {integrity: sha512-UvA/32XqrLDdZSn7Jllo1AYNcWji/G0d5M0GTViE7KoGBiMunw3a34Sb2KO4ZZyrSEhqsxFoVhWWJshdyfKqJA==}
|
||||
|
||||
electron-updater@6.8.9:
|
||||
resolution: {integrity: sha512-ZhVxM9iGONUpZGI1FxdMRgJjUFXi7AYGVa5PwKlO1tV1/4zDxQmfKpXOHVztKrd6L9rLcFjERvi1Mf2vxyTkig==}
|
||||
@@ -4534,8 +4534,8 @@ packages:
|
||||
fast-string-width@3.0.2:
|
||||
resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==}
|
||||
|
||||
fast-uri@3.1.5:
|
||||
resolution: {integrity: sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==}
|
||||
fast-uri@3.1.7:
|
||||
resolution: {integrity: sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==}
|
||||
|
||||
fast-wrap-ansi@0.2.2:
|
||||
resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==}
|
||||
@@ -4803,8 +4803,8 @@ packages:
|
||||
resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
html-parse-stringify@3.0.1:
|
||||
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
|
||||
html-parse-stringify@4.0.1:
|
||||
resolution: {integrity: sha512-0zHsZJrK7S3K2aucXWL6ycoYJ/iNtIcFHC/nYQgFklPtrv5LpJctIiSCroWZWeuoXvuyFdzp6KzjJQ+OT5MfFw==}
|
||||
|
||||
html-to-image@1.11.13:
|
||||
resolution: {integrity: sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==}
|
||||
@@ -5575,8 +5575,8 @@ packages:
|
||||
nan@2.26.2:
|
||||
resolution: {integrity: sha512-0tTvBTYkt3tdGw22nrAy50x7gpbGCCFH3AFcyS5WiUu7Eu4vWlri1woE6qHBSfy11vksDqkiwjOnlR7WV8G1Hw==}
|
||||
|
||||
nanoid@3.3.17:
|
||||
resolution: {integrity: sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==}
|
||||
nanoid@3.3.18:
|
||||
resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
@@ -5612,8 +5612,9 @@ packages:
|
||||
node-pty@1.1.0:
|
||||
resolution: {integrity: sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==}
|
||||
|
||||
node-releases@2.0.38:
|
||||
resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==}
|
||||
node-releases@2.0.54:
|
||||
resolution: {integrity: sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
nopt@9.0.0:
|
||||
resolution: {integrity: sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==}
|
||||
@@ -5868,8 +5869,8 @@ packages:
|
||||
points-on-path@0.2.1:
|
||||
resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==}
|
||||
|
||||
postcss-selector-parser@7.1.1:
|
||||
resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==}
|
||||
postcss-selector-parser@7.1.6:
|
||||
resolution: {integrity: sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
postcss@8.5.25:
|
||||
@@ -5989,8 +5990,8 @@ packages:
|
||||
engines: {node: '>=10.13.0'}
|
||||
hasBin: true
|
||||
|
||||
qs@6.15.2:
|
||||
resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==}
|
||||
qs@6.16.0:
|
||||
resolution: {integrity: sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
||||
queue-microtask@1.2.3:
|
||||
@@ -6041,14 +6042,14 @@ packages:
|
||||
react:
|
||||
optional: true
|
||||
|
||||
react-i18next@17.0.8:
|
||||
resolution: {integrity: sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw==}
|
||||
react-i18next@17.0.13:
|
||||
resolution: {integrity: sha512-Cc1PscmblIHA1kljTqDwrcVMI21ydgmUzw0UAeQBe7pAOgfuRLfzXze4EUBQoeDiICzFIXXhHFoZxuetNg5D0Q==}
|
||||
peerDependencies:
|
||||
i18next: '>= 26.2.0'
|
||||
react: '>= 16.8.0'
|
||||
react-dom: '*'
|
||||
react-native: '*'
|
||||
typescript: ^5 || ^6
|
||||
typescript: ^5 || ^6 || ^7
|
||||
peerDependenciesMeta:
|
||||
react-dom:
|
||||
optional: true
|
||||
@@ -6404,8 +6405,8 @@ packages:
|
||||
resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
side-channel@1.1.0:
|
||||
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
|
||||
side-channel@1.1.1:
|
||||
resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
siginfo@2.0.0:
|
||||
@@ -6767,8 +6768,8 @@ packages:
|
||||
unzipper@0.12.5:
|
||||
resolution: {integrity: sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A==}
|
||||
|
||||
update-browserslist-db@1.2.3:
|
||||
resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
|
||||
update-browserslist-db@1.3.2:
|
||||
resolution: {integrity: sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
browserslist: '>= 4.21.0'
|
||||
@@ -6866,10 +6867,6 @@ packages:
|
||||
jsdom:
|
||||
optional: true
|
||||
|
||||
void-elements@3.1.0:
|
||||
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
vscode-oniguruma@2.0.1:
|
||||
resolution: {integrity: sha512-poJU8iHIWnC3vgphJnrLZyI3YdqRlR27xzqDmpPXYzA93R4Gk8z7T6oqDzDoHjoikA2aS82crdXFkjELCdJsjQ==}
|
||||
|
||||
@@ -7131,7 +7128,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@babel/compat-data': 7.29.7
|
||||
'@babel/helper-validator-option': 7.29.7
|
||||
browserslist: 4.28.2
|
||||
browserslist: 4.28.9
|
||||
lru-cache: 5.1.1
|
||||
semver: 6.3.1
|
||||
|
||||
@@ -9838,41 +9835,41 @@ snapshots:
|
||||
node-addon-api: 7.1.0
|
||||
optional: true
|
||||
|
||||
'@xmldom/xmldom@0.8.13': {}
|
||||
'@xmldom/xmldom@0.8.15': {}
|
||||
|
||||
'@xterm/addon-fit@0.12.0-beta.300(@xterm/xterm@6.1.0-beta.303(patch_hash=98756bcedc402bcdb7c6ab7b015d2e59cd18e97b03a2c06a27e95bb3ba429d9d))':
|
||||
'@xterm/addon-fit@0.12.0-beta.300(@xterm/xterm@6.1.0-beta.303(patch_hash=1f36ce689bc50c703ae09aeda0e064f107e18e4a5ecba19e746fa5edc4b02ef4))':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.303(patch_hash=98756bcedc402bcdb7c6ab7b015d2e59cd18e97b03a2c06a27e95bb3ba429d9d)
|
||||
'@xterm/xterm': 6.1.0-beta.303(patch_hash=1f36ce689bc50c703ae09aeda0e064f107e18e4a5ecba19e746fa5edc4b02ef4)
|
||||
|
||||
'@xterm/addon-ligatures@0.11.0-beta.300(patch_hash=47405b9994b5acf1b4e90b49250358c1ca03649854d59560e7732b72fe336920)(@xterm/xterm@6.1.0-beta.303(patch_hash=98756bcedc402bcdb7c6ab7b015d2e59cd18e97b03a2c06a27e95bb3ba429d9d))':
|
||||
'@xterm/addon-ligatures@0.11.0-beta.300(patch_hash=47405b9994b5acf1b4e90b49250358c1ca03649854d59560e7732b72fe336920)(@xterm/xterm@6.1.0-beta.303(patch_hash=1f36ce689bc50c703ae09aeda0e064f107e18e4a5ecba19e746fa5edc4b02ef4))':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.303(patch_hash=98756bcedc402bcdb7c6ab7b015d2e59cd18e97b03a2c06a27e95bb3ba429d9d)
|
||||
'@xterm/xterm': 6.1.0-beta.303(patch_hash=1f36ce689bc50c703ae09aeda0e064f107e18e4a5ecba19e746fa5edc4b02ef4)
|
||||
lru-cache: 11.5.1
|
||||
opentype.js: 2.0.0
|
||||
|
||||
'@xterm/addon-search@0.17.0-beta.300(patch_hash=eee5338dd2621ece46e79c61ec06766cd7fadaf79ffdb24e2a8ab68e97ef31f0)(@xterm/xterm@6.1.0-beta.303(patch_hash=98756bcedc402bcdb7c6ab7b015d2e59cd18e97b03a2c06a27e95bb3ba429d9d))':
|
||||
'@xterm/addon-search@0.17.0-beta.300(patch_hash=eee5338dd2621ece46e79c61ec06766cd7fadaf79ffdb24e2a8ab68e97ef31f0)(@xterm/xterm@6.1.0-beta.303(patch_hash=1f36ce689bc50c703ae09aeda0e064f107e18e4a5ecba19e746fa5edc4b02ef4))':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.303(patch_hash=98756bcedc402bcdb7c6ab7b015d2e59cd18e97b03a2c06a27e95bb3ba429d9d)
|
||||
'@xterm/xterm': 6.1.0-beta.303(patch_hash=1f36ce689bc50c703ae09aeda0e064f107e18e4a5ecba19e746fa5edc4b02ef4)
|
||||
|
||||
'@xterm/addon-serialize@0.15.0-beta.300(patch_hash=851eac3d75e6d8c013b9f4c053e61d824b23965cb19ecc28e335e05059f3a294)(@xterm/xterm@6.1.0-beta.303(patch_hash=98756bcedc402bcdb7c6ab7b015d2e59cd18e97b03a2c06a27e95bb3ba429d9d))':
|
||||
'@xterm/addon-serialize@0.15.0-beta.300(patch_hash=851eac3d75e6d8c013b9f4c053e61d824b23965cb19ecc28e335e05059f3a294)(@xterm/xterm@6.1.0-beta.303(patch_hash=1f36ce689bc50c703ae09aeda0e064f107e18e4a5ecba19e746fa5edc4b02ef4))':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.303(patch_hash=98756bcedc402bcdb7c6ab7b015d2e59cd18e97b03a2c06a27e95bb3ba429d9d)
|
||||
'@xterm/xterm': 6.1.0-beta.303(patch_hash=1f36ce689bc50c703ae09aeda0e064f107e18e4a5ecba19e746fa5edc4b02ef4)
|
||||
|
||||
'@xterm/addon-unicode11@0.10.0-beta.300(@xterm/xterm@6.1.0-beta.303(patch_hash=98756bcedc402bcdb7c6ab7b015d2e59cd18e97b03a2c06a27e95bb3ba429d9d))':
|
||||
'@xterm/addon-unicode11@0.10.0-beta.300(@xterm/xterm@6.1.0-beta.303(patch_hash=1f36ce689bc50c703ae09aeda0e064f107e18e4a5ecba19e746fa5edc4b02ef4))':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.303(patch_hash=98756bcedc402bcdb7c6ab7b015d2e59cd18e97b03a2c06a27e95bb3ba429d9d)
|
||||
'@xterm/xterm': 6.1.0-beta.303(patch_hash=1f36ce689bc50c703ae09aeda0e064f107e18e4a5ecba19e746fa5edc4b02ef4)
|
||||
|
||||
'@xterm/addon-web-links@0.13.0-beta.300(@xterm/xterm@6.1.0-beta.303(patch_hash=98756bcedc402bcdb7c6ab7b015d2e59cd18e97b03a2c06a27e95bb3ba429d9d))':
|
||||
'@xterm/addon-web-links@0.13.0-beta.300(@xterm/xterm@6.1.0-beta.303(patch_hash=1f36ce689bc50c703ae09aeda0e064f107e18e4a5ecba19e746fa5edc4b02ef4))':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.303(patch_hash=98756bcedc402bcdb7c6ab7b015d2e59cd18e97b03a2c06a27e95bb3ba429d9d)
|
||||
'@xterm/xterm': 6.1.0-beta.303(patch_hash=1f36ce689bc50c703ae09aeda0e064f107e18e4a5ecba19e746fa5edc4b02ef4)
|
||||
|
||||
'@xterm/addon-webgl@0.20.0-beta.299(patch_hash=94687e89a0115e6e6aa102837f986debdc029c091527ee5eb4a4e17ceaf9473e)(@xterm/xterm@6.1.0-beta.303(patch_hash=98756bcedc402bcdb7c6ab7b015d2e59cd18e97b03a2c06a27e95bb3ba429d9d))':
|
||||
'@xterm/addon-webgl@0.20.0-beta.299(patch_hash=94687e89a0115e6e6aa102837f986debdc029c091527ee5eb4a4e17ceaf9473e)(@xterm/xterm@6.1.0-beta.303(patch_hash=1f36ce689bc50c703ae09aeda0e064f107e18e4a5ecba19e746fa5edc4b02ef4))':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.303(patch_hash=98756bcedc402bcdb7c6ab7b015d2e59cd18e97b03a2c06a27e95bb3ba429d9d)
|
||||
'@xterm/xterm': 6.1.0-beta.303(patch_hash=1f36ce689bc50c703ae09aeda0e064f107e18e4a5ecba19e746fa5edc4b02ef4)
|
||||
|
||||
'@xterm/headless@6.1.0-beta.302': {}
|
||||
|
||||
'@xterm/xterm@6.1.0-beta.303(patch_hash=98756bcedc402bcdb7c6ab7b015d2e59cd18e97b03a2c06a27e95bb3ba429d9d)': {}
|
||||
'@xterm/xterm@6.1.0-beta.303(patch_hash=1f36ce689bc50c703ae09aeda0e064f107e18e4a5ecba19e746fa5edc4b02ef4)': {}
|
||||
|
||||
abbrev@4.0.0: {}
|
||||
|
||||
@@ -9894,7 +9891,7 @@ snapshots:
|
||||
ajv@8.20.0:
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
fast-uri: 3.1.5
|
||||
fast-uri: 3.1.7
|
||||
json-schema-traverse: 1.0.0
|
||||
require-from-string: 2.0.2
|
||||
|
||||
@@ -10006,7 +10003,7 @@ snapshots:
|
||||
|
||||
base64-js@1.5.1: {}
|
||||
|
||||
baseline-browser-mapping@2.10.27: {}
|
||||
baseline-browser-mapping@2.11.21: {}
|
||||
|
||||
bcrypt-pbkdf@1.0.2:
|
||||
dependencies:
|
||||
@@ -10026,7 +10023,7 @@ snapshots:
|
||||
http-errors: 2.0.1
|
||||
iconv-lite: 0.7.2
|
||||
on-finished: 2.4.1
|
||||
qs: 6.15.2
|
||||
qs: 6.16.0
|
||||
raw-body: 3.0.2
|
||||
type-is: 2.1.0
|
||||
transitivePeerDependencies:
|
||||
@@ -10052,13 +10049,13 @@ snapshots:
|
||||
dependencies:
|
||||
fill-range: 7.1.1
|
||||
|
||||
browserslist@4.28.2:
|
||||
browserslist@4.28.9:
|
||||
dependencies:
|
||||
baseline-browser-mapping: 2.10.27
|
||||
caniuse-lite: 1.0.30001791
|
||||
electron-to-chromium: 1.5.351
|
||||
node-releases: 2.0.38
|
||||
update-browserslist-db: 1.2.3(browserslist@4.28.2)
|
||||
baseline-browser-mapping: 2.11.21
|
||||
caniuse-lite: 1.0.30001810
|
||||
electron-to-chromium: 1.5.422
|
||||
node-releases: 2.0.54
|
||||
update-browserslist-db: 1.3.2(browserslist@4.28.9)
|
||||
|
||||
buffer-from@1.1.2: {}
|
||||
|
||||
@@ -10136,7 +10133,7 @@ snapshots:
|
||||
|
||||
camelcase@5.3.1: {}
|
||||
|
||||
caniuse-lite@1.0.30001791: {}
|
||||
caniuse-lite@1.0.30001810: {}
|
||||
|
||||
ccount@2.0.1: {}
|
||||
|
||||
@@ -10590,7 +10587,7 @@ snapshots:
|
||||
|
||||
dom-accessibility-api@0.6.3: {}
|
||||
|
||||
dompurify@3.4.13:
|
||||
dompurify@3.4.14:
|
||||
optionalDependencies:
|
||||
'@types/trusted-types': 2.0.7
|
||||
|
||||
@@ -10664,7 +10661,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
electron-to-chromium@1.5.351: {}
|
||||
electron-to-chromium@1.5.422: {}
|
||||
|
||||
electron-updater@6.8.9(supports-color@7.2.0):
|
||||
dependencies:
|
||||
@@ -10906,7 +10903,7 @@ snapshots:
|
||||
once: 1.4.0
|
||||
parseurl: 1.3.3
|
||||
proxy-addr: 2.0.7
|
||||
qs: 6.15.2
|
||||
qs: 6.16.0
|
||||
range-parser: 1.2.1
|
||||
router: 2.2.0(supports-color@7.2.0)
|
||||
send: 1.2.1(supports-color@7.2.0)
|
||||
@@ -10939,7 +10936,7 @@ snapshots:
|
||||
dependencies:
|
||||
fast-string-truncated-width: 3.0.3
|
||||
|
||||
fast-uri@3.1.5: {}
|
||||
fast-uri@3.1.7: {}
|
||||
|
||||
fast-wrap-ansi@0.2.2:
|
||||
dependencies:
|
||||
@@ -11316,9 +11313,7 @@ snapshots:
|
||||
dependencies:
|
||||
lru-cache: 6.0.0
|
||||
|
||||
html-parse-stringify@3.0.1:
|
||||
dependencies:
|
||||
void-elements: 3.1.0
|
||||
html-parse-stringify@4.0.1: {}
|
||||
|
||||
html-to-image@1.11.13: {}
|
||||
|
||||
@@ -11378,7 +11373,7 @@ snapshots:
|
||||
minimatch: 10.2.5
|
||||
ora: 9.4.0
|
||||
react: 19.2.8
|
||||
react-i18next: 17.0.8(i18next@26.3.1(typescript@7.0.2))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2)
|
||||
react-i18next: 17.0.13(i18next@26.3.1(typescript@7.0.2))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2)
|
||||
yaml: 2.9.0
|
||||
transitivePeerDependencies:
|
||||
- '@swc/helpers'
|
||||
@@ -11948,7 +11943,7 @@ snapshots:
|
||||
d3-sankey: 0.12.3
|
||||
dagre-d3-es: 7.0.14
|
||||
dayjs: 1.11.23
|
||||
dompurify: 3.4.13
|
||||
dompurify: 3.4.14
|
||||
es-toolkit: 1.46.1
|
||||
fastdom: 1.0.12
|
||||
katex: 0.16.47
|
||||
@@ -12233,7 +12228,7 @@ snapshots:
|
||||
|
||||
monaco-editor@0.55.1:
|
||||
dependencies:
|
||||
dompurify: 3.4.13
|
||||
dompurify: 3.4.14
|
||||
marked: 14.0.0
|
||||
|
||||
ms@2.1.3: {}
|
||||
@@ -12269,7 +12264,7 @@ snapshots:
|
||||
nan@2.26.2:
|
||||
optional: true
|
||||
|
||||
nanoid@3.3.17: {}
|
||||
nanoid@3.3.18: {}
|
||||
|
||||
negotiator@1.0.0: {}
|
||||
|
||||
@@ -12308,7 +12303,7 @@ snapshots:
|
||||
dependencies:
|
||||
node-addon-api: 7.1.1
|
||||
|
||||
node-releases@2.0.38: {}
|
||||
node-releases@2.0.54: {}
|
||||
|
||||
nopt@9.0.0:
|
||||
dependencies:
|
||||
@@ -12594,7 +12589,7 @@ snapshots:
|
||||
|
||||
plist@3.1.0:
|
||||
dependencies:
|
||||
'@xmldom/xmldom': 0.8.13
|
||||
'@xmldom/xmldom': 0.8.15
|
||||
base64-js: 1.5.1
|
||||
xmlbuilder: 15.1.1
|
||||
|
||||
@@ -12609,14 +12604,14 @@ snapshots:
|
||||
path-data-parser: 0.1.0
|
||||
points-on-curve: 0.2.0
|
||||
|
||||
postcss-selector-parser@7.1.1:
|
||||
postcss-selector-parser@7.1.6:
|
||||
dependencies:
|
||||
cssesc: 3.0.0
|
||||
util-deprecate: 1.0.2
|
||||
|
||||
postcss@8.5.25:
|
||||
dependencies:
|
||||
nanoid: 3.3.17
|
||||
nanoid: 3.3.18
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
@@ -12762,9 +12757,10 @@ snapshots:
|
||||
pngjs: 5.0.0
|
||||
yargs: 15.4.1
|
||||
|
||||
qs@6.15.2:
|
||||
qs@6.16.0:
|
||||
dependencies:
|
||||
side-channel: 1.1.0
|
||||
es-define-property: 1.0.1
|
||||
side-channel: 1.1.1
|
||||
|
||||
queue-microtask@1.2.3: {}
|
||||
|
||||
@@ -12859,10 +12855,10 @@ snapshots:
|
||||
optionalDependencies:
|
||||
react: 19.2.8
|
||||
|
||||
react-i18next@17.0.8(i18next@26.3.1(typescript@7.0.2))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2):
|
||||
react-i18next@17.0.13(i18next@26.3.1(typescript@7.0.2))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.7
|
||||
html-parse-stringify: 3.0.1
|
||||
html-parse-stringify: 4.0.1
|
||||
i18next: 26.3.1(typescript@7.0.2)
|
||||
react: 19.2.8
|
||||
use-sync-external-store: 1.6.0(react@19.2.8)
|
||||
@@ -13241,7 +13237,7 @@ snapshots:
|
||||
'@dotenvx/dotenvx': 1.66.0
|
||||
'@modelcontextprotocol/sdk': 1.30.0(zod@3.25.76)
|
||||
'@types/validate-npm-package-name': 4.0.2
|
||||
browserslist: 4.28.2
|
||||
browserslist: 4.28.9
|
||||
commander: 14.0.3
|
||||
cosmiconfig: 9.0.1(typescript@7.0.2)
|
||||
dedent: 1.7.2
|
||||
@@ -13255,7 +13251,7 @@ snapshots:
|
||||
open: 11.0.0
|
||||
ora: 8.2.0
|
||||
postcss: 8.5.25
|
||||
postcss-selector-parser: 7.1.1
|
||||
postcss-selector-parser: 7.1.6
|
||||
prompts: 2.4.2
|
||||
recast: 0.23.11
|
||||
stringify-object: 5.0.0
|
||||
@@ -13315,7 +13311,7 @@ snapshots:
|
||||
object-inspect: 1.13.4
|
||||
side-channel-map: 1.0.1
|
||||
|
||||
side-channel@1.1.0:
|
||||
side-channel@1.1.1:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
object-inspect: 1.13.4
|
||||
@@ -13687,9 +13683,9 @@ snapshots:
|
||||
graceful-fs: 4.2.11
|
||||
node-int64: 0.4.0
|
||||
|
||||
update-browserslist-db@1.2.3(browserslist@4.28.2):
|
||||
update-browserslist-db@1.3.2(browserslist@4.28.9):
|
||||
dependencies:
|
||||
browserslist: 4.28.2
|
||||
browserslist: 4.28.9
|
||||
escalade: 3.2.0
|
||||
picocolors: 1.1.1
|
||||
|
||||
@@ -13766,8 +13762,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- msw
|
||||
|
||||
void-elements@3.1.0: {}
|
||||
|
||||
vscode-oniguruma@2.0.1: {}
|
||||
|
||||
vscode-textmate@9.3.2: {}
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ allowBuilds:
|
||||
windows-native-registry: false
|
||||
|
||||
overrides:
|
||||
monaco-editor>dompurify: 3.4.13
|
||||
monaco-editor>dompurify: 3.4.14
|
||||
|
||||
patchedDependencies:
|
||||
node-pty@1.1.0: config/patches/node-pty@1.1.0.patch
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { AgentJournalMessageItem } from '../../shared/agent-session-journal-types'
|
||||
import type { NativeChatBlock } from '../../shared/native-chat-types'
|
||||
import { buildImageDataUri } from '../../shared/image-data-uri'
|
||||
import { DEFAULT_JOURNAL_PAYLOAD_LIMITS } from '../native-chat/agent-session-journal/journal-payload-bounds'
|
||||
import { readString } from './codex-item-field-readers'
|
||||
import type { CodexThreadItem } from './codex-thread-item-identity'
|
||||
|
||||
// Leave room for operation text and the journal envelope beside inline image bytes.
|
||||
const MAX_IMAGE_REFERENCE_BYTES = DEFAULT_JOURNAL_PAYLOAD_LIMITS.inlineHeadBytes / 2
|
||||
|
||||
function imagePath(item: CodexThreadItem, key: string): string | null {
|
||||
const value = readString(item, key)
|
||||
return value?.trim() && Buffer.byteLength(value, 'utf8') <= MAX_IMAGE_REFERENCE_BYTES
|
||||
? value
|
||||
: null
|
||||
}
|
||||
|
||||
function generatedImageUrl(item: CodexThreadItem): string | null {
|
||||
const result = readString(item, 'result')
|
||||
if (!result || result.length > MAX_IMAGE_REFERENCE_BYTES) {
|
||||
return null
|
||||
}
|
||||
const match = /^data:(image\/(?:png|jpeg|webp));base64,(.*)$/s.exec(result)
|
||||
const base64 = (match?.[2] ?? result).replace(/\s/g, '')
|
||||
if (!base64 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(base64)) {
|
||||
return null
|
||||
}
|
||||
const url = buildImageDataUri(match?.[1] ?? 'image/png', base64)
|
||||
return url && url.length <= MAX_IMAGE_REFERENCE_BYTES ? url : null
|
||||
}
|
||||
|
||||
export function codexImageItemBody(item: CodexThreadItem): AgentJournalMessageItem {
|
||||
let image: Extract<NativeChatBlock, { type: 'image-ref' }> | null = null
|
||||
let text: string
|
||||
if (item.type === 'imageView') {
|
||||
const path = imagePath(item, 'path')
|
||||
text = path ? 'Viewed image' : 'Image view: preview unavailable'
|
||||
image = path ? { type: 'image-ref', path } : null
|
||||
} else {
|
||||
const status = readString(item, 'status')
|
||||
if (item.failure || status === 'failed') {
|
||||
text = 'Image generation failed'
|
||||
} else if (status !== 'completed') {
|
||||
text = status === 'inProgress' ? 'Generating image…' : 'Image generation: preview unavailable'
|
||||
} else {
|
||||
const path = imagePath(item, 'savedPath')
|
||||
const url = path ? null : generatedImageUrl(item)
|
||||
image = path
|
||||
? { type: 'image-ref', path }
|
||||
: url
|
||||
? { type: 'image-ref', url, alt: 'Generated image' }
|
||||
: null
|
||||
text = image ? 'Generated image' : 'Image generated: preview unavailable'
|
||||
}
|
||||
}
|
||||
return {
|
||||
kind: 'message',
|
||||
role: 'assistant',
|
||||
blocks: [{ type: 'text', text }, ...(image ? [image] : [])]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { codexItemBody, codexStreamingJournalItem } from './codex-structured-item-translation'
|
||||
import { AgentJournalItemBodySchema } from '../../shared/agent-session-journal-schemas'
|
||||
import { projectStructuredItemsToNativeChat } from '../../shared/structured-agent-session-projection'
|
||||
|
||||
describe('plan document translation', () => {
|
||||
it('marks both complete documents and streaming snapshots', () => {
|
||||
const item = { id: 'plan-1', type: 'plan', text: '# Plan\n\nReadable prose.' }
|
||||
expect(codexItemBody(item)).toEqual({
|
||||
kind: 'status',
|
||||
text: item.text,
|
||||
presentation: 'plan-document'
|
||||
})
|
||||
expect(codexStreamingJournalItem(item, '# Plan\n\nPartial').body).toEqual({
|
||||
kind: 'status',
|
||||
text: '# Plan\n\nPartial',
|
||||
presentation: 'plan-document'
|
||||
})
|
||||
expect(codexItemBody({ id: 'plan-1', type: 'plan' })).toBeNull()
|
||||
})
|
||||
it('preserves the full existing reasoning body byte for byte', () => {
|
||||
expect(
|
||||
codexItemBody({ id: 'r', type: 'reasoning', summary: ['Thinking through the problem.'] })
|
||||
).toEqual({
|
||||
kind: 'status',
|
||||
text: 'Thinking through the problem.'
|
||||
})
|
||||
expect(codexStreamingJournalItem({ id: 'r', type: 'reasoning' }, 'Thinking…')).toEqual({
|
||||
body: { kind: 'status', text: 'Thinking…' },
|
||||
handled: true
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('image item translation', () => {
|
||||
it.each(['/remote/work/image.png', 'C:\\work\\image.png'])(
|
||||
'preserves the execution-host path %s and old-reader operation text',
|
||||
(path) => {
|
||||
expect(codexItemBody({ id: 'view', type: 'imageView', path })).toEqual({
|
||||
kind: 'message',
|
||||
role: 'assistant',
|
||||
blocks: [
|
||||
{ type: 'text', text: 'Viewed image' },
|
||||
{ type: 'image-ref', path }
|
||||
]
|
||||
})
|
||||
}
|
||||
)
|
||||
it('prefers saved paths over unbounded inline image data', () => {
|
||||
const body = codexItemBody({
|
||||
id: 'gen',
|
||||
type: 'imageGeneration',
|
||||
status: 'completed',
|
||||
savedPath: '/remote/image.png',
|
||||
result: 'A'.repeat(100_000)
|
||||
})
|
||||
expect(body).toEqual({
|
||||
kind: 'message',
|
||||
role: 'assistant',
|
||||
blocks: [
|
||||
{ type: 'text', text: 'Generated image' },
|
||||
{ type: 'image-ref', path: '/remote/image.png' }
|
||||
]
|
||||
})
|
||||
expect(AgentJournalItemBodySchema.safeParse(body).success).toBe(true)
|
||||
const [message] = projectStructuredItemsToNativeChat([
|
||||
{ itemId: 'gen', revision: 1, sequence: 1, observedAt: 1, body: body! }
|
||||
])
|
||||
expect(message?.blocks).toEqual(body?.kind === 'message' ? body.blocks : [])
|
||||
})
|
||||
it.each(['AAAA', 'data:image/png;base64,AAAA'])(
|
||||
'maps bounded image data onto a meaningful existing image-ref: %s',
|
||||
(result) => {
|
||||
expect(
|
||||
codexItemBody({ id: 'gen', type: 'imageGeneration', status: 'completed', result })
|
||||
).toMatchObject({
|
||||
kind: 'message',
|
||||
role: 'assistant',
|
||||
blocks: [
|
||||
{ type: 'text', text: 'Generated image' },
|
||||
{ type: 'image-ref', url: 'data:image/png;base64,AAAA', alt: 'Generated image' }
|
||||
]
|
||||
})
|
||||
}
|
||||
)
|
||||
it.each(['A'.repeat(20_000), 'not valid image bytes', 'data:text/html;base64,AAAA', ''])(
|
||||
'keeps unavailable results bounded and readable',
|
||||
(result) => {
|
||||
const body = codexItemBody({
|
||||
id: 'gen',
|
||||
type: 'imageGeneration',
|
||||
status: 'completed',
|
||||
result
|
||||
})
|
||||
expect(body).toEqual({
|
||||
kind: 'message',
|
||||
role: 'assistant',
|
||||
blocks: [{ type: 'text', text: 'Image generated: preview unavailable' }]
|
||||
})
|
||||
}
|
||||
)
|
||||
it.each([
|
||||
[{ status: 'inProgress' }, 'Generating image…'],
|
||||
[{ status: 'failed' }, 'Image generation failed'],
|
||||
[{ status: 'completed', failure: { type: 'usageLimitExceeded' } }, 'Image generation failed'],
|
||||
[{ status: 'future-state' }, 'Image generation: preview unavailable']
|
||||
])('does not invent completed output for %j', (fields, text) => {
|
||||
expect(codexItemBody({ id: 'gen', type: 'imageGeneration', ...fields })).toEqual({
|
||||
kind: 'message',
|
||||
role: 'assistant',
|
||||
blocks: [{ type: 'text', text }]
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
DEFAULT_JOURNAL_PAYLOAD_LIMITS
|
||||
} from '../native-chat/agent-session-journal/journal-payload-bounds'
|
||||
import { unhandledProviderFrameJournalItem } from '../native-chat/agent-session-wire/unhandled-provider-frame'
|
||||
import { codexImageItemBody } from './codex-image-item-translation'
|
||||
import { commandActionFacts } from './codex-command-action-class'
|
||||
import {
|
||||
readFirstString,
|
||||
@@ -250,6 +251,23 @@ export function codexJournalItem(item: CodexThreadItem): CodexJournalItem {
|
||||
if (item.type === 'webSearch') {
|
||||
return webSearchItem(item)
|
||||
}
|
||||
if (item.type === 'imageView' || item.type === 'imageGeneration') {
|
||||
return { body: codexImageItemBody(item), handled: true }
|
||||
}
|
||||
if (item.type === 'plan') {
|
||||
const text = readTextContent(item, 'text')
|
||||
return {
|
||||
body:
|
||||
text === null
|
||||
? null
|
||||
: {
|
||||
kind: 'status',
|
||||
text: boundInlineText(text, DEFAULT_JOURNAL_PAYLOAD_LIMITS).text,
|
||||
presentation: 'plan-document'
|
||||
},
|
||||
handled: true
|
||||
}
|
||||
}
|
||||
if (item.type === 'reasoning' || item.type === 'plan') {
|
||||
const text =
|
||||
readTextContent(item, 'text') ??
|
||||
@@ -298,6 +316,16 @@ export function codexStreamingJournalItem(item: CodexThreadItem, text: string):
|
||||
handled: true
|
||||
}
|
||||
}
|
||||
if (item.type === 'plan') {
|
||||
return {
|
||||
body: {
|
||||
kind: 'status',
|
||||
text: boundInlineText(text, DEFAULT_JOURNAL_PAYLOAD_LIMITS).text,
|
||||
presentation: 'plan-document'
|
||||
},
|
||||
handled: true
|
||||
}
|
||||
}
|
||||
const bounded = boundInlineText(text, DEFAULT_JOURNAL_PAYLOAD_LIMITS)
|
||||
return { body: { kind: 'status', text: bounded.text }, handled: true }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { agentJournalItemKey } from '../../shared/agent-session-journal-item-key'
|
||||
import type { AgentJournalItemBody } from '../../shared/agent-session-journal-types'
|
||||
import { projectStructuredItemsToNativeChat } from '../../shared/structured-agent-session-projection'
|
||||
import type { StructuredAgentSessionEventSink } from '../native-chat/agent-session-wire/structured-agent-session-event-sink'
|
||||
import {
|
||||
createCodexJournalTranslator,
|
||||
MAX_CODEX_GENERIC_TURN_BUCKETS
|
||||
} from './codex-structured-journal-translation'
|
||||
|
||||
function setup() {
|
||||
const rows = new Map<string, AgentJournalItemBody>()
|
||||
let writes = 0
|
||||
const sink: StructuredAgentSessionEventSink = {
|
||||
appendItem: (identity, body) => {
|
||||
writes += 1
|
||||
rows.set(agentJournalItemKey(identity), body)
|
||||
},
|
||||
appendTombstone: (identity) => rows.delete(agentJournalItemKey(identity)),
|
||||
publish: () => {}
|
||||
}
|
||||
const translator = createCodexJournalTranslator({ sink })
|
||||
const send = (method: string, turnId = 'turn', threadId = 'thread') =>
|
||||
translator.handle({
|
||||
type: 'notification',
|
||||
sessionId: 'session',
|
||||
threadId,
|
||||
method,
|
||||
params: { turnId, item: { id: `compact-${turnId}`, type: 'contextCompaction' } }
|
||||
})
|
||||
return { rows, sink, translator, send, writes: () => writes }
|
||||
}
|
||||
|
||||
describe('compaction provider generation compatibility', () => {
|
||||
it.each([
|
||||
['item/completed'],
|
||||
['thread/compacted'],
|
||||
['item/completed', 'thread/compacted'],
|
||||
['thread/compacted', 'item/completed']
|
||||
])('projects one readable divider for %j', (...methods) => {
|
||||
const { rows, translator, send } = setup()
|
||||
expect(send('item/started')).toEqual({ accepted: true })
|
||||
expect(rows.size).toBe(0)
|
||||
for (const method of methods) {
|
||||
expect(send(method)).toEqual({ accepted: true })
|
||||
}
|
||||
const messages = projectStructuredItemsToNativeChat(
|
||||
[...rows].map(([itemId, body], index) => ({
|
||||
itemId,
|
||||
body,
|
||||
sequence: index + 1,
|
||||
revision: 1,
|
||||
observedAt: 1
|
||||
}))
|
||||
)
|
||||
expect(messages).toHaveLength(1)
|
||||
expect(messages[0]?.blocks).toEqual([
|
||||
{ type: 'text', text: 'Context compacted', presentation: 'compaction' }
|
||||
])
|
||||
translator.dispose()
|
||||
})
|
||||
|
||||
it('keeps the canonical item authoritative and scopes deduplication by thread and turn', () => {
|
||||
const { rows, translator, send, writes } = setup()
|
||||
send('thread/compacted')
|
||||
send('item/completed')
|
||||
expect(writes()).toBe(2)
|
||||
send('thread/compacted')
|
||||
send('item/completed')
|
||||
expect(writes()).toBe(2)
|
||||
send('item/completed', 'turn-2')
|
||||
send('item/completed', 'turn', 'other-thread')
|
||||
expect(rows.size).toBe(3)
|
||||
translator.dispose()
|
||||
})
|
||||
|
||||
it.each(['append', 'publish'])('does not suppress the retry after rejected %s', (stage) => {
|
||||
const { rows, sink, translator, send } = setup()
|
||||
let reject = true
|
||||
let publishes = 0
|
||||
sink.tryAppendItem = (identity, body) => {
|
||||
if (stage === 'append' && reject) {
|
||||
return { accepted: false, reason: 'backpressure' }
|
||||
}
|
||||
sink.appendItem(identity, body)
|
||||
return { accepted: true }
|
||||
}
|
||||
sink.tryPublish = () => {
|
||||
publishes += 1
|
||||
return stage === 'publish' && reject
|
||||
? { accepted: false, reason: 'backpressure' }
|
||||
: { accepted: true }
|
||||
}
|
||||
expect(send('item/completed')).toEqual({ accepted: false, reason: 'backpressure' })
|
||||
reject = false
|
||||
expect(send('item/completed')).toEqual({ accepted: true })
|
||||
expect(rows.size).toBe(1)
|
||||
expect(publishes).toBe(stage === 'publish' ? 2 : 1)
|
||||
translator.dispose()
|
||||
})
|
||||
|
||||
it('bounds retained turns and keeps the same journal identity after eviction', () => {
|
||||
const { rows, translator, send, writes } = setup()
|
||||
send('item/completed', 'oldest')
|
||||
for (let index = 0; index < MAX_CODEX_GENERIC_TURN_BUCKETS; index += 1) {
|
||||
send('item/completed', `turn-${index}`)
|
||||
}
|
||||
const before = writes()
|
||||
send('thread/compacted', 'oldest')
|
||||
expect(writes()).toBe(before + 1)
|
||||
expect(rows.size).toBe(MAX_CODEX_GENERIC_TURN_BUCKETS + 1)
|
||||
translator.dispose()
|
||||
})
|
||||
|
||||
it('restores canonical compaction history over a previously journaled legacy fallback', () => {
|
||||
const { rows, sink, translator, send } = setup()
|
||||
send('thread/compacted')
|
||||
translator.dispose()
|
||||
const restored = createCodexJournalTranslator({ sink })
|
||||
expect(
|
||||
restored.restoreThread('thread', {
|
||||
turns: [{ id: 'turn', items: [{ id: 'renumbered', type: 'contextCompaction' }] }]
|
||||
})
|
||||
).toEqual({ accepted: true })
|
||||
expect(rows.size).toBe(1)
|
||||
expect([...rows.values()][0]).toEqual({
|
||||
kind: 'status',
|
||||
text: 'Context compacted',
|
||||
presentation: 'compaction'
|
||||
})
|
||||
restored.dispose()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,66 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { isCodexCompactionComplete } from '../native-chat/agent-session-wire/structured-session-compaction'
|
||||
import type { StructuredAgentSessionEventSink } from '../native-chat/agent-session-wire/structured-agent-session-event-sink'
|
||||
import {
|
||||
CODEX_JOURNAL_ADMITTED,
|
||||
type CodexJournalTranslationAdmission
|
||||
} from './codex-structured-journal-contracts'
|
||||
import { MAX_CODEX_GENERIC_TURN_BUCKETS } from './codex-structured-journal-limits'
|
||||
import { appendCodexLifecycleItem, publishCodexLifecycle } from './codex-structured-journal-sink'
|
||||
import { readCodexTurnId } from './codex-structured-thread-facts'
|
||||
|
||||
export class CodexJournalCompactions {
|
||||
private readonly turns = new Map<string, 'item' | 'legacy'>()
|
||||
|
||||
constructor(
|
||||
private readonly sink: StructuredAgentSessionEventSink,
|
||||
private readonly activeTurn: (threadId: string) => string | null
|
||||
) {}
|
||||
|
||||
handle(event: {
|
||||
threadId: string
|
||||
method: string
|
||||
params: unknown
|
||||
}): CodexJournalTranslationAdmission | null {
|
||||
if (!isCodexCompactionComplete(event.method, event.params)) {
|
||||
return null
|
||||
}
|
||||
const turnId = readCodexTurnId(event.params) ?? this.activeTurn(event.threadId)
|
||||
if (!turnId) {
|
||||
return null
|
||||
}
|
||||
// Collapse compactions within a thread/turn; the canonical item replaces its legacy fallback.
|
||||
const key = createHash('sha256')
|
||||
.update(JSON.stringify([event.threadId, turnId]))
|
||||
.digest('hex')
|
||||
const source = event.method === 'item/completed' ? 'item' : 'legacy'
|
||||
const previous = this.turns.get(key)
|
||||
if (previous === 'item' || previous === source) {
|
||||
return CODEX_JOURNAL_ADMITTED
|
||||
}
|
||||
const admission = appendCodexLifecycleItem(
|
||||
this.sink,
|
||||
{ provider: 'orca', clientMessageId: `codex-compaction:${key}` },
|
||||
{ kind: 'status', text: 'Context compacted', presentation: 'compaction' }
|
||||
)
|
||||
if (!admission.accepted) {
|
||||
return admission
|
||||
}
|
||||
const published = publishCodexLifecycle(this.sink)
|
||||
if (!published.accepted) {
|
||||
return published
|
||||
}
|
||||
this.turns.set(key, source)
|
||||
while (this.turns.size > MAX_CODEX_GENERIC_TURN_BUCKETS) {
|
||||
const oldest = this.turns.keys().next().value
|
||||
if (oldest !== undefined) {
|
||||
this.turns.delete(oldest)
|
||||
}
|
||||
}
|
||||
return CODEX_JOURNAL_ADMITTED
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.turns.clear()
|
||||
}
|
||||
}
|
||||
@@ -78,6 +78,9 @@ export class CodexJournalItems {
|
||||
if (source === 'live' && item.type === 'userMessage') {
|
||||
return { handled: true, admission: CODEX_JOURNAL_ADMITTED }
|
||||
}
|
||||
if (item.type === 'contextCompaction' && event.method === 'item/started') {
|
||||
return { handled: true, admission: CODEX_JOURNAL_ADMITTED }
|
||||
}
|
||||
const translated = codexJournalItem(item)
|
||||
const command = readCodexJournalString(item, 'command')
|
||||
if (command) {
|
||||
|
||||
@@ -603,3 +603,68 @@ describe('codex journal translation', () => {
|
||||
expect(tap.rows).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('notice journal pipeline', () => {
|
||||
it('replaces a legacy compaction divider with its canonical item at the same journal key', () => {
|
||||
const { translator, tap } = translatorWith()
|
||||
translator.handle(notification('thread/compacted', { threadId: THREAD_ID, turnId: TURN_ID }))
|
||||
translator.handle(
|
||||
notification('item/completed', {
|
||||
turnId: TURN_ID,
|
||||
item: { id: 'compact', type: 'contextCompaction' }
|
||||
})
|
||||
)
|
||||
expect(tap.rows).toHaveLength(2)
|
||||
expect([...new Map(tap.rows.map((row) => [row.key, row.body])).values()]).toEqual([
|
||||
expect.objectContaining({
|
||||
kind: 'status',
|
||||
text: 'Context compacted',
|
||||
presentation: 'compaction'
|
||||
})
|
||||
])
|
||||
translator.dispose()
|
||||
})
|
||||
it('preserves every notice after generic traffic reaches its cap', () => {
|
||||
const { translator, tap, window } = translatorWith()
|
||||
translator.handle(TURN_STARTED)
|
||||
for (let index = 0; index < MAX_CODEX_GENERIC_ROWS_PER_TURN; index += 1) {
|
||||
translator.handle(notification('future/notification', { value: index }))
|
||||
}
|
||||
for (const method of ['warning', 'guardianWarning', 'configWarning', 'deprecationNotice']) {
|
||||
translator.handle(notification(method, { message: method, summary: method }))
|
||||
}
|
||||
window.fire()
|
||||
expect(tap.rows.slice(-4).map((row) => row.body)).toEqual([
|
||||
expect.objectContaining({ text: 'warning', tone: 'warning' }),
|
||||
expect.objectContaining({ text: 'guardianWarning', tone: 'warning' }),
|
||||
expect.objectContaining({ text: 'configWarning', tone: 'warning' }),
|
||||
expect.objectContaining({ text: 'deprecationNotice', tone: 'notice' })
|
||||
])
|
||||
translator.dispose()
|
||||
})
|
||||
it('keeps the plan document marker during streamed updates and completion', () => {
|
||||
const { translator, tap, window } = translatorWith()
|
||||
translator.handle(
|
||||
notification('item/started', {
|
||||
turnId: TURN_ID,
|
||||
item: { id: 'plan', type: 'plan', text: '' }
|
||||
})
|
||||
)
|
||||
translator.handle(
|
||||
notification('item/plan/delta', { turnId: TURN_ID, itemId: 'plan', delta: '# Plan' })
|
||||
)
|
||||
window.fire()
|
||||
expect(tap.rows.at(-1)?.body).toMatchObject({ text: '# Plan', presentation: 'plan-document' })
|
||||
translator.handle(
|
||||
notification('item/completed', {
|
||||
turnId: TURN_ID,
|
||||
item: { id: 'plan', type: 'plan', text: '# Plan\n\nComplete' }
|
||||
})
|
||||
)
|
||||
expect(tap.rows.at(-1)?.body).toMatchObject({
|
||||
text: '# Plan\n\nComplete',
|
||||
presentation: 'plan-document'
|
||||
})
|
||||
translator.dispose()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import { CodexSubagentRoster } from './codex-subagent-roster'
|
||||
import { readCodexThreadItem } from './codex-structured-item-translation'
|
||||
import { CodexJournalGenericFrames } from './codex-structured-journal-generic-frames'
|
||||
import { CodexJournalCompactions } from './codex-structured-journal-compactions'
|
||||
import { CodexJournalItems } from './codex-structured-journal-items'
|
||||
import { CodexJournalPrompts } from './codex-structured-journal-prompts'
|
||||
import {
|
||||
@@ -46,6 +47,9 @@ export function createCodexJournalTranslator(
|
||||
deps: CodexJournalTranslatorDeps
|
||||
): CodexJournalTranslator {
|
||||
const activeTurns = new CodexJournalActiveTurns()
|
||||
const compactions = new CodexJournalCompactions(deps.sink, (threadId) =>
|
||||
activeTurns.current(threadId)
|
||||
)
|
||||
const genericFrames = new CodexJournalGenericFrames(deps, (threadId) =>
|
||||
activeTurns.current(threadId)
|
||||
)
|
||||
@@ -94,6 +98,10 @@ export function createCodexJournalTranslator(
|
||||
currentTurnIds: activeTurns.byThread,
|
||||
ordinals: items.ordinals,
|
||||
handleItem: (event) => {
|
||||
const compaction = compactions.handle(event)
|
||||
if (compaction) {
|
||||
return compaction
|
||||
}
|
||||
const translated = items.handle(event, 'history')
|
||||
return translated.handled
|
||||
? translated.admission
|
||||
@@ -135,6 +143,7 @@ export function createCodexJournalTranslator(
|
||||
items.activeItems.clear()
|
||||
prompts.pending.clear()
|
||||
activeTurns.clear()
|
||||
compactions.clear()
|
||||
return CODEX_JOURNAL_ADMITTED
|
||||
}
|
||||
if (event.type === 'notification') {
|
||||
@@ -168,6 +177,10 @@ export function createCodexJournalTranslator(
|
||||
if (event.method === 'turn/started') {
|
||||
return startTurn(event)
|
||||
}
|
||||
const compaction = compactions.handle(event)
|
||||
if (compaction) {
|
||||
return publishActivity(event, compaction)
|
||||
}
|
||||
if (event.method === 'turn/completed') {
|
||||
return completeTurn(event)
|
||||
}
|
||||
@@ -223,17 +236,15 @@ export function createCodexJournalTranslator(
|
||||
genericFrames.dispose()
|
||||
subagents.dispose()
|
||||
activeTurns.clear()
|
||||
compactions.clear()
|
||||
}
|
||||
}
|
||||
|
||||
/** Settles the item a notification the transport refused to carry left
|
||||
* mid-flight; null when the frame is not one. */
|
||||
function settleOversizedNotification(event: {
|
||||
sessionId: string
|
||||
threadId: string
|
||||
kind: string
|
||||
payload: unknown
|
||||
}): CodexJournalTranslationAdmission | null {
|
||||
function settleOversizedNotification(
|
||||
event: Extract<CodexStructuredSessionEvent, { type: 'provider-frame' }>
|
||||
): CodexJournalTranslationAdmission | null {
|
||||
return settleCodexOversizedNotificationFrame({
|
||||
...event,
|
||||
sink: deps.sink,
|
||||
@@ -242,11 +253,9 @@ export function createCodexJournalTranslator(
|
||||
})
|
||||
}
|
||||
|
||||
function startTurn(event: {
|
||||
sessionId: string
|
||||
threadId: string
|
||||
params: unknown
|
||||
}): CodexJournalTranslationAdmission {
|
||||
function startTurn(
|
||||
event: Extract<CodexStructuredSessionEvent, { type: 'notification' }>
|
||||
): CodexJournalTranslationAdmission {
|
||||
const turnId = readCodexTurnId(event.params)
|
||||
if (!turnId) {
|
||||
return CODEX_JOURNAL_ADMITTED
|
||||
|
||||
@@ -104,11 +104,12 @@ describe('provider frame classification catalog', () => {
|
||||
})
|
||||
|
||||
it('dispositions codex item-form frames, which the method catalog never matches', () => {
|
||||
// `thread/compacted` is already chrome; its item form is the same event and
|
||||
// must not leak `codex · item:contextCompaction` into the transcript.
|
||||
expect(classifyProviderFrame('codex', 'item:contextCompaction', {})).toBe('status-chrome')
|
||||
// Both provider generations reach the journal's compaction deduplication.
|
||||
expect(classifyProviderFrame('codex', 'item:contextCompaction', {})).toBe(
|
||||
'timeline-substantive'
|
||||
)
|
||||
expect(classifyProviderFrame('codex', 'notification:thread/compacted', {})).toBe(
|
||||
'status-chrome'
|
||||
'timeline-substantive'
|
||||
)
|
||||
// An item type nobody has dispositioned still falls through visibly.
|
||||
expect(classifyProviderFrame('codex', 'item:futureThing', {})).toBe('timeline-substantive')
|
||||
@@ -165,6 +166,21 @@ describe('provider frame classification catalog', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('notice disposition boundaries', () => {
|
||||
it.each(['warning', 'guardianWarning', 'deprecationNotice', 'configWarning'])(
|
||||
'retains the error-surface cap exemption for %s',
|
||||
(method) => {
|
||||
expect(classifyProviderFrame('codex', `notification:${method}`, {})).toBe('error-surface')
|
||||
}
|
||||
)
|
||||
it('does not change usage or rate-limit classifications', () => {
|
||||
expect(classifyProviderFrame('codex', 'thread/tokenUsage/updated', {})).toBe('status-chrome')
|
||||
expect(classifyProviderFrame('codex', 'account/rateLimits/updated', {})).toBe(
|
||||
'suppressed-benign'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('codex subagent item disposition', () => {
|
||||
it('keeps subagent lifecycle out of the transcript now that it renders as a roster row', () => {
|
||||
expect(
|
||||
|
||||
@@ -66,7 +66,7 @@ export const PROVIDER_FRAME_CLASSIFICATIONS = {
|
||||
'item/reasoning/summaryTextDelta': 'stream-into-item',
|
||||
'item/reasoning/summaryPartAdded': 'stream-into-item',
|
||||
'item/reasoning/textDelta': 'stream-into-item',
|
||||
'thread/compacted': 'status-chrome',
|
||||
'thread/compacted': 'timeline-substantive',
|
||||
'model/rerouted': 'status-chrome',
|
||||
'model/verification': 'status-chrome',
|
||||
'turn/moderationMetadata': 'suppressed-benign',
|
||||
@@ -196,9 +196,8 @@ function hasProviderError(payload: unknown): boolean {
|
||||
* new item type cannot leak `codex · item:<type>` into the transcript. The
|
||||
* notification catalog above is keyed by METHOD and never matches these. */
|
||||
const CODEX_ITEM_CLASSIFICATIONS: Record<string, ProviderFrameClassification> = {
|
||||
// The `thread/compacted` notification is already chrome; its item form is the
|
||||
// same event and must not read as a mysterious opcode row.
|
||||
contextCompaction: 'status-chrome',
|
||||
// The journal coalesces this canonical completion with the legacy notification.
|
||||
contextCompaction: 'timeline-substantive',
|
||||
// Subagent lifecycle renders as the spawn-group roster row, so its raw items
|
||||
// must not print a gray `codex · item:<type>` row beside it. The live
|
||||
// notification path intercepts them before this catalog is reached;
|
||||
|
||||
@@ -11,6 +11,13 @@ function record(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === 'object' ? (value as Record<string, unknown>) : {}
|
||||
}
|
||||
|
||||
export function isCodexCompactionComplete(method: string, params: unknown): boolean {
|
||||
return (
|
||||
method === 'thread/compacted' ||
|
||||
(method === 'item/completed' && record(record(params).item).type === 'contextCompaction')
|
||||
)
|
||||
}
|
||||
|
||||
/** A receipt is not completion; keep listening through the provider's terminal frame. */
|
||||
export class StructuredSessionCompaction {
|
||||
private readonly pending = new Map<string, PendingCompaction>()
|
||||
@@ -95,10 +102,7 @@ export class StructuredSessionCompaction {
|
||||
if (method === 'turn/started' && typeof turn.id === 'string') {
|
||||
pending.turnId = turn.id
|
||||
}
|
||||
if (
|
||||
method === 'thread/compacted' ||
|
||||
(method === 'item/completed' && record(params.item).type === 'contextCompaction')
|
||||
) {
|
||||
if (isCodexCompactionComplete(method, params)) {
|
||||
pending.compacted = true
|
||||
}
|
||||
if (method === 'turn/completed' && turn.id === pending.turnId) {
|
||||
|
||||
@@ -235,3 +235,41 @@ describe('a failed provider dependency', () => {
|
||||
).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('typed notice metadata', () => {
|
||||
it('publishes readable compaction statuses for both provider forms', () => {
|
||||
expect(
|
||||
unhandledProviderFrameJournalItem('codex', 'notification:thread/compacted', {})
|
||||
).toMatchObject({
|
||||
classification: 'timeline-substantive',
|
||||
body: { kind: 'status', text: 'Context compacted', presentation: 'compaction' }
|
||||
})
|
||||
expect(unhandledProviderFrameJournalItem('codex', 'item:contextCompaction', {})).toMatchObject({
|
||||
body: { kind: 'status', text: 'Context compacted', presentation: 'compaction' }
|
||||
})
|
||||
})
|
||||
it.each([
|
||||
['warning', { message: 'Check this' }, 'warning', 'Check this'],
|
||||
['guardianWarning', { message: 'Review required' }, 'warning', 'Review required'],
|
||||
[
|
||||
'configWarning',
|
||||
{ summary: 'Invalid option', details: 'Remove the option' },
|
||||
'warning',
|
||||
'Invalid option\n\nRemove the option'
|
||||
],
|
||||
[
|
||||
'deprecationNotice',
|
||||
{ summary: 'Old option', details: 'Use its replacement' },
|
||||
'notice',
|
||||
'Old option\n\nUse its replacement'
|
||||
],
|
||||
['error', { error: { message: 'Connection failed' } }, 'error', 'Connection failed']
|
||||
])('assigns the tone and readable text for %s', (method, payload, tone, text) => {
|
||||
expect(
|
||||
unhandledProviderFrameJournalItem('codex', `notification:${method}`, payload)
|
||||
).toMatchObject({
|
||||
classification: 'error-surface',
|
||||
body: { kind: 'status', text, tone }
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -90,12 +90,38 @@ export function unhandledProviderFrameJournalItem(
|
||||
// Why: the opcode alone ("codex · notification:warning") tells the user nothing
|
||||
// and reads as protocol noise. Lead with the provider's own sentence when it has
|
||||
// one; the raw frame stays behind the row's disclosure either way.
|
||||
const message = readableProviderFrameText(payload)
|
||||
const method = kind.startsWith('notification:') ? kind.slice('notification:'.length) : kind
|
||||
const compaction =
|
||||
provider === 'codex' && (method === 'thread/compacted' || method === 'item:contextCompaction')
|
||||
const noticeTone =
|
||||
provider === 'codex'
|
||||
? method === 'deprecationNotice'
|
||||
? 'notice'
|
||||
: ['warning', 'guardianWarning', 'configWarning'].includes(method)
|
||||
? 'warning'
|
||||
: undefined
|
||||
: undefined
|
||||
const tone = noticeTone ?? (classification === 'error-surface' ? 'error' : undefined)
|
||||
let message = readableProviderFrameText(payload)
|
||||
if (
|
||||
provider === 'codex' &&
|
||||
(method === 'configWarning' || method === 'deprecationNotice') &&
|
||||
typeof payload === 'object' &&
|
||||
payload !== null
|
||||
) {
|
||||
const record = payload as Record<string, unknown>
|
||||
message =
|
||||
[record.summary, record.details]
|
||||
.filter((part): part is string => typeof part === 'string' && part.trim().length > 0)
|
||||
.join('\n\n') || message
|
||||
}
|
||||
const display = message ? boundInlineText(message, limits) : null
|
||||
return {
|
||||
body: {
|
||||
kind: 'status',
|
||||
text: display?.text ?? `${provider} · ${kind}`,
|
||||
text: compaction ? 'Context compacted' : (display?.text ?? `${provider} · ${kind}`),
|
||||
...(compaction ? { presentation: 'compaction' } : {}),
|
||||
...(tone ? { tone } : {}),
|
||||
providerFrame: { provider, kind, payload: bounded }
|
||||
},
|
||||
classification: classification === 'error-surface' ? 'error-surface' : 'timeline-substantive'
|
||||
|
||||
@@ -30,6 +30,7 @@ export function migrate(this: OrchestrationDb): void {
|
||||
migrateV37.call(this, current)
|
||||
migrateV38.call(this, current)
|
||||
migrateV39.call(this, current)
|
||||
this.createMailboxDeliveryIndexesIfPossible()
|
||||
this.db.pragma(`user_version = ${SCHEMA_VERSION}`)
|
||||
this.db.exec('COMMIT')
|
||||
} catch (err) {
|
||||
|
||||
@@ -27,6 +27,13 @@ export function createMailboxDeliveryIndexesIfPossible(this: OrchestrationDb): v
|
||||
ON messages(to_handle, sequence)
|
||||
WHERE read = 0 AND pointer_enter_pending > 0;
|
||||
`)
|
||||
if (this.hasColumn('messages', 'pointer_pty_id')) {
|
||||
// Working-title frames release one PTY's reservations, including already-read rows.
|
||||
this.db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_pending_pointer_pty
|
||||
ON messages(pointer_pty_id) WHERE pointer_enter_pending > 0;
|
||||
`)
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import { mkdtempSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import Database from '../../sqlite/sync-database'
|
||||
import { OrchestrationDb } from './db'
|
||||
|
||||
function assertTargetedRelease(db: OrchestrationDb): void {
|
||||
const prepare = vi.spyOn(db.db, 'prepare')
|
||||
db.releasePendingMailboxPointerForPty('absent-pty')
|
||||
const sql = prepare.mock.calls.find(([query]) => query.startsWith('UPDATE messages'))?.[0]
|
||||
prepare.mockRestore()
|
||||
expect(sql).toBeDefined()
|
||||
const plan = db.db.prepare(`EXPLAIN QUERY PLAN ${sql}`).all(1, 'absent-pty') as {
|
||||
detail: string
|
||||
}[]
|
||||
expect(plan.some(({ detail }) => detail.includes('SCAN messages'))).toBe(false)
|
||||
expect(plan.some(({ detail }) => detail.includes('idx_messages_pending_pointer_pty'))).toBe(true)
|
||||
}
|
||||
|
||||
describe('PTY mailbox reservation cleanup', () => {
|
||||
it('uses a PTY lookup and preserves reservation settlement semantics', () => {
|
||||
const db = new OrchestrationDb(':memory:')
|
||||
try {
|
||||
const seed = db.db.prepare(`INSERT INTO messages
|
||||
(id, from_handle, to_handle, subject, read, pointer_enter_pending,
|
||||
pointer_pty_id, pointer_process_incarnation, delivered_at)
|
||||
VALUES (?, 'sender', 'recipient', 'test', ?, ?, ?, 'incarnation', ?)`)
|
||||
db.db.exec('BEGIN')
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
seed.run(`history-${i}`, 1, 0, null, null)
|
||||
}
|
||||
seed.run('reserved', 0, 1, 'target', '2026-01-01 00:00:00')
|
||||
seed.run('written', 0, 2, 'target', null)
|
||||
seed.run('entered', 0, 3, 'target', null)
|
||||
seed.run('read', 1, 2, 'target', '2026-01-01 00:00:00')
|
||||
seed.run('other', 0, 1, 'other-pty', null)
|
||||
db.db.exec('COMMIT')
|
||||
assertTargetedRelease(db)
|
||||
db.releasePendingMailboxPointerForPty('target')
|
||||
const read = (id: string) => db.db.prepare('SELECT * FROM messages WHERE id = ?').get(id)
|
||||
for (const id of ['reserved', 'written', 'entered', 'read']) {
|
||||
expect(read(id)).toMatchObject({
|
||||
pointer_enter_pending: 0,
|
||||
pointer_pty_id: null,
|
||||
pointer_process_incarnation: null
|
||||
})
|
||||
}
|
||||
expect(read('reserved')).toMatchObject({ delivered_at: null, read: 0 })
|
||||
expect(read('written')).toMatchObject({ delivered_at: expect.any(String), read: 0 })
|
||||
expect(read('entered')).toMatchObject({ delivered_at: expect.any(String), read: 0 })
|
||||
expect(read('read')).toMatchObject({ delivered_at: '2026-01-01 00:00:00', read: 1 })
|
||||
expect(read('other')).toMatchObject({ pointer_enter_pending: 1, pointer_pty_id: 'other-pty' })
|
||||
expect(read('history-0')).toMatchObject({ read: 1, delivered_at: null })
|
||||
} finally {
|
||||
db.close()
|
||||
}
|
||||
})
|
||||
|
||||
it.each(['current', 'before-pointer-columns'])(
|
||||
'installs the lookup on first open of an existing %s database',
|
||||
(version) => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'orca-pointer-release-'))
|
||||
const path = join(dir, 'orchestration.db')
|
||||
try {
|
||||
new OrchestrationDb(path).close()
|
||||
const raw = new Database(path)
|
||||
try {
|
||||
raw.exec('DROP INDEX idx_messages_pending_pointer_pty')
|
||||
if (version === 'before-pointer-columns') {
|
||||
raw.exec(`DROP INDEX idx_messages_pending_pointer_enter;
|
||||
ALTER TABLE messages DROP COLUMN pointer_enter_pending;
|
||||
ALTER TABLE messages DROP COLUMN pointer_pty_id;
|
||||
ALTER TABLE messages DROP COLUMN pointer_process_incarnation;`)
|
||||
raw.pragma('user_version = 32')
|
||||
}
|
||||
} finally {
|
||||
raw.close()
|
||||
}
|
||||
const reopened = new OrchestrationDb(path)
|
||||
try {
|
||||
assertTargetedRelease(reopened)
|
||||
} finally {
|
||||
reopened.close()
|
||||
}
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -431,7 +431,9 @@ describe('OrchestrationDb version-skew migration', () => {
|
||||
|
||||
const raw = new Database(dbPath)
|
||||
raw.exec(
|
||||
'DROP INDEX IF EXISTS idx_messages_pending_pointer_enter; ALTER TABLE messages DROP COLUMN pointer_enter_pending;'
|
||||
`DROP INDEX IF EXISTS idx_messages_pending_pointer_enter;
|
||||
DROP INDEX IF EXISTS idx_messages_pending_pointer_pty;
|
||||
ALTER TABLE messages DROP COLUMN pointer_enter_pending;`
|
||||
)
|
||||
raw.pragma('user_version = 33')
|
||||
expect(resolveOrchestrationMigrationStartVersion(raw, 33, SCHEMA_VERSION)).toBe(6)
|
||||
|
||||
@@ -200,7 +200,7 @@ describe('NativeChatMessageList assistant messages', () => {
|
||||
/>
|
||||
)
|
||||
|
||||
const settledTool = screen.getByText('shell pnpm test')
|
||||
const settledTool = screen.getByText('shell')
|
||||
const activity = screen.getByText('Working…')
|
||||
expect(activity.textContent).not.toBe(settledTool.textContent)
|
||||
expect(activity).not.toHaveTextContent('shell')
|
||||
@@ -243,7 +243,8 @@ describe('NativeChatMessageList assistant messages', () => {
|
||||
/>
|
||||
)
|
||||
|
||||
const settledTool = screen.getByText('shell pnpm test')
|
||||
const settledTool = screen.getByText('shell')
|
||||
expect(settledTool).toHaveTextContent('shell pnpm test')
|
||||
expect(settledTool.closest('button')?.querySelector('.animate-pulse')).toBeNull()
|
||||
expect(settledTool.closest('button')?.querySelector('.lucide-check')).toBeInTheDocument()
|
||||
const activity = screen.getByText('Preparing the answer')
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { isSubagentGroupBlock, type NativeChatMessage } from '../../../../shared/native-chat-types'
|
||||
import { splitNativeChatBlocks } from './native-chat-tool-fold'
|
||||
import { NativeChatToolRun } from './NativeChatToolRun'
|
||||
import { NativeChatNoticeRow } from './NativeChatNoticeRow'
|
||||
import { NativeChatMessageTimestamp } from './NativeChatMessageTimestamp'
|
||||
import { nativeChatProseToMarkdown } from './native-chat-prose'
|
||||
import {
|
||||
@@ -94,6 +95,24 @@ export const MessageRow = memo(function MessageRow({
|
||||
return null
|
||||
}
|
||||
|
||||
const notice = isSystem
|
||||
? message.blocks.find(
|
||||
(block) =>
|
||||
block.type === 'text' && (block.presentation !== undefined || block.tone !== undefined)
|
||||
)
|
||||
: undefined
|
||||
if (notice?.type === 'text') {
|
||||
return (
|
||||
<div ref={rowRef}>
|
||||
<NativeChatNoticeRow
|
||||
block={notice}
|
||||
onLinkClick={onLinkClick}
|
||||
allowFileUriLinks={allowFileUriLinks}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (providerFrame) {
|
||||
return (
|
||||
<div ref={rowRef}>
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
// @vitest-environment happy-dom
|
||||
import '@testing-library/jest-dom/vitest'
|
||||
import { cleanup, render, screen } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { AgentJournalItemBodySchema } from '../../../../shared/agent-session-journal-schemas'
|
||||
import { projectStructuredItemsToNativeChat } from '../../../../shared/structured-agent-session-projection'
|
||||
import type { AgentJournalStatusItem } from '../../../../shared/agent-session-journal-types'
|
||||
import { MessageRow } from './NativeChatMessageRow'
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
function renderStatus(body: AgentJournalStatusItem) {
|
||||
const [message] = projectStructuredItemsToNativeChat([
|
||||
{ itemId: 'notice', sequence: 1, revision: 1, observedAt: 1, body }
|
||||
])
|
||||
return render(
|
||||
<MessageRow message={message!} expandSignal={false} onScrollMessageToTop={vi.fn()} />
|
||||
)
|
||||
}
|
||||
|
||||
describe('notice rows', () => {
|
||||
it('renders compaction as a centered separator', () => {
|
||||
renderStatus({ kind: 'status', text: 'Context compacted', presentation: 'compaction' })
|
||||
expect(screen.getByRole('separator', { name: 'Context compacted' })).toHaveClass(
|
||||
'text-muted-foreground'
|
||||
)
|
||||
expect(
|
||||
screen.getByText('Context compacted').parentElement?.querySelectorAll('.bg-border')
|
||||
).toHaveLength(2)
|
||||
})
|
||||
it.each([
|
||||
['warning', 'text-[color:var(--warning,#f59e0b)]'],
|
||||
['error', 'text-destructive'],
|
||||
['notice', 'text-muted-foreground']
|
||||
])('renders %s using its existing color treatment', (tone, className) => {
|
||||
renderStatus({ kind: 'status', text: 'Readable notice', tone })
|
||||
expect(screen.getByText('Readable notice').parentElement?.parentElement).toHaveClass(className)
|
||||
})
|
||||
it('renders a plan as readable markdown in the card primitive', () => {
|
||||
renderStatus({
|
||||
kind: 'status',
|
||||
text: '# Steps\n\nA **readable** document.',
|
||||
presentation: 'plan-document'
|
||||
})
|
||||
expect(screen.getByText('Plan').closest('[data-slot="card"]')).toBeInTheDocument()
|
||||
expect(screen.getByRole('heading', { name: 'Steps' })).toBeInTheDocument()
|
||||
expect(screen.getByText('readable').tagName).toBe('STRONG')
|
||||
expect(screen.getByText('readable').closest('[data-slot="card-content"]')).toHaveClass(
|
||||
'text-sm',
|
||||
'text-foreground'
|
||||
)
|
||||
})
|
||||
it('shows provider notice text once while retaining its diagnostic disclosure', () => {
|
||||
renderStatus({
|
||||
kind: 'status',
|
||||
text: 'Check the configuration',
|
||||
tone: 'warning',
|
||||
providerFrame: {
|
||||
provider: 'codex',
|
||||
kind: 'notification:warning',
|
||||
payload: {
|
||||
head: '{"message":"Check the configuration"}',
|
||||
byteLength: 37,
|
||||
digest: 'digest',
|
||||
truncated: false
|
||||
}
|
||||
}
|
||||
})
|
||||
expect(screen.getAllByText('Check the configuration')).toHaveLength(1)
|
||||
const disclosure = screen.getByText('Details').closest('details')
|
||||
expect(disclosure?.querySelector('summary')).not.toHaveTextContent('Check the configuration')
|
||||
expect(disclosure?.querySelector('pre')).toHaveTextContent('Check the configuration')
|
||||
})
|
||||
it('renders future presentation and tone values as untinted text', () => {
|
||||
renderStatus({
|
||||
kind: 'status',
|
||||
text: 'Future readable text',
|
||||
tone: 'future-tone',
|
||||
presentation: 'future-presentation'
|
||||
})
|
||||
expect(screen.getByText('Future readable text').parentElement?.parentElement).toHaveClass(
|
||||
'text-foreground'
|
||||
)
|
||||
expect(screen.getByText('Future readable text').parentElement?.querySelector('svg')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('old-reader compatibility', () => {
|
||||
// Derive the prior status shape without its new optional hints.
|
||||
const statusSchema = AgentJournalItemBodySchema.options.find(
|
||||
(schema): schema is (typeof AgentJournalItemBodySchema.options)[5] =>
|
||||
schema.shape.kind.value === 'status'
|
||||
)!
|
||||
const oldStatusSchema = statusSchema.omit({ tone: true, presentation: true })
|
||||
it.each([
|
||||
{ presentation: 'compaction' },
|
||||
{ presentation: 'plan-document' },
|
||||
{ tone: 'warning' },
|
||||
{ tone: 'error' },
|
||||
{ tone: 'notice' },
|
||||
{ tone: 'future-tone', presentation: 'future-presentation' }
|
||||
])('accepts new metadata and still renders text with an old reader: %j', (metadata) => {
|
||||
const body = { kind: 'status', text: 'Text survives version skew', ...metadata }
|
||||
expect(AgentJournalItemBodySchema.safeParse(body).success).toBe(true)
|
||||
const oldBody = oldStatusSchema.parse(body) as AgentJournalStatusItem
|
||||
expect(oldBody).toEqual({ kind: 'status', text: body.text })
|
||||
renderStatus(oldBody)
|
||||
expect(screen.getByText(body.text)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,86 @@
|
||||
import { AlertCircle, AlertTriangle, Info } from 'lucide-react'
|
||||
import CommentMarkdown, {
|
||||
type CommentMarkdownLinkClickHandler
|
||||
} from '@/components/sidebar/CommentMarkdown'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { NativeChatTextBlock } from '../../../../shared/native-chat-types'
|
||||
import { ProviderFrameRow } from './NativeChatTranscriptChrome'
|
||||
|
||||
export function NativeChatNoticeRow({
|
||||
block,
|
||||
onLinkClick,
|
||||
allowFileUriLinks = false
|
||||
}: {
|
||||
block: NativeChatTextBlock
|
||||
onLinkClick?: CommentMarkdownLinkClickHandler
|
||||
allowFileUriLinks?: boolean
|
||||
}): React.JSX.Element {
|
||||
if (block.presentation === 'compaction') {
|
||||
const label = translate('components.native-chat.notices.compaction', 'Context compacted')
|
||||
return (
|
||||
<div
|
||||
role="separator"
|
||||
aria-label={label}
|
||||
className="flex items-center gap-3 py-2 text-xs text-muted-foreground"
|
||||
>
|
||||
<span className="h-px flex-1 bg-border" />
|
||||
<span>{label}</span>
|
||||
<span className="h-px flex-1 bg-border" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (block.presentation === 'plan-document') {
|
||||
return (
|
||||
<Card className="gap-3 py-3 shadow-xs">
|
||||
<CardHeader className="px-4">
|
||||
<CardTitle className="text-sm">
|
||||
{translate('components.native-chat.notices.plan', 'Plan')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="px-4 text-sm leading-relaxed text-foreground">
|
||||
<CommentMarkdown
|
||||
content={block.text}
|
||||
variant="document"
|
||||
className="text-sm"
|
||||
onLinkClick={onLinkClick}
|
||||
allowFileUriLinks={allowFileUriLinks}
|
||||
linkifyFilePaths={onLinkClick !== undefined}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
const tone = block.tone
|
||||
const Icon =
|
||||
tone === 'warning'
|
||||
? AlertTriangle
|
||||
: tone === 'error'
|
||||
? AlertCircle
|
||||
: tone === 'notice'
|
||||
? Info
|
||||
: null
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'space-y-2 text-sm text-foreground',
|
||||
Icon && 'rounded-md border border-border bg-muted/20 p-3',
|
||||
tone === 'warning' && 'text-[color:var(--warning,#f59e0b)]',
|
||||
tone === 'error' && 'text-destructive',
|
||||
tone === 'notice' && 'text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
{Icon ? <Icon aria-hidden="true" className="mt-0.5 size-4 shrink-0" /> : null}
|
||||
<p className="min-w-0 whitespace-pre-wrap break-words">{block.text}</p>
|
||||
</div>
|
||||
{block.providerFrame ? (
|
||||
<ProviderFrameRow
|
||||
block={block}
|
||||
summary={translate('components.native-chat.notices.details', 'Details')}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -299,7 +299,7 @@ describe('NativeChatToolRun with a spawn group', () => {
|
||||
)
|
||||
|
||||
expect(screen.getByText('Ran 1 subagent')).toBeInTheDocument()
|
||||
expect(screen.queryByText('shell ls')).toBeNull()
|
||||
expect(screen.queryByText('shell')).toBeNull()
|
||||
})
|
||||
|
||||
it('renders the roster alongside the tool activity of its turn', () => {
|
||||
@@ -313,6 +313,6 @@ describe('NativeChatToolRun with a spawn group', () => {
|
||||
)
|
||||
|
||||
expect(screen.getByText('Ran 1 subagent')).toBeInTheDocument()
|
||||
expect(screen.getByText('shell ls')).toBeInTheDocument()
|
||||
expect(screen.getByText('shell').closest('button')).toHaveTextContent('shell ls')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -70,7 +70,7 @@ describe('inline tool annotations', () => {
|
||||
/>
|
||||
)
|
||||
expect(screen.queryByRole('link')).toBeNull()
|
||||
fireEvent.click(screen.getByText('web_search').closest('button')!)
|
||||
fireEvent.click(screen.getByText('web_search', { selector: 'code' }).closest('button')!)
|
||||
const link = screen.getByRole('link', { name: /Reference docs/ })
|
||||
expect(link.getAttribute('href')).toBe('https://example.com/docs')
|
||||
expect(link.closest('button')).toBeNull()
|
||||
@@ -112,7 +112,10 @@ it.each(['running', 'completed'] as const)(
|
||||
expect(screen.getByText('My server')).toBeTruthy()
|
||||
expect(screen.getByText('ns.tool')).toBeTruthy()
|
||||
expect(screen.getByTitle(name)).toBeTruthy()
|
||||
expect(document.querySelectorAll('.lucide-plug')).toHaveLength(2)
|
||||
// Header glyph plus the row's. A settled header also names each member in a
|
||||
// pill, which carries that member's own glyph — so three, all plug: the
|
||||
// identity holds wherever it is drawn.
|
||||
expect(document.querySelectorAll('.lucide-plug')).toHaveLength(state === 'completed' ? 3 : 2)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -23,6 +23,16 @@ function leadingGlyphs(container: HTMLElement): (string | null)[] {
|
||||
)
|
||||
}
|
||||
|
||||
/** The run header — the first button in a run, above its member rows. Its
|
||||
* members render as separate pills, so it has no single joined summary node. */
|
||||
function runHeader(container: HTMLElement): HTMLElement {
|
||||
const header = container.querySelector('button')
|
||||
if (!header) {
|
||||
throw new Error('run header did not render')
|
||||
}
|
||||
return header
|
||||
}
|
||||
|
||||
describe('NativeChatToolRun', () => {
|
||||
it('uses the shared clean label for a desktop tool row', () => {
|
||||
const blocks: NativeChatBlock[] = [
|
||||
@@ -244,6 +254,107 @@ describe('NativeChatToolRun', () => {
|
||||
expect(writeClipboardText).toHaveBeenCalledWith(' ctx\n-was\n+now\n+tail')
|
||||
})
|
||||
|
||||
describe('reading a batch as a group', () => {
|
||||
const batch: NativeChatBlock[] = [
|
||||
{
|
||||
type: 'tool-call',
|
||||
name: 'mcp__linear__list_issues',
|
||||
input: { query: 'todo' },
|
||||
state: 'completed',
|
||||
mcpIdentity: { server: 'linear', tool: 'list_issues' }
|
||||
},
|
||||
{ type: 'tool-call', name: 'Bash', input: { command: 'ls -la' }, state: 'completed' },
|
||||
{
|
||||
type: 'tool-call',
|
||||
name: 'tools/read',
|
||||
input: { file_path: 'README.md' },
|
||||
state: 'completed'
|
||||
}
|
||||
]
|
||||
|
||||
it('gives each member its own glyph-led segment instead of one joined string', () => {
|
||||
const { container } = render(<NativeChatToolRun blocks={batch} expandSignal={false} />)
|
||||
|
||||
const pills = runHeader(container).querySelectorAll('[data-tool-run-member]')
|
||||
expect([...pills].map((pill) => pill.textContent)).toEqual([
|
||||
'mcp__linear__list_issues todo',
|
||||
'Bash ls -la',
|
||||
'tools/read README.md'
|
||||
])
|
||||
})
|
||||
|
||||
// The header still prints the raw identifier while the row beneath it prints
|
||||
// the split MCP name. Pinned, not endorsed: reconciling the two changes what
|
||||
// a tool is called, which is a naming decision rather than a layout one.
|
||||
it('leaves the header naming a member differently from the row below it', () => {
|
||||
const { container } = render(<NativeChatToolRun blocks={batch} expandSignal />)
|
||||
|
||||
expect(runHeader(container)).toHaveTextContent('mcp__linear__list_issues')
|
||||
expect(screen.getByText('Linear')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('names each member with its own glyph, not the run-wide fallback', () => {
|
||||
const { container } = render(<NativeChatToolRun blocks={batch} expandSignal={false} />)
|
||||
|
||||
const header = runHeader(container)
|
||||
expect(header.querySelector('[data-tool-run-member] .lucide-plug')).toBeInTheDocument()
|
||||
expect(
|
||||
header.querySelector('[data-tool-run-member] .lucide-square-terminal')
|
||||
).toBeInTheDocument()
|
||||
// The run-wide glyph still reads generic, the categories being mixed.
|
||||
expect(header.firstElementChild?.querySelector('.lucide-wrench')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('counts the members it could not show rather than ending mid-name', () => {
|
||||
const wide: NativeChatBlock[] = [
|
||||
...batch,
|
||||
{ type: 'tool-call', name: 'Grep', input: { pattern: 'todo' }, state: 'completed' },
|
||||
{ type: 'tool-call', name: 'Write', input: { file_path: 'a.ts' }, state: 'completed' }
|
||||
]
|
||||
|
||||
const { container } = render(<NativeChatToolRun blocks={wide} expandSignal={false} />)
|
||||
|
||||
expect(runHeader(container)).toHaveTextContent('+2 more')
|
||||
expect(runHeader(container).querySelectorAll('[data-tool-run-member]')).toHaveLength(3)
|
||||
})
|
||||
|
||||
// A margin is invisible to a copied selection and to the accessible name, so
|
||||
// the boundary needs a real space too — otherwise the header reads
|
||||
// `ls -latools/read`.
|
||||
it('separates members with real whitespace, not only a margin', () => {
|
||||
const { container } = render(<NativeChatToolRun blocks={batch} expandSignal={false} />)
|
||||
|
||||
expect(runHeader(container).textContent).toBe(
|
||||
'3\u00d7mcp__linear__list_issues todo Bash ls -la tools/read README.md'
|
||||
)
|
||||
})
|
||||
|
||||
it('leaves no remainder marker when every member is shown', () => {
|
||||
const { container } = render(<NativeChatToolRun blocks={batch} expandSignal={false} />)
|
||||
|
||||
expect(runHeader(container)).not.toHaveTextContent('more')
|
||||
})
|
||||
|
||||
it('indents opened members so the run has a visible end', () => {
|
||||
const { container } = render(<NativeChatToolRun blocks={batch} expandSignal />)
|
||||
|
||||
const members = runHeader(container).parentElement?.querySelector('.pl-4')
|
||||
expect(members).toBeInTheDocument()
|
||||
expect(members?.querySelectorAll('button').length).toBe(batch.length)
|
||||
})
|
||||
|
||||
it('falls back to the call count when a run names no tool', () => {
|
||||
const { container } = render(
|
||||
<NativeChatToolRun
|
||||
blocks={[{ type: 'tool-call', name: ' ', input: {}, state: 'completed' }]}
|
||||
expandSignal={false}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(runHeader(container)).toHaveTextContent('1 tool call')
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps a grouped active run to one stable row showing only the latest tool', () => {
|
||||
const blocks: NativeChatBlock[] = [
|
||||
{ type: 'tool-call', name: 'shell', input: { command: 'date' }, state: 'completed' },
|
||||
@@ -293,7 +404,9 @@ describe('NativeChatToolRun', () => {
|
||||
const runningBlocks: NativeChatBlock[] = [
|
||||
{ type: 'tool-call', name: 'shell', input: { command: 'sleep 1' }, state: 'running' }
|
||||
]
|
||||
const { rerender } = render(<NativeChatToolRun blocks={runningBlocks} expandSignal={false} />)
|
||||
const { rerender, container } = render(
|
||||
<NativeChatToolRun blocks={runningBlocks} expandSignal={false} />
|
||||
)
|
||||
|
||||
expect(screen.getByText('Running sleep 1')).toBeInTheDocument()
|
||||
|
||||
@@ -308,7 +421,7 @@ describe('NativeChatToolRun', () => {
|
||||
)
|
||||
|
||||
expect(screen.queryByText('Running sleep 1')).toBeNull()
|
||||
expect(screen.getByText('shell sleep 1')).toBeInTheDocument()
|
||||
expect(runHeader(container)).toHaveTextContent('shell sleep 1')
|
||||
})
|
||||
|
||||
it('never animates a settled tool row with its completion check', () => {
|
||||
@@ -323,9 +436,10 @@ describe('NativeChatToolRun', () => {
|
||||
/>
|
||||
)
|
||||
|
||||
const settledRow = screen.getByText('shell pnpm test').closest('button')
|
||||
expect(settledRow?.querySelector('.lucide-check')).toBeInTheDocument()
|
||||
expect(settledRow?.querySelector('.animate-pulse')).toBeNull()
|
||||
const settledRow = runHeader(container)
|
||||
expect(settledRow).toHaveTextContent('shell pnpm test')
|
||||
expect(settledRow.querySelector('.lucide-check')).toBeInTheDocument()
|
||||
expect(settledRow.querySelector('.animate-pulse')).toBeNull()
|
||||
expect(container.querySelector('.animate-pulse')).toBeNull()
|
||||
})
|
||||
|
||||
@@ -348,7 +462,7 @@ describe('NativeChatToolRun', () => {
|
||||
{ type: 'tool-result', output: 'exit 128', isError: true }
|
||||
]
|
||||
|
||||
const { rerender } = render(
|
||||
const { rerender, container } = render(
|
||||
<NativeChatToolRun
|
||||
blocks={blocks}
|
||||
expandSignal={false}
|
||||
@@ -369,7 +483,7 @@ describe('NativeChatToolRun', () => {
|
||||
/>
|
||||
)
|
||||
|
||||
expect(screen.getByText('shell git log -1')).toBeInTheDocument()
|
||||
expect(runHeader(container)).toHaveTextContent('shell git log -1')
|
||||
})
|
||||
|
||||
it('settles an orphaned running call when its turn lifecycle has ended', () => {
|
||||
@@ -401,7 +515,7 @@ describe('NativeChatToolRun', () => {
|
||||
const glyph = container.querySelector('.lucide-eye')
|
||||
expect(glyph).toBeInTheDocument()
|
||||
expect(glyph).toHaveAttribute('aria-hidden')
|
||||
expect(screen.getByText('read')).toBeInTheDocument()
|
||||
expect(screen.getByText('read', { selector: 'code' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('holds one glyph for a category across running, completed, and failed', () => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
NativeChatCommandMetadata,
|
||||
NativeChatSearchResults
|
||||
} from './NativeChatToolAnnotations'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Fragment, useEffect, useMemo, useState } from 'react'
|
||||
import { Check, ChevronRight } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
@@ -26,8 +26,9 @@ import type { NativeChatEditFile } from '../../../../shared/native-chat-edit-mod
|
||||
import {
|
||||
countToolCalls,
|
||||
createToolInputDisplay,
|
||||
summarizeToolRun,
|
||||
truncateToolDetail
|
||||
toolRunSummaryMembers,
|
||||
truncateToolDetail,
|
||||
type ToolRunMember
|
||||
} from './native-chat-tool-summary'
|
||||
import {
|
||||
NATIVE_CHAT_TOOL_ACTIVITY_COPY,
|
||||
@@ -237,7 +238,22 @@ export function NativeChatToolRun({
|
||||
.filter(isRenderableSubagentGroup)
|
||||
.map((group) => <NativeChatSubagentRun key={group.groupId} block={group} />)
|
||||
const callCount = countToolCalls(blocks) || blocks.length
|
||||
const summary = summarizeToolRun(blocks)
|
||||
// Members stay separate all the way to the markup: joining them into one
|
||||
// string is what made a run read as a single call, because the separator also
|
||||
// occurs inside tool names like `browser.open` and `tools/read`.
|
||||
const summaryMembers = toolRunSummaryMembers(blocks)
|
||||
const hiddenCallCount = Math.max(0, callCount - summaryMembers.length)
|
||||
// Same content-signature keying the member rows below use: two identical calls
|
||||
// in one run are distinguished by occurrence, never by list position.
|
||||
const keyedSummaryMembers = ((): (ToolRunMember & { key: string })[] => {
|
||||
const seen = new Map<string, number>()
|
||||
return summaryMembers.map((member) => {
|
||||
const signature = `${member.name}:${member.arg}`
|
||||
const occurrence = seen.get(signature) ?? 0
|
||||
seen.set(signature, occurrence + 1)
|
||||
return { ...member, key: `${signature}:${occurrence}` }
|
||||
})
|
||||
})()
|
||||
const latestActiveCall = structuredActivityUi
|
||||
? selectActiveToolCall(blocks, { activeTurnIsWorking })
|
||||
: null
|
||||
@@ -251,7 +267,7 @@ export function NativeChatToolRun({
|
||||
() => (open ? buildEditCards(blocks) : NO_EDIT_CARDS),
|
||||
[open, blocks]
|
||||
)
|
||||
// Only the settled header reads this. It stands over `summary`, which speaks
|
||||
// Only the settled header reads this. It stands over `summaryMembers`, which speaks
|
||||
// for the run's first calls rather than its last, so a glyph taken from one
|
||||
// call would assert a category the text beside it doesn't describe. A run that
|
||||
// spans categories therefore heads with the generic tool glyph. The glyph is
|
||||
@@ -329,9 +345,52 @@ export function NativeChatToolRun({
|
||||
<span className="shrink-0 font-mono text-[11px] font-bold text-muted-foreground transition-colors group-hover:text-foreground/80">
|
||||
{callCount}×
|
||||
</span>
|
||||
<span className="min-w-0 truncate font-mono text-[11px] text-muted-foreground transition-colors group-hover:text-foreground/80">
|
||||
{summary || fallbackLabel}
|
||||
</span>
|
||||
{summaryMembers.length > 0 ? (
|
||||
<>
|
||||
{/* Each member is led by its own category glyph, which is what marks
|
||||
the boundary. A separator character cannot: `·` occurs inside
|
||||
`browser.open` and `tools/read`. The list stays one line and
|
||||
truncates as a whole rather than wrapping into a block — a
|
||||
header that grows to three rows stops reading as a header. */}
|
||||
<span className="min-w-0 truncate font-mono text-[11px] text-muted-foreground transition-colors group-hover:text-foreground/80">
|
||||
{keyedSummaryMembers.map((member, index) => (
|
||||
<Fragment key={member.key}>
|
||||
{/* A real space, not just the margin: a CSS gap is invisible to
|
||||
a copied selection and to the button's accessible name, which
|
||||
would otherwise run one member's argument into the next
|
||||
member's name. The margin is trimmed to pay for its width. */}
|
||||
{index > 0 ? ' ' : null}
|
||||
<span data-tool-run-member className={cn(index > 0 && 'ml-2')}>
|
||||
<NativeChatToolIcon
|
||||
rowWord={member.name}
|
||||
mcpIdentity={member.mcpIdentity}
|
||||
className="mr-1 inline-flex size-3.5 align-middle"
|
||||
/>
|
||||
{member.name}
|
||||
{member.arg ? (
|
||||
<span className="text-muted-foreground/70">{` ${member.arg}`}</span>
|
||||
) : null}
|
||||
</span>
|
||||
</Fragment>
|
||||
))}
|
||||
</span>
|
||||
{hiddenCallCount > 0 ? (
|
||||
/* Outside the truncating span, so the count of what is not shown
|
||||
survives a list the pane is too narrow to print. */
|
||||
<span className="shrink-0 font-mono text-[11px] text-muted-foreground transition-colors group-hover:text-foreground/80">
|
||||
{translate(
|
||||
'components.native-chat.tool.moreCalls',
|
||||
NATIVE_CHAT_TOOL_ACTIVITY_COPY.moreCalls,
|
||||
{ value0: hiddenCallCount }
|
||||
)}
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<span className="min-w-0 truncate font-mono text-[11px] text-muted-foreground transition-colors group-hover:text-foreground/80">
|
||||
{fallbackLabel}
|
||||
</span>
|
||||
)}
|
||||
{/* Completion reads as a trailing mark so the leading glyph can stay fixed. */}
|
||||
{structuredActivityUi ? (
|
||||
<Check aria-hidden className="size-3 shrink-0 text-muted-foreground" />
|
||||
@@ -346,7 +405,10 @@ export function NativeChatToolRun({
|
||||
</button>
|
||||
)}
|
||||
{open ? (
|
||||
<div className="mt-1">
|
||||
// Members are indented under the header because nothing else marks the
|
||||
// run's extent — flush rows are indistinguishable from the blocks after
|
||||
// them, so the batch has no visible end.
|
||||
<div className="mt-1 pl-4">
|
||||
{(() => {
|
||||
const seen = new Map<string, number>()
|
||||
return blocks.map((block) => {
|
||||
|
||||
@@ -274,7 +274,13 @@ export function NativeChatAgentControls({
|
||||
)
|
||||
}
|
||||
|
||||
export function ProviderFrameRow({ block }: { block: NativeChatBlock }): React.JSX.Element | null {
|
||||
export function ProviderFrameRow({
|
||||
block,
|
||||
summary
|
||||
}: {
|
||||
block: NativeChatBlock
|
||||
summary?: string
|
||||
}): React.JSX.Element | null {
|
||||
if (block.type !== 'text' || !block.providerFrame) {
|
||||
return null
|
||||
}
|
||||
@@ -284,7 +290,7 @@ export function ProviderFrameRow({ block }: { block: NativeChatBlock }): React.J
|
||||
<summary className="flex cursor-pointer list-none items-center gap-2 rounded-md px-2 py-1 font-mono hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
|
||||
<span className="transition-transform group-open:rotate-90">›</span>
|
||||
<span className="font-medium text-foreground">{frame.provider}</span>
|
||||
<span className="truncate">{nativeChatProviderFrameSummary(block)}</span>
|
||||
<span className="truncate">{summary ?? nativeChatProviderFrameSummary(block)}</span>
|
||||
{frame.payload.truncated ? (
|
||||
<span>
|
||||
·{' '}
|
||||
|
||||
@@ -6,5 +6,7 @@ export {
|
||||
summarizeToolInput,
|
||||
summarizeToolRun,
|
||||
toolFilePath,
|
||||
toolRunSummaryMembers,
|
||||
truncateToolDetail
|
||||
} from '../../../../shared/native-chat-tool-summary'
|
||||
export type { ToolRunMember } from '../../../../shared/native-chat-tool-summary'
|
||||
|
||||
@@ -236,7 +236,8 @@ const WorktreeList = React.memo(function WorktreeList({
|
||||
useSidebarRevealRequests({
|
||||
groupBy,
|
||||
renderedSidebarRowKeys: rowModel.renderedSidebarRowKeys,
|
||||
renderedWorktreeIdentities: selection.renderedWorktreeIdentities,
|
||||
visibleWorktrees,
|
||||
visibleFolderWorkspaces: visibleScope.visibleFolderWorkspacesForRows,
|
||||
currentSidebarWorktreeId,
|
||||
currentSidebarExecutionHostId: activeWorkspaceExecutionHostId,
|
||||
worktreeMap,
|
||||
|
||||
+80
-36
@@ -82,7 +82,8 @@ beforeEach(() => {
|
||||
args = {
|
||||
groupBy: 'repo',
|
||||
renderedSidebarRowKeys: new Set(),
|
||||
renderedWorktreeIdentities: [],
|
||||
visibleWorktrees: [],
|
||||
visibleFolderWorkspaces: [],
|
||||
currentSidebarWorktreeId: worktree.id,
|
||||
currentSidebarExecutionHostId: 'ssh:dev',
|
||||
worktreeMap: new Map([[worktree.id, worktree]]),
|
||||
@@ -133,7 +134,7 @@ describe('revealing a filtered workspace', () => {
|
||||
args = {
|
||||
...args,
|
||||
hasFilters: visible,
|
||||
renderedWorktreeIdentities: visible ? ['ssh:dev|wt-1'] : []
|
||||
visibleWorktrees: visible ? args.worktrees : []
|
||||
}
|
||||
await render()
|
||||
await act(async () => requestScrollToCurrentWorkspaceReveal())
|
||||
@@ -143,6 +144,41 @@ describe('revealing a filtered workspace', () => {
|
||||
}
|
||||
)
|
||||
|
||||
it.each(['ssh:dev', null] as const)(
|
||||
'reveals a collapsed workspace that passes filters with active host %s',
|
||||
async (executionHostId) => {
|
||||
args = {
|
||||
...args,
|
||||
currentSidebarExecutionHostId: executionHostId,
|
||||
visibleWorktrees: args.worktrees
|
||||
}
|
||||
await render()
|
||||
await act(async () => requestScrollToCurrentWorkspaceReveal())
|
||||
expect(document.querySelector('[role="dialog"]')).toBeNull()
|
||||
expect(args.clearFilters).not.toHaveBeenCalled()
|
||||
expect(state.revealWorktreeInSidebar).toHaveBeenCalledTimes(1)
|
||||
}
|
||||
)
|
||||
|
||||
it('does not let a visible same-id workspace on another host bypass confirmation', async () => {
|
||||
args = { ...args, visibleWorktrees: [{ ...args.worktrees[0], hostId: 'local' }] }
|
||||
await render()
|
||||
await act(async () => requestScrollToCurrentWorkspaceReveal())
|
||||
expect(document.querySelector('[role="dialog"]')).not.toBeNull()
|
||||
expect(state.revealWorktreeInSidebar).not.toHaveBeenCalled()
|
||||
await click('Keep filters')
|
||||
})
|
||||
|
||||
it('preserves filters when the target becomes included while confirmation is open', async () => {
|
||||
await render()
|
||||
await act(async () => requestScrollToCurrentWorkspaceReveal())
|
||||
args = { ...args, visibleWorktrees: args.worktrees }
|
||||
await render()
|
||||
await click('Clear filters and reveal')
|
||||
expect(args.clearFilters).not.toHaveBeenCalled()
|
||||
expect(state.revealWorktreeInSidebar).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('does not apply a stale confirmation after switching workspaces', async () => {
|
||||
await render()
|
||||
await act(async () => requestScrollToCurrentWorkspaceReveal())
|
||||
@@ -153,39 +189,47 @@ describe('revealing a filtered workspace', () => {
|
||||
expect(state.revealWorktreeInSidebar).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('confirms filtered folder workspaces and preserves the rename request', async () => {
|
||||
args = {
|
||||
...args,
|
||||
currentSidebarWorktreeId: folderWorkspaceKey('folder-1'),
|
||||
currentSidebarExecutionHostId: null,
|
||||
folderWorkspaces: [
|
||||
{
|
||||
id: 'folder-1',
|
||||
projectGroupId: 'project-1',
|
||||
name: 'Notes',
|
||||
folderPath: '/notes',
|
||||
linkedTask: null,
|
||||
comment: '',
|
||||
isArchived: false,
|
||||
isUnread: false,
|
||||
isPinned: false,
|
||||
sortOrder: 1,
|
||||
lastActivityAt: 1,
|
||||
createdAt: 1,
|
||||
updatedAt: 1
|
||||
}
|
||||
]
|
||||
it.each([true, false])(
|
||||
'reveals folder workspaces and preserves rename (filtered: %s)',
|
||||
async (filtered) => {
|
||||
args = {
|
||||
...args,
|
||||
currentSidebarWorktreeId: folderWorkspaceKey('folder-1'),
|
||||
currentSidebarExecutionHostId: null,
|
||||
folderWorkspaces: [
|
||||
{
|
||||
id: 'folder-1',
|
||||
projectGroupId: 'project-1',
|
||||
name: 'Notes',
|
||||
folderPath: '/notes',
|
||||
linkedTask: null,
|
||||
comment: '',
|
||||
isArchived: false,
|
||||
isUnread: false,
|
||||
isPinned: false,
|
||||
sortOrder: 1,
|
||||
lastActivityAt: 1,
|
||||
createdAt: 1,
|
||||
updatedAt: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
args.visibleFolderWorkspaces = filtered ? [] : args.folderWorkspaces
|
||||
await render()
|
||||
await act(async () => requestScrollToCurrentWorkspaceRevealAndRename())
|
||||
expect(args.clearFilters).not.toHaveBeenCalled()
|
||||
if (filtered) {
|
||||
await click('Clear filters and reveal')
|
||||
expect(args.clearFilters).toHaveBeenCalledTimes(1)
|
||||
} else {
|
||||
expect(document.querySelector('[role="dialog"]')).toBeNull()
|
||||
}
|
||||
expect(state.revealWorktreeInSidebar).toHaveBeenCalledWith(folderWorkspaceKey('folder-1'), {
|
||||
behavior: 'smooth',
|
||||
highlight: true,
|
||||
beginRename: true,
|
||||
executionHostId: undefined
|
||||
})
|
||||
}
|
||||
await render()
|
||||
await act(async () => requestScrollToCurrentWorkspaceRevealAndRename())
|
||||
expect(args.clearFilters).not.toHaveBeenCalled()
|
||||
await click('Clear filters and reveal')
|
||||
expect(args.clearFilters).toHaveBeenCalledTimes(1)
|
||||
expect(state.revealWorktreeInSidebar).toHaveBeenCalledWith(folderWorkspaceKey('folder-1'), {
|
||||
behavior: 'smooth',
|
||||
highlight: true,
|
||||
beginRename: true,
|
||||
executionHostId: undefined
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
+33
-10
@@ -10,16 +10,30 @@ import {
|
||||
import type { FolderWorkspace } from '../../../../../../shared/folder-workspace-types'
|
||||
import type { Worktree } from '../../../../../../shared/worktree/types'
|
||||
import type { ExecutionHostId } from '../../../../../../shared/execution-host'
|
||||
import { composeWorktreeHostIdentity } from '../../../../../../shared/worktree/host-qualified-identity'
|
||||
import { getWorktreeHostIdentity } from '../../../../../../shared/worktree/host-qualified-identity'
|
||||
import { folderWorkspaceKey } from '../../../../../../shared/workspace-scope'
|
||||
import type { WorktreeGroupBy } from '../grouping/row-types'
|
||||
import { getKnownSidebarWorktreeById } from './folder-reveal'
|
||||
|
||||
function workspacePassesFilters(
|
||||
worktree: Worktree,
|
||||
worktrees: readonly Worktree[],
|
||||
folderWorkspaces: readonly FolderWorkspace[]
|
||||
): boolean {
|
||||
const identity = getWorktreeHostIdentity(worktree)
|
||||
return (
|
||||
worktrees.some((candidate) => getWorktreeHostIdentity(candidate) === identity) ||
|
||||
folderWorkspaces.some((workspace) => folderWorkspaceKey(workspace.id) === worktree.id)
|
||||
)
|
||||
}
|
||||
|
||||
// Turns a "show me the current workspace" request into whatever the sidebar must change
|
||||
// first — grouping mode, active filters — before the viewport can scroll to it.
|
||||
export function useSidebarRevealRequests(args: {
|
||||
groupBy: WorktreeGroupBy
|
||||
renderedSidebarRowKeys: ReadonlySet<string>
|
||||
renderedWorktreeIdentities: readonly string[]
|
||||
visibleWorktrees: readonly Worktree[]
|
||||
visibleFolderWorkspaces: readonly FolderWorkspace[]
|
||||
currentSidebarWorktreeId: string | null
|
||||
currentSidebarExecutionHostId: ExecutionHostId | null
|
||||
worktreeMap: Map<string, Worktree>
|
||||
@@ -31,7 +45,8 @@ export function useSidebarRevealRequests(args: {
|
||||
const {
|
||||
groupBy,
|
||||
renderedSidebarRowKeys,
|
||||
renderedWorktreeIdentities,
|
||||
visibleWorktrees,
|
||||
visibleFolderWorkspaces,
|
||||
currentSidebarWorktreeId,
|
||||
currentSidebarExecutionHostId,
|
||||
worktreeMap,
|
||||
@@ -106,11 +121,11 @@ export function useSidebarRevealRequests(args: {
|
||||
if (!activeWorktree || activeWorktree.isArchived) {
|
||||
return
|
||||
}
|
||||
const currentIdentity = composeWorktreeHostIdentity(
|
||||
currentSidebarExecutionHostId ?? undefined,
|
||||
currentSidebarWorktreeId
|
||||
)
|
||||
if (hasFilters && !renderedWorktreeIdentities.includes(currentIdentity)) {
|
||||
// Collapsed groups hide rows without excluding their workspaces from the filter results.
|
||||
if (
|
||||
hasFilters &&
|
||||
!workspacePassesFilters(activeWorktree, visibleWorktrees, visibleFolderWorkspaces)
|
||||
) {
|
||||
if (confirmationPending.current) {
|
||||
return
|
||||
}
|
||||
@@ -141,7 +156,14 @@ export function useSidebarRevealRequests(args: {
|
||||
) {
|
||||
return
|
||||
}
|
||||
if (latest.hasFilters && !latest.renderedWorktreeIdentities.includes(currentIdentity)) {
|
||||
if (
|
||||
latest.hasFilters &&
|
||||
!workspacePassesFilters(
|
||||
activeWorktree,
|
||||
latest.visibleWorktrees,
|
||||
latest.visibleFolderWorkspaces
|
||||
)
|
||||
) {
|
||||
latest.clearFilters()
|
||||
}
|
||||
}
|
||||
@@ -159,7 +181,8 @@ export function useSidebarRevealRequests(args: {
|
||||
currentSidebarExecutionHostId,
|
||||
folderWorkspaces,
|
||||
revealSidebarRow,
|
||||
renderedWorktreeIdentities,
|
||||
visibleWorktrees,
|
||||
visibleFolderWorkspaces,
|
||||
revealWorktreeInSidebar,
|
||||
worktreeMap,
|
||||
worktrees
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { SearchAddon } from '@xterm/addon-search'
|
||||
import { Terminal } from '@xterm/xterm'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
type Entry = { line: number; id: number }
|
||||
type SortedList = {
|
||||
insert(value: Entry): void
|
||||
delete(value: Entry): boolean
|
||||
clear(): void
|
||||
values(): IterableIterator<Entry>
|
||||
getKeyIterator(key: number): IterableIterator<Entry>
|
||||
_getKey(value: Entry): number
|
||||
}
|
||||
type Decoration = { marker: { line: number } }
|
||||
type Service = {
|
||||
_decorations: SortedList
|
||||
decorations: Iterable<Decoration>
|
||||
onDecorationRemoved(callback: () => void): { dispose(): void }
|
||||
}
|
||||
|
||||
const terminals: Terminal[] = []
|
||||
function setup(scrollback = 1000): { terminal: Terminal; service: Service } {
|
||||
const terminal = new Terminal({ allowProposedApi: true, scrollback, rows: 24 })
|
||||
terminals.push(terminal)
|
||||
const container = document.createElement('div')
|
||||
document.body.append(container)
|
||||
terminal.open(container)
|
||||
const service = (terminal as unknown as { _core: { _decorationService: Service } })._core
|
||||
._decorationService
|
||||
return { terminal, service }
|
||||
}
|
||||
|
||||
function list(): SortedList {
|
||||
const { service } = setup()
|
||||
const Constructor = service._decorations.constructor as new (
|
||||
key: (entry: Entry) => number,
|
||||
log: { warn(): void }
|
||||
) => SortedList
|
||||
return new Constructor((entry) => entry?.line, { warn() {} })
|
||||
}
|
||||
|
||||
function write(terminal: Terminal, data: string): Promise<void> {
|
||||
return new Promise((resolve) => terminal.write(data, resolve))
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue({
|
||||
measureText: () => ({ width: 10 })
|
||||
} as unknown as CanvasRenderingContext2D)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
for (const terminal of terminals.splice(0)) {
|
||||
terminal.dispose()
|
||||
}
|
||||
vi.restoreAllMocks()
|
||||
document.body.replaceChildren()
|
||||
})
|
||||
|
||||
describe('scrollback decoration eviction (#10879)', () => {
|
||||
it.each([250, 500, 1000])('removes %i disposed keys with linear lookup work', (count) => {
|
||||
const sorted = list()
|
||||
const entries = Array.from({ length: count }, (_, id) => ({ line: id, id }))
|
||||
entries.forEach((entry) => sorted.insert(entry))
|
||||
expect([...sorted.values()]).toEqual(entries)
|
||||
const keyReads = vi.spyOn(sorted, '_getKey')
|
||||
try {
|
||||
for (const entry of entries) {
|
||||
entry.line = -1
|
||||
expect(sorted.delete(entry)).toBe(true)
|
||||
}
|
||||
expect([...sorted.values()]).toEqual([])
|
||||
console.info(`evict ${count}: ${keyReads.mock.calls.length} key reads`)
|
||||
expect(keyReads.mock.calls.length).toBeLessThanOrEqual(count * 4)
|
||||
} finally {
|
||||
sorted.clear()
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps duplicate identities, live-key queries, reinsertion and snapshots correct', () => {
|
||||
const sorted = list()
|
||||
const a = { line: 10, id: 1 }
|
||||
const b = { line: 20, id: 2 }
|
||||
try {
|
||||
sorted.insert(b)
|
||||
sorted.insert(a)
|
||||
sorted.insert(a)
|
||||
const snapshot = sorted.values()
|
||||
a.line -= 5
|
||||
b.line -= 5
|
||||
expect([...sorted.getKeyIterator(5)]).toEqual([a, a])
|
||||
expect(sorted.delete(a)).toBe(true)
|
||||
expect(sorted.delete(a)).toBe(true)
|
||||
expect(sorted.delete(a)).toBe(false)
|
||||
expect([...sorted.values()]).toEqual([b])
|
||||
expect([...snapshot]).toEqual([a, a, b])
|
||||
sorted.insert(a)
|
||||
expect([...sorted.values()]).toEqual([a, b])
|
||||
sorted.clear()
|
||||
expect(sorted.delete(a)).toBe(false)
|
||||
sorted.insert(b)
|
||||
expect(sorted.delete(b)).toBe(true)
|
||||
expect([...sorted.values()]).toEqual([])
|
||||
} finally {
|
||||
sorted.clear()
|
||||
}
|
||||
})
|
||||
|
||||
it('preserves duplicate counts through partial compaction and reinsertion', () => {
|
||||
const sorted = list()
|
||||
const entry = { line: 0, id: 0 }
|
||||
try {
|
||||
sorted.insert(entry)
|
||||
sorted.insert(entry)
|
||||
sorted.insert(entry)
|
||||
expect(sorted.delete(entry)).toBe(true)
|
||||
expect([...sorted.values()]).toEqual([entry, entry])
|
||||
expect(sorted.delete(entry)).toBe(true)
|
||||
expect([...sorted.values()]).toEqual([entry])
|
||||
sorted.insert(entry)
|
||||
expect(sorted.delete(entry)).toBe(true)
|
||||
expect(sorted.delete(entry)).toBe(true)
|
||||
expect(sorted.delete(entry)).toBe(false)
|
||||
expect([...sorted.values()]).toEqual([])
|
||||
} finally {
|
||||
sorted.clear()
|
||||
}
|
||||
})
|
||||
|
||||
it('matches identity membership through mixed key mutation and compaction', () => {
|
||||
const sorted = list()
|
||||
let seed = 10879
|
||||
const random = (): number => (seed = (seed * 1664525 + 1013904223) >>> 0) / 2 ** 32
|
||||
const entries = Array.from({ length: 100 }, (_, id) => ({ line: id, id }))
|
||||
const live = new Set<Entry>()
|
||||
try {
|
||||
for (let step = 0; step < 2000; step++) {
|
||||
const entry = entries[Math.floor(random() * entries.length)]
|
||||
if (random() < 0.45 && !live.has(entry)) {
|
||||
entry.line = entry.id
|
||||
sorted.insert(entry)
|
||||
live.add(entry)
|
||||
} else {
|
||||
entry.line = -1
|
||||
expect(sorted.delete(entry)).toBe(live.delete(entry))
|
||||
}
|
||||
if (step % 17 === 0) {
|
||||
expect([...sorted.values()]).toEqual([...live].sort((a, b) => a.line - b.line))
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
sorted.clear()
|
||||
}
|
||||
})
|
||||
|
||||
it('evicts real search highlights without changing marker events or retained output', async () => {
|
||||
const { terminal, service } = setup()
|
||||
const search = new SearchAddon()
|
||||
terminal.loadAddon(search)
|
||||
await write(terminal, 'needle\r\n'.repeat(1000))
|
||||
expect(
|
||||
search.findNext('needle', {
|
||||
decorations: {
|
||||
matchBackground: '#ffcc00',
|
||||
matchOverviewRuler: '#ffcc00',
|
||||
activeMatchColorOverviewRuler: '#ff9900'
|
||||
}
|
||||
})
|
||||
).toBe(true)
|
||||
const decorations = [...service.decorations]
|
||||
expect(decorations.length).toBeGreaterThanOrEqual(1000)
|
||||
const removed = vi.fn()
|
||||
service.onDecorationRemoved(removed)
|
||||
const marker = terminal.registerMarker(0)
|
||||
expect(marker).toBeDefined()
|
||||
const disposed = vi.fn(() => {
|
||||
expect(marker?.isDisposed).toBe(true)
|
||||
expect(marker?.line).toBe(-1)
|
||||
})
|
||||
marker?.onDispose(disposed)
|
||||
const keyReads = vi.spyOn(service._decorations, '_getKey')
|
||||
await write(terminal, 'plain\r\n'.repeat(2000))
|
||||
expect(removed).toHaveBeenCalledTimes(decorations.length)
|
||||
expect([...service.decorations]).toEqual([])
|
||||
expect(disposed).toHaveBeenCalledOnce()
|
||||
expect(terminal.buffer.active.length).toBe(1024)
|
||||
expect(terminal.buffer.active.getLine(1000)?.translateToString(true)).toBe('plain')
|
||||
expect(keyReads.mock.calls.length).toBeLessThanOrEqual(decorations.length * 4)
|
||||
search.clearDecorations()
|
||||
expect(
|
||||
search.findNext('plain', {
|
||||
decorations: {
|
||||
matchBackground: '#ffcc00',
|
||||
matchOverviewRuler: '#ffcc00',
|
||||
activeMatchColorOverviewRuler: '#ff9900'
|
||||
}
|
||||
})
|
||||
).toBe(true)
|
||||
expect([...service.decorations].length).toBeGreaterThan(0)
|
||||
search.clearDecorations()
|
||||
expect([...service.decorations]).toEqual([])
|
||||
})
|
||||
})
|
||||
+1
@@ -2644,6 +2644,7 @@
|
||||
"tool": {
|
||||
"countN": "{{value0}} tool calls",
|
||||
"countOne": "1 tool call",
|
||||
"moreCalls": "+{{value0}} more",
|
||||
"ranCommandManyToolsSummary": "Ran {{commandCount}} command and used {{toolCount}} tools",
|
||||
"ranCommandOneToolSummary": "Ran {{commandCount}} command and used {{toolCount}} tool",
|
||||
"ranCommandsManyToolsSummary": "Ran {{commandCount}} commands and used {{toolCount}} tools",
|
||||
|
||||
@@ -16968,6 +16968,11 @@
|
||||
"empty": "No users found"
|
||||
},
|
||||
"native-chat": {
|
||||
"notices": {
|
||||
"compaction": "Context compacted",
|
||||
"details": "Details",
|
||||
"plan": "Plan"
|
||||
},
|
||||
"composer": {
|
||||
"imageUnsupported": "Image paste is not supported for this agent.",
|
||||
"send": "Send",
|
||||
@@ -17039,6 +17044,7 @@
|
||||
"result": "Result",
|
||||
"countOne": "1 tool call",
|
||||
"countN": "{{value0}} tool calls",
|
||||
"moreCalls": "+{{value0}} more",
|
||||
"runningPreview": "Running {{preview}}",
|
||||
"runningCommand": "Running command",
|
||||
"runningNamedPreview": "Running {{toolName}} {{preview}}",
|
||||
|
||||
@@ -190,6 +190,34 @@ describe('forward tolerance', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('optional notice metadata', () => {
|
||||
it.each([
|
||||
{},
|
||||
{ presentation: 'compaction' },
|
||||
{ presentation: 'plan-document' },
|
||||
{ tone: 'warning' },
|
||||
{ tone: 'error' },
|
||||
{ tone: 'notice' },
|
||||
{ presentation: 'future-presentation', tone: 'future-tone' }
|
||||
])('admits existing status and text kinds with %j', (metadata) => {
|
||||
expect(
|
||||
isAdmissibleAgentJournalItemBody({ kind: 'status', text: 'Readable fallback', ...metadata })
|
||||
).toBe(true)
|
||||
expect(
|
||||
isAdmissibleAgentJournalItemBody({
|
||||
kind: 'message',
|
||||
role: 'system',
|
||||
blocks: [{ type: 'text', text: 'Readable fallback', ...metadata }]
|
||||
})
|
||||
).toBe(true)
|
||||
})
|
||||
it.each([{ tone: false }, { presentation: {} }])('rejects malformed metadata: %j', (metadata) => {
|
||||
expect(isAdmissibleAgentJournalItemBody({ kind: 'status', text: 'Text', ...metadata })).toBe(
|
||||
false
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('optional tool annotations', () => {
|
||||
const body = { kind: 'tool-call', name: 'shell', input: null, state: 'completed' }
|
||||
it('admits old rows and rows with optional annotations without a new kind', () => {
|
||||
|
||||
@@ -68,6 +68,8 @@ const Block = z.union([
|
||||
z.object({
|
||||
type: z.literal('text'),
|
||||
text: z.string(),
|
||||
presentation: z.string().optional(),
|
||||
tone: z.string().optional(),
|
||||
providerFrame: ProviderFrame.optional()
|
||||
}),
|
||||
// `input: undefined` loses its key under JSON.stringify, so a persisted
|
||||
@@ -160,6 +162,8 @@ export const AgentJournalItemBodySchema = z.discriminatedUnion('kind', [
|
||||
z.object({
|
||||
kind: z.literal('status'),
|
||||
text: z.string(),
|
||||
presentation: z.string().optional(),
|
||||
tone: z.string().optional(),
|
||||
turnLifecycle: z.object({ turnId: z.string(), state: z.string().min(1) }).optional(),
|
||||
providerFrame: ProviderFrame.optional()
|
||||
})
|
||||
|
||||
@@ -148,6 +148,9 @@ export type AgentJournalQuestionItem = {
|
||||
export type AgentJournalStatusItem = {
|
||||
kind: 'status'
|
||||
text: string
|
||||
/** Optional display hints; unknown values retain the ordinary text fallback. */
|
||||
presentation?: string
|
||||
tone?: string
|
||||
/** Durable root-turn lifecycle used by clients to expose cancellation only
|
||||
* while the provider can still accept it. */
|
||||
turnLifecycle?: { turnId: string; state: 'running' | 'completed' }
|
||||
|
||||
@@ -13,7 +13,8 @@ export const NATIVE_CHAT_TOOL_ACTIVITY_COPY = {
|
||||
runningNamedPreview: 'Running {{toolName}} {{preview}}',
|
||||
runningNamed: 'Running {{toolName}}',
|
||||
countOne: '1 tool call',
|
||||
countN: '{{value0}} tool calls'
|
||||
countN: '{{value0}} tool calls',
|
||||
moreCalls: '+{{value0}} more'
|
||||
} as const
|
||||
|
||||
/** Tools whose call is a shell command, so the row reads as terminal activity
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
MAX_TOOL_DETAIL_LENGTH,
|
||||
summarizeToolInput,
|
||||
summarizeToolRun,
|
||||
toolRunSummaryMembers,
|
||||
toolFilePath,
|
||||
truncateToolDetail
|
||||
} from './native-chat-tool-summary'
|
||||
@@ -254,6 +255,73 @@ describe('briefToolArg', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('toolRunSummaryMembers', () => {
|
||||
it('keeps each call separate so a boundary can be drawn between them', () => {
|
||||
const blocks: NativeChatBlock[] = [
|
||||
{ type: 'tool-call', name: 'Bash', input: { command: 'ls -la' } },
|
||||
{ type: 'tool-call', name: 'tools/read', input: { file_path: 'README.md' } }
|
||||
]
|
||||
|
||||
expect(toolRunSummaryMembers(blocks)).toEqual([
|
||||
{ name: 'Bash', arg: 'ls -la', mcpIdentity: undefined },
|
||||
{ name: 'tools/read', arg: 'README.md', mcpIdentity: undefined }
|
||||
])
|
||||
})
|
||||
|
||||
// `url` is a PRIMARY_ARG_KEY but not a BRIEF_ARG_KEY, so a call carrying only
|
||||
// a url falls through to the bounded JSON preview and is cut at 28 chars —
|
||||
// mid-token, brace unbalanced. Pinned as-is: the header renders whatever this
|
||||
// returns, and changing the key list would move mobile's summary too.
|
||||
it('still falls through to a clipped JSON preview for a url-only call', () => {
|
||||
const blocks: NativeChatBlock[] = [
|
||||
{ type: 'tool-call', name: 'browser.open', input: { url: 'https://example.com' } }
|
||||
]
|
||||
|
||||
expect(toolRunSummaryMembers(blocks)[0]?.arg).toBe('{"url":"https://example.com"')
|
||||
})
|
||||
|
||||
it('caps at the summary limit and skips nameless calls, as the joined string does', () => {
|
||||
const blocks: NativeChatBlock[] = [
|
||||
{ type: 'tool-call', name: ' ', input: {} },
|
||||
{ type: 'tool-call', name: 'Bash', input: { command: 'ls' } },
|
||||
{ type: 'tool-call', name: 'Read', input: { file_path: 'a.ts' } },
|
||||
{ type: 'tool-call', name: 'Edit', input: { file_path: 'b.ts' } },
|
||||
{ type: 'tool-call', name: 'Write', input: { file_path: 'c.ts' } }
|
||||
]
|
||||
|
||||
const members = toolRunSummaryMembers(blocks)
|
||||
expect(members.map((member) => member.name)).toEqual(['Bash', 'Read', 'Edit'])
|
||||
// The joined string is derived from these, so the two can never disagree.
|
||||
expect(summarizeToolRun(blocks)).toBe(
|
||||
members.map((member) => `${member.name} ${member.arg}`).join(' · ')
|
||||
)
|
||||
})
|
||||
|
||||
it('carries provider MCP identity through, so a pill can draw the server glyph', () => {
|
||||
const blocks: NativeChatBlock[] = [
|
||||
{
|
||||
type: 'tool-call',
|
||||
name: 'mcp__linear__list_issues',
|
||||
input: {},
|
||||
mcpIdentity: { server: 'linear', tool: 'list_issues' }
|
||||
}
|
||||
]
|
||||
|
||||
expect(toolRunSummaryMembers(blocks)[0]?.mcpIdentity).toEqual({
|
||||
server: 'linear',
|
||||
tool: 'list_issues'
|
||||
})
|
||||
})
|
||||
|
||||
it('reports a blank argument rather than standing raw JSON in for one', () => {
|
||||
const blocks: NativeChatBlock[] = [{ type: 'tool-call', name: 'Bash', input: { command: '' } }]
|
||||
|
||||
expect(toolRunSummaryMembers(blocks)).toEqual([
|
||||
{ name: 'Bash', arg: '', mcpIdentity: undefined }
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('summarizeToolRun', () => {
|
||||
it('caps the run summary and skips nameless calls', () => {
|
||||
const blocks: NativeChatBlock[] = [
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { NativeChatMcpIdentity } from './native-chat-tool-identity'
|
||||
import { isToolCallBlock, type NativeChatBlock } from './native-chat-types'
|
||||
|
||||
const MAX_PREVIEW_LENGTH = 80
|
||||
@@ -260,8 +261,20 @@ function summarizePrimaryToolArg(input: unknown): string | null {
|
||||
return null
|
||||
}
|
||||
|
||||
export function summarizeToolRun(blocks: readonly NativeChatBlock[]): string {
|
||||
const parts: string[] = []
|
||||
/** One named call in a run header, kept apart rather than pre-joined so a
|
||||
* surface can draw the boundary between members itself. */
|
||||
export type ToolRunMember = {
|
||||
name: string
|
||||
/** Brief argument, or '' when the call has none worth showing. */
|
||||
arg: string
|
||||
mcpIdentity?: NativeChatMcpIdentity
|
||||
}
|
||||
|
||||
/** The run header's leading calls. Capped at the same limit the joined string
|
||||
* has always used, so the two can never disagree about which calls speak for
|
||||
* a run. */
|
||||
export function toolRunSummaryMembers(blocks: readonly NativeChatBlock[]): ToolRunMember[] {
|
||||
const members: ToolRunMember[] = []
|
||||
for (const block of blocks) {
|
||||
if (!isToolCallBlock(block)) {
|
||||
continue
|
||||
@@ -270,13 +283,18 @@ export function summarizeToolRun(blocks: readonly NativeChatBlock[]): string {
|
||||
if (!name) {
|
||||
continue
|
||||
}
|
||||
const detail = briefToolArg(block.input)
|
||||
parts.push(detail ? `${name} ${detail}` : name)
|
||||
if (parts.length >= MAX_TOOL_RUN_SUMMARY_PARTS) {
|
||||
members.push({ name, arg: briefToolArg(block.input), mcpIdentity: block.mcpIdentity })
|
||||
if (members.length >= MAX_TOOL_RUN_SUMMARY_PARTS) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return parts.join(' · ')
|
||||
return members
|
||||
}
|
||||
|
||||
export function summarizeToolRun(blocks: readonly NativeChatBlock[]): string {
|
||||
return toolRunSummaryMembers(blocks)
|
||||
.map((member) => (member.arg ? `${member.name} ${member.arg}` : member.name))
|
||||
.join(' · ')
|
||||
}
|
||||
|
||||
export function countToolCalls(blocks: readonly NativeChatBlock[]): number {
|
||||
|
||||
@@ -32,6 +32,9 @@ export type NativeChatRole = (typeof NATIVE_CHAT_ROLES)[number]
|
||||
export type NativeChatTextBlock = {
|
||||
type: 'text'
|
||||
text: string
|
||||
/** Optional journal display hints; readers narrow only the values they know. */
|
||||
presentation?: string
|
||||
tone?: string
|
||||
/** Optional structured detail for an otherwise ordinary fallback line. */
|
||||
providerFrame?: {
|
||||
provider: string
|
||||
|
||||
@@ -284,6 +284,29 @@ describe('structured agent session status projection', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('notice projection for desktop and mobile consumers', () => {
|
||||
it.each([
|
||||
{ presentation: 'compaction' },
|
||||
{ presentation: 'plan-document' },
|
||||
{ tone: 'warning' },
|
||||
{ tone: 'error' },
|
||||
{ tone: 'notice' },
|
||||
{ presentation: 'future-presentation', tone: 'future-tone' }
|
||||
])('preserves readable text alongside optional metadata: %j', (metadata) => {
|
||||
const projected = projectStructuredItemToNativeChat(
|
||||
item('notice', 1, {
|
||||
kind: 'status',
|
||||
text: 'A readable document or notice',
|
||||
...metadata
|
||||
})
|
||||
)
|
||||
expect(projected).toMatchObject({
|
||||
role: 'system',
|
||||
blocks: [{ type: 'text', text: 'A readable document or notice', ...metadata }]
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('preserves optional tool annotations for desktop and mobile projection', () => {
|
||||
const metadata = {
|
||||
exitCode: 127,
|
||||
|
||||
@@ -113,6 +113,8 @@ function itemBlocks(item: AgentJournalRenderItem): {
|
||||
{
|
||||
type: 'text',
|
||||
text: body.text,
|
||||
...(body.presentation !== undefined ? { presentation: body.presentation } : {}),
|
||||
...(body.tone !== undefined ? { tone: body.tone } : {}),
|
||||
...(body.providerFrame ? { providerFrame: body.providerFrame } : {})
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user