diff --git a/.agents/notes/implemented/bug-fix/2026-07-30-approval-panel-command-cap.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-07-30-approval-panel-command-cap.i18n.yaml index 908f03860e..148914309c 100644 --- a/.agents/notes/implemented/bug-fix/2026-07-30-approval-panel-command-cap.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-07-30-approval-panel-command-cap.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-07-30-approval-panel-command-cap.md -2026-07-30-approval-panel-command-cap.md: a9282f132e655833cfe687409c287c5afd538d50 -2026-07-30-approval-panel-command-cap.zh.md: 7eb40942e10134d43478e53e06c584bb97d3bb8f +2026-07-30-approval-panel-command-cap.md: f16edd337a568bc5eb8e2f0d5ca04158a77f6cdf +2026-07-30-approval-panel-command-cap.zh.md: c41589d07dd617f61c89ce2182d274477ea78b86 diff --git a/.agents/notes/implemented/bug-fix/2026-07-30-approval-panel-command-cap.md b/.agents/notes/implemented/bug-fix/2026-07-30-approval-panel-command-cap.md index a9282f132e..f16edd337a 100644 --- a/.agents/notes/implemented/bug-fix/2026-07-30-approval-panel-command-cap.md +++ b/.agents/notes/implemented/bug-fix/2026-07-30-approval-panel-command-cap.md @@ -43,6 +43,8 @@ Confirmed both directions against the built client. With the cap reverted, the r Reproducing the off-screen buttons needs a card taller than the scrollport, not merely a tall card. The composer seat is `position: sticky; bottom: 0`, so while the card still fits it stays pinned to the viewport bottom and the buttons remain visible — at 900x1000 the uncapped card ate the whole transcript yet kept its action row on screen. Only once the card outgrows the scrollport does sticky stop being able to hold the bottom edge, and the row goes under. -The geometry block and the goldens are replay-only, so record mode reaches the fixture write instead of aborting on layout. +The geometry block and the golden are replay-only, so record mode reaches the fixture write instead of aborting on layout. + +The scenario keeps exactly one golden — the waiting panel — and asserts the answered state on the world instead (the decided outcome, the file the escalated command wrote, `DONE`, the panel gone, the composer re-enabled). An answered-transcript golden was recorded first and failed on Linux CI: the denied first attempt renders the OS's own refusal, and that text is platform-specific (`bash: notes.txt: Operation not permitted` on macOS against `bash: line 1: notes.txt: Read-only file system` on Linux). Any scenario whose transcript contains a sandbox-denied command inherits that, so the denial belongs in assertions, never in a golden. The panel ships as a client-module bundle: `pnpm run build:web` alone does not pick up a change to `ApprovalPanel.module.css` or a new `data-` hook in `ApprovalPanel.tsx` — the package build must run first, or the browser lane asserts against an older client than the tree. diff --git a/.agents/notes/implemented/bug-fix/2026-07-30-approval-panel-command-cap.zh.md b/.agents/notes/implemented/bug-fix/2026-07-30-approval-panel-command-cap.zh.md index 7eb40942e1..c41589d07d 100644 --- a/.agents/notes/implemented/bug-fix/2026-07-30-approval-panel-command-cap.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-07-30-approval-panel-command-cap.zh.md @@ -45,4 +45,6 @@ Status: implemented 几何断言块与 golden 仅在回放模式下执行,这样录制模式才能走到写入 fixture 那一步,而不是在布局检查处中断。 +该场景只保留一份 golden —— 等待中的面板;回应之后的状态改为对世界作断言(决策结果、越权命令写出的那个文件、`DONE`、面板消失、输入框重新可用)。最初还录了一份"已回应会话流"的 golden,它在 Linux CI 上失败了:第一次被拒绝的尝试渲染的是操作系统自己的拒绝文本,而这段文本因平台而异(macOS 为 `bash: notes.txt: Operation not permitted`,Linux 为 `bash: line 1: notes.txt: Read-only file system`)。任何会话流中含有被沙箱拒绝命令的场景都会继承这一点,因此这类拒绝只能进断言,绝不能进 golden。 + 该面板以客户端模组包的形式发布:单跑 `pnpm run build:web` 不会带上对 `ApprovalPanel.module.css` 的改动,也不会带上 `ApprovalPanel.tsx` 中新增的 `data-` 钩子——必须先执行包构建,否则浏览器测试通道会对着一个比工作树更旧的客户端做断言。 diff --git a/apps/web/tests/approval-composer.e2e.ts b/apps/web/tests/approval-composer.e2e.ts index c9d5c493c6..2c6a78d2e5 100644 --- a/apps/web/tests/approval-composer.e2e.ts +++ b/apps/web/tests/approval-composer.e2e.ts @@ -29,10 +29,9 @@ import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './suppor const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/approval-composer', import.meta.url)) const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl') +// The scenario's one golden: the waiting panel. Everything the answered state +// proves is asserted directly — see the world-state block at the end. const UI_EXPECTED = join(SNAPSHOT_DIR, 'ui.expected.md') -// Second golden: the answered transcript — the granted escalation ran and the -// turn finished, the state the waiting golden cannot see. -const ANSWERED_EXPECTED = join(SNAPSHOT_DIR, 'answered.expected.md') const MODE = webSnapshotMode() // Irreducible payload: the command has to be long enough to pass the card's @@ -160,19 +159,23 @@ describe('web e2e: approval takeover keeps its actions reachable', () => { return } // World state: the granted escalation is what let the command run, and the - // panel leaves with the regular composer restored. + // panel leaves with the regular composer restored. Asserted on the world + // and the DOM rather than through a transcript golden — the denied first + // attempt renders the OS's own refusal ("Operation not permitted" on + // macOS, "Read-only file system" on Linux), so the answered transcript is + // not a platform-neutral golden surface. expect(JSON.stringify(sessionEvents.filter(e => e.type === 'approval/decided').at(-1))) .toContain('allowed-once') + const written = await readFile(join(scaffold.workspaceCwd, 'workspace', 'notes.txt'), 'utf8') + expect(written).toContain(TOKENS.slice(0, 64)) await expect.poll(() => page.getByText('DONE', { exact: true }).count(), { timeout: 20_000 }).toBeGreaterThanOrEqual(1) expect(await page.locator('[data-approval-key]').count()).toBe(0) await expect.poll(() => page.locator('textarea').first().isEnabled(), { timeout: 10_000 }).toBe(true) - const answered = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd) - await compareOrRefreshGolden(ANSWERED_EXPECTED, answered, MODE) expect(tripwire.pageErrors).toEqual([]) expect(tripwire.warnings).toEqual([]) }, 300_000) it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => { - await assertFixtureInventory(SNAPSHOT_DIR, ['session.jsonl', 'ui.expected.md', 'answered.expected.md']) + await assertFixtureInventory(SNAPSHOT_DIR, ['session.jsonl', 'ui.expected.md']) }) }) diff --git a/apps/web/tests/snapshots/approval-composer/answered.expected.md b/apps/web/tests/snapshots/approval-composer/answered.expected.md deleted file mode 100644 index ac2e9b4941..0000000000 --- a/apps/web/tests/snapshots/approval-composer/answered.expected.md +++ /dev/null @@ -1,57 +0,0 @@ -- banner: - - navigation "Session hierarchy": - - button "Write a file named notes.txt" [disabled] - - tablist: - - tab "Chat" [selected] - - tab "Trajectory" -- img -- text: "/permission read-only Permission preset: read-only. Write a file named notes.txt in the workspace containing exactly this text on one line: tok63z tokc7y tokibx tokofw tokujv tok10nu tok16rt tok1cvs tok1izr tok1p3q tok1v7p tok21bo tok2a4 tok8e3 tokei2 tokkm1 tokqq0 tokwtz tok12xy tok191x tok1f5w tok1l9v tok1rdu tok1xht tok23ls tok4k8 tokao7 tokgs6 tokmw5 tokt04 tokz43 tok1582 tok1bc1 tok1hg0 tok1njz tok1tny tok1zrx tokqd tok6uc tokcyb tokj2a tokp69 tokva8 tok11e7 tok17i6 tok1dm5 tok1jq4 tok1pu3 tok1vy2 tok2221 tok30h tok94g tokf8f toklce tokrgd tokxkc tok13ob tok19sa tok1fw9 tok1m08 tok1s47 tok1y86 tok24c5 tok5al tokbek tokhij toknmi toktqh tokzug tok15yf tok1c2e tok1i6d tok1oac tok1ueb tok20ia tok1gq tok7kp tokdoo tokjsn tokpwm tokw0l tok124k tok188j tok1eci tok1kgh tok1qkg tok1wof tok22se tok3qu tok9ut tokfys tokm2r toks6q tokyap tok14eo tok1ain tok1gmm tok1mql tok1suk tok1yyj tok252i tok60y tokc4x toki8w tokocv tokugu tok10kt tok16os tok1csr tok1iwq tok1p0p tok1v4o tok218n tok273 tok8b2 tokef1 tokkj0 tokqmz tokwqy tok12ux tok18yw tok1f2v tok1l6u tok1rat tok1xes tok23ir tok4h7 tokal6 tokgp5 tokmt4 toksx3 tokz12 tok1551 tok1b90 tok1hcz tok1ngy tok1tkx tok1zow toknc tok6rb tokcva tokiz9 tokp38 tokv77 tok11b6 tok17f5 tok1dj4 tok1jn3 tok1pr2 tok1vv1 tok21z0 tok2xg tok91f tokf5e tokl9d tokrdc tokxhb tok13la tok19p9 tok1ft8 tok1lx7 tok1s16 tok1y55 tok2494 tok57k tokbbj tokhfi toknjh toktng tokzrf tok15ve tok1bzd tok1i3c tok1o7b tok1uba tok20f9 tok1dp tok7ho tokdln tokjpm tokptl tokvxk tok121j tok185i tok1e9h tok1kdg tok1qhf tok1wle tok22pd tok3nt tok9rs tokfvr toklzq toks3p toky7o tok14bn tok1afm tok1gjl tok1mnk tok1srj tok1yvi tok24zh tok5xx tokc1w toki5v toko9u tokudt tok10hs tok16lr tok1cpq tok1itp tok1oxo tok1v1n tok215m tok242 tok881 tokec0 tokkfz tokqjy tokwnx. Use one bash command with the literal text inline. Then reply with the single word DONE and stop. {{clock}}" -- button "复制": - - img -- button "在新对话中分支": - - img -- button "编辑": - - img -- button "Think The user wants me to write a file named notes.txt with a specific line of text. Let me do this with a single bash command using echo.": - - img - - img - - text: Think The user wants me to write a file named notes.txt with a specific line of text. Let me do this with a single bash command using echo. -- img -- text: Bash Write notes.txt with the specified text 失败 workspace echo 'tok63z tokc7y tokibx tokofw tokujv tok10nu tok16rt tok1cvs tok1izr tok1p3q tok1v7p tok21bo tok2a4 tok8e3 tokei2 tokkm1 tokqq0 tokwtz tok12xy tok191x tok1f5w tok1l9v tok1rdu tok1xht tok23ls tok4k8 tokao7 tokgs6 tokmw5 tokt04 tokz43 tok1582 tok1bc1 tok1hg0 tok1njz tok1tny tok1zrx tokqd tok6uc tokcyb tokj2a tokp69 tokva8 tok11e7 tok17i6 tok1dm5 tok1jq4 tok1pu3 tok1vy2 tok2221 tok30h tok94g tokf8f toklce tokrgd tokxkc tok13ob tok19sa tok1fw9 tok1m08 tok1s47 tok1y86 tok24c5 tok5al tokbek tokhij toknmi toktqh tokzug tok15yf tok1c2e tok1i6d tok1oac tok1ueb tok20ia tok1gq tok7kp tokdoo tokjsn tokpwm tokw0l tok124k tok188j tok1eci tok1kgh tok1qkg tok1wof tok22se tok3qu tok9ut tokfys tokm2r toks6q tokyap tok14eo tok1ain tok1gmm tok1mql tok1suk tok1yyj tok252i tok60y tokc4x toki8w tokocv tokugu tok10kt tok16os tok1csr tok1iwq tok1p0p tok1v4o tok218n tok273 tok8b2 tokef1 tokkj0 tokqmz tokwqy tok12ux tok18yw tok1f2v tok1l6u tok1rat tok1xes tok23ir tok4h7 tokal6 tokgp5 tokmt4 toksx3 tokz12 tok1551 tok1b90 tok1hcz tok1ngy tok1tkx tok1zow toknc tok6rb tokcva tokiz9 tokp38 tokv77 tok11b6 tok17f5 tok1dj4 tok1jn3 tok1pr2 tok1vv1 tok21z0 tok2xg tok91f tokf5e tokl9d tokrdc tokxhb tok13la tok19p9 tok1ft8 tok1lx7 tok1s16 tok1y55 tok2494 tok57k tokbbj tokhfi toknjh tokktng tokzrf tok15ve tok1bzd tok1i3c tok1o7b tok1uba tok20f9 tok1dp tok7ho tokdln tokjpm tokptl tokvxk tok121j tok185i tok1e9h tok1kdg tok1qhf tok1wle tok22pd tok3nt tok9rs tokfvr toklzq toks3p toky7o tok14bn tok1afm tok1gjl tok1mnk tok1srj tok1yvi tok24zh tok5xx tokc1w toki5v toko9u tokudt tok10hs tok16lr tok1cpq tok1itp tok1oxo tok1v1n tok215m tok242 tok881 tokec0 tokfz tokqjy tokwnx' > notes.txt 退出码 1 -- button "复制" -- text: "[stderr] bash: notes.txt: Operation not permitted [sandbox: file access denied under read-only mode] [sandbox: escalation available — retry this exact command once with sandbox_permissions (the narrowest wider mode that suffices) + justification; the approval prompt asks the user]" -- button "Think The sandbox denied the file write. I need to retry with sandbox_permissions set to \"workspace-write\" (the narrowest wider mode) and provide a justification.": - - img - - img - - text: Think The sandbox denied the file write. I need to retry with sandbox_permissions set to "workspace-write" (the narrowest wider mode) and provide a justification. -- img -- text: Bash Write notes.txt with the specified text 已完成 workspace echo 'tok63z tokc7y tokibx tokofw tokujv tok10nu tok16rt tok1cvs tok1izr tok1p3q tok1v7p tok21bo tok2a4 tok8e3 tokei2 tokkm1 tokqq0 tokwtz tok12xy tok191x tok1f5w tok1l9v tok1rdu tok1xht tok23ls tok4k8 tokao7 tokgs6 tokmw5 tokt04 tokz43 tok1582 tok1bc1 tok1hg0 tok1njz tok1tny tok1zrx tokqd tok6uc tokcyb tokj2a tokp69 tokva8 tok11e7 tok17i6 tok1dm5 tok1jq4 tok1pu3 tok1vy2 tok2221 tok30h tok94g tokf8f toklce tokrgd tokxkc tok13ob tok19sa tok1fw9 tok1m08 tok1s47 tok1y86 tok24c5 tok5al tokbek tokhij toknmi toktqh tokzug tok15yf tok1c2e tok1i6d tok1oac tok1ueb tok20ia tok1gq tok7kp tokdoo tokjsn tokpwm tokw0l tok124k tok188j tok1eci tok1kgh tok1qkg tok1wof tok22se tok3qu tok9ut tokfys tokm2r toks6q tokyap tok14eo tok1ain tok1gmm tok1mql tok1suk tok1yyj tok252i tok60y tokc4x toki8w tokocv tokugu tok10kt tok16os tok1csr tok1iwq tok1p0p tok1v4o tok218n tok273 tok8b2 tokef1 tokkj0 tokqmz tokwqy tok12ux tok18yw tok1f2v tok1l6u tok1rat tok1xes tok23ir tok4h7 tokal6 tokgp5 tokmt4 toksx3 tokz12 tok1551 tok1b90 tok1hcz tok1ngy tok1tkx tok1zow toknc tok6rb tokcva tokiz9 tokp38 tokv77 tok11b6 tok17f5 tok1dj4 tok1jn3 tok1pr2 tok1vv1 tok21z0 tok2xg tok91f tokf5e tokl9d tokrdc tokxhb tok13la tok19p9 tok1ft8 tok1lx7 tok1s16 tok1y55 tok2494 tok57k tokbbj tokhfi toknjh tokktng tokzrf tok15ve tok1bzd tok1i3c tok1o7b tok1uba tok20f9 tok1dp tok7ho tokdln tokjpm tokptl tokvxk tok121j tok185i tok1e9h tok1kdg tok1qhf tok1wle tok22pd tok3nt tok9rs tokfvr toklzq toks3p toky7o tok14bn tok1afm tok1gjl tok1mnk tok1srj tok1yvi tok24zh tok5xx tokc1w toki5v toko9u tokudt tok10hs tok16lr tok1cpq tok1itp tok1oxo tok1v1n tok215m tok242 tok881 tokec0 tokfz tokqjy tokwnx' > notes.txt -- button "复制" -- text: (no output) -- button "Think The file was written successfully. Let me verify it was created correctly.": - - img - - img - - text: Think The file was written successfully. Let me verify it was created correctly. -- img -- text: Read -- button "notes.txt" -- button "Think The file was created successfully with the exact text on one line as requested. Now I need to reply with the single word DONE.": - - img - - img - - text: Think The file was created successfully with the exact text on one line as requested. Now I need to reply with the single word DONE. -- paragraph: DONE -- button "复制": - - img -- button "在新对话中分支": - - img -- text: {{clock}} -- textbox "Message the agent" -- button "Add attachment": - - img -- 'button "Access mode, current: Read Only"': Read Only -- button "Plan mode off, press to turn on": Plan off -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img -- button "Send message" [disabled] -- text: 1 turns · 4 steps Tool call {{duration}} Cache hit 96% Input 27.4K tok · Output 1.9K tok