mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-07 07:59:42 +02:00
fix(watching): close the review findings on the label and its window
A dual review (Codex CLI and Claude's code-reviewer, same diff, same brief) found the trust boundary weaker than the comments around it claimed. Eleven findings, all applied. The two blockers were both about who can write the row the label is read from. Claude's window covered two rows, and the second one is the status line, whose command a session running with permissions bypassed can write into its own `.claude/settings.json` — so an agent could print `· 1 monitor ·` onto a row of its own and silence its own idle alert. The default window is one row now, which is the footer and nothing else, and the constant says why. Separately, the label reached `data-tab-meta-sig` unescaped while the row is installed with innerHTML, which is an injection sink for any config-supplied pattern whose capture group is permissive; it goes through escapeHtml() like every other untrusted string in that file. The Codex entry could not be fixed the same way, and now says so. Its row is third from the bottom only while a terminal runs; with none running that slot holds the last row of the transcript, so matching the complete row (with the `/stop to close` tail, window narrowed to three) raises the bar without closing it. What contains it is `hooks: 'none'`: no hook event from a codex session reaches notePrompt(), so a forged label costs a wrong badge and cannot quiet an alert. The registry comment, `docs/cli-registry.md` and the test all state that rather than claiming a guarantee the code does not have. Also from the review: the TUI header badge no longer counts an acknowledged item, which was the same gate the classifier fix already went through and was wrong for human acknowledgement too; the TUI approval card reads the quiet reason and drops to a new `info` tone instead of asking for a reply; the badge carries an aria-label, because the phone it was built for has no hover target; the schema refuses `watchingLines` without a `watchingLine`; and the pattern and its window are resolved together rather than one memoized and one not. Documentation moved with it. The mechanism now lives in `docs/architecture-invariants.md` with CLAUDE.md keeping the rule and a pointer, `docs/wiki/Notifications-And-Approvals.md` tells users why a session stopped buzzing, and both that page and the changeset name the limitation neither did before: a question asked in plain prose is not a dialog, so it is silenced along with the false alarms while background work runs. Verified live again after the narrowing, on an isolated beta: a Claude session reported `1 monitor` and took its idle prompt acknowledged, and a Codex session reported `1 background terminal` against the full-row anchor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
64c288a683
commit
05c788ce9d
@@ -59,6 +59,24 @@ describe('approvalCard', () => {
|
||||
expect(card.hint).toBe('p to reply');
|
||||
});
|
||||
|
||||
it('says why an acknowledged idle prompt is quiet, in the drawer own words', () => {
|
||||
// The session is watching work it started itself. Asking for a reply would be the
|
||||
// same false alarm the acknowledgement exists to remove, so the card states the
|
||||
// reason and drops out of the warning vocabulary — while staying answerable.
|
||||
const card = approvalCard(
|
||||
item({
|
||||
kind: 'idle',
|
||||
message: 'Claude is waiting for your input',
|
||||
options: undefined,
|
||||
acknowledgedAt: 1_700_000_000_000,
|
||||
acknowledgedReason: 'watching 1 monitor',
|
||||
})
|
||||
);
|
||||
expect(card.tone).toBe('info');
|
||||
expect(card.title).toBe('quiet, watching 1 monitor');
|
||||
expect(card.hint).toBe('p to reply');
|
||||
});
|
||||
|
||||
it('drops the approve/deny-only hint when the frame did not parse', () => {
|
||||
const card = approvalCard(item({ options: undefined }));
|
||||
expect(card.options).toEqual([]);
|
||||
@@ -81,6 +99,12 @@ describe('approvalTone', () => {
|
||||
expect(approvalTone(item({ kind: 'question' }))).toBe('err');
|
||||
expect(approvalTone(item({ kind: 'idle' }))).toBe('warn');
|
||||
});
|
||||
|
||||
it('is neither for a prompt that opened acknowledged', () => {
|
||||
expect(approvalTone(item({ kind: 'idle', acknowledgedReason: 'watching 2 shells' }))).toBe('info');
|
||||
// A dialog stays red whatever else the session started.
|
||||
expect(approvalTone(item({ kind: 'permission', acknowledgedReason: 'watching 2 shells' }))).toBe('err');
|
||||
});
|
||||
});
|
||||
|
||||
describe('approvalAnswerForKey', () => {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { charWidth, stripStyles, toDisplayLines, visibleWidth } from '../../src/
|
||||
import { composerMove, createComposer } from '../../src/tui/tui-composer.js';
|
||||
import { computeLayout, needsBanner } from '../../src/tui/tui-layout.js';
|
||||
import { createTuiModel, type TuiModelStore } from '../../src/tui/tui-model.js';
|
||||
import type { ApprovalItem } from '../../src/web/approval-inbox.js';
|
||||
import {
|
||||
composerCursorCell,
|
||||
detectGlyphTier,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
formatPlanUsage,
|
||||
formatTokens,
|
||||
glyphsFor,
|
||||
pendingApprovalCount,
|
||||
renderFrame,
|
||||
rowLabel,
|
||||
type TuiRenderOptions,
|
||||
@@ -682,3 +684,33 @@ describe('the unicode glyph set is safe to render', () => {
|
||||
expect(every.join('')).not.toContain('\u270B');
|
||||
});
|
||||
});
|
||||
|
||||
describe('pendingApprovalCount', () => {
|
||||
const prompt = (over: Partial<ApprovalItem>): ApprovalItem => ({
|
||||
id: 'bbb2:1',
|
||||
sessionId: 'bbb2',
|
||||
sessionName: 'w6-docs',
|
||||
kind: 'idle',
|
||||
createdAt: NOW - 30_000,
|
||||
...over,
|
||||
});
|
||||
|
||||
it('counts a prompt nobody has seen', () => {
|
||||
const model = fixture();
|
||||
model.setApprovals([prompt({})]);
|
||||
expect(pendingApprovalCount(model)).toBe(1);
|
||||
});
|
||||
|
||||
it('does not count one whose alert is already spent', () => {
|
||||
// Both ways an item gets acknowledged: a human opening the session elsewhere, and the
|
||||
// inbox opening it that way for a session watching its own background work. The row
|
||||
// has left NEEDS YOU by the same flag, so a number in the header would point at a
|
||||
// group the reader can see is empty.
|
||||
const model = fixture();
|
||||
model.setApprovals([prompt({ acknowledgedAt: NOW - 20_000 })]);
|
||||
expect(pendingApprovalCount(model)).toBe(0);
|
||||
|
||||
model.setApprovals([prompt({ acknowledgedAt: NOW - 20_000, acknowledgedReason: 'watching 1 monitor' })]);
|
||||
expect(pendingApprovalCount(model)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user