oc-sounds 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 +78 -23
- package/dist/config.d.ts +12 -3
- package/dist/config.js +25 -15
- package/dist/index.d.ts +2 -2
- package/dist/index.js +7 -3
- package/dist/player.d.ts +1 -1
- package/dist/scheduler.d.ts +4 -0
- package/dist/scheduler.js +66 -0
- package/examples/opencode.json +55 -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,10 +94,11 @@ hooks. The built-in tool palette is:
|
|
|
81
94
|
| Tool action | Sound |
|
|
82
95
|
| --- | --- |
|
|
83
96
|
| Read files | `page` |
|
|
84
|
-
| Glob
|
|
85
|
-
|
|
|
86
|
-
| Write
|
|
87
|
-
|
|
|
97
|
+
| Glob and grep | `release` |
|
|
98
|
+
| Web search and fetch | `arrival` |
|
|
99
|
+
| Write and edit | `toggle` |
|
|
100
|
+
| Patch a file | `arrival` |
|
|
101
|
+
| Shell / code execution | `tick` |
|
|
88
102
|
| Delegate to a subagent | `arrival` |
|
|
89
103
|
| Load a skill | `sparkle` |
|
|
90
104
|
| Ask the user a question | `chime` |
|
|
@@ -95,11 +109,11 @@ Session actions also have cues. The option keys are listed below:
|
|
|
95
109
|
|
|
96
110
|
| Option key | Action | Default |
|
|
97
111
|
| --- | --- | --- |
|
|
98
|
-
| `prompt` | Input enters the session inbox |
|
|
112
|
+
| `prompt` | Input enters the session inbox | muted |
|
|
99
113
|
| `start` | Agent execution starts | `loading` |
|
|
100
|
-
| `step` | Model step starts |
|
|
114
|
+
| `step` | Model step starts | muted |
|
|
101
115
|
| `thinking` | Reasoning block starts | `whisper` |
|
|
102
|
-
| `reply` |
|
|
116
|
+
| `reply` | Model text / progress message starts | `scan` |
|
|
103
117
|
| `tool` | Fallback tool-start cue | `tick` |
|
|
104
118
|
| `toolComplete` | Tool completes | `release` |
|
|
105
119
|
| `toolError` | Tool fails | `error` |
|
|
@@ -119,10 +133,27 @@ Reasoning and replies sound once per block, rather than on every streamed token.
|
|
|
119
133
|
Tool progress updates do not repeat the start cue. Public events are scoped to
|
|
120
134
|
the plugin's location and repeated event IDs are deduplicated.
|
|
121
135
|
|
|
136
|
+
`reply` covers model text, including mid-task updates such as "I'll check the
|
|
137
|
+
files now." OpenCode's text-start event does not distinguish intermediate text
|
|
138
|
+
from the final reply, so both use `scan`. The separate `complete` cue plays
|
|
139
|
+
`success` when the agent finishes its execution.
|
|
140
|
+
|
|
141
|
+
OpenCode emits `prompt`, `start`, and `step` almost simultaneously for ordinary
|
|
142
|
+
prompts, including before any text is visible. Only `start` is audible by
|
|
143
|
+
default, producing one `loading` cue rather than a three-sound burst. Set
|
|
144
|
+
`prompt` to `"press"` or `step` to `"pulse"` in `cues` if you want those extra
|
|
145
|
+
lifecycle cues.
|
|
146
|
+
|
|
122
147
|
## Customize
|
|
123
148
|
|
|
149
|
+
**Every action's sound is configurable.** Use `cues` for session actions such as
|
|
150
|
+
thinking, replying, and completion, and `tools` for individual tool-start sounds
|
|
151
|
+
such as read, edit, and web fetch. Set a value to any Cuelume sound name, or `false`
|
|
152
|
+
to mute it. Omitted settings keep their defaults.
|
|
153
|
+
|
|
124
154
|
Edit the existing plugin entry in `~/.config/opencode/opencode.jsonc` (global)
|
|
125
|
-
or your project's `opencode.jsonc`. Merge it with your other settings
|
|
155
|
+
or your project's `opencode.jsonc`. Merge it with your other settings. If you use
|
|
156
|
+
a local checkout, keep its directory path in `package`:
|
|
126
157
|
|
|
127
158
|
```jsonc
|
|
128
159
|
{
|
|
@@ -132,16 +163,25 @@ or your project's `opencode.jsonc`. Merge it with your other settings:
|
|
|
132
163
|
"package": "oc-sounds",
|
|
133
164
|
"options": {
|
|
134
165
|
"enabled": true,
|
|
135
|
-
"volume": 0.
|
|
166
|
+
"volume": 0.85,
|
|
136
167
|
"player": "auto",
|
|
168
|
+
"playback": {
|
|
169
|
+
"minIntervalMs": 180,
|
|
170
|
+
"dedupeWindowMs": 300
|
|
171
|
+
},
|
|
137
172
|
"cues": {
|
|
173
|
+
"prompt": "press",
|
|
138
174
|
"thinking": "whisper",
|
|
175
|
+
"reply": "scan",
|
|
139
176
|
"complete": "ready",
|
|
140
177
|
"step": false
|
|
141
178
|
},
|
|
142
179
|
"tools": {
|
|
143
180
|
"read": "tick",
|
|
181
|
+
"edit": "toggle",
|
|
144
182
|
"shell": "pulse",
|
|
183
|
+
"webfetch": "arrival",
|
|
184
|
+
"websearch": "arrival",
|
|
145
185
|
"my_mcp_tool": "sparkle"
|
|
146
186
|
}
|
|
147
187
|
}
|
|
@@ -150,10 +190,24 @@ or your project's `opencode.jsonc`. Merge it with your other settings:
|
|
|
150
190
|
}
|
|
151
191
|
```
|
|
152
192
|
|
|
193
|
+
This example changes reads to `tick`, completion to `ready`, and mutes model-step
|
|
194
|
+
cues. Change any name to choose a different sound. For all available action and
|
|
195
|
+
tool mappings in one file, see [`examples/opencode.json`](examples/opencode.json),
|
|
196
|
+
which is also included in the npm package.
|
|
197
|
+
|
|
198
|
+
Save your OpenCode config to apply the changes. If the running plugin does not
|
|
199
|
+
reload automatically, run `opencode2 service restart`. Changing these options
|
|
200
|
+
does not require rebuilding or reinstalling the plugin.
|
|
201
|
+
|
|
153
202
|
- `enabled`: defaults to `true`. Set to `false` to disable all sounds.
|
|
154
|
-
- `volume`: from `0` to `1`, defaults to `
|
|
203
|
+
- `volume`: from `0` to `1`, defaults to `0.85` (85%). Zero disables playback.
|
|
155
204
|
- `player`: `auto`, `paplay`, `pw-play`, `afplay`, or `ffplay`. An explicit player
|
|
156
205
|
disables automatic fallback to other players.
|
|
206
|
+
- `playback.minIntervalMs`: minimum time between sound starts, defaults to `180`.
|
|
207
|
+
- `playback.dedupeWindowMs`: cooldown for repeating the same sound, defaults to
|
|
208
|
+
`300`. Both timing values accept integers from `0` to `5000` milliseconds. Set
|
|
209
|
+
both to `0` for immediate, unthrottled playback. For a calmer pace, try `250`
|
|
210
|
+
and `500` respectively.
|
|
157
211
|
- `cues`: override any action in the table, or set it to `false` to mute it.
|
|
158
212
|
Setting `cues.tool` replaces the built-in starting palette for all ordinary
|
|
159
213
|
tools. `question` is configured separately.
|
|
@@ -192,7 +246,7 @@ project OpenCode configuration:
|
|
|
192
246
|
"plugins": [
|
|
193
247
|
{
|
|
194
248
|
"package": "/absolute/path/to/oc-sounds",
|
|
195
|
-
"options": { "volume":
|
|
249
|
+
"options": { "volume": 0.85 }
|
|
196
250
|
}
|
|
197
251
|
]
|
|
198
252
|
}
|
|
@@ -217,8 +271,9 @@ device. Cuelume and the native offline renderer are development dependencies;
|
|
|
217
271
|
neither is needed by the installed plugin.
|
|
218
272
|
|
|
219
273
|
Tests cover the V2 plugin lifecycle, event isolation/deduplication, concurrent
|
|
220
|
-
tools,
|
|
221
|
-
|
|
274
|
+
tools, burst coalescing, playback spacing and priority, bounded queues, muting,
|
|
275
|
+
cleanup, player fallback, and bundled WAV integrity. Scheduler tests use a fake
|
|
276
|
+
clock; Linux player tests use fake executables and do not play audio.
|
|
222
277
|
|
|
223
278
|
### Publish to npm
|
|
224
279
|
|
|
@@ -230,8 +285,8 @@ npm pack --dry-run
|
|
|
230
285
|
```
|
|
231
286
|
|
|
232
287
|
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.
|
|
288
|
+
`sounds/`, `examples/`, the README, and the license notices; development
|
|
289
|
+
dependencies and rendering scripts are excluded.
|
|
235
290
|
|
|
236
291
|
Sign in to an npm account with publishing access, then publish:
|
|
237
292
|
|
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";
|
|
@@ -24,10 +24,19 @@ export declare const defaultCues: {
|
|
|
24
24
|
export type Action = keyof typeof defaultCues;
|
|
25
25
|
export declare const defaultToolCues: Record<string, SoundName>;
|
|
26
26
|
export type PlayerName = "auto" | "paplay" | "pw-play" | "afplay" | "ffplay";
|
|
27
|
+
export declare const defaultPlayback: {
|
|
28
|
+
minIntervalMs: number;
|
|
29
|
+
dedupeWindowMs: number;
|
|
30
|
+
};
|
|
31
|
+
export interface PlaybackOptions {
|
|
32
|
+
minIntervalMs: number;
|
|
33
|
+
dedupeWindowMs: number;
|
|
34
|
+
}
|
|
27
35
|
export interface Options {
|
|
28
36
|
enabled: boolean;
|
|
29
37
|
volume: number;
|
|
30
38
|
player: PlayerName;
|
|
39
|
+
playback: PlaybackOptions;
|
|
31
40
|
cues: Record<Action, SoundName | false>;
|
|
32
41
|
tools: Record<string, SoundName | false>;
|
|
33
42
|
}
|
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,22 @@ 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: "arrival",
|
|
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 defaultPlayback = { minIntervalMs: 180, dedupeWindowMs: 300 };
|
|
44
45
|
function object(value) {
|
|
45
46
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
46
47
|
return value;
|
|
@@ -55,14 +56,14 @@ function cue(value) {
|
|
|
55
56
|
export function parseOptions(input) {
|
|
56
57
|
const raw = object(input);
|
|
57
58
|
for (const key of Object.keys(raw)) {
|
|
58
|
-
if (!["enabled", "volume", "player", "cues", "tools"].includes(key)) {
|
|
59
|
+
if (!["enabled", "volume", "player", "playback", "cues", "tools"].includes(key)) {
|
|
59
60
|
throw new Error(`oc-sounds: unknown option ${key}`);
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
const enabled = raw.enabled ?? true;
|
|
63
64
|
if (typeof enabled !== "boolean")
|
|
64
65
|
throw new Error("oc-sounds: enabled must be a boolean");
|
|
65
|
-
const volume = raw.volume ??
|
|
66
|
+
const volume = raw.volume ?? 0.85;
|
|
66
67
|
if (typeof volume !== "number" || !Number.isFinite(volume) || volume < 0 || volume > 1) {
|
|
67
68
|
throw new Error("oc-sounds: volume must be between 0 and 1");
|
|
68
69
|
}
|
|
@@ -79,5 +80,14 @@ export function parseOptions(input) {
|
|
|
79
80
|
const tools = Object.create(null);
|
|
80
81
|
for (const [key, value] of Object.entries(object(raw.tools ?? {})))
|
|
81
82
|
tools[key] = cue(value);
|
|
82
|
-
|
|
83
|
+
const playback = { ...defaultPlayback };
|
|
84
|
+
for (const [key, value] of Object.entries(object(raw.playback ?? {}))) {
|
|
85
|
+
if (!Object.hasOwn(playback, key))
|
|
86
|
+
throw new Error(`oc-sounds: unknown playback option ${key}`);
|
|
87
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > 5000) {
|
|
88
|
+
throw new Error(`oc-sounds: playback.${key} must be an integer between 0 and 5000 milliseconds`);
|
|
89
|
+
}
|
|
90
|
+
playback[key] = value;
|
|
91
|
+
}
|
|
92
|
+
return { enabled, volume, player: player, playback, cues, tools };
|
|
83
93
|
}
|
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, 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
|
@@ -3,7 +3,8 @@ import { resolve } from "node:path";
|
|
|
3
3
|
import { setTimeout as delay } from "node:timers/promises";
|
|
4
4
|
import { defaultToolCues, parseOptions } from "./config.js";
|
|
5
5
|
import { createPlayer } from "./player.js";
|
|
6
|
-
|
|
6
|
+
import { schedulePlayer } from "./scheduler.js";
|
|
7
|
+
export { defaultCues, defaultToolCues, defaultPlayback, sounds } from "./config.js";
|
|
7
8
|
const eventActions = {
|
|
8
9
|
"session.inbox.enqueued": "prompt",
|
|
9
10
|
"session.execution.started": "start",
|
|
@@ -25,12 +26,15 @@ const eventActions = {
|
|
|
25
26
|
export function actionForEvent(event) {
|
|
26
27
|
return eventActions[event.type];
|
|
27
28
|
}
|
|
29
|
+
const importantActions = new Set([
|
|
30
|
+
"complete", "error", "toolError", "permission", "question", "interrupt",
|
|
31
|
+
]);
|
|
28
32
|
// Exported separately to exercise the actual plugin lifecycle with a fake player.
|
|
29
33
|
export async function setup(ctx, makePlayer = createPlayer) {
|
|
30
34
|
const options = parseOptions(ctx.options);
|
|
31
35
|
if (!options.enabled || options.volume === 0)
|
|
32
36
|
return;
|
|
33
|
-
const player = makePlayer(options.player, options.volume);
|
|
37
|
+
const player = schedulePlayer(makePlayer(options.player, options.volume), options.playback);
|
|
34
38
|
const controller = new AbortController();
|
|
35
39
|
const registrations = [];
|
|
36
40
|
const emit = (action, tool) => {
|
|
@@ -51,7 +55,7 @@ export async function setup(ctx, makePlayer = createPlayer) {
|
|
|
51
55
|
}
|
|
52
56
|
}
|
|
53
57
|
if (sound)
|
|
54
|
-
player.play(sound);
|
|
58
|
+
player.play(sound, importantActions.has(action));
|
|
55
59
|
};
|
|
56
60
|
const cleanup = async () => {
|
|
57
61
|
controller.abort();
|
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,55 @@
|
|
|
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": "arrival",
|
|
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
|
+
}
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oc-sounds",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
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",
|