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 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 (preserves existing entries and comments)
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
- // Read existing config to preserve comments and other sections
111
- let existingContent = "";
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
- for (const [event, sound] of Object.entries(mergedSounds).sort()) {
151
- content += `"${event}" = "${sound}"\n`;
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 - 4; // Reserve space for header(2) + footer(2)
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 event enter play ctrl+s save ←/esc close\x1b[0m'
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`, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-plugin-boops",
3
- "version": "2.2.1",
3
+ "version": "2.4.0",
4
4
  "description": "Sound notifications for OpenCode - plays pleasant sounds when tasks complete or input is needed",
5
5
  "main": "index.ts",
6
6
  "type": "module",