@oh-my-pi/pi-tui 18.1.12 → 18.1.13

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/CHANGELOG.md CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  ### Fixed
8
8
 
9
+ - Fixed notifications never arriving in a Herdr pane. Herdr multiplexes panes like tmux but swallows bare OSC 9 / OSC 99 and has no passthrough envelope, so a backgrounded pane got no signal at all; delivery now goes through `herdr notification show` (a waiting question or an error rings `request`, a settled turn rings `done`), and the in-band write stays as the fallback when the pane id or the `herdr` binary is missing.
9
10
  - Avoid inserting a trailing space when auto-completing directory paths with `@`, and keep autocomplete open when accepting a directory with Tab or Enter.
10
11
  - Horizontal wheel reports (the sideways drift of a two-finger trackpad scroll) no longer decode as a vertical wheel direction, so fullscreen selectors such as `/copy` and the rewind picker stop jumping up and back down at the end of a scroll gesture.
11
12
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-tui",
4
- "version": "18.1.12",
4
+ "version": "18.1.13",
5
5
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Stencil Labs, Inc.",
@@ -37,8 +37,8 @@
37
37
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
38
38
  },
39
39
  "dependencies": {
40
- "@oh-my-pi/pi-natives": "18.1.12",
41
- "@oh-my-pi/pi-utils": "18.1.12"
40
+ "@oh-my-pi/pi-natives": "18.1.13",
41
+ "@oh-my-pi/pi-utils": "18.1.13"
42
42
  },
43
43
  "devDependencies": {
44
44
  "kitty-vt-wasm": "^0.2.0"
@@ -43,6 +43,12 @@ export type TerminalId =
43
43
  const CMUX_NOTIFICATION_TITLE = "Oh My Pi";
44
44
  const CMUX_SURFACE_ID_PATTERN = /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/iu;
45
45
 
46
+ /** Title and body for an out-of-band multiplexer notification (cmux, Herdr). */
47
+ function notificationTitleAndBody(message: string | TerminalNotification): { title: string; body: string } {
48
+ if (typeof message === "string") return { title: CMUX_NOTIFICATION_TITLE, body: message };
49
+ return { title: message.title?.trim() || CMUX_NOTIFICATION_TITLE, body: message.body ?? "" };
50
+ }
51
+
46
52
  /**
47
53
  * Route a notification through cmux when the process belongs to a concrete
48
54
  * surface. Workspace/socket state alone is not enough: only the injected
@@ -54,9 +60,7 @@ function sendCmuxNotification(message: string | TerminalNotification, env: NodeJ
54
60
  const surfaceId = env.CMUX_SURFACE_ID?.trim();
55
61
  if (!surfaceId || !CMUX_SURFACE_ID_PATTERN.test(surfaceId)) return false;
56
62
 
57
- const title =
58
- typeof message === "string" ? CMUX_NOTIFICATION_TITLE : message.title?.trim() || CMUX_NOTIFICATION_TITLE;
59
- const body = typeof message === "string" ? message : (message.body ?? "");
63
+ const { title, body } = notificationTitleAndBody(message);
60
64
  try {
61
65
  const child = Bun.spawn({
62
66
  cmd: ["cmux", "notify", "--surface", surfaceId, "--title", title, "--body", body],
@@ -72,6 +76,56 @@ function sendCmuxNotification(message: string | TerminalNotification, env: NodeJ
72
76
  return true;
73
77
  }
74
78
 
79
+ const HERDR_PANE_ID_PATTERN = /^[0-9A-Za-z:_-]{1,64}$/u;
80
+ /**
81
+ * `herdr notification show` takes the title as its first positional and reads
82
+ * exactly these three values there as a help request; it has no `--`
83
+ * terminator. Any other text, including one starting with `-`, is a title.
84
+ */
85
+ const HERDR_USAGE_TOKENS = new Set(["help", "--help", "-h"]);
86
+
87
+ /**
88
+ * Route a notification through Herdr when the process runs inside one of its
89
+ * panes. Herdr multiplexes panes like tmux but swallows bare OSC 9 / OSC 99 and
90
+ * has no DCS passthrough envelope, and its bell relay does not flag a
91
+ * backgrounded tab — so without this branch a backgrounded pane gets no signal
92
+ * at all that the agent finished or is waiting for input.
93
+ *
94
+ * `sound` maps the notification kind onto what Herdr offers: a question waiting
95
+ * on the user and a turn that stopped with an error both need the human and
96
+ * ring `request`, a settled turn rings `done`, anything else stays
97
+ * silent. Returns whether Herdr owns delivery, so every existing terminal
98
+ * fallback is preserved when the pane id is absent or the binary is missing.
99
+ */
100
+ function sendHerdrNotification(message: string | TerminalNotification, env: NodeJS.ProcessEnv = Bun.env): boolean {
101
+ // Pane-only detection, like `isInsideHerdr`: an env-sanitizing launcher can
102
+ // drop HERDR_ENV and keep the pane identity, and that pane can still be
103
+ // backgrounded. The pane id itself is what the CLI needs, so it stays required.
104
+ if (!isInsideHerdr(env)) return false;
105
+ const paneId = env.HERDR_PANE_ID?.trim();
106
+ if (!paneId || !HERDR_PANE_ID_PATTERN.test(paneId)) return false;
107
+
108
+ const parsed = notificationTitleAndBody(message);
109
+ const title = HERDR_USAGE_TOKENS.has(parsed.title) ? CMUX_NOTIFICATION_TITLE : parsed.title;
110
+ const body = parsed.body;
111
+ const kinds = typeof message === "string" ? [] : [message.type ?? []].flat();
112
+ const sound =
113
+ kinds.includes("ask") || kinds.includes("error") ? "request" : kinds.includes("completion") ? "done" : "none";
114
+ try {
115
+ const child = Bun.spawn({
116
+ cmd: ["herdr", "notification", "show", title, "--body", body, "--sound", sound],
117
+ stdin: "ignore",
118
+ stdout: "ignore",
119
+ stderr: "ignore",
120
+ });
121
+ child.unref();
122
+ } catch {
123
+ // A missing herdr binary leaves delivery to the existing terminal fallback.
124
+ return false;
125
+ }
126
+ return true;
127
+ }
128
+
75
129
  function hasNeedleBefore(line: string, needle: string, limit: number): boolean {
76
130
  const index = line.indexOf(needle);
77
131
  return index !== -1 && index + needle.length <= limit;
@@ -159,6 +213,11 @@ export class TerminalInfo {
159
213
 
160
214
  sendNotification(message: string | TerminalNotification): void {
161
215
  if (isNotificationSuppressed() || isTerminalHeadless()) return;
216
+ // Innermost surface first. A Herdr pane launched inside a cmux surface
217
+ // inherits both `HERDR_PANE_ID` and the outer `CMUX_SURFACE_ID`; routing to
218
+ // cmux there would flag the containing surface and leave the backgrounded
219
+ // Herdr pane — the one actually waiting — without a sound or a marker.
220
+ if (sendHerdrNotification(message)) return;
162
221
  if (sendCmuxNotification(message)) return;
163
222
  const formatted = this.formatNotification(message);
164
223
  // Under tmux, terminals whose notify protocol is OSC 9 / OSC 99 would