Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions lib/src/lib/alert-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,29 @@ describe('AlertManager in isolation', () => {
expect(manager.getState(id).watchingEnabled).toBe(false);
});

it('notifies subscribers when WATCHING turns off as a watched command finishes', () => {
const id = 'rule-finish-notify';
manager.setWatchedCommands(['claude']);

manager.applyTerminalSemanticEvents(id, [
{ type: 'commandLine', commandLine: 'claude' },
{ type: 'commandStart', source: 'osc633_E', startedAt: Date.now() },
]);
expect(manager.getState(id).watchingEnabled).toBe(true);

// Subscribe after the command has started so we only capture the finish.
const watching: boolean[] = [];
manager.onStateChange((_id, state) => {
if (_id === id) watching.push(state.watchingEnabled);
});

manager.applyTerminalSemanticEvents(id, [{ type: 'commandFinish', exitCode: 0 }]);

expect(manager.getState(id).watchingEnabled).toBe(false);
// The off-transition must reach subscribers, not just live getState reads.
expect(watching).toContain(false);
});

it('matches on the bare program name, not the whole command line', () => {
const id = 'rule-argv0';
manager.setWatchedCommands(['claude']);
Expand Down
8 changes: 6 additions & 2 deletions lib/src/lib/alert-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,14 +397,18 @@ export class AlertManager {
const watch = entry.commandExitWatch;
entry.commandExitWatch = null;
entry.pendingCommandLine = null;
this.applyWatchingRule(id, entry);
// Disposing the WATCHING monitor flips `watchingEnabled`/status, so its
// change must propagate even when command-exit never armed — otherwise a
// watched command that finishes leaves subscribers on stale WATCHING state
// until the next command starts.
const watchingChanged = this.applyWatchingRule(id, entry);

const wasArmed = entry.commandExitStatus === 'COMMAND_EXIT_ARMED';
if (entry.commandExitStatus !== 'ALERT_RINGING') {
entry.commandExitStatus = 'IDLE';
}

if (!watch || !wasArmed) return wasArmed;
if (!watch || !wasArmed) return watchingChanged;
if (this.hasAttention(id)) return true;

if (Date.now() - watch.startedAt < this.inactivityTimeoutMs) return true;
Expand Down
Loading