opencode-plugin-boops 2.2.1 → 2.4.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 +2 -0
- package/cli/browse +65 -43
- package/index.ts +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -69,6 +69,8 @@ The plugin listens to OpenCode events and plays sounds based on your configurati
|
|
|
69
69
|
- **`permission.asked`** - AI needs permission → gentle "relax" chime
|
|
70
70
|
- **`session.error`** - An error occurs → friendly "magic" alert
|
|
71
71
|
|
|
72
|
+
**Instant config reload:** The config is reloaded on every event, so changes take effect immediately without restarting OpenCode! Edit your config, trigger an event, and hear the new sound instantly.
|
|
73
|
+
|
|
72
74
|
Sounds are downloaded once on first use and cached in `~/.cache/opencode/boops/` for instant playback.
|
|
73
75
|
|
|
74
76
|
## Configuration
|
package/cli/browse
CHANGED
|
@@ -56,6 +56,37 @@ function isPluginInstalled() {
|
|
|
56
56
|
|
|
57
57
|
const PLUGIN_INSTALLED = isPluginInstalled();
|
|
58
58
|
|
|
59
|
+
// Event descriptions for the picker
|
|
60
|
+
const eventDescriptions = {
|
|
61
|
+
"session.idle": "AI completes response",
|
|
62
|
+
"permission.asked": "AI needs permission",
|
|
63
|
+
"session.error": "Error occurs",
|
|
64
|
+
"command.executed": "Command executed",
|
|
65
|
+
"file.edited": "File edited",
|
|
66
|
+
"file.watcher.updated": "File watcher detected change",
|
|
67
|
+
"installation.updated": "Installation/package updated",
|
|
68
|
+
"lsp.client.diagnostics": "LSP diagnostics received",
|
|
69
|
+
"lsp.updated": "LSP server updated",
|
|
70
|
+
"message.part.removed": "Message part removed",
|
|
71
|
+
"message.part.updated": "Message part updated",
|
|
72
|
+
"message.removed": "Message removed",
|
|
73
|
+
"message.updated": "Message updated",
|
|
74
|
+
"permission.replied": "Permission response given",
|
|
75
|
+
"server.connected": "Connected to server",
|
|
76
|
+
"session.created": "New session created",
|
|
77
|
+
"session.compacted": "Session compacted",
|
|
78
|
+
"session.deleted": "Session deleted",
|
|
79
|
+
"session.diff": "Session diff generated",
|
|
80
|
+
"session.status": "Session status changed",
|
|
81
|
+
"session.updated": "Session updated",
|
|
82
|
+
"todo.updated": "Todo list updated",
|
|
83
|
+
"tool.execute.before": "Before tool execution",
|
|
84
|
+
"tool.execute.after": "After tool execution",
|
|
85
|
+
"tui.prompt.append": "Text appended to prompt",
|
|
86
|
+
"tui.command.execute": "TUI command executed",
|
|
87
|
+
"tui.toast.show": "Toast notification shown"
|
|
88
|
+
};
|
|
89
|
+
|
|
59
90
|
// Load current boops.toml config
|
|
60
91
|
function loadCurrentConfig() {
|
|
61
92
|
const configPath = join(homedir(), ".config", "opencode", "plugins", "boops", "boops.toml");
|
|
@@ -96,7 +127,7 @@ function loadCurrentConfig() {
|
|
|
96
127
|
}
|
|
97
128
|
}
|
|
98
129
|
|
|
99
|
-
// Save config to boops.toml
|
|
130
|
+
// Save config to boops.toml
|
|
100
131
|
function saveConfig(config) {
|
|
101
132
|
if (!PLUGIN_INSTALLED) {
|
|
102
133
|
console.error("\n⚠️ Cannot save: Plugin not installed");
|
|
@@ -107,48 +138,14 @@ function saveConfig(config) {
|
|
|
107
138
|
|
|
108
139
|
const configPath = join(homedir(), ".config", "opencode", "plugins", "boops", "boops.toml");
|
|
109
140
|
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
if (existsSync(configPath)) {
|
|
113
|
-
existingContent = readFileSync(configPath, "utf-8");
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Parse existing [sounds] section to merge with new values
|
|
117
|
-
const existingSounds = {};
|
|
118
|
-
if (existingContent) {
|
|
119
|
-
const lines = existingContent.split("\n");
|
|
120
|
-
let inSoundsSection = false;
|
|
121
|
-
|
|
122
|
-
for (const line of lines) {
|
|
123
|
-
const trimmed = line.trim();
|
|
124
|
-
|
|
125
|
-
if (trimmed === "[sounds]") {
|
|
126
|
-
inSoundsSection = true;
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (trimmed.startsWith("[") && trimmed !== "[sounds]") {
|
|
131
|
-
inSoundsSection = false;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (inSoundsSection && !trimmed.startsWith("#") && trimmed) {
|
|
135
|
-
const match = trimmed.match(/^"?([^"=]+)"?\s*=\s*"?([^"#]+)"?/);
|
|
136
|
-
if (match) {
|
|
137
|
-
const [, key, value] = match;
|
|
138
|
-
existingSounds[key.trim()] = value.trim().replace(/"/g, "");
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Merge new sounds with existing
|
|
145
|
-
const mergedSounds = { ...existingSounds, ...(config.sounds || {}) };
|
|
146
|
-
|
|
147
|
-
// Write back, preserving the header
|
|
141
|
+
// Write config as-is (don't merge with file, since config was loaded from file)
|
|
142
|
+
// This allows deletions to work properly
|
|
148
143
|
let content = "# OpenCode Boops Plugin Configuration\n# Sounds can be local file paths OR URLs (automatically downloaded and cached)\n\n[sounds]\n";
|
|
149
144
|
|
|
150
|
-
|
|
151
|
-
|
|
145
|
+
if (config.sounds) {
|
|
146
|
+
for (const [event, sound] of Object.entries(config.sounds).sort()) {
|
|
147
|
+
content += `"${event}" = "${sound}"\n`;
|
|
148
|
+
}
|
|
152
149
|
}
|
|
153
150
|
|
|
154
151
|
writeFileSync(configPath, content);
|
|
@@ -1115,10 +1112,14 @@ async function browse() {
|
|
|
1115
1112
|
const boxHeight = Math.min(availableEvents.length + 4, termHeight - boxStartY - fixedFooterLines);
|
|
1116
1113
|
|
|
1117
1114
|
// Calculate scroll offset for event list
|
|
1118
|
-
const maxVisibleEvents = boxHeight -
|
|
1115
|
+
const maxVisibleEvents = boxHeight - 6; // Reserve space for header(2) + description(2) + footer(2)
|
|
1119
1116
|
const eventScrollOffset = Math.max(0, Math.min(state.pickerSelectedEvent - Math.floor(maxVisibleEvents / 2), availableEvents.length - maxVisibleEvents));
|
|
1120
1117
|
const eventEndIndex = Math.min(eventScrollOffset + maxVisibleEvents, availableEvents.length);
|
|
1121
1118
|
|
|
1119
|
+
// Get selected event info
|
|
1120
|
+
const selectedEvent = availableEvents[state.pickerSelectedEvent];
|
|
1121
|
+
const eventDesc = eventDescriptions[selectedEvent] || "No description available";
|
|
1122
|
+
|
|
1122
1123
|
// Draw each line of the picker box
|
|
1123
1124
|
for (let row = 0; row < boxHeight; row++) {
|
|
1124
1125
|
const screenY = boxStartY + row;
|
|
@@ -1141,6 +1142,14 @@ async function browse() {
|
|
|
1141
1142
|
} else if (row === 2) {
|
|
1142
1143
|
// Separator
|
|
1143
1144
|
process.stdout.write(`\x1b[38;5;240m├${'─'.repeat(pickerWidth - 2)}┤\x1b[0m`);
|
|
1145
|
+
} else if (row === boxHeight - 3) {
|
|
1146
|
+
// Separator before description
|
|
1147
|
+
process.stdout.write(`\x1b[38;5;240m├${'─'.repeat(pickerWidth - 2)}┤\x1b[0m`);
|
|
1148
|
+
} else if (row === boxHeight - 2) {
|
|
1149
|
+
// Event description
|
|
1150
|
+
const desc = eventDesc.length > pickerWidth - 4 ? eventDesc.slice(0, pickerWidth - 7) + '...' : eventDesc;
|
|
1151
|
+
const padding = ' '.repeat(Math.max(0, pickerWidth - desc.length - 4));
|
|
1152
|
+
process.stdout.write(`\x1b[38;5;240m│\x1b[0m \x1b[38;5;246m${desc}\x1b[0m${padding} \x1b[38;5;240m│\x1b[0m`);
|
|
1144
1153
|
} else if (row === boxHeight - 1) {
|
|
1145
1154
|
// Bottom border
|
|
1146
1155
|
process.stdout.write(`\x1b[38;5;240m└${'─'.repeat(pickerWidth - 2)}┘\x1b[0m`);
|
|
@@ -1231,7 +1240,7 @@ async function browse() {
|
|
|
1231
1240
|
let controls;
|
|
1232
1241
|
if (state.pickerMode) {
|
|
1233
1242
|
controls = PLUGIN_INSTALLED
|
|
1234
|
-
? ' \x1b[38;5;240m↑/↓ select
|
|
1243
|
+
? ' \x1b[38;5;240m↑/↓ select enter play ctrl+s save d unassign ←/esc close\x1b[0m'
|
|
1235
1244
|
: ' \x1b[38;5;240m↑/↓ select event enter play ←/esc close \x1b[38;5;208m(save disabled)\x1b[0m';
|
|
1236
1245
|
} else {
|
|
1237
1246
|
controls = ' \x1b[38;5;240m↑/↓ navigate ←/→ tags / search s assign enter play q quit\x1b[0m';
|
|
@@ -1896,6 +1905,19 @@ async function browse() {
|
|
|
1896
1905
|
// Stay in picker mode, just show it was saved
|
|
1897
1906
|
render();
|
|
1898
1907
|
return;
|
|
1908
|
+
} else if (key === 'd') {
|
|
1909
|
+
// 'd' - unassign/delete sound from selected event
|
|
1910
|
+
const config = loadCurrentConfig();
|
|
1911
|
+
const event = availableEvents[state.pickerSelectedEvent];
|
|
1912
|
+
|
|
1913
|
+
if (config.sounds && config.sounds[event]) {
|
|
1914
|
+
delete config.sounds[event];
|
|
1915
|
+
saveConfig(config);
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
// Stay in picker mode
|
|
1919
|
+
render();
|
|
1920
|
+
return;
|
|
1899
1921
|
}
|
|
1900
1922
|
return; // Ignore other keys in picker mode
|
|
1901
1923
|
}
|
package/index.ts
CHANGED
|
@@ -96,10 +96,13 @@ export const BoopsPlugin: Plugin = async ({ client }) => {
|
|
|
96
96
|
return config
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
// Load initial config (will be reloaded on every event for instant updates)
|
|
99
100
|
let config = await loadConfig()
|
|
100
101
|
|
|
101
102
|
// Detect available sound player
|
|
102
103
|
const detectPlayer = async (): Promise<string> => {
|
|
104
|
+
// Reload config to check for player changes
|
|
105
|
+
config = await loadConfig()
|
|
103
106
|
if (config.player) return config.player
|
|
104
107
|
|
|
105
108
|
const players = ["paplay", "aplay", "afplay"]
|
|
@@ -306,6 +309,9 @@ export const BoopsPlugin: Plugin = async ({ client }) => {
|
|
|
306
309
|
|
|
307
310
|
return {
|
|
308
311
|
event: async ({ event }) => {
|
|
312
|
+
// Reload config on every event for instant updates (no restart needed!)
|
|
313
|
+
config = await loadConfig()
|
|
314
|
+
|
|
309
315
|
// Log session.idle events for debugging (helpful for users configuring filters)
|
|
310
316
|
if (event.type === "session.idle") {
|
|
311
317
|
await log(`session.idle event received`, {
|