@d3lm/pr-stats 0.2.14 → 0.2.16

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/README.md CHANGED
@@ -125,11 +125,13 @@ The Desktop notifications row in the settings dialog makes the TUI send a deskto
125
125
  }
126
126
  ```
127
127
 
128
- Notifications go through the terminal itself when it supports a notification escape sequence, which covers iTerm2, Kitty, Ghostty, WezTerm, and most VTE-based terminals like GNOME Terminal. The terminal posts the notification under its own notification permission and the sequence travels through SSH, so this path needs no setup. Some terminals only show the banner while their window is unfocused. Terminals without such support get the platform's own command instead, `osascript` on macOS and `notify-send` on Linux. A notification that fails to send reports in the footer, for example when `notify-send` is missing on a Linux machine.
128
+ Notifications go through the terminal itself when it supports a notification escape sequence, which covers iTerm2, Kitty, Ghostty, WezTerm, and most VTE-based terminals like GNOME Terminal. The terminal posts the notification under its own notification permission and the sequence travels through SSH, so this path needs no setup beyond what the terminal asks for. Some terminals only show the banner while their window is unfocused. Terminals without such support get the platform's own command instead, the bundled `pr-stats.app` helper on macOS and `notify-send` on Linux. A notification that fails to send reports in the footer, for example when `notify-send` is missing on a Linux machine.
129
129
 
130
- The "Notification channel" row picks the path, and the choice persists as `notifyChannel` with the values `auto`, `terminal`, and `command`. On `auto` the TUI tries the terminal first and falls back to the platform command, while the other two force one path. The dialog shows the `command` value as the command's name on this platform. Forcing the command pays off inside editor terminals like the one in VS Code, which render the terminal path as a small in-editor toast instead of a system notification. The "Send test notification" row below it names the channel the next send takes and sends a sample notification, so you can check that your desktop displays it before relying on it.
130
+ The "Notification channel" row picks the path, and the choice persists as `notifyChannel` with the values `auto`, `terminal`, `command`, and `bell`. On `auto` the TUI tries the terminal first and falls back to the platform command, `terminal` and `command` force one of those paths, and `bell` rings the terminal bell instead of posting any text. The dialog shows the `command` value as the command's name on this platform. Forcing the command pays off inside editor terminals like the one in VS Code, which render the terminal path as a small in-editor toast instead of a system notification. The bell reaches every terminal, including the ones that swallow notifications, and each terminal turns it into its own signal, a sound, a flashing screen, a bell icon on the tab, or a badge on the dock icon. The "Send test notification" row below the channel names the channel the next send takes and sends a sample notification, so you can check that your desktop displays it before relying on it.
131
131
 
132
- The macOS fallback posts through the built-in Script Editor, and since macOS 15 those notifications stay invisible until Script Editor holds notification permission, without any prompt appearing. To grant it once, open the Script Editor app, run the one-line script `display notification "test"`, and allow the prompt that appears. Script Editor then shows up under Notifications in the System Settings like any other app.
132
+ Two terminals accept the notification sequence without showing anything, and the test row warns about both. iTerm2 drops it until the profile has "Notification Center Alerts" turned on under Settings → Profiles → Terminal, which is off by default. Behind the "Filter Alerts…" button next to it, "Send escape sequence-generated alerts" has to stay on as well, and macOS asks for iTerm2's notification permission on the first send. iTerm2 prefixes the banner with the session name unless its advanced setting "Omit session identifier from notifications" is on. Apple Terminal never implemented the sequence at all, so `auto` skips the terminal there and goes straight to the platform command.
133
+
134
+ On macOS the command path runs a small app bundle that ships inside the package, `dist/pr-stats.app`, so the banner carries the pr-stats name and icon and the app gets its own entry under System Settings › Notifications. The first send asks for permission the way any app does, and the helper waits for your answer, because macOS treats a prompt that goes unanswered until the requesting app quits as a refusal. Should notifications end up turned off, the footer says so, and the switch to turn them back on sits under System Settings › Notifications › pr-stats. The helper runs on macOS 13 and later on both Apple silicon and Intel Macs. A source checkout without the bundle falls back to `osascript`, whose notifications post under Script Editor and stay invisible since macOS 15 until Script Editor holds notification permission, which the Script Editor app grants after running the one-line script `display notification "test"` once.
133
135
 
134
136
  ## Theming
135
137
 
@@ -152,6 +154,8 @@ The `preset` key names the active theme, one of `default`, `green`, `blue`, `pur
152
154
 
