mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-07 07:59:42 +02:00
feat(approvals): let a watching session keep quiet, and fix the tui gate
The badge alone left the row in NEEDS YOU, which is the thing the issue was about. The fix is the alert that does not fire. An idle prompt from a session that is watching its own background work now opens ALREADY acknowledged. `hook-event-routes` passes `Session.watching` to `notePrompt()`, which sets `acknowledgedAt` and records why in a new `acknowledgedReason`. Nothing new suppresses anything: `acknowledge()` has always meant "the alert this prompt armed is spent", and the prompt itself stays pending, answerable and available as Read My Mind context. A wrong label therefore costs a card that does not blink, never an alert that was never created. Every surface follows from that. The broadcast carries the reason, so a live page declines to arm the tab alert and raises no desktop notification. The push is skipped, since a false alarm is hardest to ignore on a phone. A reloading page reads `acknowledgedAt` in `seedApprovals()`, which it already did. And `classifySession()` now reads it too, which is a pre-existing bug fixed here: acknowledging on one device cleared the alert everywhere except `codeman tui`. It re-arms for free, because the next idle prompt supersedes the item and is built fresh. Only `idle` is eligible, so a dialog that blocks the agent still goes red whatever else it started. The label is pane-derived and therefore prompt-injectable, so it is now read from the last two rows of the screen only, with Claude's pattern anchored on the `·` its footer joins items with, ANSI-stripped and length-capped at the source. An agent that prints `· 1 monitor ·` into its own output finds no match. Verified on an isolated beta: a session that armed a monitor took its idle prompt acknowledged with no alert on any surface, wore the badge, and showed "quiet, watching 1 monitor" on its still-answerable card; the same session with the monitor killed alerted normally on the next prompt. `test/watching-no-alert.test.ts` pins both directions across all four surfaces. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3f2cde2db7
commit
74884a20eb
@@ -71,6 +71,32 @@ describe('classifySession', () => {
|
||||
expect(classifySession(row, approval({ sessionId: 'a', kind: 'idle' }))).toBe('waiting');
|
||||
});
|
||||
|
||||
it('stops an ACKNOWLEDGED prompt deciding the row', () => {
|
||||
// Acknowledgement means the alert this prompt armed has been spent, either because
|
||||
// a human opened the session elsewhere or because the inbox opened the item that way
|
||||
// for a session watching its own background work. The web has honoured that since
|
||||
// acknowledgement existed; this gate used to read past it, so an alert cleared on a
|
||||
// phone stayed lit here alone.
|
||||
const quiet = session({ sessionId: 'a', status: 'idle' });
|
||||
const seen = approval({ sessionId: 'a', kind: 'idle', acknowledgedAt: NOW - 1_000 });
|
||||
expect(classifySession(quiet, seen)).toBe('idle');
|
||||
expect(classifySession(session({ sessionId: 'a', status: 'busy' }), seen)).toBe('working');
|
||||
});
|
||||
|
||||
it('keeps a blocking dialog lit whatever its acknowledgement says', () => {
|
||||
// `acknowledge()` is idle-only by construction, so this cannot happen through the
|
||||
// routes. It is pinned because the cost of the two being wired together later is a
|
||||
// permission dialog that stops asking.
|
||||
const row = session({ sessionId: 'a' });
|
||||
const at = NOW - 1_000;
|
||||
expect(classifySession(row, approval({ sessionId: 'a', kind: 'permission', acknowledgedAt: at }))).toBe(
|
||||
'blocked-permission'
|
||||
);
|
||||
expect(classifySession(row, approval({ sessionId: 'a', kind: 'question', acknowledgedAt: at }))).toBe(
|
||||
'blocked-question'
|
||||
);
|
||||
});
|
||||
|
||||
it('classifies a row the server no longer has live as history', () => {
|
||||
expect(classifySession(session({ sessionId: 'a', sources: ['history'], status: 'busy' }))).toBe('recent');
|
||||
expect(classifySession(session({ sessionId: 'a', sources: ['persisted', 'lifecycle'] }))).toBe('recent');
|
||||
@@ -78,6 +104,46 @@ describe('classifySession', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('an acknowledged prompt on a watching session', () => {
|
||||
const sessions = [session({ sessionId: 'watcher', status: 'idle', lastActivityAt: NOW - 120_000 })];
|
||||
const approvals = approvalMap([
|
||||
approval({
|
||||
sessionId: 'watcher',
|
||||
kind: 'idle',
|
||||
createdAt: NOW - 30_000,
|
||||
acknowledgedAt: NOW - 30_000,
|
||||
acknowledgedReason: 'watching 1 monitor',
|
||||
}),
|
||||
]);
|
||||
|
||||
it('raises no alert: the row leaves NEEDS YOU entirely', () => {
|
||||
const groups = groupSessions(buildRows(sessions, approvals));
|
||||
const byKey = Object.fromEntries(groups.map((group) => [group.key, group.rows.map((r) => r.session.sessionId)]));
|
||||
expect(byKey['needs-you']).toEqual([]);
|
||||
expect(byKey['idle']).toEqual(['watcher']);
|
||||
});
|
||||
|
||||
it('keeps the item on the row, because it is still pending and still answerable', () => {
|
||||
const [row] = buildRows(sessions, approvals);
|
||||
expect(row.approval?.acknowledgedReason).toBe('watching 1 monitor');
|
||||
});
|
||||
|
||||
it('dates the row from the pane going quiet, not from the prompt', () => {
|
||||
// The prompt's age measures the state only while the prompt is what put the row in
|
||||
// it. Reading it here would report "idle 30s" for a session quiet for two minutes.
|
||||
const [row] = buildRows(sessions, approvals);
|
||||
expect(row.since).toBe(NOW - 120_000);
|
||||
});
|
||||
|
||||
it('alerts again once the same session goes quiet for an ordinary reason', () => {
|
||||
// The inbox supersedes the acknowledged item and builds a fresh one, so this is the
|
||||
// next prompt rather than the same one changing its mind.
|
||||
const fresh = approvalMap([approval({ sessionId: 'watcher', kind: 'idle', createdAt: NOW - 1_000 })]);
|
||||
const groups = groupSessions(buildRows(sessions, fresh));
|
||||
expect(groups[0].rows.map((row) => row.session.sessionId)).toEqual(['watcher']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('groupSessions', () => {
|
||||
it('always returns the four groups in display order', () => {
|
||||
expect(groupSessions([]).map((group) => group.key)).toEqual(['needs-you', 'working', 'idle', 'recent']);
|
||||
|
||||
Reference in New Issue
Block a user