ai-notify 0.4.4 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-notify",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "description": "Desktop, sound, and spoken notifications for terminal AI coding agents (Claude Code, Codex, Gemini, ...) — with one mute switch that covers all of them, across every terminal.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/notify.mjs CHANGED
@@ -184,10 +184,15 @@ export const emit = ({ provider = 'default', event = 'done', label = '', message
184
184
  setPaneWaiting(tty, event === 'waiting'); // waiting -> yellow menu bar status; done clears it
185
185
  const pane = readPaneSetting(tty);
186
186
 
187
- // Name this pane in the read-out. An explicit per-pane name (set from the menu
188
- // bar) is ALWAYS spoken; the auto-derived label (often just the working dir)
189
- // is prefixed only when speakLabel is on it's slow filler otherwise.
190
- const spokenName = pane.speakName || (config.speakLabel === true && label ? label : '');
187
+ // Name this pane in the read-out, most-reliable identity first:
188
+ // 1. $AI_NOTIFY_LABEL set in the pane's shell, inherited by the hook even
189
+ // when the agent runs it detached (no tty). Always spoken: setting it is
190
+ // explicit intent. The reliable way to name a pane for Claude Code.
191
+ // 2. pane.speakName — set from the menu bar, keyed by tty. Works only when
192
+ // the hook resolves to the pane's tty (see controllingTty's tree walk).
193
+ // 3. the auto-derived label — only when speakLabel is on (else slow filler).
194
+ const envName = (process.env.AI_NOTIFY_LABEL || '').trim();
195
+ const spokenName = envName || pane.speakName || (config.speakLabel === true && label ? label : '');
191
196
  const speakText = spokenName ? `${spokenName}、${spokenBody}` : spokenBody;
192
197
 
193
198
  // Per-pane voice (precedence: $AI_NOTIFY_* env > this pane's pick > global).
package/src/util.mjs CHANGED
@@ -40,18 +40,34 @@ export const isEphemeralInstall = (cliPath) => /[/\\]_npx[/\\]/.test(cliPath);
40
40
 
41
41
  export const MARKER = 'ai-notify'; // substring used to detect our own wiring
42
42
 
43
- // The controlling terminal of this process (e.g. "/dev/ttys010"), which is
44
- // stable per terminal pane — used to scope per-pane settings. null if none.
43
+ // The controlling terminal of the agent's pane (e.g. "/dev/ttys010"), used to
44
+ // scope per-pane settings. Returns null if none can be found.
45
+ //
46
+ // Agents often run the notify hook detached (Claude Code wires it `async`), so
47
+ // the hook process itself frequently has NO controlling tty — but its parent
48
+ // (the agent, e.g. `claude`) still owns the pane's terminal. So we walk up the
49
+ // process tree until we find a real tty, which makes the hook resolve to the
50
+ // SAME tty the menu bar lists the pane under (it scans the agent process).
45
51
  export const controllingTty = () => {
46
- try {
47
- const t = execFileSync('ps', ['-o', 'tty=', '-p', String(process.pid)], {
48
- stdio: ['ignore', 'pipe', 'ignore'],
49
- })
50
- .toString()
51
- .trim();
52
- if (!t || t === '??' || t === '?') return null;
53
- return t.startsWith('/dev/') ? t : `/dev/${t}`;
54
- } catch {
55
- return null;
52
+ let pid = process.pid;
53
+ for (let depth = 0; depth < 8 && pid > 1; depth++) {
54
+ try {
55
+ const line = execFileSync('ps', ['-o', 'tty=', '-o', 'ppid=', '-p', String(pid)], {
56
+ stdio: ['ignore', 'pipe', 'ignore'],
57
+ })
58
+ .toString()
59
+ .trim();
60
+ if (!line) return null;
61
+ // "ttys010 1234" or "?? 1234" (no controlling tty for this pid)
62
+ const sp = line.lastIndexOf(' ');
63
+ const tty = line.slice(0, sp).trim();
64
+ const ppid = parseInt(line.slice(sp + 1).trim(), 10);
65
+ if (tty && tty !== '??' && tty !== '?') return tty.startsWith('/dev/') ? tty : `/dev/${tty}`;
66
+ if (!Number.isFinite(ppid) || ppid <= 1) return null;
67
+ pid = ppid;
68
+ } catch {
69
+ return null;
70
+ }
56
71
  }
72
+ return null;
57
73
  };