153
155
  The TUI runs with `bun tui/main.tsx`, or under Node with `pnpm tui:node`. The `--debug tui/testdata` flag serves canned data from the fake `gh` binary in that directory instead of fetching from GitHub, and `bun test tui` drives the whole pipeline against it. `pnpm build` bundles everything into `dist/`, where `tui.mjs` is the launcher and `tui-app.mjs` is the app bundle it loads.
154
156
 
157
+ The macOS notification helper lives in `native/macos` as a Swift source, an `Info.plist`, and the icon PNG, and `pnpm build:notifier` compiles it into `dist/pr-stats.app` as a universal, ad-hoc signed bundle. The build needs the Xcode command line tools and only runs on macOS, so the publish workflow builds the bundle on a macOS runner and adds it to the package before publishing. Without the bundle the TUI falls back to `osascript`, so the rest of the development setup does not depend on it. The bundle identifier `dev.d3lm.pr-stats.notifier` keys the notification permission, so it must stay the same across releases.
158
+
155
159
  ## License
156
160
 
157
161
  OpenTUI is licensed under the [MIT License](LICENSE).
@@ -0,0 +1,34 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>CFBundleDevelopmentRegion</key>
6
+ <string>en</string>
7
+ <key>CFBundleDisplayName</key>
8
+ <string>pr-stats</string>
9
+ <key>CFBundleExecutable</key>
10
+ <string>pr-stats-notifier</string>
11
+ <key>CFBundleIconFile</key>
12
+ <string>AppIcon</string>
13
+ <key>CFBundleIdentifier</key>
14
+ <string>dev.d3lm.pr-stats.notifier</string>
15
+ <key>CFBundleInfoDictionaryVersion</key>
16
+ <string>6.0</string>
17
+ <key>CFBundleName</key>
18
+ <string>pr-stats</string>
19
+ <key>CFBundlePackageType</key>
20
+ <string>APPL</string>
21
+ <key>CFBundleShortVersionString</key>
22
+ <string>0.2.16</string>
23
+ <key>CFBundleVersion</key>
24
+ <string>0.2.16</string>
25
+ <key>LSMinimumSystemVersion</key>
26
+ <string>13.0</string>
27
+ <key>LSUIElement</key>
28
+ <true/>
29
+ <key>NSHighResolutionCapable</key>
30
+ <true/>
31
+ <key>NSPrincipalClass</key>
32
+ <string>NSApplication</string>
33
+ </dict>
34
+ </plist>
@@ -0,0 +1,128 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>files</key>
6
+ <dict>
7
+ <key>Resources/AppIcon.icns</key>
8
+ <data>
9
+ zC/K7ZKrclIys50SVpETzoH69zo=
10
+ </data>
11
+ </dict>
12
+ <key>files2</key>
13
+ <dict>
14
+ <key>Resources/AppIcon.icns</key>
15
+ <dict>
16
+ <key>hash2</key>
17
+ <data>
18
+ YNQOX7kwkBCY5/mW8PxQjE1wMJeXJdfF0dhZ3n7hBAo=
19
+ </data>
20
+ </dict>
21
+ </dict>
22
+ <key>rules</key>
23
+ <dict>
24
+ <key>^Resources/</key>
25
+ <true/>
26
+ <key>^Resources/.*\.lproj/</key>
27
+ <dict>
28
+ <key>optional</key>
29
+ <true/>
30
+ <key>weight</key>
31
+ <real>1000</real>
32
+ </dict>
33
+ <key>^Resources/.*\.lproj/locversion.plist$</key>
34
+ <dict>
35
+ <key>omit</key>
36
+ <true/>
37
+ <key>weight</key>
38
+ <real>1100</real>
39
+ </dict>
40
+ <key>^Resources/Base\.lproj/</key>
41
+ <dict>
42
+ <key>weight</key>
43
+ <real>1010</real>
44
+ </dict>
45
+ <key>^version.plist$</key>
46
+ <true/>
47
+ </dict>
48
+ <key>rules2</key>
49
+ <dict>
50
+ <key>.*\.dSYM($|/)</key>
51
+ <dict>
52
+ <key>weight</key>
53
+ <real>11</real>
54
+ </dict>
55
+ <key>^(.*/)?\.DS_Store$</key>
56
+ <dict>
57
+ <key>omit</key>
58
+ <true/>
59
+ <key>weight</key>
60
+ <real>2000</real>
61
+ </dict>
62
+ <key>^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/</key>
63
+ <dict>
64
+ <key>nested</key>
65
+ <true/>
66
+ <key>weight</key>
67
+ <real>10</real>
68
+ </dict>
69
+ <key>^.*</key>
70
+ <true/>
71
+ <key>^Info\.plist$</key>
72
+ <dict>
73
+ <key>omit</key>
74
+ <true/>
75
+ <key>weight</key>
76
+ <real>20</real>
77
+ </dict>
78
+ <key>^PkgInfo$</key>
79
+ <dict>
80
+ <key>omit</key>
81
+ <true/>
82
+ <key>weight</key>
83
+ <real>20</real>
84
+ </dict>
85
+ <key>^Resources/</key>
86
+ <dict>
87
+ <key>weight</key>
88
+ <real>20</real>
89
+ </dict>
90
+ <key>^Resources/.*\.lproj/</key>
91
+ <dict>
92
+ <key>optional</key>
93
+ <true/>
94
+ <key>weight</key>
95
+ <real>1000</real>
96
+ </dict>
97
+ <key>^Resources/.*\.lproj/locversion.plist$</key>
98
+ <dict>
99
+ <key>omit</key>
100
+ <true/>
101
+ <key>weight</key>
102
+ <real>1100</real>
103
+ </dict>
104
+ <key>^Resources/Base\.lproj/</key>
105
+ <dict>
106
+ <key>weight</key>
107
+ <real>1010</real>
108
+ </dict>
109
+ <key>^[^/]+$</key>
110
+ <dict>
111
+ <key>nested</key>
112
+ <true/>
113
+ <key>weight</key>
114
+ <real>10</real>
115
+ </dict>
116
+ <key>^embedded\.provisionprofile$</key>
117
+ <dict>
118
+ <key>weight</key>
119
+ <real>20</real>
120
+ </dict>
121
+ <key>^version\.plist$</key>
122
+ <dict>
123
+ <key>weight</key>
124
+ <real>20</real>
125
+ </dict>
126
+ </dict>
127
+ </dict>
128
+ </plist>
package/dist/tui-app.mjs CHANGED
@@ -279,7 +279,7 @@ function percentile(sorted, percent) {
279
279
  }
