Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"name": "narrator",
"source": "./plugins/narrator",
"description": "Click ⏸︎ narrator off in the footer, or /narrator, and after each turn Claude tells you out loud what it did, in your language, with the best free voice your system has.",
"version": "0.5.0",
"version": "0.5.1",
"category": "productivity"
}
]
Expand Down
2 changes: 1 addition & 1 deletion plugins/narrator/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "narrator",
"version": "0.5.0",
"version": "0.5.1",
"description": "Click ⏸︎ narrator off in the footer, or /narrator, and after each turn Claude tells you out loud what it did, in your language, with the best free voice your system has: Siri's on macOS, Narrator's on Windows, speech-dispatcher's on Linux.",
"author": {
"name": "Miguel Ángel Durán (midudev)",
Expand Down
2 changes: 1 addition & 1 deletion plugins/narrator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ When a turn ends, Haiku (at low effort, so it is quick) reads your request, the
| WSL | Windows' voices, through `powershell.exe`; Linux's where that does not run |
| Linux | `spd-say` (speech-dispatcher, with whatever module it is set up with: espeak-ng, RHVoice, Piper...), else `espeak-ng` or `espeak` |

A new prompt cuts the narration short. It stays on in your next sessions until you switch it off.
A new prompt cuts the narration short. It stays on in your next sessions until you switch it off. A session nobody is at (`claude -p`, as a script or a bot that runs Claude Code uses) is not narrated.

```text
/narrator switch on or off
Expand Down
12 changes: 12 additions & 0 deletions plugins/narrator/hooks/register.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,18 @@ test("a subagent's turn and an interrupted turn are not narrated", async ($, on)
expect(w.prompts[0]).not.toContain('subagent')
})

test('a session nobody is at (claude -p) is not narrated: no model call, no speaker', async ($, on) => {
const w = world(on, { store: { enabled: true } })
await $.session.start({ cwd: '/project', surface: 'terminal', isInteractive: false })
await turn($)
await turn($, { answer: 'ok' })
await recap($)
await w.settled()
expect(w.prompts).toHaveLength(0)
expect(w.spoken).toHaveLength(0)
expect(w.said).toHaveLength(0)
})

test('a new prompt silences the narration on its way', async ($, on) => {
const w = world(on, { store: { enabled: true }, speaker: 'talks on' })
await startSession($)
Expand Down
11 changes: 8 additions & 3 deletions plugins/narrator/hooks/register.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,9 @@ function roomOf (e: { surface: string; viewport?: RenderViewport }) {
}

export const register: Register = (on, options) => {
// Whether someone is at the session. Until session.start says otherwise (a mod loaded in the middle
// of a session does not see it), someone is.
let isInteractive = true
const speed = Number(options.speed)
const shortAnswer = Number(options.shortAnswer ?? SHORT_ANSWER)
settings = {
Expand All @@ -509,6 +512,8 @@ export const register: Register = (on, options) => {
}

on('session.start', async ($, e, next) => {
// A session nobody is at (`claude -p`: a script, a bot that runs Claude Code) is not narrated.
isInteractive = e.isInteractive
const asked = String(options.language ?? '').trim()
if (asked && !settings.language) $.ui.log(`narrator: "${cut(asked, 40)}" is not a language code (es, en, pt...): speaking the language you write in`, { to: 'debug' })
await $.command.register({
Expand Down Expand Up @@ -566,22 +571,22 @@ export const register: Register = (on, options) => {

on('tool.call', async ($, e, next) => {
const result = await next(e)
if (e.agentId === undefined && actions.length < MAX_ACTIONS && (await read($, enabled))) {
if (isInteractive && e.agentId === undefined && actions.length < MAX_ACTIONS && (await read($, enabled))) {
actions.push(describe(e) + (result.deny !== undefined ? ' (denied)' : result.isError ? ' (failed)' : ''))
}
return result
}).catch(($, e, next) => next(e))

on('turn.complete', async ($, e, next) => {
const result = await next(e)
if (e.agentId === undefined && e.reason === 'answer' && e.answer.trim() && (await read($, enabled))) void narrate($, e.answer)
if (isInteractive && e.agentId === undefined && e.reason === 'answer' && e.answer.trim() && (await read($, enabled))) void narrate($, e.answer)
return result
})

on('command.run', { command: 'recap' }, async ($, e, next) => {
silence()
const result = await next(e)
if (result.text?.trim() && !NO_RECAP.test(result.text) && (await read($, enabled))) {
if (isInteractive && result.text?.trim() && !NO_RECAP.test(result.text) && (await read($, enabled))) {
actions = []
void narrate($, result.text)
}
Expand Down