{"version":3,"file":"completion-chime.d.ts","sourceRoot":"","sources":["../../../src/modes/interactive/completion-chime.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,uFAAuF;AACvF,eAAO,MAAM,IAAI,WAAS,CAAC;AAK3B,MAAM,WAAW,sBAAsB;IACtC,0FAA0F;IAC1F,SAAS,EAAE,MAAM,OAAO,CAAC;IACzB;;;OAGG;IACH,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,iFAAiF;IACjF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sDAAsD;IACtD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iEAAiE;IACjE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACnB;AAED,qBAAa,eAAe;IAC3B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgB;IAC1C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAa;IACpC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IAEnC,yEAAyE;IACzE,OAAO,CAAC,SAAS,CAAqB;IACtC,mDAAmD;IACnD,OAAO,CAAC,WAAW,CAAqB;IAExC,YAAY,OAAO,EAAE,sBAAsB,EAM1C;IAED;;;;OAIG;IACH,WAAW,IAAI,IAAI,CAIlB;IAED;;;OAGG;IACH,cAAc,CAAC,OAAO,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAUlD;IAED;;;;OAIG;IACH,iBAAiB,IAAI,IAAI,CAExB;IAED,OAAO,CAAC,IAAI;CAeZ","sourcesContent":["/**\n * Completion chime — a short audible cue (terminal BEL) played when an assistant\n * turn finishes after the user has likely stepped away, or when the agent blocks\n * awaiting the user's input (the ask_options pane).\n *\n * Design constraints:\n *   - Output-only, zero dependencies: the cue is a single BEL byte written to the\n *     terminal. No bundled audio, no subprocesses.\n *   - Side-effect-free from the turn's perspective: the ring is wrapped in a\n *     try/catch so a failed write can never throw into the turn path. Worst case\n *     is no sound.\n *\n * Firing conditions (the caller drives these; this class only decides whether a\n * given signal should actually ring):\n *   - Turn-complete: the turn ran longer than {@link CompletionChimeOptions.thresholdMs}\n *     (default 10s), measured from the turn's start to when the agent goes truly\n *     idle, and the turn was not user-aborted.\n *   - Blocked-for-input: the agent opened the ask_options pane. This bypasses the\n *     duration threshold — \"it needs you\" is worth surfacing immediately.\n *\n * Both paths are gated by an enable check and share a single debounce window so\n * rapid successive turns (or a long turn that ends in an ask_options prompt) do\n * not spam the bell.\n */\n\n/** Terminal bell (BEL, `\\a`). Rings the terminal's configured audible/visual alert. */\nexport const BELL = \"\\x07\";\n\nconst DEFAULT_THRESHOLD_MS = 10_000;\nconst DEFAULT_DEBOUNCE_MS = 5_000;\n\nexport interface CompletionChimeOptions {\n\t/** Read fresh on every potential ring so live setting changes take effect immediately. */\n\tisEnabled: () => boolean;\n\t/**\n\t * Emit the audible cue. Called inside a try/catch — implementations need not\n\t * guard against throwing, but nothing they throw will reach the turn path.\n\t */\n\tring: () => void;\n\t/** Minimum turn duration before a turn-complete chime fires. Defaults to 10s. */\n\tthresholdMs?: number;\n\t/** Minimum gap between two chimes. Defaults to 5s. */\n\tdebounceMs?: number;\n\t/** Clock, injectable for tests. Defaults to {@link Date.now}. */\n\tnow?: () => number;\n}\n\nexport class CompletionChime {\n\tprivate readonly isEnabled: () => boolean;\n\tprivate readonly doRing: () => void;\n\tprivate readonly thresholdMs: number;\n\tprivate readonly debounceMs: number;\n\tprivate readonly now: () => number;\n\n\t/** Start of the current turn, or undefined when no turn is in flight. */\n\tprivate turnStart: number | undefined;\n\t/** Timestamp of the last chime, for debouncing. */\n\tprivate lastChimeAt: number | undefined;\n\n\tconstructor(options: CompletionChimeOptions) {\n\t\tthis.isEnabled = options.isEnabled;\n\t\tthis.doRing = options.ring;\n\t\tthis.thresholdMs = options.thresholdMs ?? DEFAULT_THRESHOLD_MS;\n\t\tthis.debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;\n\t\tthis.now = options.now ?? Date.now;\n\t}\n\n\t/**\n\t * Anchor the start of a turn. Only the first call per turn takes effect, so a\n\t * turn that internally retries (re-entering agent_start) still measures its\n\t * duration from the original start.\n\t */\n\tonTurnStart(): void {\n\t\tif (this.turnStart === undefined) {\n\t\t\tthis.turnStart = this.now();\n\t\t}\n\t}\n\n\t/**\n\t * The agent has gone truly idle after a turn. Rings if the turn ran longer than\n\t * the threshold and was not aborted. Clears the turn anchor either way.\n\t */\n\tonTurnComplete(options: { aborted: boolean }): void {\n\t\tconst start = this.turnStart;\n\t\tthis.turnStart = undefined;\n\t\tif (options.aborted || start === undefined) {\n\t\t\treturn;\n\t\t}\n\t\tif (this.now() - start < this.thresholdMs) {\n\t\t\treturn;\n\t\t}\n\t\tthis.fire();\n\t}\n\n\t/**\n\t * The agent is blocked awaiting user input (ask_options pane). Rings\n\t * immediately, bypassing the duration threshold. Leaves the turn anchor intact\n\t * — the turn is still in flight and will complete once the user answers.\n\t */\n\tonBlockedForInput(): void {\n\t\tthis.fire();\n\t}\n\n\tprivate fire(): void {\n\t\tif (!this.isEnabled()) {\n\t\t\treturn;\n\t\t}\n\t\tconst at = this.now();\n\t\tif (this.lastChimeAt !== undefined && at - this.lastChimeAt < this.debounceMs) {\n\t\t\treturn;\n\t\t}\n\t\tthis.lastChimeAt = at;\n\t\ttry {\n\t\t\tthis.doRing();\n\t\t} catch {\n\t\t\t// Side-effect-free: worst case is no sound. Never throw into the turn path.\n\t\t}\n\t}\n}\n"]}