280
280
 
281
281
  // src/settings.ts
282
- var NOTIFY_CHANNELS = ["auto", "terminal", "command"];
282
+ var NOTIFY_CHANNELS = ["auto", "terminal", "command", "bell"];
283
283
  var DEFAULT_RELOAD_INTERVAL = "10m";
284
284
  var MAX_RELOAD_INTERVAL_MS = 24 * 60 * 60 * 1e3;
285
285
  function reloadIntervalMs(input) {
@@ -348,7 +348,7 @@ function loadSettings() {
348
348
  throw new CliError(`"notifications" in ${settingsFile()} must be true or false`);
349
349
  }
350
350
  if (settings.notifyChannel !== void 0 && !NOTIFY_CHANNELS.includes(settings.notifyChannel)) {
351
- throw new CliError(`"notifyChannel" in ${settingsFile()} must be "auto", "terminal", or "command"`);
351
+ throw new CliError(`"notifyChannel" in ${settingsFile()} must be "auto", "terminal", "command", or "bell"`);
352
352
  }
353
353
  current = settings;
354
354
  return current;
@@ -2070,7 +2070,7 @@ function buildPendingReviewView(raw, repo = null, grouped = false) {
2070
2070
  empty: null,
2071
2071
  sections: [
2072
2072
  ...awaiting.length === 0 ? [] : [sectionOf(`Awaiting your review (n=${awaiting.length})`, awaiting)],
2073
- ...reviewing.length === 0 ? [] : [sectionOf(`Reviewing (n=${reviewing.length})`, reviewing)]
2073
+ ...reviewing.length === 0 ? [] : [sectionOf(`Reviewed (n=${reviewing.length})`, reviewing)]
2074
2074
  ]
2075
2075
  };
2076
2076
  }
@@ -4095,7 +4095,7 @@ var SETTINGS = [
4095
4095
  key: "notifyChannel",
4096
4096
  section: "Notifications",
4097
4097
  label: "Notification channel",
4098
- hint: "auto posts through the terminal and falls back to the platform command \xB7 the other values force one channel"
4098
+ hint: "auto tries the terminal, then the platform command \xB7 the others force one path \xB7 bell rings the terminal bell"
4099
4099
  },
