diff --git a/CHANGELOG.md b/CHANGELOG.md index 46cd05b..9913154 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- **A Claude Code card stops spinning when an API error ends the turn.** A usage + limit, an overload or a failed login ends a Claude Code turn with + `StopFailure` instead of `Stop`, which the plugin did not register, so the card + kept its spinner until the next prompt. It now reports the turn done, and a + lich that reads the limit off the transcript picks the turn up again once the + limit resets. + ## [0.18.0] - 2026-10-07 ### Added diff --git a/docs/providers.md b/docs/providers.md index 5ba374b..7ada90d 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -73,7 +73,7 @@ swallows its own errors and never awaits a report. | `busy` | `UserPromptSubmit`, `PostToolUse` | `UserPromptSubmit`, `PostToolUse` | `PreInvocation` | `session.status` (`busy`) | `input`, `turn_start` | — | | `busy` + tool | `PreToolUse` | `PreToolUse` | `PreToolUse` | `tool.execute.before` | `tool_call` | — | | `waiting` + reason | `Notification` | `PermissionRequest` | — (not measured) | any `*.asked` | — (see below) | — | -| `done` | `Stop` | `Stop` | `Stop` | `session.status` (`idle`) | `session_stop` | — | +| `done` | `Stop`, `StopFailure` | `Stop` | `Stop` | `session.status` (`idle`) | `session_stop` | — | | title | `Stop` | `Stop` | `Stop` | `session.updated` | `session_stop`, `turn_start` | — | | `idle` | `SessionEnd` | — (registered, never fires) | — (never fires) | — (nothing outlives it) | — (nothing outlives it) | — | | touched | `PostToolUse` (write tools) | `PostToolUse` (write tools) | `PostToolUse` (write tools) | `file.edited` | `tool_result` (write tools) | `PreToolUse` (write tools) | diff --git a/hooks/hooks.json b/hooks/hooks.json index d161ae4..ceb0258 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -82,6 +82,16 @@ ] } ], + "StopFailure": [ + { + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/report-state.sh done" + } + ] + } + ], "SessionEnd": [ { "hooks": [ diff --git a/tests/hooks.test.mjs b/tests/hooks.test.mjs index 648e313..5859a55 100644 --- a/tests/hooks.test.mjs +++ b/tests/hooks.test.mjs @@ -520,6 +520,21 @@ test('every registered state argument is an accepted state', () => { } }) +// Claude Code fires StopFailure *instead of* Stop when an API error ends the +// turn (a usage limit, an overload, a failed login), so a registration with +// Stop alone leaves the card spinning, and lich never gets to read the limit +// off the transcript (lich docs/hooks/session-state.md). +test('claude ends a turn an API error stopped like any other', () => { + const failure = REGISTRATIONS.filter( + (r) => r.provider === 'claude' && r.event === 'StopFailure' && r.script === 'report-state.sh', + ) + assert.deepEqual( + failure.map((r) => [r.argument, r.matcher]), + [['done', undefined]], + 'StopFailure reports done for every error', + ) +}) + test('every registered provider argument is a registered provider', () => { for (const r of REGISTRATIONS.filter((r) => r.script === 'report-session-start.sh')) { assert.ok(