fix(approvals): merge-time fixes for the watching badge (#473)

- session.ts: a pane capture that fails now CLEARS the watching label
  (and emits watchingChanged so pages drop the badge) instead of keeping
  the last one, so a failed capture degrades toward an alert rather than
  pre-acknowledging the next real idle prompt. Test updated; invariant
  noted in architecture-invariants.
- approvals-ui.js: the header bell counts only unacknowledged items
  (pendingApprovalsCount), matching codeman tui's pendingApprovalCount();
  pinned in watching-no-alert.test.ts.
- mobile-overview.js: move the orphaned "Pill copy per state" JSDoc back
  onto MOBILE_OVERVIEW_PILL_LABEL.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-09-23 11:40:03 +02:00
parent da6fa663e7
commit 0462a5d5a0
6 changed files with 40 additions and 13 deletions
+8 -4
View File
@@ -279,19 +279,23 @@ describe('Session.watching', () => {
expect(changes).toEqual(['1 monitor']);
});
it('keeps its last answer when the screen cannot be read', () => {
it('drops its answer when the screen cannot be read, and says so', () => {
vi.useFakeTimers();
const screen: { text: string | null } = { text: WITH_MONITOR };
const session = withFakePane(() => screen.text as string);
const changes: (string | null)[] = [];
session.on('watchingChanged', () => changes.push(session.watching));
runAndSettle(session);
expect(session.watching).toBe('1 monitor');
// A capture that fails is not evidence that nothing is running, which is the same
// rule the working probe applies to its own null.
// A stale label would open the next idle prompt already acknowledged, so a failed
// capture must degrade toward the alert, not toward silence. The page is told too,
// or every open tab would go on drawing the badge.
screen.text = null;
runAndSettle(session);
expect(session.watching).toBe('1 monitor');
expect(session.watching).toBeNull();
expect(changes).toEqual(['1 monitor', null]);
});
it('reads Codex own row, three up from the bottom of its screen', () => {
+7
View File
@@ -160,6 +160,13 @@ describe('a watching session raises no alert on any surface', () => {
expect(app.approvals.get(watched.id)?.acknowledgedReason).toBe('watching 1 monitor');
});
it('the header bell does not count the card, matching codeman tui', () => {
const app = loadFrontend() as FrontendApp & { pendingApprovalsCount(): number };
app.approvals.set('watched', watched);
app.approvals.set('plain', itemFor(null));
expect(app.pendingApprovalsCount()).toBe(1);
});
it('so both home screens classify the session as plainly idle', () => {
const app = loadFrontend();
app._onHookIdlePrompt({ sessionId: SESSION, acknowledgedReason: watched.acknowledgedReason });