@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.
- package/README.md +124 -0
- package/package.json +14 -0
- 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
|
+

|
|
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
|
+
};
|