Skip to content

Commit 30c571b

Browse files
gloryfromcaclaude
andcommitted
fix(ui-web): say on the sheet when the engine shortened the evidence
Review found the half of the cap that was missing. The gate clips an oversized account of a call down to what a person reads and sets `truncated`, but the sheet rendered the clipped diff, input and arguments without ever reading that flag. The reader saw a change that looked whole, and answered about the part they could see. Every layout now carries the line, below the evidence, where the reader has just run out of it -- and in amber rather than the faint grey of "there was nothing to show", because this says the opposite: there is more than this. Run against the code with the line removed: the new case fails on the first layout it checks. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
1 parent af30e65 commit 30c571b

6 files changed

Lines changed: 48 additions & 2 deletions

File tree

‎i18n/messages.json‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5555,6 +5555,10 @@
55555555
"en": "No preview of the change",
55565556
"zh": "无法预览改动"
55575557
},
5558+
"gui.confirm.ev.cut": {
5559+
"en": "shortened to what fits -- the rest is not shown",
5560+
"zh": "内容过长,这里只显示了前一部分"
5561+
},
55585562
"gui.confirm.land.once": {
55595563
"en": "Allowed, just this once",
55605564
"zh": "已允许 · 只这一次"

‎ui-tui/src/i18n/messages.generated.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1588,6 +1588,7 @@ export const UI_TEXT: Record<Locale, Record<string, string>> = {
15881588
'gui.confirm.why.unknown': '{who} wants to do this; you have not allowed it yet.',
15891589
'gui.confirm.ev.created': 'new file',
15901590
'gui.confirm.ev.nodiff': 'No preview of the change',
1591+
'gui.confirm.ev.cut': 'shortened to what fits -- the rest is not shown',
15911592
'gui.confirm.land.once': 'Allowed, just this once',
15921593
'gui.confirm.land.always': "Won't ask again for: {pattern}, in every working directory",
15931594
'gui.confirm.land.revoke': 'Undo',
@@ -3453,6 +3454,7 @@ export const UI_TEXT: Record<Locale, Record<string, string>> = {
34533454
'gui.confirm.why.unknown': '{who} 要执行这个操作,你还没授权过。',
34543455
'gui.confirm.ev.created': '新建文件',
34553456
'gui.confirm.ev.nodiff': '无法预览改动',
3457+
'gui.confirm.ev.cut': '内容过长,这里只显示了前一部分',
34563458
'gui.confirm.land.once': '已允许 · 只这一次',
34573459
'gui.confirm.land.always': '以后不再问:{pattern},在所有工作目录生效',
34583460
'gui.confirm.land.revoke': '收回',

‎ui-web/src/features/composer/GateSheet.tsx‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export interface GateWords {
2828
readonly deny: string
2929
readonly created: string
3030
readonly nodiff: string
31+
readonly cut: string
3132
}
3233

3334
export interface GateProps {
@@ -50,6 +51,12 @@ const diffClass = (line: string): string =>
5051
function EvidenceBlock(
5152
{ kind, evidence, command, words }: { kind: string; evidence: Evidence; command: string; words: GateWords },
5253
): JSX.Element {
54+
/* The gate cuts an oversized account down to what a person reads and says so
55+
here. Answering about a change you can only see part of is the one thing
56+
the cap must not cause, so the sheet says it was shortened wherever the
57+
flag is set -- and says it below the evidence, where the reader has just
58+
run out of it. */
59+
const cut = evidence.truncated === true ? <div className="cp-ev-cut">{words.cut}</div> : null
5360
if (kind === 'file.write') {
5461
/* The file header names the path the line above already shows, and a
5562
two-line change should not spend its room on it. */
@@ -64,6 +71,7 @@ function EvidenceBlock(
6471
</pre>
6572
)
6673
: <div className="cp-ev-none">{words.nodiff}</div>}
74+
{cut}
6775
</div>
6876
)
6977
}
@@ -72,11 +80,19 @@ function EvidenceBlock(
7280
<div className="what cp-ev">
7381
<div className="cp-ev-path">{str(evidence.server)}.{str(evidence.tool)}</div>
7482
<pre className="cp-json">{JSON.stringify(evidence.input ?? {}, null, 2)}</pre>
83+
{cut}
7584
</div>
7685
)
7786
}
78-
if (kind === 'shell.exec') return <div className="what">{str(evidence.command) || command}</div>
79-
return <pre className="what cp-json">{JSON.stringify(evidence.input ?? evidence, null, 2)}</pre>
87+
if (kind === 'shell.exec') {
88+
return <div className="what">{str(evidence.command) || command}{cut}</div>
89+
}
90+
return (
91+
<div className="what cp-ev">
92+
<pre className="cp-json">{JSON.stringify(evidence.input ?? evidence, null, 2)}</pre>
93+
{cut}
94+
</div>
95+
)
8096
}
8197

8298
export function GateSheet({ kind, evidence, command, words, opts, onDeny }: GateProps): JSX.Element {

‎ui-web/src/features/composer/approve.test.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,26 @@ describe('the permission approval sheet', () => {
441441
}
442442
})
443443

444+
/* The gate cuts an oversized account down to what a person reads
445+
(PermissionGate._clamped) and marks it. Rendering the clipped value without
446+
the mark is the one thing the cap must not cause: the reader would answer
447+
about a change they can only see part of, with nothing saying so. */
448+
it('says so in every layout when the engine shortened the evidence', () => {
449+
const cases = [
450+
{ kind: 'file.write', evidence: { path: '/w/big.py', created: false, diff: '-a\n+b', truncated: true } },
451+
{ kind: 'mcp.call', evidence: { server: 's', tool: 't', input: 'x'.repeat(40), truncated: true } },
452+
{ kind: 'shell.exec', evidence: { command: 'rm -rf x', cwd: '/w', truncated: true } },
453+
{ kind: 'unknown', evidence: { input: 'y'.repeat(40), truncated: true } },
454+
]
455+
for (const c of cases) {
456+
openApproval(fresh({ ...base, ...c, family: '' }), handlers())
457+
expect(document.querySelector('.cp-ev-cut')?.textContent, c.kind).toBe('gui.confirm.ev.cut')
458+
}
459+
/* And stays quiet when nothing was cut. */
460+
openApproval(fresh({ ...base, kind: 'file.write', family: '', evidence: { path: '/w/a.py', diff: '-a\n+b' } }), handlers())
461+
expect(document.querySelector('.cp-ev-cut')).toBeNull()
462+
})
463+
444464
it('lays out a write as its path and diff, an MCP call as its tool and input, the rest as arguments', () => {
445465
const diff = '--- a\n+++ b\n@@ -1 +1 @@\n-x = 1\n+x = 2'
446466
openApproval(fresh({ ...base, kind: 'file.write', family: '', evidence: { path: '/w/a.py', created: false, diff } }), handlers())

‎ui-web/src/features/composer/approve.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ function wordsFor(req: ApprovalReq): GateWords {
231231
deny: t('gui.confirm.deny'),
232232
created: t('gui.confirm.ev.created'),
233233
nodiff: t('gui.confirm.ev.nodiff'),
234+
cut: t('gui.confirm.ev.cut'),
234235
}
235236
}
236237

‎ui-web/src/features/composer/styles.css‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@
6666
.cp-ev { display: grid; gap: 6px; }
6767
.cp-ev-path { font-weight: 600; }
6868
.cp-ev-none { color: var(--faint); font-style: italic; }
69+
/* Not faint like `.cp-ev-none`: that says there was nothing to show, this says
70+
there is more than is shown, and the reader is about to answer about it. */
71+
.cp-ev-cut { color: var(--amber); font-size: 12px; }
6972
.cp-diff, .cp-json { margin: 0; font: inherit; white-space: pre-wrap; overflow-wrap: anywhere; }
7073
.cp-add { color: var(--moss); }
7174
.cp-del { color: var(--clay); }

0 commit comments

Comments
 (0)