Merge main and retain reviewed Electron and DOMPurify pins

This commit is contained in:
m4air
2026-09-07 17:44:26 -07:00
42 changed files with 1229 additions and 250 deletions
+2 -2
View File
@@ -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": {
+2 -2
View File
@@ -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": {
+3 -3
View File
@@ -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": {
+32 -32
View File
@@ -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: {}
-5
View File
@@ -9,8 +9,3 @@ inline src/main/ssh/ssh-relay-deploy.ts
inline src/main/ssh/ssh-relay-session.ts
inline src/relay/pty-handler.ts
inline src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts
mobile-config app/h/*/files/*.tsx
mobile-config app/h/*/source-control/*.tsx
mobile-config app/index.tsx
mobile-config scripts/mock-server.ts
mobile-config src/transport/rpc-client.ts
+8 -8
View File
@@ -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: {}
-30
View File
@@ -20,36 +20,6 @@
"rules": {
"max-lines": ["error", { "max": 379, "skipBlankLines": true, "skipComments": true }]
}
},
{
"files": ["app/h/*/source-control/*.tsx"],
"rules": {
"max-lines": ["error", { "max": 2152, "skipBlankLines": true, "skipComments": true }]
}
},
{
"files": ["app/index.tsx"],
"rules": {
"max-lines": ["error", { "max": 1422, "skipBlankLines": true, "skipComments": true }]
}
},
{
"files": ["src/transport/rpc-client.ts"],
"rules": {
"max-lines": ["error", { "max": 1074, "skipBlankLines": true, "skipComments": true }]
}
},
{
"files": ["scripts/mock-server.ts"],
"rules": {
"max-lines": ["error", { "max": 407, "skipBlankLines": true, "skipComments": true }]
}
},
{
"files": ["app/h/*/files/*.tsx"],
"rules": {
"max-lines": ["error", { "max": 402, "skipBlankLines": true, "skipComments": true }]
}
}
]
}
+9 -9
View File
@@ -3075,12 +3075,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-unicode11@0.10.0-beta.300':
@@ -9072,13 +9072,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
@@ -10574,9 +10574,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-unicode11@0.10.0-beta.300(@xterm/xterm@6.1.0-beta.303)':
dependencies:
@@ -14451,7 +14451,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
View File
@@ -176,7 +176,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",
@@ -235,7 +235,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.6.0",
"electron-builder": "^26.15.3",
"electron-builder-squirrel-windows": "^26.15.3",
+75 -81
View File
@@ -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
@@ -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)
@@ -346,8 +346,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.6.0
version: 43.6.0(supports-color@7.2.0)
@@ -3513,8 +3513,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':
@@ -3677,8 +3677,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
@@ -3715,8 +3715,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
@@ -3779,8 +3779,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==}
@@ -4261,8 +4261,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==}
@@ -4306,8 +4306,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==}
@@ -4525,8 +4525,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==}
@@ -4794,8 +4794,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==}
@@ -5563,8 +5563,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
@@ -5600,8 +5600,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==}
@@ -5856,8 +5857,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:
@@ -5977,8 +5978,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:
@@ -6029,14 +6030,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
@@ -6392,8 +6393,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:
@@ -6755,8 +6756,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'
@@ -6854,10 +6855,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==}
@@ -7119,7 +7116,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
@@ -7739,7 +7736,7 @@ snapshots:
eventsource: 3.0.7
eventsource-parser: 3.0.8
express: 5.2.1(supports-color@7.2.0)
express-rate-limit: 8.5.2(express@5.2.1(supports-color@7.2.0))
express-rate-limit: 8.5.2(express@5.2.1)
hono: 4.13.0
jose: 6.2.3
json-schema-typed: 8.0.2
@@ -7761,7 +7758,7 @@ snapshots:
eventsource: 3.0.7
eventsource-parser: 3.0.8
express: 5.2.1(supports-color@7.2.0)
express-rate-limit: 8.5.2(express@5.2.1(supports-color@7.2.0))
express-rate-limit: 8.5.2(express@5.2.1)
hono: 4.13.0
jose: 6.2.3
json-schema-typed: 8.0.2
@@ -9826,7 +9823,7 @@ 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))':
dependencies:
@@ -9882,7 +9879,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
@@ -9994,7 +9991,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:
@@ -10014,7 +10011,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:
@@ -10040,13 +10037,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: {}
@@ -10119,7 +10116,7 @@ snapshots:
camelcase@5.3.1: {}
caniuse-lite@1.0.30001791: {}
caniuse-lite@1.0.30001810: {}
ccount@2.0.1: {}
@@ -10573,7 +10570,7 @@ snapshots:
dom-accessibility-api@0.6.3: {}
dompurify@3.4.13:
dompurify@3.4.14:
optionalDependencies:
'@types/trusted-types': 2.0.7
@@ -10647,7 +10644,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:
@@ -10862,7 +10859,7 @@ snapshots:
exponential-backoff@3.1.3: {}
express-rate-limit@8.5.2(express@5.2.1(supports-color@7.2.0)):
express-rate-limit@8.5.2(express@5.2.1):
dependencies:
express: 5.2.1(supports-color@7.2.0)
ip-address: 10.4.0
@@ -10889,7 +10886,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)
@@ -10922,7 +10919,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:
@@ -11299,9 +11296,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: {}
@@ -11361,7 +11356,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'
@@ -11929,7 +11924,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
@@ -12214,7 +12209,7 @@ snapshots:
monaco-editor@0.55.1:
dependencies:
dompurify: 3.4.13
dompurify: 3.4.14
marked: 14.0.0
ms@2.1.3: {}
@@ -12250,7 +12245,7 @@ snapshots:
nan@2.26.2:
optional: true
nanoid@3.3.17: {}
nanoid@3.3.18: {}
negotiator@1.0.0: {}
@@ -12289,7 +12284,7 @@ snapshots:
dependencies:
node-addon-api: 7.1.1
node-releases@2.0.38: {}
node-releases@2.0.54: {}
nopt@9.0.0:
dependencies:
@@ -12575,7 +12570,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
@@ -12590,14 +12585,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
@@ -12743,9 +12738,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: {}
@@ -12840,10 +12836,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)
@@ -13222,7 +13218,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
@@ -13236,7 +13232,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
@@ -13296,7 +13292,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
@@ -13668,9 +13664,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
@@ -13747,8 +13743,6 @@ snapshots:
transitivePeerDependencies:
- msw
void-elements@3.1.0: {}
vscode-oniguruma@2.0.1: {}
vscode-textmate@9.3.2: {}
+1 -1
View File
@@ -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)
@@ -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>
)
}
@@ -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>
·{' '}
@@ -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,
@@ -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
})
})
)
})
@@ -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
+5
View File
@@ -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",
@@ -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' }
+3
View File
@@ -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 } : {})
}
]