@erzhtor/opencode-tmux-notify 0.1.0

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.
Files changed (3) hide show
  1. package/README.md +124 -0
  2. package/package.json +14 -0
  3. package/tmux-notify.ts +124 -0
package/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # opencode-tmux-notify
2
+
3
+ Shows OpenCode agent activity status in your tmux window name — without overwriting it.
4
+
5
+ Instead of replacing the window name, this plugin **prepends** a status icon:
6
+
7
+ ![demo](demo.gif)
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ opencode plugin @erzhtor/opencode-tmux-notify --global
13
+ ```
14
+
15
+ Or add to `opencode.json`:
16
+
17
+ ```json
18
+ { "plugin": ["@erzhtor/opencode-tmux-notify"] }
19
+ ```
20
+
21
+ ## Configure
22
+
23
+ ```ts
24
+ type Options {
25
+ working?: string | { spinner: string; spinMs?: number }
26
+ done?: string | { icon: string; clearMs?: number }
27
+ }
28
+ ```
29
+
30
+ - **`working`** — string for static icon, or `{ spinner, spinMs }` for animation. Default: braille spinner at 80ms.
31
+ - **`done`** — string for icon, or `{ icon, clearMs }` to control the auto-clear. Default: `✔`, 5000ms.
32
+
33
+ ### Examples
34
+
35
+ Use defaults:
36
+
37
+ ```jsonc
38
+ { "plugin": ["@erzhtor/opencode-tmux-notify"] }
39
+ ```
40
+
41
+ Static icon:
42
+
43
+ ```jsonc
44
+ ["@erzhtor/opencode-tmux-notify", { "working": "⏳", "done": "✅" }]
45
+ ```
46
+
47
+ Custom spinner + timing:
48
+
49
+ ```jsonc
50
+ ["@erzhtor/opencode-tmux-notify", {
51
+ "working": { "spinner": "◐◓◑◒", "spinMs": 120 },
52
+ "done": { "icon": "✓", "clearMs": 10000 }
53
+ }]
54
+ ```
55
+
56
+ ### Spinner presets
57
+
58
+ braille *(default)* — dots walking around:
59
+
60
+ ```jsonc
61
+ ["@erzhtor/opencode-tmux-notify", { "working": { "spinner": "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏" } }]
62
+ ```
63
+
64
+ half-circles — rotating circle fills:
65
+
66
+ ```jsonc
67
+ ["@erzhtor/opencode-tmux-notify", { "working": { "spinner": "◐◓◑◒" } }]
68
+ ```
69
+
70
+ quadrant squares — corner fills rotating:
71
+
72
+ ```jsonc
73
+ ["@erzhtor/opencode-tmux-notify", { "working": { "spinner": "◰◳◲◱" } }]
74
+ ```
75
+
76
+ quadrant circles — quadrant fills rotating:
77
+
78
+ ```jsonc
79
+ ["@erzhtor/opencode-tmux-notify", { "working": { "spinner": "◴◷◶◵" } }]
80
+ ```
81
+
82
+ quadrant blocks — block fills rotating:
83
+
84
+ ```jsonc
85
+ ["@erzhtor/opencode-tmux-notify", { "working": { "spinner": "▖▘▝▗" } }]
86
+ ```
87
+
88
+ triangles — corners rotating:
89
+
90
+ ```jsonc
91
+ ["@erzhtor/opencode-tmux-notify", { "working": { "spinner": "◢◣◤◥" } }]
92
+ ```
93
+
94
+ block pulse — growing/shrinking bar:
95
+
96
+ ```jsonc
97
+ ["@erzhtor/opencode-tmux-notify", { "working": { "spinner": "▁▂▃▄▅▆▇█▇▆▅▄▃▂" } }]
98
+ ```
99
+
100
+ arrows — compass cycle:
101
+
102
+ ```jsonc
103
+ ["@erzhtor/opencode-tmux-notify", { "working": { "spinner": "←↖↑↗→↘↓↙" } }]
104
+ ```
105
+
106
+ classic ascii — universal:
107
+
108
+ ```jsonc
109
+ ["@erzhtor/opencode-tmux-notify", { "working": { "spinner": "|/-\\" } }]
110
+ ```
111
+
112
+ ## How it works
113
+
114
+ | State | Icon | Trigger |
115
+ | ------- | ---- | -------------------------------- |
116
+ | Working | ⠋ | Agent starts generating a response |
117
+ | Done | ✔ | Session becomes idle |
118
+ | Clean | — | 5 seconds after done, or on exit |
119
+
120
+ - **Multi-instance safe**: Uses `$TMUX_PANE` with `-t` targeting — each OpenCode instance only modifies its own window
121
+ - **No-op outside tmux**: Silently returns if `$TMUX_PANE` is not set
122
+ - **No shell injection**: Uses `execFileSync` (no shell interpolation)
123
+ - **Exit cleanup**: Strips the icon when OpenCode exits
124
+ - **Debounced**: Only sets the working icon once per agent turn
package/package.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "@erzhtor/opencode-tmux-notify",
3
+ "version": "0.1.0",
4
+ "description": "OpenCode plugin that shows agent activity status in tmux window names",
5
+ "type": "module",
6
+ "main": "tmux-notify.ts",
7
+ "files": ["tmux-notify.ts"],
8
+ "keywords": ["opencode", "opencode-plugin", "tmux", "notify", "notification", "status"],
9
+ "license": "MIT",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/erzhtor/opencode-tmux-notify"
13
+ }
14
+ }
package/tmux-notify.ts ADDED
@@ -0,0 +1,124 @@
1
+ import { execFileSync } from "child_process";
2
+
3
+ // Spinner presets for the `working.spinner` option:
4
+ // braille (default): "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
5
+ // half-circles: "◐◓◑◒"
6
+ // quadrant squares: "◰◳◲◱"
7
+ // quadrant circles: "◴◷◶◵"
8
+ // quadrant blocks: "▖▘▝▗"
9
+ // triangles: "◢◣◤◥"
10
+ // block pulse: "▁▂▃▄▅▆▇█▇▆▅▄▃▂"
11
+ // arrows: "←↖↑↗→↘↓↙"
12
+ // classic ascii: "|/-\\"
13
+ const DEFAULT_SPINNER = "\u280b\u2819\u2839\u2838\u283c\u2834\u2826\u2827\u2807\u280f"; // ⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏
14
+
15
+ type Options = {
16
+ working?: string | { spinner: string; spinMs?: number };
17
+ done?: string | { icon: string; clearMs?: number };
18
+ };
19
+
20
+ type Resolved = {
21
+ spinner: string;
22
+ spinnerMs: number;
23
+ useSpinner: boolean;
24
+ iconWorking: string;
25
+ iconDone: string;
26
+ clearMs: number;
27
+ };
28
+
29
+ export const TmuxNotifyPlugin = async (
30
+ _input: any,
31
+ options?: Options,
32
+ ) => {
33
+ if (!process.env.TMUX_PANE) return {};
34
+
35
+ const w = options?.working;
36
+ const useSpinner = w == null || typeof w === "object";
37
+
38
+ const cfg: Resolved = {
39
+ spinner: useSpinner ? (typeof w === "object" ? w.spinner : DEFAULT_SPINNER) : DEFAULT_SPINNER,
40
+ spinnerMs: typeof w === "object" ? (w.spinMs ?? 80) : 80,
41
+ useSpinner,
42
+ iconWorking: typeof w === "string" ? w : "",
43
+ iconDone:
44
+ options?.done == null ? "\u2714" // ✔
45
+ : typeof options.done === "object" ? options.done.icon
46
+ : options.done,
47
+ clearMs:
48
+ options?.done == null ? 5000
49
+ : typeof options.done === "object" ? (options.done.clearMs ?? 5000)
50
+ : 5000,
51
+ };
52
+
53
+ const pane = process.env.TMUX_PANE;
54
+ let working = false;
55
+ let clearTimer: any = null;
56
+ let spinnerId: any = null;
57
+ let spinnerIdx = 0;
58
+ let baseName: string | null = null;
59
+
60
+ const tmux = (...args: string[]) => {
61
+ try {
62
+ return execFileSync("tmux", args, { encoding: "utf-8" }).trim();
63
+ } catch {
64
+ return null;
65
+ }
66
+ };
67
+
68
+ const getName = () => tmux("display-message", "-t", pane, "-p", "#W");
69
+ const setName = (n: string) => tmux("rename-window", "-t", pane, n);
70
+
71
+ const base = () => {
72
+ if (baseName !== null) return baseName;
73
+ const n = getName();
74
+ if (!n) return null;
75
+ baseName = n;
76
+ return baseName;
77
+ };
78
+
79
+ const set = (icon?: string) => {
80
+ const b = base();
81
+ if (!b) return;
82
+ setName(icon ? `${icon} ${b}` : b);
83
+ };
84
+
85
+ const stopSpinner = () => {
86
+ if (spinnerId) {
87
+ clearInterval(spinnerId);
88
+ spinnerId = null;
89
+ }
90
+ };
91
+
92
+ process.on("exit", () => {
93
+ stopSpinner();
94
+ set();
95
+ });
96
+
97
+ return {
98
+ event: async ({ event }: { event: any }) => {
99
+ if (event.type === "message.part.updated" && !working) {
100
+ working = true;
101
+ if (clearTimer) {
102
+ clearTimeout(clearTimer);
103
+ clearTimer = null;
104
+ }
105
+ if (cfg.useSpinner) {
106
+ spinnerIdx = 0;
107
+ spinnerId = setInterval(() => {
108
+ set(cfg.spinner[spinnerIdx]);
109
+ spinnerIdx = (spinnerIdx + 1) % cfg.spinner.length;
110
+ }, cfg.spinnerMs);
111
+ set(cfg.spinner[0]);
112
+ } else {
113
+ set(cfg.iconWorking);
114
+ }
115
+ }
116
+ if (event.type === "session.idle") {
117
+ working = false;
118
+ stopSpinner();
119
+ set(cfg.iconDone);
120
+ clearTimer = setTimeout(() => set(), cfg.clearMs);
121
+ }
122
+ },
123
+ };
124
+ };