projmux 0.5.1 → 0.5.2

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/docs/cli.md CHANGED
@@ -116,8 +116,10 @@ does not diagnose terminal key delivery; use `projmux setup` for that.
116
116
 
117
117
  ```
118
118
  projmux focus --target SESSION[:WINDOW[.PANE]] [--socket <path>]
119
- [--source ai|status-bar|external|os-notification]
120
- [--kind reply-ready|busy-cleared|segment-click|custom]
119
+ [--source ai|status-bar|external|os-notification|toast]
120
+ [--kind reply-ready|busy-cleared|segment-click|toast-click|custom]
121
+ [--json]
122
+ projmux focus --uri "projmux://focus?pane_id=%N&socket=<path>&source=toast"
121
123
  [--json]
122
124
  ```
123
125
 
@@ -128,6 +130,13 @@ client is attached on that socket, it emits the configured desktop
128
130
  notification instead. `--socket` is explicit; when omitted, the socket is
129
131
  derived from `$TMUX`.
130
132
 
133
+ `--uri` is the entry point used by the WSL Toast click handler (see
134
+ [configuration.md](configuration.md#toast-click-handler-wsl--windows-terminal)).
135
+ The pane id from the URI is resolved to a `SESSION:WINDOW.%paneID` target
136
+ via `tmux display-message`, and the URI's `socket` overrides any
137
+ `--socket` flag so the click round-trips back to the right tmux server.
138
+ `--uri` and `--target` are mutually exclusive.
139
+
131
140
  Exit codes:
132
141
 
133
142
  - `0` — focused (or the notify-only fallback fired).
@@ -108,6 +108,8 @@ configured key opens and closes the popup.
108
108
  | `PROJMUX_MANAGED_ROOTS` | Search-root override. Uses the OS-native path-list separator and takes priority over the saved workdirs file and default weak probes. |
109
109
  | `TMUX_SESSIONIZER_ROOTS` | Legacy alias still honored at runtime for managed roots. |
110
110
  | `PROJMUX_NOTIFY_HOOK` | External executable that receives AI desktop notifications instead of the built-in Linux/WSL sender. |
111
+ | `PROJMUX_DESKTOP_NOTIFY_MODE` | OS desktop notification mode override. `none` / `notify` / `raise` (case insensitive). When set, this takes priority over every other resolution rung. The in-app notify queue is not affected. |
112
+ | `PROJMUX_DESKTOP_NOTIFY` | Legacy on/off override kept for backward compatibility. `on` maps to `notify`, `off` maps to `none`. Honored only when `PROJMUX_DESKTOP_NOTIFY_MODE` is unset. |
111
113
  | `PROJMUX_WSL_TOAST_ICON_DIR` | Directory used when copying the WSL toast icon into a Windows-readable path. |
112
114
  | `PROJMUX_USAGE_STATE_DIR` | Override directory for AI usage snapshots. Defaults to `<state>/projmux/usage`. Point this at a synced directory to share authoritative usage across machines. |
113
115
  | `PROJMUX_USAGE_DEBUG` | When non-empty, prints adapter errors from `projmux status usage` to stderr. |
@@ -149,9 +151,119 @@ When the hook is set, projmux invokes it with positional arguments:
149
151
  summary body urgency app-name tag group icon-path
150
152
  ```
151
153
 
154
+ The app-name argument is `com.crevisse.projmux` (reverse-domain id used as
155
+ the Linux `--app-name`, the macOS sender label, and the Windows
156
+ `AppUserModelID`). Earlier releases used `projmux.TmuxCodex`; the new id
157
+ ships with a one-shot Windows cleanup that removes the legacy Start Menu
158
+ shortcut and registry entry the first time projmux runs.
159
+
160
+ ### Desktop notification mode
161
+
162
+ The OS-level dispatch carries three modes. The in-app notify queue, the
163
+ statusbar segment, and the attention badge stay live regardless of which
164
+ mode is active — only the toast / notify-send / auto-raise fan-out is
165
+ gated here.
166
+
167
+ | Mode | On push | On click |
168
+ | --- | --- | --- |
169
+ | `none` | no toast | n/a |
170
+ | `notify` | toast / notify-send fires | toast click invokes `projmux focus --uri` via the `projmux://` handler |
171
+ | `raise` | toast / notify-send fires AND the host terminal is auto-raised via the osfocus chain | same as `notify` — click is always available |
172
+
173
+ Click activation is always wired. The `projmux://` URI handler is
174
+ registered on the first Notify of each tmux server (gated by the
175
+ `@projmux_uri_protocol_registered_v2` marker) regardless of mode. The
176
+ mode only controls whether a toast fires at all and whether to follow it
177
+ up with an on-push auto-raise.
178
+
179
+ Resolution order (highest priority first):
180
+
181
+ 1. `PROJMUX_DESKTOP_NOTIFY_MODE` env (`none` / `notify` / `raise`).
182
+ 2. `PROJMUX_DESKTOP_NOTIFY` env (legacy `on` / `off`; `on` → `notify`,
183
+ `off` → `none`).
184
+ 3. Tmux global option `@projmux_desktop_notify_mode`.
185
+ 4. Tmux global option `@projmux_desktop_notify` (legacy `1` / `0`, same
186
+ mapping as the env above).
187
+ 5. Default = `raise` when running inside WSL with `$WT_SESSION` set
188
+ (Windows Terminal × WSL is the measured-working cell for osfocus
189
+ raise today); otherwise `notify`.
190
+
191
+ Migration is intentionally read-time. Users with the previous legacy
192
+ toggle set keep their behavior — `@projmux_desktop_notify=0` resolves to
193
+ `none`, `@projmux_desktop_notify=1` resolves to `notify`. The first
194
+ Settings press through the new row writes the new key and the legacy key
195
+ goes unused. No eager rewrite of tmux state.
196
+
197
+ Toggle from Settings > AI Settings > `Desktop notifications`. The
198
+ Settings info row labels the effective source as `env`, `env (legacy)`,
199
+ `setting`, `setting (legacy)`, or `default` so users see which rung of
200
+ the cascade pinned the value.
201
+
152
202
  Hook details for new-session lifecycle hooks and project-local
153
203
  `.projmux/config.toml` live in [Hooks](hooks.md).
154
204
 
205
+ ### Toast click handler (WSL + Windows Terminal)
206
+
207
+ On WSL, projmux registers a `projmux://` URL scheme on the Windows side
208
+ the first time a Toast is dispatched on each tmux server. Clicking the
209
+ toast hands control back to projmux inside WSL via the registered command:
210
+
211
+ ```text
212
+ wsl.exe -d $WSL_DISTRO_NAME --exec <absolute-path-to-projmux> focus --uri "%1"
213
+ ```
214
+
215
+ For click activation to work in our unpackaged Win32 setup, four
216
+ conditions must hold simultaneously — all of them are arranged by the
217
+ notify path so users do not configure anything:
218
+
219
+ 1. No COM Toast Activator is registered. The shortcut writes only
220
+ `PKEY_AppUserModel_ID` (pid=5) and intentionally omits
221
+ `PKEY_AppUserModel_ToastActivatorCLSID` (pid=26). When a COM activator
222
+ is registered alongside the AppID, Windows tries COM first, silently
223
+ fails for unpackaged exes, and does *not* fall through to the launch
224
+ URI. Stripping the COM side makes Windows ShellExecute the launch URI
225
+ on click.
226
+ 2. The Start Menu shortcut exists with the AppID set. The shortcut is
227
+ never launched — it is a property bag so the toast can route under
228
+ the right DisplayName + icon.
229
+ 3. The shortcut target is `cmd.exe /c exit`. Earlier code used
230
+ `powershell.exe -WindowStyle Hidden -Command exit`; Windows Defender
231
+ silently quarantines such shortcuts moments after creation, which
232
+ leaves no AppID-tagged shortcut and breaks both the routing and the
233
+ click path. `cmd.exe /c exit` is treated as benign and survives.
234
+ 4. The WSL handler command uses `--exec`, not `--`. `wsl.exe -- <cmd>`
235
+ routes its tail through the user's login shell, which parses `&`
236
+ query-string separators as background-job operators (zsh emits
237
+ `parse error near '&'`). `--exec` skips the shell and invokes the
238
+ binary directly. The absolute WSL filesystem path to the binary is
239
+ captured at registration so PATH does not need to be populated under
240
+ `--exec`.
241
+
242
+ The URI carries the originating pane id and tmux socket so the click
243
+ round-trips back to the exact pane that fired the notification, which
244
+ the `projmux focus` path then redirects via `tmux switch-client`.
245
+
246
+ Registration markers and the writes involved:
247
+
248
+ - Registry keys (HKCU): `SOFTWARE\Classes\projmux\(Default)`,
249
+ `SOFTWARE\Classes\projmux\URL Protocol`, and
250
+ `SOFTWARE\Classes\projmux\shell\open\command\(Default)`.
251
+ - tmux user-option marker `@projmux_uri_protocol_registered_v2` records that
252
+ registration has been attempted on this server so the script runs at most
253
+ once per server boot. (The v1 marker `@projmux_uri_protocol_registered`
254
+ was bumped when the registry command switched to `--exec`; existing v1
255
+ users re-register transparently on the next Notify after upgrade and the
256
+ orphaned v1 key requires no cleanup.)
257
+
258
+ Limitations:
259
+
260
+ - The handler captures the user's current `WSL_DISTRO_NAME` at registration
261
+ time. Users running multiple WSL distros get one handler bound to the
262
+ first distro that fired a toast — clicks from a different distro's toast
263
+ will route back through the first distro. Tier-2 follow-up.
264
+ - WSL2 cold-start latency: the first click after a long idle goes through
265
+ WSL boot and typically takes 2-3s to surface the focus on the pane.
266
+
155
267
  ## Usage Tracking
156
268
 
157
269
  `projmux usage` and the status-bar usage segment store snapshots under:
@@ -0,0 +1,318 @@
1
+ # Notify OS Focus Spike
2
+
3
+ This document is the on-floor matrix for the "Notify 시 터미널 OS 포커스"
4
+ roadmap item. The first step of that roadmap is explicitly a **spike with no
5
+ code** — fill in the tables below with real measurements, then make the
6
+ follow-on decisions before any adapter code lands.
7
+
8
+ The full design lives in the project notes (`로드맵 디테일 - Notify 시 터미널 OS
9
+ 포커스`). This file is the in-tree, fill-in-the-blanks transcription so the
10
+ spike trail lives next to the code it informs.
11
+
12
+ ## Background and scope
13
+
14
+ `projmux focus` (see [`internal/app/focus.go`](../internal/app/focus.go))
15
+ currently handles tmux pane focus only. Once a tmux client is already attached
16
+ and on screen, it can resolve a target session/window/pane, redirect an
17
+ existing client if one is attached, or emit a desktop notification as a last
18
+ resort. The notify path that produces the entries this focuses on lives in
19
+ [`internal/app/notify.go`](../internal/app/notify.go),
20
+ [`internal/app/notify_producer.go`](../internal/app/notify_producer.go), and
21
+ [`internal/app/notify_reconcile.go`](../internal/app/notify_reconcile.go).
22
+
23
+ The realistic stack for "go to the pane that fired this notification" has four
24
+ layers:
25
+
26
+ 1. **tmux pane** — already solved by `projmux focus`.
27
+ 2. **tmux session / client switch** — partially solved
28
+ (`tmux switch-client -c <tty>`).
29
+ 3. **Terminal app window / tab** — terminal-specific IPC, requires an adapter
30
+ per terminal.
31
+ 4. **OS-level window focus** — raise the app itself to foreground; platform
32
+ policy varies and the result is best-effort.
33
+
34
+ Layers 3 and 4 are the unresolved area and the scope of this spike.
35
+
36
+ ## Trigger mode question
37
+
38
+ Two entry points are possible:
39
+
40
+ - **(a) on-push (automatic)**: `projmux notify push` fires OS-level focus
41
+ immediately when the notification is enqueued.
42
+ - **(b) on-click / keypress (user-initiated)**: extend the existing
43
+ `projmux focus` path so a statusbar / sidebar click goes
44
+ tmux pane focus → terminal tab focus → OS window focus.
45
+
46
+ **Recommendation: (b) is primary, (a) is opt-in only.**
47
+
48
+ - (a) steals the screen while the user is doing something else and is hostile
49
+ UX by default.
50
+ - (b) only runs at the moment the user explicitly says "I want this now," so a
51
+ larger focus action is acceptable.
52
+ - A system notification daemon (libnotify, macOS UserNotificationCenter,
53
+ Windows Toast) is the (a) substitute: the OS shows the notification, the
54
+ user clicks it, and that click drops into the (b) path.
55
+
56
+ Lock the decision once the matrix below is filled in:
57
+
58
+ - [x] Primary trigger mode confirmed as (b).
59
+ - [x] (a) shipped as opt-in only, or deferred entirely. — **deferred**.
60
+ - [x] System notification daemon path adopted in place of (a), or deferred. —
61
+ **deferred**; the WSL toast path already in place covers the (a) substitute
62
+ role for tier-1.
63
+
64
+ ## Terminal × OS matrix
65
+
66
+ Fill the **Result** column with one of `OK`, `Partial`, `No`, plus a one-line
67
+ note. Tick **Tested?** once measured. Candidate commands are pre-filled from
68
+ the roadmap and are starting points, not prescriptions — note in **Result** if
69
+ a different command worked better.
70
+
71
+ | Terminal | OS | Candidate command(s) for window/tab focus | Result | Tested? |
72
+ |---|---|---|---|---|
73
+ | Ghostty | macOS | `osascript -e 'tell application "Ghostty" to activate'` + window-id match | | [ ] |
74
+ | Ghostty | Linux X11 | `wmctrl -ia <wid>` | | [ ] |
75
+ | Ghostty | Linux Wayland | Compositor-specific: gnome `dbus` (`org.gnome.Shell.Eval`), KDE `kdotool`, sway `swaymsg` | | [ ] |
76
+ | Kitty | Any | `kitty @ focus-window --match <expr>` | | [ ] |
77
+ | WezTerm | Any | `wezterm cli activate-pane --pane-id <id>` | | [ ] |
78
+ | Windows Terminal | Windows | `wt -w <id> focus-tab -t <n>` (same instance only) | | [ ] |
79
+ | Windows Terminal | WSL → Windows | WSL interop + `wt.exe -w <id> focus-tab` | OK — `wt.exe -w 0 focus-tab -t 0` raises the WT window (verified WSL2 Ubuntu-24.04, WT host) | [x] |
80
+ | iTerm2 | macOS | AppleScript (`tell application "iTerm" ...`) or Python API | | [ ] |
81
+ | Alacritty | Any | No remote IPC. OS-window focus only — fall back to the table below. | | [ ] |
82
+ | Foot | Linux | `footclient` is limited. OS-window focus only — fall back to the table below. | | [ ] |
83
+ | VS Code embedded | Any | Best-effort via `vscode://` URL handler or `code --command` | | [ ] |
84
+
85
+ ## OS-level window activation matrix
86
+
87
+ Independent of the terminal: "raise this app to the foreground." Fill the
88
+ **Result** column the same way.
89
+
90
+ | OS | Candidate command(s) | Notes | Result | Tested? |
91
+ |---|---|---|---|---|
92
+ | macOS | `osascript -e 'tell application "<AppName>" to activate'` | Stable; no special permission. | | [ ] |
93
+ | Linux X11 | `wmctrl -a <window-name>` or `xdotool windowactivate <wid>` | Requires `wmctrl` / `xdotool` installed; standard. | | [ ] |
94
+ | Linux Wayland (GNOME) | `gdbus call --session --dest org.gnome.Shell --object-path /org/gnome/Shell --method org.gnome.Shell.Eval ...` | Depends on shell version; recent GNOME locks `Eval` outside dev mode. | | [ ] |
95
+ | Linux Wayland (KDE) | `kdotool windowactivate <wid>` | KWin script bridge; requires `kdotool`. | | [ ] |
96
+ | Linux Wayland (sway) | `swaymsg '[con_id=<id>] focus'` | Works for sway / wlroots compositors. | | [ ] |
97
+ | Windows | `SetForegroundWindow` via PowerShell + P/Invoke | Foreground-lock policy: if the target hasn't recently been active, the OS will only flash the taskbar entry. | | [ ] |
98
+ | WSL → Windows | Same as Windows, invoked through `powershell.exe` interop | Inherits Windows foreground-lock. | Partial — raw `SetForegroundWindow` returns `False` under foreground-lock; the `AttachThreadInput` + `BringWindowToTop` + `SetForegroundWindow` workaround does bypass the lock (verified WSL2 Ubuntu-24.04, WT host). Caveat: pairing it with `ShowWindow(SW_RESTORE)` unconditionally restores **maximized** windows to normal size — adapter must `IsIconic`-guard `ShowWindow` so only minimized windows are restored. | [x] |
99
+
100
+ ## Detection signals
101
+
102
+ Each terminal adapter has to detect that it's the right one to run before
103
+ touching IPC. These are the env vars / files we can rely on. The precedent for
104
+ this style of detection is
105
+ [`internal/app/init_ghostty.go`](../internal/app/init_ghostty.go) and
106
+ [`internal/app/init_windows_terminal.go`](../internal/app/init_windows_terminal.go) —
107
+ the new `osfocus` adapters should reuse the same detect-then-dispatch pattern.
108
+
109
+ | Terminal | Signal(s) |
110
+ |---|---|
111
+ | iTerm2 | `TERM_PROGRAM=iTerm.app` (plus `ITERM_SESSION_ID`) |
112
+ | Apple Terminal | `TERM_PROGRAM=Apple_Terminal` |
113
+ | VS Code (embedded) | `TERM_PROGRAM=vscode` (plus `VSCODE_INJECTION`, `VSCODE_PID`) |
114
+ | Windows Terminal | `WT_SESSION`, `WT_PROFILE_ID` |
115
+ | Ghostty | `GHOSTTY_RESOURCES_DIR`, `GHOSTTY_BIN_DIR` |
116
+ | Kitty | `KITTY_PID`, `KITTY_LISTEN_ON`, `KITTY_WINDOW_ID` |
117
+ | WezTerm | `WEZTERM_PANE`, `WEZTERM_EXECUTABLE`, `WEZTERM_UNIX_SOCKET` |
118
+ | Alacritty | `ALACRITTY_LOG`, `ALACRITTY_WINDOW_ID`, `ALACRITTY_SOCKET` |
119
+ | Foot | `FOOT_SOCK_*`, `FOOTCLIENT` (varies by build) |
120
+
121
+ Notes for adapter authors (from Test 1, WSL2 Ubuntu-24.04 inside Windows
122
+ Terminal):
123
+
124
+ - Inside tmux, `TERM_PROGRAM` is rewritten to `tmux` and masks the host
125
+ terminal. Detect Windows Terminal via `WT_SESSION` / `WT_PROFILE_ID`
126
+ (forwarded into WSL via `WSLENV`), not `TERM_PROGRAM`.
127
+ - `WAYLAND_DISPLAY=wayland-0` is present in WSL2 via WSLg. Do **not** treat it
128
+ as a "running on native Wayland" signal — gate Wayland adapters on a real
129
+ Wayland session (e.g. absence of `WSL_INTEROP`).
130
+ - `WSL_INTEROP` + `WSL_DISTRO_NAME` reliably identify WSL2; use them to gate
131
+ the WSL → Windows adapter branch.
132
+
133
+ Notes from Test 2 (WSL → Windows OS-level activation):
134
+
135
+ - Raw `SetForegroundWindow` is unreliable under Windows foreground-lock; it
136
+ returns `False` whenever the calling process isn't already foreground (the
137
+ common case for a background-triggered focus).
138
+ - The `AttachThreadInput(fgThread, thisThread, true)` + `BringWindowToTop` +
139
+ `SetForegroundWindow` + detach combo bypasses the lock and does raise the
140
+ target window in this configuration.
141
+ - `ShowWindow(handle, SW_RESTORE)` un-maximizes maximized windows as a side
142
+ effect. The adapter must `IsIconic`-guard the call so it only restores
143
+ minimized windows; do not call `ShowWindow` unconditionally.
144
+ - For Windows Terminal specifically, prefer the Test 1 path
145
+ (`wt.exe -w 0 focus-tab`) — it raises WT without any of the above
146
+ ceremony and has no side effect on window state. The `SetForegroundWindow`
147
+ combo is the fallback for non-WT Windows apps.
148
+
149
+ Notes from Test 3 (`wt.exe -w 0` bare, no subcommand):
150
+
151
+ - Raises the WT window OK and preserves maximization, **but adds a new tab
152
+ as a side effect** (the bare invocation defaults to "new tab in window
153
+ 0"). Unsuitable as a raise-only call.
154
+ - Always pair `wt.exe -w 0` with a no-op subcommand such as `focus-tab -t 0`
155
+ (Test 1) to raise without creating a tab. Treat the bare form as
156
+ reserved for the "open new tab" case only.
157
+
158
+ If multiple signals are present (e.g. tmux inside VS Code's embedded
159
+ terminal), the adapter chain picks the innermost match the IPC can actually
160
+ talk to and falls through to the next on detect failure.
161
+
162
+ ## Decisions to lock after measurement
163
+
164
+ - [x] Tier-1 support matrix — which terminal × OS cells ship in the first
165
+ adapter PR. → **Windows Terminal × WSL → Windows only** (the single
166
+ combination measured this session). Other matrix rows stay pending tier-2
167
+ measurements.
168
+ - [x] Trigger mode — confirm (a), (b), or both (b primary + (a) opt-in). →
169
+ **(b) on-click/keypress is primary; (a) on-push is deferred** (the WSL
170
+ toast already serves as the (a) substitute).
171
+ - [x] System notification daemon integration — in scope for tier-1, or
172
+ deferred to a follow-on PR. → **shipped (Windows scope only)**. The WSL
173
+ toast now carries a `projmux://focus?pane_id=…&socket=…&source=toast`
174
+ launch URI; a one-shot HKCU registration on first dispatch wires
175
+ `wsl.exe -d <distro> -- projmux focus --uri "%1"` so the click
176
+ round-trips back into the (b) focus path. Tier-2 covers
177
+ non-WSL/non-Windows daemons (libnotify, UNUserNotificationCenter) and
178
+ the multi-distro handler.
179
+ - [x] Adapter module path — proposed `internal/integrations/osfocus/`. Confirm
180
+ or pick alternative. → confirmed `internal/integrations/osfocus/`.
181
+ - [x] Adapter call style — synchronous from `projmux focus`, or background
182
+ (`tmux run-shell -b`-style) so the click → toast path stays responsive. →
183
+ **background goroutine, non-blocking** inside the adapter; the chain
184
+ itself returns immediately to the caller.
185
+ - [x] Failure policy — silent fallback (keep entry in queue, no error
186
+ surfaced) confirmed as the default. → confirmed; the chain returns nil
187
+ even when an adapter's Focus errors.
188
+
189
+ Locked 2026-05-11.
190
+
191
+ ### Tier-1 status
192
+
193
+ Tier-1 adapter shipped: `internal/integrations/osfocus/` with
194
+ `WindowsTerminalWSLAdapter`. Other matrix rows remain pending tier-2
195
+ measurements.
196
+
197
+ ### Tier-1.5 shipped — Toast click handler (WSL + Windows Terminal)
198
+
199
+ The toast notification produced by `aiDesktopNotifier.Notify` on WSL
200
+ carries a `launch="projmux://focus?..." activationType="protocol"`
201
+ attribute, and the first dispatch of a tmux server boot registers the
202
+ `projmux://` scheme in `HKCU\SOFTWARE\Classes\projmux\…` so Windows
203
+ routes the click to
204
+ `wsl.exe -d <distro> --exec <abs-path> focus --uri "%1"`. Inside WSL,
205
+ `projmux focus --uri` parses the URI, resolves the pane id to its
206
+ `session:window.%paneID` target via `tmux display-message`, and reuses
207
+ the existing focus dispatch. This closes the previously-deferred (a)
208
+ on-push trigger mode in the Windows-only scope: the toast becomes the
209
+ (a) surface and its click drops into the (b) `projmux focus` path.
210
+
211
+ Trigger mode is a 3-way selector now (Settings > AI Settings > Desktop
212
+ notifications, tmux option `@projmux_desktop_notify_mode`):
213
+
214
+ | Mode | On push | On click |
215
+ |---|---|---|
216
+ | `none` | no toast | n/a |
217
+ | `notify` | show toast | toast click → `projmux focus` via URI handler |
218
+ | `raise` | show toast + raise host terminal via osfocus chain | toast click → `projmux focus` via URI handler |
219
+
220
+ Click handling is always wired regardless of mode — the URI handler
221
+ registration is gated on its own one-shot marker, not on the mode.
222
+
223
+ #### Retrospective — what the working configuration is
224
+
225
+ An earlier Tier-1.5 spike explored a COM Toast Activator path (PR-H
226
+ tree: the `feat/toast-com-activator` branch and tooling under
227
+ `tools/win-toast-activator`). That path is **abandoned**: an unpackaged
228
+ Win32 binary running under WSL cannot reliably take a
229
+ `INotificationActivationCallback` dispatch even with a fully-wired
230
+ registry chain. The configuration that works in live testing on this
231
+ machine is:
232
+
233
+ 1. **No COM activator** at all. The shortcut writes only
234
+ `PKEY_AppUserModel_ID` (pid=5) and intentionally omits
235
+ `PKEY_AppUserModel_ToastActivatorCLSID` (pid=26). When a COM activator
236
+ is registered, Windows tries COM first and silently fails — it does
237
+ not fall through to the launch URI. Stripping the COM side makes
238
+ Windows ShellExecute the URI on click.
239
+ 2. **AppID-tagged shortcut present** (`projmux.lnk` in the Start Menu's
240
+ Programs folder, target `cmd.exe /c exit`). The toast platform
241
+ requires an AppID-tagged shortcut to be discoverable for the toast to
242
+ route under the right DisplayName + icon. The target is never
243
+ launched — it is a property bag.
244
+ 3. **Shortcut target = `cmd.exe /c exit`**. Earlier code used
245
+ `powershell.exe -WindowStyle Hidden -Command exit`. Windows Defender
246
+ silently quarantines such shortcuts within seconds of creation, which
247
+ leaves no AppID-tagged shortcut at all and breaks both the toast
248
+ routing and (since the AppID has to be live when the toast fires) the
249
+ click path. `cmd.exe /c exit` is benign and survives.
250
+ 4. **WSL handler command uses `--exec` not `--`**. `wsl.exe -- <cmd>`
251
+ routes its tail through the user's default login shell, which parses
252
+ `&` query-string separators as background-job operators (zsh emits
253
+ `parse error near '&'`). `--exec` skips the shell and invokes the
254
+ binary directly. PATH is empty under `--exec`, so the registry
255
+ command uses the absolute WSL filesystem path captured at registration
256
+ time.
257
+
258
+ #### Lessons (so future readers don't repeat them)
259
+
260
+ - Do not "fix" the shortcut target back to `powershell.exe -WindowStyle
261
+ Hidden -Command exit`. It triggers Defender quarantine and breaks
262
+ every routing path that depends on the AppID shortcut existing.
263
+ - Do not add `PKEY_AppUserModel_ToastActivatorCLSID` (pid=26) thinking
264
+ it would "unlock click activation". For unpackaged binaries it does
265
+ the opposite — Windows routes through the COM path, the COM call
266
+ silently fails, and the launch URI is never invoked.
267
+ - Do not use `wsl.exe -- projmux ...` in the registry handler.
268
+ Re-introducing the login-shell hop will surface as `parse error near
269
+ '&'` from the user's shell at click time.
270
+ - The `@projmux_uri_protocol_registered_v2` marker exists because the
271
+ v1 marker came before the `--exec` fix. Re-registration is idempotent
272
+ so upgrades from v1 transparently install the new handler — the old
273
+ marker key just goes orphaned.
274
+
275
+ Multi-distro dispatch (one handler per distro, or a distro-selector
276
+ arg) is a known tier-2 follow-up — current registration captures the
277
+ first distro to fire a toast and pins the handler to it. See
278
+ docs/configuration.md → "Toast click handler" for the limitation
279
+ summary.
280
+
281
+ ## Out of scope
282
+
283
+ - Guaranteeing identical behavior across every environment. Cells the matrix
284
+ marks `No` stay fallback-only (notification stays in the queue); we do not
285
+ try to force-support them.
286
+ - Shipping on-push automatic focus (a) as the default. If (a) ships at all it
287
+ is opt-in.
288
+ - Bypassing OS foreground-lock policy (Windows in particular). The adapter
289
+ does what the OS allows and no more.
290
+
291
+ ## Risks and cost
292
+
293
+ - Adapter calls spawn external processes, which adds latency on the focus
294
+ path. Tier-1 adapters should be invoked asynchronously (background) so the
295
+ visible click → focus response stays snappy.
296
+ - `wmctrl`, `xdotool`, `wt.exe`, `osascript`, `kdotool`, etc. are not all
297
+ installed by default on user systems. Adapter `Detect()` must verify the
298
+ binary is on `PATH` before claiming the slot.
299
+ - System notification daemon integration drags in OS permission flows (macOS
300
+ notification permission, Windows Toast registration, libnotify
301
+ availability). Estimate that cost in the spike before committing.
302
+
303
+ ## Source links
304
+
305
+ - [`internal/app/focus.go`](../internal/app/focus.go) — current tmux pane
306
+ focus dispatcher; the planned hook point for the adapter chain.
307
+ - [`internal/app/notify.go`](../internal/app/notify.go) — notify queue API.
308
+ - [`internal/app/notify_producer.go`](../internal/app/notify_producer.go) —
309
+ pushes notifications from the attention state machine.
310
+ - [`internal/app/notify_reconcile.go`](../internal/app/notify_reconcile.go) —
311
+ back-fills the queue from live tmux state.
312
+ - [`internal/app/init_ghostty.go`](../internal/app/init_ghostty.go),
313
+ [`internal/app/init_windows_terminal.go`](../internal/app/init_windows_terminal.go) —
314
+ precedent for terminal detection and per-terminal adapter dispatch.
315
+ - [`internal/integrations/tmux/`](../internal/integrations/tmux/) — external
316
+ process helpers; pattern to reuse for the new adapters.
317
+ - Planned: `internal/integrations/osfocus/` — new home for the OS / terminal
318
+ focus adapters once the matrix above is filled in.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "projmux",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "tmux project session manager",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/crevissepartners/projmux#readme",
@@ -24,10 +24,10 @@
24
24
  "LICENSE"
25
25
  ],
26
26
  "optionalDependencies": {
27
- "@projmux/darwin-arm64": "0.5.1",
28
- "@projmux/darwin-x64": "0.5.1",
29
- "@projmux/linux-arm64": "0.5.1",
30
- "@projmux/linux-x64": "0.5.1"
27
+ "@projmux/darwin-arm64": "0.5.2",
28
+ "@projmux/darwin-x64": "0.5.2",
29
+ "@projmux/linux-arm64": "0.5.2",
30
+ "@projmux/linux-x64": "0.5.2"
31
31
  },
32
32
  "scripts": {
33
33
  "package:npm": "scripts/package-npm.sh",