@erzhtor/opencode-tmux-notify 0.1.0 → 0.1.1
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 +40 -10
- package/package.json +12 -3
- package/tmux-notify.ts +96 -32
package/README.md
CHANGED
|
@@ -23,12 +23,17 @@ Or add to `opencode.json`:
|
|
|
23
23
|
```ts
|
|
24
24
|
type Options {
|
|
25
25
|
working?: string | { spinner: string; spinMs?: number }
|
|
26
|
-
|
|
26
|
+
waiting?: string | { icon: string }
|
|
27
|
+
error?: string | { icon: string; clearMs?: number | "never" }
|
|
28
|
+
done?: string | { icon: string; clearMs?: number | "never" }
|
|
27
29
|
}
|
|
28
30
|
```
|
|
29
31
|
|
|
30
|
-
- **`working`** — string for static icon, or `{ spinner, spinMs }` for animation. Default: braille spinner at 80ms.
|
|
31
|
-
- **`
|
|
32
|
+
- **`working`** — string for static icon, or `{ spinner, spinMs }` for animation. Default: heavy braille spinner at 80ms.
|
|
33
|
+
- **`waiting`** — icon when agent is blocked waiting for your input (e.g. asking a question). Default: `⚠`. Always persists until the agent resumes.
|
|
34
|
+
- **`error`** — icon when agent encounters an error. Default: `✘`, persists by default.
|
|
35
|
+
- **`done`** — icon when agent finishes. Default: `✔` with 5000ms auto-clear.
|
|
36
|
+
- **`clearMs: "never"`** — keep the icon until the next state (for `error` and `done` only). Omitting `clearMs` defaults to `"never"` for `error`, and `5000` for `done`.
|
|
32
37
|
|
|
33
38
|
### Examples
|
|
34
39
|
|
|
@@ -38,29 +43,51 @@ Use defaults:
|
|
|
38
43
|
{ "plugin": ["@erzhtor/opencode-tmux-notify"] }
|
|
39
44
|
```
|
|
40
45
|
|
|
41
|
-
Static
|
|
46
|
+
Static icons + persist done forever:
|
|
42
47
|
|
|
43
48
|
```jsonc
|
|
44
49
|
["@erzhtor/opencode-tmux-notify", { "working": "⏳", "done": "✅" }]
|
|
45
50
|
```
|
|
46
51
|
|
|
47
|
-
Custom spinner +
|
|
52
|
+
Custom spinner + flash error:
|
|
48
53
|
|
|
49
54
|
```jsonc
|
|
50
55
|
["@erzhtor/opencode-tmux-notify", {
|
|
51
56
|
"working": { "spinner": "◐◓◑◒", "spinMs": 120 },
|
|
52
|
-
"done": { "icon": "✓", "clearMs": 10000 }
|
|
57
|
+
"done": { "icon": "✓", "clearMs": 10000 },
|
|
58
|
+
"error": { "icon": "⚠", "clearMs": 5000 }
|
|
59
|
+
}]
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Disable waiting/error:
|
|
63
|
+
|
|
64
|
+
```jsonc
|
|
65
|
+
["@erzhtor/opencode-tmux-notify", {
|
|
66
|
+
"waiting": null,
|
|
67
|
+
"error": null
|
|
53
68
|
}]
|
|
54
69
|
```
|
|
55
70
|
|
|
56
71
|
### Spinner presets
|
|
57
72
|
|
|
58
|
-
braille
|
|
73
|
+
braille — dots walking around:
|
|
59
74
|
|
|
60
75
|
```jsonc
|
|
61
76
|
["@erzhtor/opencode-tmux-notify", { "working": { "spinner": "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏" } }]
|
|
62
77
|
```
|
|
63
78
|
|
|
79
|
+
heavy braille *(default)* — denser 8-dot pattern:
|
|
80
|
+
|
|
81
|
+
```jsonc
|
|
82
|
+
["@erzhtor/opencode-tmux-notify", { "working": { "spinner": "⣷⣯⣟⡿⢿⣻⣽⣾" } }]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
arcs — crescent rotations:
|
|
86
|
+
|
|
87
|
+
```jsonc
|
|
88
|
+
["@erzhtor/opencode-tmux-notify", { "working": { "spinner": "◜◠◝◞◡◟" } }]
|
|
89
|
+
```
|
|
90
|
+
|
|
64
91
|
half-circles — rotating circle fills:
|
|
65
92
|
|
|
66
93
|
```jsonc
|
|
@@ -113,12 +140,15 @@ classic ascii — universal:
|
|
|
113
140
|
|
|
114
141
|
| State | Icon | Trigger |
|
|
115
142
|
| ------- | ---- | -------------------------------- |
|
|
116
|
-
| Working |
|
|
117
|
-
|
|
|
118
|
-
|
|
|
143
|
+
| Working | ⣷ | Agent starts generating a response |
|
|
144
|
+
| Waiting | ⚠ | Agent needs your input (question, permission) |
|
|
145
|
+
| Error | ✘ | Agent encounters an error |
|
|
146
|
+
| Done | ✔ | Session becomes idle |
|
|
147
|
+
| Clean | — | After `clearMs` timeout, or on exit |
|
|
119
148
|
|
|
120
149
|
- **Multi-instance safe**: Uses `$TMUX_PANE` with `-t` targeting — each OpenCode instance only modifies its own window
|
|
121
150
|
- **No-op outside tmux**: Silently returns if `$TMUX_PANE` is not set
|
|
122
151
|
- **No shell injection**: Uses `execFileSync` (no shell interpolation)
|
|
123
152
|
- **Exit cleanup**: Strips the icon when OpenCode exits
|
|
124
153
|
- **Debounced**: Only sets the working icon once per agent turn
|
|
154
|
+
- **State priority**: `waiting` > `error` > `done` — a blocked agent isn't truly idle
|
package/package.json
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@erzhtor/opencode-tmux-notify",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "OpenCode plugin that shows agent activity status in tmux window names",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "tmux-notify.ts",
|
|
7
|
-
"files": [
|
|
8
|
-
|
|
7
|
+
"files": [
|
|
8
|
+
"tmux-notify.ts"
|
|
9
|
+
],
|
|
10
|
+
"keywords": [
|
|
11
|
+
"opencode",
|
|
12
|
+
"opencode-plugin",
|
|
13
|
+
"tmux",
|
|
14
|
+
"notify",
|
|
15
|
+
"notification",
|
|
16
|
+
"status"
|
|
17
|
+
],
|
|
9
18
|
"license": "MIT",
|
|
10
19
|
"repository": {
|
|
11
20
|
"type": "git",
|
package/tmux-notify.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { execFileSync } from "child_process";
|
|
2
2
|
|
|
3
3
|
// Spinner presets for the `working.spinner` option:
|
|
4
|
-
// braille
|
|
4
|
+
// braille: "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
|
|
5
|
+
// heavy braille (default): "⣷⣯⣟⡿⢿⣻⣽⣾"
|
|
6
|
+
// arcs: "◜◠◝◞◡◟"
|
|
5
7
|
// half-circles: "◐◓◑◒"
|
|
6
8
|
// quadrant squares: "◰◳◲◱"
|
|
7
9
|
// quadrant circles: "◴◷◶◵"
|
|
@@ -10,11 +12,13 @@ import { execFileSync } from "child_process";
|
|
|
10
12
|
// block pulse: "▁▂▃▄▅▆▇█▇▆▅▄▃▂"
|
|
11
13
|
// arrows: "←↖↑↗→↘↓↙"
|
|
12
14
|
// classic ascii: "|/-\\"
|
|
13
|
-
const DEFAULT_SPINNER = "\
|
|
15
|
+
const DEFAULT_SPINNER = "\u28f7\u28ef\u28df\u28bf\u28bf\u28fb\u28fd\u28fe"; // ⣷⣯⣟⡿⢿⣻⣽⣾
|
|
14
16
|
|
|
15
17
|
type Options = {
|
|
16
18
|
working?: string | { spinner: string; spinMs?: number };
|
|
17
|
-
|
|
19
|
+
waiting?: string | { icon: string };
|
|
20
|
+
error?: string | { icon: string; clearMs?: number | "never" };
|
|
21
|
+
done?: string | { icon: string; clearMs?: number | "never" };
|
|
18
22
|
};
|
|
19
23
|
|
|
20
24
|
type Resolved = {
|
|
@@ -22,38 +26,59 @@ type Resolved = {
|
|
|
22
26
|
spinnerMs: number;
|
|
23
27
|
useSpinner: boolean;
|
|
24
28
|
iconWorking: string;
|
|
29
|
+
iconWaiting: string;
|
|
30
|
+
iconError: string;
|
|
31
|
+
errorClearMs: number | "never";
|
|
25
32
|
iconDone: string;
|
|
26
|
-
|
|
33
|
+
doneClearMs: number | "never";
|
|
27
34
|
};
|
|
28
35
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
) => {
|
|
36
|
+
type State = "clean" | "working" | "waiting" | "error" | "done";
|
|
37
|
+
|
|
38
|
+
export const TmuxNotifyPlugin = async (_input: any, options?: Options) => {
|
|
33
39
|
if (!process.env.TMUX_PANE) return {};
|
|
34
40
|
|
|
35
41
|
const w = options?.working;
|
|
36
42
|
const useSpinner = w == null || typeof w === "object";
|
|
37
43
|
|
|
38
44
|
const cfg: Resolved = {
|
|
39
|
-
spinner:
|
|
45
|
+
spinner:
|
|
46
|
+
useSpinner
|
|
47
|
+
? typeof w === "object" && w.spinner ? w.spinner : DEFAULT_SPINNER
|
|
48
|
+
: DEFAULT_SPINNER,
|
|
40
49
|
spinnerMs: typeof w === "object" ? (w.spinMs ?? 80) : 80,
|
|
41
50
|
useSpinner,
|
|
42
51
|
iconWorking: typeof w === "string" ? w : "",
|
|
52
|
+
|
|
53
|
+
iconWaiting:
|
|
54
|
+
options?.waiting == null ? "\u26A0"
|
|
55
|
+
: typeof options.waiting === "string" ? options.waiting
|
|
56
|
+
: options.waiting.icon,
|
|
57
|
+
|
|
58
|
+
iconError:
|
|
59
|
+
options?.error == null ? "\u2718"
|
|
60
|
+
: typeof options.error === "string" ? options.error
|
|
61
|
+
: options.error.icon,
|
|
62
|
+
errorClearMs:
|
|
63
|
+
options?.error == null ? "never"
|
|
64
|
+
: typeof options.error === "string" ? "never"
|
|
65
|
+
: options.error.clearMs ?? "never",
|
|
66
|
+
|
|
43
67
|
iconDone:
|
|
44
|
-
options?.done == null ? "\u2714"
|
|
68
|
+
options?.done == null ? "\u2714"
|
|
45
69
|
: typeof options.done === "object" ? options.done.icon
|
|
46
70
|
: options.done,
|
|
47
|
-
|
|
71
|
+
doneClearMs:
|
|
48
72
|
options?.done == null ? 5000
|
|
49
|
-
: typeof options.done === "object" ? (options.done.clearMs ??
|
|
50
|
-
:
|
|
73
|
+
: typeof options.done === "object" ? (options.done.clearMs ?? "never")
|
|
74
|
+
: "never",
|
|
51
75
|
};
|
|
52
76
|
|
|
53
77
|
const pane = process.env.TMUX_PANE;
|
|
54
|
-
|
|
55
|
-
let
|
|
56
|
-
let
|
|
78
|
+
|
|
79
|
+
let state: State = "clean";
|
|
80
|
+
let clearTimer: ReturnType<typeof setTimeout> | null = null;
|
|
81
|
+
let spinnerId: ReturnType<typeof setInterval> | null = null;
|
|
57
82
|
let spinnerIdx = 0;
|
|
58
83
|
let baseName: string | null = null;
|
|
59
84
|
|
|
@@ -89,19 +114,20 @@ export const TmuxNotifyPlugin = async (
|
|
|
89
114
|
}
|
|
90
115
|
};
|
|
91
116
|
|
|
92
|
-
|
|
117
|
+
const clearClearTimer = () => {
|
|
118
|
+
if (clearTimer) {
|
|
119
|
+
clearTimeout(clearTimer);
|
|
120
|
+
clearTimer = null;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const transition = (next: State) => {
|
|
93
125
|
stopSpinner();
|
|
94
|
-
|
|
95
|
-
|
|
126
|
+
clearClearTimer();
|
|
127
|
+
state = next;
|
|
96
128
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (event.type === "message.part.updated" && !working) {
|
|
100
|
-
working = true;
|
|
101
|
-
if (clearTimer) {
|
|
102
|
-
clearTimeout(clearTimer);
|
|
103
|
-
clearTimer = null;
|
|
104
|
-
}
|
|
129
|
+
switch (next) {
|
|
130
|
+
case "working":
|
|
105
131
|
if (cfg.useSpinner) {
|
|
106
132
|
spinnerIdx = 0;
|
|
107
133
|
spinnerId = setInterval(() => {
|
|
@@ -112,12 +138,50 @@ export const TmuxNotifyPlugin = async (
|
|
|
112
138
|
} else {
|
|
113
139
|
set(cfg.iconWorking);
|
|
114
140
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
141
|
+
break;
|
|
142
|
+
case "waiting":
|
|
143
|
+
set(cfg.iconWaiting);
|
|
144
|
+
break;
|
|
145
|
+
case "error":
|
|
146
|
+
set(cfg.iconError);
|
|
147
|
+
if (typeof cfg.errorClearMs === "number") {
|
|
148
|
+
clearTimer = setTimeout(() => transition("clean"), cfg.errorClearMs);
|
|
149
|
+
}
|
|
150
|
+
break;
|
|
151
|
+
case "done":
|
|
119
152
|
set(cfg.iconDone);
|
|
120
|
-
|
|
153
|
+
if (typeof cfg.doneClearMs === "number") {
|
|
154
|
+
clearTimer = setTimeout(() => transition("clean"), cfg.doneClearMs);
|
|
155
|
+
}
|
|
156
|
+
break;
|
|
157
|
+
case "clean":
|
|
158
|
+
set();
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
process.on("exit", () => {
|
|
164
|
+
stopSpinner();
|
|
165
|
+
set();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
event: async ({ event }: { event: any }) => {
|
|
170
|
+
if (event.type === "session.status") {
|
|
171
|
+
const status = event.properties?.status?.type;
|
|
172
|
+
if (status === "busy" && state !== "working") {
|
|
173
|
+
transition("working");
|
|
174
|
+
} else if (
|
|
175
|
+
status === "idle" &&
|
|
176
|
+
state !== "waiting" &&
|
|
177
|
+
state !== "error"
|
|
178
|
+
) {
|
|
179
|
+
transition("done");
|
|
180
|
+
}
|
|
181
|
+
} else if (event.type === "question.asked") {
|
|
182
|
+
transition("waiting");
|
|
183
|
+
} else if (event.type === "session.error") {
|
|
184
|
+
transition("error");
|
|
121
185
|
}
|
|
122
186
|
},
|
|
123
187
|
};
|