oc-sounds 0.1.0 → 0.1.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/README.md +87 -23
- package/dist/config.d.ts +14 -3
- package/dist/config.js +31 -15
- package/dist/index.d.ts +2 -2
- package/dist/index.js +36 -16
- package/dist/player.d.ts +1 -1
- package/dist/scheduler.d.ts +4 -0
- package/dist/scheduler.js +66 -0
- package/examples/opencode.json +58 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -15,9 +15,10 @@ Once the first release is published to npm, install globally with:
|
|
|
15
15
|
opencode2 plugin add oc-sounds
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
All sounds are enabled at **
|
|
19
|
-
is required.
|
|
20
|
-
sound
|
|
18
|
+
All sounds are enabled at **85% volume** by default. No individual sound setup
|
|
19
|
+
is required. You can [change the sound for any action](#customize) in your OpenCode
|
|
20
|
+
config. The package includes all 17 sound files and needs no browser or sound
|
|
21
|
+
renderer at runtime.
|
|
21
22
|
|
|
22
23
|
For project-only installation, add `oc-sounds` to `plugins` in your project's
|
|
23
24
|
`opencode.jsonc` instead:
|
|
@@ -28,7 +29,7 @@ For project-only installation, add `oc-sounds` to `plugins` in your project's
|
|
|
28
29
|
"plugins": [
|
|
29
30
|
{
|
|
30
31
|
"package": "oc-sounds",
|
|
31
|
-
"options": { "volume":
|
|
32
|
+
"options": { "volume": 0.85 }
|
|
32
33
|
}
|
|
33
34
|
]
|
|
34
35
|
}
|
|
@@ -67,10 +68,22 @@ Sound plays on the **machine running the OpenCode server**. For a local TUI,
|
|
|
67
68
|
desktop app, or browser connected to a local server, that is your computer. A
|
|
68
69
|
remote/headless server needs its own accessible audio output.
|
|
69
70
|
|
|
70
|
-
Playback runs asynchronously
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
Playback runs asynchronously with burst smoothing enabled by default:
|
|
72
|
+
|
|
73
|
+
- Sound starts are spaced at least **180ms** apart per plugin instance.
|
|
74
|
+
- Repeated copies of the same sound within **300ms** are coalesced, including
|
|
75
|
+
duplicate cues waiting to play. Five simultaneous commands produce one start
|
|
76
|
+
cue and one return cue instead of ten overlapping sounds.
|
|
77
|
+
- At most **three cues** wait to play. Older routine cues are dropped when the
|
|
78
|
+
queue fills, and routine cues older than a second expire.
|
|
79
|
+
- Completion, errors, permission requests, questions, and interruptions take
|
|
80
|
+
priority over queued routine activity. Custom sound mappings retain this
|
|
81
|
+
priority.
|
|
82
|
+
|
|
83
|
+
The quiet tails of longer sounds can still overlap. If a player fails, automatic
|
|
84
|
+
mode tries the next player; if none work, the plugin logs one warning and lets
|
|
85
|
+
the agent continue. Unloading stops its queued cues, audio processes, and event
|
|
86
|
+
subscription.
|
|
74
87
|
|
|
75
88
|
## What makes a sound?
|
|
76
89
|
|
|
@@ -81,25 +94,28 @@ hooks. The built-in tool palette is:
|
|
|
81
94
|
| Tool action | Sound |
|
|
82
95
|
| --- | --- |
|
|
83
96
|
| Read files | `page` |
|
|
84
|
-
| Glob
|
|
85
|
-
|
|
|
86
|
-
|
|
|
87
|
-
|
|
|
97
|
+
| Glob and grep | `release` |
|
|
98
|
+
| Web search | `arrival` |
|
|
99
|
+
| Web fetch | `loading` |
|
|
100
|
+
| Write and edit | `toggle` |
|
|
101
|
+
| Patch a file | `arrival` |
|
|
102
|
+
| Shell / code execution | `tick` |
|
|
88
103
|
| Delegate to a subagent | `arrival` |
|
|
89
104
|
| Load a skill | `sparkle` |
|
|
90
105
|
| Ask the user a question | `chime` |
|
|
91
106
|
| Other tools | `tick` |
|
|
92
107
|
| Tool succeeds / fails | `release` / `error` |
|
|
108
|
+
| Web fetch succeeds | `ready` |
|
|
93
109
|
|
|
94
110
|
Session actions also have cues. The option keys are listed below:
|
|
95
111
|
|
|
96
112
|
| Option key | Action | Default |
|
|
97
113
|
| --- | --- | --- |
|
|
98
|
-
| `prompt` | Input enters the session inbox |
|
|
114
|
+
| `prompt` | Input enters the session inbox | muted |
|
|
99
115
|
| `start` | Agent execution starts | `loading` |
|
|
100
|
-
| `step` | Model step starts |
|
|
116
|
+
| `step` | Model step starts | muted |
|
|
101
117
|
| `thinking` | Reasoning block starts | `whisper` |
|
|
102
|
-
| `reply` |
|
|
118
|
+
| `reply` | Model text / progress message starts | `scan` |
|
|
103
119
|
| `tool` | Fallback tool-start cue | `tick` |
|
|
104
120
|
| `toolComplete` | Tool completes | `release` |
|
|
105
121
|
| `toolError` | Tool fails | `error` |
|
|
@@ -119,10 +135,27 @@ Reasoning and replies sound once per block, rather than on every streamed token.
|
|
|
119
135
|
Tool progress updates do not repeat the start cue. Public events are scoped to
|
|
120
136
|
the plugin's location and repeated event IDs are deduplicated.
|
|
121
137
|
|
|
138
|
+
`reply` covers model text, including mid-task updates such as "I'll check the
|
|
139
|
+
files now." OpenCode's text-start event does not distinguish intermediate text
|
|
140
|
+
from the final reply, so both use `scan`. The separate `complete` cue plays
|
|
141
|
+
`success` when the agent finishes its execution.
|
|
142
|
+
|
|
143
|
+
OpenCode emits `prompt`, `start`, and `step` almost simultaneously for ordinary
|
|
144
|
+
prompts, including before any text is visible. Only `start` is audible by
|
|
145
|
+
default, producing one `loading` cue rather than a three-sound burst. Set
|
|
146
|
+
`prompt` to `"press"` or `step` to `"pulse"` in `cues` if you want those extra
|
|
147
|
+
lifecycle cues.
|
|
148
|
+
|
|
122
149
|
## Customize
|
|
123
150
|
|
|
151
|
+
**Every action's sound is configurable.** Use `cues` for session actions such as
|
|
152
|
+
thinking, replying, and completion, `tools` for individual tool-start sounds,
|
|
153
|
+
and `returns` for successful per-tool return sounds. Set a value to any Cuelume
|
|
154
|
+
sound name, or `false` to mute it. Omitted settings keep their defaults.
|
|
155
|
+
|
|
124
156
|
Edit the existing plugin entry in `~/.config/opencode/opencode.jsonc` (global)
|
|
125
|
-
or your project's `opencode.jsonc`. Merge it with your other settings
|
|
157
|
+
or your project's `opencode.jsonc`. Merge it with your other settings. If you use
|
|
158
|
+
a local checkout, keep its directory path in `package`:
|
|
126
159
|
|
|
127
160
|
```jsonc
|
|
128
161
|
{
|
|
@@ -132,17 +165,29 @@ or your project's `opencode.jsonc`. Merge it with your other settings:
|
|
|
132
165
|
"package": "oc-sounds",
|
|
133
166
|
"options": {
|
|
134
167
|
"enabled": true,
|
|
135
|
-
"volume": 0.
|
|
168
|
+
"volume": 0.85,
|
|
136
169
|
"player": "auto",
|
|
170
|
+
"playback": {
|
|
171
|
+
"minIntervalMs": 180,
|
|
172
|
+
"dedupeWindowMs": 300
|
|
173
|
+
},
|
|
137
174
|
"cues": {
|
|
175
|
+
"prompt": "press",
|
|
138
176
|
"thinking": "whisper",
|
|
177
|
+
"reply": "scan",
|
|
139
178
|
"complete": "ready",
|
|
140
179
|
"step": false
|
|
141
180
|
},
|
|
142
181
|
"tools": {
|
|
143
182
|
"read": "tick",
|
|
183
|
+
"edit": "toggle",
|
|
144
184
|
"shell": "pulse",
|
|
185
|
+
"webfetch": "loading",
|
|
186
|
+
"websearch": "arrival",
|
|
145
187
|
"my_mcp_tool": "sparkle"
|
|
188
|
+
},
|
|
189
|
+
"returns": {
|
|
190
|
+
"webfetch": "ready"
|
|
146
191
|
}
|
|
147
192
|
}
|
|
148
193
|
}
|
|
@@ -150,10 +195,24 @@ or your project's `opencode.jsonc`. Merge it with your other settings:
|
|
|
150
195
|
}
|
|
151
196
|
```
|
|
152
197
|
|
|
198
|
+
This example changes reads to `tick`, completion to `ready`, and mutes model-step
|
|
199
|
+
cues. Change any name to choose a different sound. For all available action and
|
|
200
|
+
tool mappings in one file, see [`examples/opencode.json`](examples/opencode.json),
|
|
201
|
+
which is also included in the npm package.
|
|
202
|
+
|
|
203
|
+
Save your OpenCode config to apply the changes. If the running plugin does not
|
|
204
|
+
reload automatically, run `opencode2 service restart`. Changing these options
|
|
205
|
+
does not require rebuilding or reinstalling the plugin.
|
|
206
|
+
|
|
153
207
|
- `enabled`: defaults to `true`. Set to `false` to disable all sounds.
|
|
154
|
-
- `volume`: from `0` to `1`, defaults to `
|
|
208
|
+
- `volume`: from `0` to `1`, defaults to `0.85` (85%). Zero disables playback.
|
|
155
209
|
- `player`: `auto`, `paplay`, `pw-play`, `afplay`, or `ffplay`. An explicit player
|
|
156
210
|
disables automatic fallback to other players.
|
|
211
|
+
- `playback.minIntervalMs`: minimum time between sound starts, defaults to `180`.
|
|
212
|
+
- `playback.dedupeWindowMs`: cooldown for repeating the same sound, defaults to
|
|
213
|
+
`300`. Both timing values accept integers from `0` to `5000` milliseconds. Set
|
|
214
|
+
both to `0` for immediate, unthrottled playback. For a calmer pace, try `250`
|
|
215
|
+
and `500` respectively.
|
|
157
216
|
- `cues`: override any action in the table, or set it to `false` to mute it.
|
|
158
217
|
Setting `cues.tool` replaces the built-in starting palette for all ordinary
|
|
159
218
|
tools. `question` is configured separately.
|
|
@@ -161,6 +220,10 @@ or your project's `opencode.jsonc`. Merge it with your other settings:
|
|
|
161
220
|
name (`read` also matches `functions.read` / `functions_read`). Exact names
|
|
162
221
|
take precedence. `false` mutes that tool's starting cue; completion and error
|
|
163
222
|
cues are controlled separately. Tool overrides take precedence over `cues.tool`.
|
|
223
|
+
- `returns`: override successful return cues by exact effective tool name or
|
|
224
|
+
built-in short name. The default `webfetch` return is `ready`; other successful
|
|
225
|
+
tools fall back to `cues.toolComplete`. Set a value to `false` to mute that
|
|
226
|
+
tool's successful return without muting its starting cue.
|
|
164
227
|
|
|
165
228
|
All 17 Cuelume sounds are available:
|
|
166
229
|
`chime`, `sparkle`, `droplet`, `bloom`, `whisper`, `tick`, `press`, `release`,
|
|
@@ -192,7 +255,7 @@ project OpenCode configuration:
|
|
|
192
255
|
"plugins": [
|
|
193
256
|
{
|
|
194
257
|
"package": "/absolute/path/to/oc-sounds",
|
|
195
|
-
"options": { "volume":
|
|
258
|
+
"options": { "volume": 0.85 }
|
|
196
259
|
}
|
|
197
260
|
]
|
|
198
261
|
}
|
|
@@ -217,8 +280,9 @@ device. Cuelume and the native offline renderer are development dependencies;
|
|
|
217
280
|
neither is needed by the installed plugin.
|
|
218
281
|
|
|
219
282
|
Tests cover the V2 plugin lifecycle, event isolation/deduplication, concurrent
|
|
220
|
-
tools,
|
|
221
|
-
|
|
283
|
+
tools, burst coalescing, playback spacing and priority, bounded queues, muting,
|
|
284
|
+
cleanup, player fallback, and bundled WAV integrity. Scheduler tests use a fake
|
|
285
|
+
clock; Linux player tests use fake executables and do not play audio.
|
|
222
286
|
|
|
223
287
|
### Publish to npm
|
|
224
288
|
|
|
@@ -230,8 +294,8 @@ npm pack --dry-run
|
|
|
230
294
|
```
|
|
231
295
|
|
|
232
296
|
The `prepack` hook builds the plugin automatically. The package contains `dist/`,
|
|
233
|
-
`sounds/`, the README, and the license notices; development
|
|
234
|
-
rendering scripts are excluded.
|
|
297
|
+
`sounds/`, `examples/`, the README, and the license notices; development
|
|
298
|
+
dependencies and rendering scripts are excluded.
|
|
235
299
|
|
|
236
300
|
Sign in to an npm account with publishing access, then publish:
|
|
237
301
|
|
package/dist/config.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
export declare const sounds: readonly ["chime", "sparkle", "droplet", "bloom", "whisper", "tick", "press", "release", "toggle", "success", "error", "page", "loading", "ready", "pulse", "scan", "arrival"];
|
|
2
2
|
export type SoundName = (typeof sounds)[number];
|
|
3
3
|
export declare const defaultCues: {
|
|
4
|
-
readonly prompt:
|
|
4
|
+
readonly prompt: false;
|
|
5
5
|
readonly start: "loading";
|
|
6
|
-
readonly step:
|
|
6
|
+
readonly step: false;
|
|
7
7
|
readonly thinking: "whisper";
|
|
8
|
-
readonly reply: "
|
|
8
|
+
readonly reply: "scan";
|
|
9
9
|
readonly tool: "tick";
|
|
10
10
|
readonly toolComplete: "release";
|
|
11
11
|
readonly toolError: "error";
|
|
@@ -23,12 +23,23 @@ export declare const defaultCues: {
|
|
|
23
23
|
};
|
|
24
24
|
export type Action = keyof typeof defaultCues;
|
|
25
25
|
export declare const defaultToolCues: Record<string, SoundName>;
|
|
26
|
+
export declare const defaultToolReturnCues: Record<string, SoundName>;
|
|
26
27
|
export type PlayerName = "auto" | "paplay" | "pw-play" | "afplay" | "ffplay";
|
|
28
|
+
export declare const defaultPlayback: {
|
|
29
|
+
minIntervalMs: number;
|
|
30
|
+
dedupeWindowMs: number;
|
|
31
|
+
};
|
|
32
|
+
export interface PlaybackOptions {
|
|
33
|
+
minIntervalMs: number;
|
|
34
|
+
dedupeWindowMs: number;
|
|
35
|
+
}
|
|
27
36
|
export interface Options {
|
|
28
37
|
enabled: boolean;
|
|
29
38
|
volume: number;
|
|
30
39
|
player: PlayerName;
|
|
40
|
+
playback: PlaybackOptions;
|
|
31
41
|
cues: Record<Action, SoundName | false>;
|
|
32
42
|
tools: Record<string, SoundName | false>;
|
|
43
|
+
returns: Record<string, SoundName | false>;
|
|
33
44
|
}
|
|
34
45
|
export declare function parseOptions(input: unknown): Options;
|
package/dist/config.js
CHANGED
|
@@ -4,11 +4,11 @@ export const sounds = [
|
|
|
4
4
|
"pulse", "scan", "arrival",
|
|
5
5
|
];
|
|
6
6
|
export const defaultCues = {
|
|
7
|
-
prompt:
|
|
7
|
+
prompt: false,
|
|
8
8
|
start: "loading",
|
|
9
|
-
step:
|
|
9
|
+
step: false,
|
|
10
10
|
thinking: "whisper",
|
|
11
|
-
reply: "
|
|
11
|
+
reply: "scan",
|
|
12
12
|
tool: "tick",
|
|
13
13
|
toolComplete: "release",
|
|
14
14
|
toolError: "error",
|
|
@@ -26,21 +26,25 @@ export const defaultCues = {
|
|
|
26
26
|
};
|
|
27
27
|
export const defaultToolCues = {
|
|
28
28
|
read: "page",
|
|
29
|
-
glob: "
|
|
30
|
-
grep: "
|
|
31
|
-
websearch: "
|
|
32
|
-
webfetch: "
|
|
29
|
+
glob: "release",
|
|
30
|
+
grep: "release",
|
|
31
|
+
websearch: "arrival",
|
|
32
|
+
webfetch: "loading",
|
|
33
33
|
write: "toggle",
|
|
34
34
|
edit: "toggle",
|
|
35
|
-
patch: "
|
|
36
|
-
apply_patch: "
|
|
37
|
-
shell: "
|
|
38
|
-
bash: "
|
|
39
|
-
execute: "
|
|
35
|
+
patch: "arrival",
|
|
36
|
+
apply_patch: "arrival",
|
|
37
|
+
shell: "tick",
|
|
38
|
+
bash: "tick",
|
|
39
|
+
execute: "tick",
|
|
40
40
|
subagent: "arrival",
|
|
41
41
|
task: "arrival",
|
|
42
42
|
skill: "sparkle",
|
|
43
43
|
};
|
|
44
|
+
export const defaultToolReturnCues = {
|
|
45
|
+
webfetch: "ready",
|
|
46
|
+
};
|
|
47
|
+
export const defaultPlayback = { minIntervalMs: 180, dedupeWindowMs: 300 };
|
|
44
48
|
function object(value) {
|
|
45
49
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
46
50
|
return value;
|
|
@@ -55,14 +59,14 @@ function cue(value) {
|
|
|
55
59
|
export function parseOptions(input) {
|
|
56
60
|
const raw = object(input);
|
|
57
61
|
for (const key of Object.keys(raw)) {
|
|
58
|
-
if (!["enabled", "volume", "player", "cues", "tools"].includes(key)) {
|
|
62
|
+
if (!["enabled", "volume", "player", "playback", "cues", "tools", "returns"].includes(key)) {
|
|
59
63
|
throw new Error(`oc-sounds: unknown option ${key}`);
|
|
60
64
|
}
|
|
61
65
|
}
|
|
62
66
|
const enabled = raw.enabled ?? true;
|
|
63
67
|
if (typeof enabled !== "boolean")
|
|
64
68
|
throw new Error("oc-sounds: enabled must be a boolean");
|
|
65
|
-
const volume = raw.volume ??
|
|
69
|
+
const volume = raw.volume ?? 0.85;
|
|
66
70
|
if (typeof volume !== "number" || !Number.isFinite(volume) || volume < 0 || volume > 1) {
|
|
67
71
|
throw new Error("oc-sounds: volume must be between 0 and 1");
|
|
68
72
|
}
|
|
@@ -79,5 +83,17 @@ export function parseOptions(input) {
|
|
|
79
83
|
const tools = Object.create(null);
|
|
80
84
|
for (const [key, value] of Object.entries(object(raw.tools ?? {})))
|
|
81
85
|
tools[key] = cue(value);
|
|
82
|
-
|
|
86
|
+
const returns = Object.create(null);
|
|
87
|
+
for (const [key, value] of Object.entries(object(raw.returns ?? {})))
|
|
88
|
+
returns[key] = cue(value);
|
|
89
|
+
const playback = { ...defaultPlayback };
|
|
90
|
+
for (const [key, value] of Object.entries(object(raw.playback ?? {}))) {
|
|
91
|
+
if (!Object.hasOwn(playback, key))
|
|
92
|
+
throw new Error(`oc-sounds: unknown playback option ${key}`);
|
|
93
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > 5000) {
|
|
94
|
+
throw new Error(`oc-sounds: playback.${key} must be an integer between 0 and 5000 milliseconds`);
|
|
95
|
+
}
|
|
96
|
+
playback[key] = value;
|
|
97
|
+
}
|
|
98
|
+
return { enabled, volume, player: player, playback, cues, tools, returns };
|
|
83
99
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Plugin } from "@opencode/plugin";
|
|
2
2
|
import { type Action, type Options } from "./config.js";
|
|
3
3
|
import { type Player } from "./player.js";
|
|
4
|
-
export { defaultCues, defaultToolCues, sounds } from "./config.js";
|
|
5
|
-
export type { Action, Options, SoundName } from "./config.js";
|
|
4
|
+
export { defaultCues, defaultToolCues, defaultToolReturnCues, defaultPlayback, sounds } from "./config.js";
|
|
5
|
+
export type { Action, Options, PlaybackOptions, SoundName } from "./config.js";
|
|
6
6
|
type Event = ReturnType<Plugin.Context["event"]["subscribe"]> extends AsyncIterable<infer E> ? E : never;
|
|
7
7
|
export declare function actionForEvent(event: Event): Action | undefined;
|
|
8
8
|
export declare function setup(ctx: Plugin.Context, makePlayer?: (selection: Options["player"], volume: number) => Player): Promise<Plugin.Cleanup | void>;
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Plugin } from "@opencode/plugin";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { setTimeout as delay } from "node:timers/promises";
|
|
4
|
-
import { defaultToolCues, parseOptions } from "./config.js";
|
|
4
|
+
import { defaultToolCues, defaultToolReturnCues, parseOptions } from "./config.js";
|
|
5
5
|
import { createPlayer } from "./player.js";
|
|
6
|
-
|
|
6
|
+
import { schedulePlayer } from "./scheduler.js";
|
|
7
|
+
export { defaultCues, defaultToolCues, defaultToolReturnCues, defaultPlayback, sounds } from "./config.js";
|
|
7
8
|
const eventActions = {
|
|
8
9
|
"session.inbox.enqueued": "prompt",
|
|
9
10
|
"session.execution.started": "start",
|
|
@@ -25,33 +26,49 @@ const eventActions = {
|
|
|
25
26
|
export function actionForEvent(event) {
|
|
26
27
|
return eventActions[event.type];
|
|
27
28
|
}
|
|
29
|
+
function shortToolName(tool) {
|
|
30
|
+
return tool.split(".").at(-1).replace(/^(functions|tools)_/, "");
|
|
31
|
+
}
|
|
32
|
+
const importantActions = new Set([
|
|
33
|
+
"complete", "error", "toolError", "permission", "question", "interrupt",
|
|
34
|
+
]);
|
|
28
35
|
// Exported separately to exercise the actual plugin lifecycle with a fake player.
|
|
29
36
|
export async function setup(ctx, makePlayer = createPlayer) {
|
|
30
37
|
const options = parseOptions(ctx.options);
|
|
31
38
|
if (!options.enabled || options.volume === 0)
|
|
32
39
|
return;
|
|
33
|
-
const player = makePlayer(options.player, options.volume);
|
|
40
|
+
const player = schedulePlayer(makePlayer(options.player, options.volume), options.playback);
|
|
34
41
|
const controller = new AbortController();
|
|
35
42
|
const registrations = [];
|
|
43
|
+
const calls = new Map();
|
|
36
44
|
const emit = (action, tool) => {
|
|
37
45
|
if (controller.signal.aborted)
|
|
38
46
|
return;
|
|
39
47
|
let sound = options.cues[action];
|
|
40
48
|
if (tool !== undefined) {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
const short = shortToolName(tool);
|
|
50
|
+
if (action === "tool") {
|
|
51
|
+
if (Object.hasOwn(options.tools, tool))
|
|
52
|
+
sound = options.tools[tool];
|
|
53
|
+
else if (Object.hasOwn(options.tools, short))
|
|
54
|
+
sound = options.tools[short];
|
|
55
|
+
// An explicit general cue replaces the built-in palette for all tools.
|
|
56
|
+
else if (!Object.hasOwn(ctx.options.cues ?? {}, "tool")) {
|
|
57
|
+
if (Object.hasOwn(defaultToolCues, short))
|
|
58
|
+
sound = defaultToolCues[short];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else if (action === "toolComplete") {
|
|
62
|
+
if (Object.hasOwn(options.returns, tool))
|
|
63
|
+
sound = options.returns[tool];
|
|
64
|
+
else if (Object.hasOwn(options.returns, short))
|
|
65
|
+
sound = options.returns[short];
|
|
66
|
+
else if (Object.hasOwn(defaultToolReturnCues, short))
|
|
67
|
+
sound = defaultToolReturnCues[short];
|
|
51
68
|
}
|
|
52
69
|
}
|
|
53
70
|
if (sound)
|
|
54
|
-
player.play(sound);
|
|
71
|
+
player.play(sound, importantActions.has(action));
|
|
55
72
|
};
|
|
56
73
|
const cleanup = async () => {
|
|
57
74
|
controller.abort();
|
|
@@ -60,11 +77,14 @@ export async function setup(ctx, makePlayer = createPlayer) {
|
|
|
60
77
|
};
|
|
61
78
|
try {
|
|
62
79
|
registrations.push(await ctx.tool.hook("execute.before", (event) => {
|
|
63
|
-
const name = event.tool
|
|
80
|
+
const name = shortToolName(event.tool);
|
|
81
|
+
calls.set(event.id, event.tool);
|
|
64
82
|
emit(name === "question" ? "question" : "tool", event.tool);
|
|
65
83
|
}));
|
|
66
84
|
registrations.push(await ctx.tool.hook("execute.after", (event) => {
|
|
67
|
-
|
|
85
|
+
const tool = calls.get(event.id) ?? event.tool;
|
|
86
|
+
calls.delete(event.id);
|
|
87
|
+
emit(event.status === "error" ? "toolError" : "toolComplete", typeof tool === "string" ? tool : undefined);
|
|
68
88
|
}));
|
|
69
89
|
}
|
|
70
90
|
catch (error) {
|
package/dist/player.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { PlayerName, SoundName } from "./config.js";
|
|
2
2
|
type Backend = Exclude<PlayerName, "auto">;
|
|
3
3
|
export interface Player {
|
|
4
|
-
play(sound: SoundName): void;
|
|
4
|
+
play(sound: SoundName, important?: boolean): void;
|
|
5
5
|
dispose(): void;
|
|
6
6
|
}
|
|
7
7
|
export declare function playerArgs(player: Backend, file: string, volume: number): string[];
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { PlaybackOptions } from "./config.js";
|
|
2
|
+
import type { Player } from "./player.js";
|
|
3
|
+
/** One queue per plugin instance, shared by hooks and the session event stream. */
|
|
4
|
+
export declare function schedulePlayer(player: Player, options: PlaybackOptions, now?: () => number): Player;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const MAX_PENDING = 3;
|
|
2
|
+
const MAX_AGE_MS = 1000;
|
|
3
|
+
/** One queue per plugin instance, shared by hooks and the session event stream. */
|
|
4
|
+
export function schedulePlayer(player, options, now = () => performance.now()) {
|
|
5
|
+
let queue = [];
|
|
6
|
+
const recent = new Map();
|
|
7
|
+
let lastStart = -Infinity;
|
|
8
|
+
let timer;
|
|
9
|
+
let disposed = false;
|
|
10
|
+
function pump() {
|
|
11
|
+
if (disposed || timer)
|
|
12
|
+
return;
|
|
13
|
+
const time = now();
|
|
14
|
+
queue = queue.filter((item) => item.important || time - item.time <= MAX_AGE_MS);
|
|
15
|
+
if (!queue.length)
|
|
16
|
+
return;
|
|
17
|
+
const wait = options.minIntervalMs - (time - lastStart);
|
|
18
|
+
if (wait > 0) {
|
|
19
|
+
timer = setTimeout(() => {
|
|
20
|
+
timer = undefined;
|
|
21
|
+
pump();
|
|
22
|
+
}, Math.ceil(wait));
|
|
23
|
+
timer.unref();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const item = queue.shift();
|
|
27
|
+
lastStart = time;
|
|
28
|
+
recent.set(item.sound, { time, important: item.important });
|
|
29
|
+
player.play(item.sound, item.important);
|
|
30
|
+
pump();
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
play(sound, important = false) {
|
|
34
|
+
if (disposed)
|
|
35
|
+
return;
|
|
36
|
+
const time = now();
|
|
37
|
+
const previous = recent.get(sound);
|
|
38
|
+
if (previous && time - previous.time < options.dedupeWindowMs
|
|
39
|
+
&& (!important || previous.important))
|
|
40
|
+
return;
|
|
41
|
+
// Attention/finish cues replace stale routine work instead of waiting
|
|
42
|
+
// behind a flood of tools. Priority follows the action, not its sound.
|
|
43
|
+
if (important)
|
|
44
|
+
queue = queue.filter((item) => item.important);
|
|
45
|
+
else if (queue.some((item) => item.important))
|
|
46
|
+
return;
|
|
47
|
+
if (queue.some((item) => item.sound === sound))
|
|
48
|
+
return;
|
|
49
|
+
if (queue.length >= MAX_PENDING)
|
|
50
|
+
queue.shift();
|
|
51
|
+
queue.push({ sound, important, time });
|
|
52
|
+
pump();
|
|
53
|
+
},
|
|
54
|
+
dispose() {
|
|
55
|
+
if (disposed)
|
|
56
|
+
return;
|
|
57
|
+
disposed = true;
|
|
58
|
+
if (timer)
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
timer = undefined;
|
|
61
|
+
queue = [];
|
|
62
|
+
recent.clear();
|
|
63
|
+
player.dispose();
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://opencode.ai/config.json",
|
|
3
|
+
"plugins": [
|
|
4
|
+
{
|
|
5
|
+
"package": "oc-sounds",
|
|
6
|
+
"options": {
|
|
7
|
+
"enabled": true,
|
|
8
|
+
"volume": 0.85,
|
|
9
|
+
"player": "auto",
|
|
10
|
+
"playback": {
|
|
11
|
+
"minIntervalMs": 180,
|
|
12
|
+
"dedupeWindowMs": 300
|
|
13
|
+
},
|
|
14
|
+
"cues": {
|
|
15
|
+
"prompt": false,
|
|
16
|
+
"start": "loading",
|
|
17
|
+
"step": false,
|
|
18
|
+
"thinking": "whisper",
|
|
19
|
+
"reply": "scan",
|
|
20
|
+
"tool": "tick",
|
|
21
|
+
"toolComplete": "release",
|
|
22
|
+
"toolError": "error",
|
|
23
|
+
"permission": "bloom",
|
|
24
|
+
"permissionReply": "toggle",
|
|
25
|
+
"question": "chime",
|
|
26
|
+
"retry": "scan",
|
|
27
|
+
"complete": "success",
|
|
28
|
+
"error": "error",
|
|
29
|
+
"interrupt": "droplet",
|
|
30
|
+
"compact": "page",
|
|
31
|
+
"compactComplete": "ready",
|
|
32
|
+
"skill": "sparkle",
|
|
33
|
+
"agent": "arrival"
|
|
34
|
+
},
|
|
35
|
+
"tools": {
|
|
36
|
+
"read": "page",
|
|
37
|
+
"glob": "release",
|
|
38
|
+
"grep": "release",
|
|
39
|
+
"websearch": "arrival",
|
|
40
|
+
"webfetch": "loading",
|
|
41
|
+
"write": "toggle",
|
|
42
|
+
"edit": "toggle",
|
|
43
|
+
"patch": "arrival",
|
|
44
|
+
"apply_patch": "arrival",
|
|
45
|
+
"shell": "tick",
|
|
46
|
+
"bash": "tick",
|
|
47
|
+
"execute": "tick",
|
|
48
|
+
"subagent": "arrival",
|
|
49
|
+
"task": "arrival",
|
|
50
|
+
"skill": "sparkle"
|
|
51
|
+
},
|
|
52
|
+
"returns": {
|
|
53
|
+
"webfetch": "ready"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oc-sounds",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Cuelume sound feedback for OpenCode V2 agent actions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"exports": {
|
|
10
10
|
".": "./dist/index.js"
|
|
11
11
|
},
|
|
12
|
-
"files": ["dist", "sounds", "THIRD_PARTY_NOTICES.md"],
|
|
12
|
+
"files": ["dist", "sounds", "examples", "THIRD_PARTY_NOTICES.md"],
|
|
13
13
|
"engines": { "node": ">=22" },
|
|
14
14
|
"scripts": {
|
|
15
15
|
"build": "tsc",
|