projmux 0.5.1 → 0.5.3
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 +11 -2
- package/docs/configuration.md +112 -0
- package/docs/notify-os-focus-poc.md +318 -0
- package/package.json +5 -5
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).
|
package/docs/configuration.md
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.5.3",
|
|
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.
|
|
28
|
-
"@projmux/darwin-x64": "0.5.
|
|
29
|
-
"@projmux/linux-arm64": "0.5.
|
|
30
|
-
"@projmux/linux-x64": "0.5.
|
|
27
|
+
"@projmux/darwin-arm64": "0.5.3",
|
|
28
|
+
"@projmux/darwin-x64": "0.5.3",
|
|
29
|
+
"@projmux/linux-arm64": "0.5.3",
|
|
30
|
+
"@projmux/linux-x64": "0.5.3"
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|
|
33
33
|
"package:npm": "scripts/package-npm.sh",
|