4100
4100
  {
4101
4101
  key: "testNotification",
@@ -4169,28 +4169,104 @@ var CACHE_MESSAGES = {
4169
4169
 
4170
4170
  // src/tui/utils/notify.ts
4171
4171
  import { spawn } from "node:child_process";
4172
+ import { existsSync } from "node:fs";
4173
+ import { fileURLToPath } from "node:url";
4174
+ function notificationBoundary(renderer2) {
4175
+ const raw = renderer2;
4176
+ return {
4177
+ get capabilities() {
4178
+ return renderer2.capabilities;
4179
+ },
4180
+ triggerNotification: (message, title) => renderer2.triggerNotification(message, title),
4181
+ writeOut: (data) => {
4182
+ if (typeof raw.writeOut === "function") {
4183
+ raw.writeOut(data);
4184
+ } else {
4185
+ process.stdout.write(data);
4186
+ }
4187
+ }
4188
+ };
4189
+ }
4190
+ var BELL = "\x07";
4191
+ function terminalQuirk(renderer2) {
4192
+ const name = renderer2.capabilities?.terminal.name ?? "";
4193
+ if (/iterm/i.test(name)) {
4194
+ return "iterm";
4195
+ }
4196
+ if (/apple_terminal|terminal\.app/i.test(name)) {
4197
+ return "apple-terminal";
4198
+ }
4199
+ return null;
4200
+ }
4172
4201
  function createNotifier(renderer2, channel) {
4173
4202
  return (title, body, onError) => {
4174
- if (channel !== "command" && renderer2.triggerNotification(body, title)) {
4203
+ if (channel === "bell") {
4204
+ renderer2.writeOut(BELL);
4205
+ return;
4206
+ }
4207
+ const tryTerminal = channel === "terminal" || channel === "auto" && terminalQuirk(renderer2) !== "apple-terminal";
4208
+ if (tryTerminal && renderer2.triggerNotification(body, title)) {
4175
4209
  return;
4176
4210
  }
4177
4211
  if (channel === "terminal") {
4178
- onError("the terminal has no notification support, switch the channel to auto or the platform command");
4212
+ onError("the terminal has no notification support, switch the channel to auto, the platform command, or bell");
4179
4213
  return;
4180
4214
  }
4181
4215
  sendNotification(title, body, onError);
4182
4216
  };
4183
4217
  }
4184
4218
  function notificationChannel(renderer2, channel) {
4185
- if (channel === "terminal" || channel === "auto" && renderer2.capabilities?.notifications === true) {
4219
+ if (channel === "bell" || channel === "terminal") {
4220
+ return channel;
4221
+ }
4222
+ if (channel === "auto" && renderer2.capabilities?.notifications === true && terminalQuirk(renderer2) !== "apple-terminal") {
4186
4223
  return "terminal";
4187
4224
  }
4188
4225
  return notificationTool();
4189
4226
  }
4190
- function notificationTool() {
4227
+ function notificationCaveat(renderer2, channel) {
4228
+ if (channel === "command" || channel === "bell") {
4229
+ return null;
4230
+ }
4231
+ switch (terminalQuirk(renderer2)) {
4232
+ case "iterm": {
4233
+ return "iTerm2 shows these only once Notification Center Alerts is on under Settings \u203A Profiles \u203A Terminal";
4234
+ }
4235
+ case "apple-terminal": {
4236
+ return channel === "auto" ? "Terminal.app ignores terminal notifications, so auto takes the platform command here" : "Terminal.app ignores terminal notifications \xB7 switch the channel to auto, command, or bell";
4237
+ }
4238
+ default: {
4239
+ return null;
4240
+ }
4241
+ }
4242
+ }
4243
+ var HELPER_BINARY = "pr-stats.app/Contents/MacOS/pr-stats-notifier";
4244
+ var HELPER_NAME = "pr-stats.app";
4245
+ var HELPER_DENIED_EXIT = 3;
4246
+ var PROMPT_GRACE_MS = 5e3;
4247
+ var helperLookup;
4248
+ function notificationHelper() {
4249
+ if (helperLookup === void 0) {
4250
+ helperLookup = locateHelper();
4251
+ }
4252
+ return helperLookup;
4253
+ }
4254
+ function locateHelper() {
4255
+ if (process.platform !== "darwin") {
4256
+ return null;
4257
+ }
4258
+ for (const base of ["./", "../../../dist/"]) {
4259
+ const path = fileURLToPath(new URL(base + HELPER_BINARY, import.meta.url));
4260
+ if (existsSync(path)) {
4261
+ return path;
4262
+ }
4263
+ }
4264
+ return null;
4265
+ }
4266
+ function notificationTool(helper = notificationHelper()) {
4191
4267
  switch (process.platform) {
4192
4268
  case "darwin": {
4193
- return "osascript";
4269
+ return helper === null ? "osascript" : HELPER_NAME;
4194
4270
  }
4195
4271
  case "linux": {
4196
4272
  return "notify-send";
@@ -4203,9 +4279,12 @@ function notificationTool() {
4203
4279
  function escapeMarkup(text) {
4204
4280
  return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
4205
4281
  }
4206
- function notifyCommand(title, body) {
4282
+ function notifyCommand(title, body, helper = notificationHelper()) {
4207
4283
  switch (process.platform) {
4208
4284
  case "darwin": {
4285
+ if (helper !== null) {
4286
+ return { command: helper, args: [title, body], name: HELPER_NAME };
4287
+ }
4209
4288
  return {
4210
4289
  command: "osascript",
4211
4290
  args: [
@@ -4217,31 +4296,54 @@ function notifyCommand(title, body) {
4217
4296
  "end run",
4218
4297
  title,
4219
4298
  body
4220
- ]
4299
+ ],
4300
+ name: "osascript"
4221
4301
  };
4222
4302
  }
4223
4303
  case "linux": {
4224
- return { command: "notify-send", args: ["--app-name=pr-stats", "--", title, escapeMarkup(body)] };
4304
+ return {
4305
+ command: "notify-send",
4306
+ args: ["--app-name=pr-stats", "--", title, escapeMarkup(body)],
4307
+ name: "notify-send"
4308
+ };
4225
4309
  }
4226
4310
  default: {
4227
4311
  return null;
4228
4312
  }
4229
4313
  }
4230
4314
  }
4231
- function sendNotification(title, body, onError) {
4232
- const spec = notifyCommand(title, body);
4315
+ var spawnDetached = (command, args) => spawn(command, args, { stdio: "ignore", detached: true });
4316
+ var runningHelpers = /* @__PURE__ */ new Set();
4317
+ function sendNotification(title, body, onError, spawnProcess = spawnDetached, helper = notificationHelper()) {
4318
+ const spec = notifyCommand(title, body, helper);
4233
4319
  if (spec === null) {
4234
4320
  onError("desktop notifications are not supported on this platform");
4235
4321
  return;
4236
4322
  }
4237
- const child = spawn(spec.command, spec.args, { stdio: "ignore", detached: true });
4323
+ const isHelper = spec.name === HELPER_NAME;
4324
+ const run = { started: Date.now() };
4325
+ if (isHelper && [...runningHelpers].some(({ started }) => run.started - started > PROMPT_GRACE_MS)) {
4326
+ onError("the notification permission prompt for pr-stats is still waiting for an answer");
4327
+ return;
4328
+ }
4329
+ const child = spawnProcess(spec.command, spec.args);
4330
+ if (isHelper) {
4331
+ runningHelpers.add(run);
4332
+ }
4238
4333
  child.on("error", (error) => {
4334
+ runningHelpers.delete(run);
4239
4335
  onError(`could not send the notification (${error.message})`);
4240
4336
  });
4241
4337
  child.on("exit", (code) => {
4242
- if (code !== null && code !== 0) {
4243
- onError(`could not send the notification (${spec.command} exited with ${code})`);
4338
+ runningHelpers.delete(run);
4339
+ if (code === null || code === 0) {
4340
+ return;
4341
+ }
4342
+ if (isHelper && code === HELPER_DENIED_EXIT) {
4343
+ onError("notifications for pr-stats are turned off, allow them under System Settings \u203A Notifications \u203A pr-stats");
4344
+ return;
4244
4345
  }
4346
+ onError(`could not send the notification (${spec.name} exited with ${code})`);
4245
4347
  });
4246
4348
  child.unref();
4247
4349
  }
@@ -4273,9 +4375,11 @@ function SettingsModal({
4273
4375
  onSubmit
4274
4376
  }) {
4275
4377
  const message = cacheAction === null ? null : CACHE_MESSAGES[cacheAction];
4276
- const renderer2 = useRenderer2();
4378
+ const renderer2 = notificationBoundary(useRenderer2());
4277
4379
  const channelValue = notifyChannel2 === "command" ? notificationTool() ?? "unsupported" : notifyChannel2;
4278
4380
  const deliveryValue = notificationChannel(renderer2, notifyChannel2) ?? "unsupported";
4381
+ const caveat = SETTINGS[selected].key === "testNotification" ? notificationCaveat(renderer2, notifyChannel2) : null;
4382
+ const bottomLine = error !== null ? { text: error, fg: theme.error } : message !== null ? { text: message.text, fg: message.warn ? theme.warn : theme.muted } : caveat !== null ? { text: caveat, fg: theme.warn } : { text: SETTINGS[selected].hint, fg: theme.muted };
4279
4383
  return /* @__PURE__ */ jsxs11(ModalFrame, { title: "Settings", children: [
4280
4384
  SECTIONS2.map((section) => /* @__PURE__ */ jsxs11("box", { flexDirection: "column", marginBottom: 1, children: [
4281
4385
  /* @__PURE__ */ jsx12("text", { wrapMode: "none", fg: theme.accent, marginLeft: 2, children: section.title }),
@@ -4302,17 +4406,7 @@ function SettingsModal({
4302
4406
  ) }, setting.key);
4303
4407
  })
4304
4408
  ] }, section.title)),
4305
- /* @__PURE__ */ jsx12(
4306
- "text",
4307
- {
4308
- wrapMode: "word",
4309
- height: 2,
4310
- fg: error !== null ? theme.error : message?.warn ? theme.warn : theme.muted,
4311
- marginLeft: 2,
4312
- marginRight: 2,
4313
- children: error ?? message?.text ?? SETTINGS[selected].hint
4314
- }
4315
- )
4409
+ /* @__PURE__ */ jsx12("text", { wrapMode: "word", height: 2, fg: bottomLine.fg, marginLeft: 2, marginRight: 2, children: bottomLine.text })
4316
4410
  ] });
4317
4411
  }
4318
4412
  function SettingValue({
@@ -4782,7 +4876,7 @@ function buildSizeRepoOptions(raw) {
4782
4876
  }
4783
4877
  function pendingDetail(counts) {
4784
4878
  const awaiting = `${counts.awaiting} ${counts.awaiting === 1 ? "PR" : "PRs"} awaiting your review`;
4785
- return awaiting + (counts.reviewing > 0 ? `, ${counts.reviewing} reviewing` : "");
4879
+ return awaiting + (counts.reviewing > 0 ? `, ${counts.reviewing} reviewed` : "");
4786
4880
  }
4787
4881
  function buildPendingRepoOptions(raw) {
4788
4882
  const countsByRepo = /* @__PURE__ */ new Map();
@@ -6905,7 +6999,10 @@ function App({
6905
6999
  const [notifyChannel2, setNotifyChannel] = useState4(initialNotifyChannel);
6906
7000
  const [copyLinks2, setCopyLinks] = useState4(initialCopyLinks);
6907
7001
  const [themeState, setThemeState] = useState4(initialTheme);
6908
- const defaultNotify = useMemo2(() => createNotifier(renderer2, notifyChannel2), [renderer2, notifyChannel2]);
7002
+ const defaultNotify = useMemo2(
7003
+ () => createNotifier(notificationBoundary(renderer2), notifyChannel2),
7004
+ [renderer2, notifyChannel2]
7005
+ );
6909
7006
  const notifier = notify ?? defaultNotify;
6910
7007
  const copyRow = (row) => {
6911
7008
  copyLink(row.url, (message) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@d3lm/pr-stats",
3
- "version": "0.2.14",
3
+ "version": "0.2.16",
4
4
  "description": "GitHub PR stats in an interactive terminal UI, via the gh CLI or an access token",
5
5
  "type": "module",
6
6
  "author": "Dominic Elm",
@@ -33,6 +33,7 @@
33
33
  "build": "pnpm build:app && pnpm build:bin",
34
34
  "build:app": "esbuild src/tui/main.tsx --bundle --platform=node --format=esm --target=esnext --jsx=automatic --jsx-import-source=@opentui/react --external:@opentui/core --external:@opentui/react --external:react --outfile=dist/tui-app.mjs",
35
35
  "build:bin": "esbuild src/tui/launch.ts --platform=node --format=esm --target=node20 --outfile=dist/tui.mjs",
36
+ "build:notifier": "bash scripts/build-notifier.sh",
36
37
  "tui": "bun src/tui/main.tsx",
37
38
  "tui:node": "pnpm build && node dist/tui.mjs",
38
39
  "check": "tsc --noEmit",