opencode-plugin-boops 2.2.0 → 2.3.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.
Files changed (2) hide show
  1. package/cli/browse +103 -13
  2. package/package.json +1 -1
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 (preserves existing entries and comments)
100
131
  function saveConfig(config) {
101
132
  if (!PLUGIN_INSTALLED) {
102
133
  console.error("\n⚠️ Cannot save: Plugin not installed");
@@ -106,14 +137,51 @@ function saveConfig(config) {
106
137
  }
107
138
 
108
139
  const configPath = join(homedir(), ".config", "opencode", "plugins", "boops", "boops.toml");
109
- let content = "# OpenCode Boops Plugin Configuration\n\n[sounds]\n";
110
140
 
111
- if (config.sounds) {
112
- for (const [event, sound] of Object.entries(config.sounds)) {
113
- content += `"${event}" = "${sound}"\n`;
141
+ // Read existing config to preserve comments and other sections
142
+ let existingContent = "";
143
+ if (existsSync(configPath)) {
144
+ existingContent = readFileSync(configPath, "utf-8");
145
+ }
146
+
147
+ // Parse existing [sounds] section to merge with new values
148
+ const existingSounds = {};
149
+ if (existingContent) {
150
+ const lines = existingContent.split("\n");
151
+ let inSoundsSection = false;
152
+
153
+ for (const line of lines) {
154
+ const trimmed = line.trim();
155
+
156
+ if (trimmed === "[sounds]") {
157
+ inSoundsSection = true;
158
+ continue;
159
+ }
160
+
161
+ if (trimmed.startsWith("[") && trimmed !== "[sounds]") {
162
+ inSoundsSection = false;
163
+ }
164
+
165
+ if (inSoundsSection && !trimmed.startsWith("#") && trimmed) {
166
+ const match = trimmed.match(/^"?([^"=]+)"?\s*=\s*"?([^"#]+)"?/);
167
+ if (match) {
168
+ const [, key, value] = match;
169
+ existingSounds[key.trim()] = value.trim().replace(/"/g, "");
170
+ }
171
+ }
114
172
  }
115
173
  }
116
174
 
175
+ // Merge new sounds with existing
176
+ const mergedSounds = { ...existingSounds, ...(config.sounds || {}) };
177
+
178
+ // Write back, preserving the header
179
+ let content = "# OpenCode Boops Plugin Configuration\n# Sounds can be local file paths OR URLs (automatically downloaded and cached)\n\n[sounds]\n";
180
+
181
+ for (const [event, sound] of Object.entries(mergedSounds).sort()) {
182
+ content += `"${event}" = "${sound}"\n`;
183
+ }
184
+
117
185
  writeFileSync(configPath, content);
118
186
  return true;
119
187
  }
@@ -1078,10 +1146,14 @@ async function browse() {
1078
1146
  const boxHeight = Math.min(availableEvents.length + 4, termHeight - boxStartY - fixedFooterLines);
1079
1147
 
1080
1148
  // Calculate scroll offset for event list
1081
- const maxVisibleEvents = boxHeight - 4; // Reserve space for header(2) + footer(2)
1149
+ const maxVisibleEvents = boxHeight - 6; // Reserve space for header(2) + description(2) + footer(2)
1082
1150
  const eventScrollOffset = Math.max(0, Math.min(state.pickerSelectedEvent - Math.floor(maxVisibleEvents / 2), availableEvents.length - maxVisibleEvents));
1083
1151
  const eventEndIndex = Math.min(eventScrollOffset + maxVisibleEvents, availableEvents.length);
1084
1152
 
1153
+ // Get selected event info
1154
+ const selectedEvent = availableEvents[state.pickerSelectedEvent];
1155
+ const eventDesc = eventDescriptions[selectedEvent] || "No description available";
1156
+
1085
1157
  // Draw each line of the picker box
1086
1158
  for (let row = 0; row < boxHeight; row++) {
1087
1159
  const screenY = boxStartY + row;
@@ -1104,6 +1176,14 @@ async function browse() {
1104
1176
  } else if (row === 2) {
1105
1177
  // Separator
1106
1178
  process.stdout.write(`\x1b[38;5;240m├${'─'.repeat(pickerWidth - 2)}┤\x1b[0m`);
1179
+ } else if (row === boxHeight - 3) {
1180
+ // Separator before description
1181
+ process.stdout.write(`\x1b[38;5;240m├${'─'.repeat(pickerWidth - 2)}┤\x1b[0m`);
1182
+ } else if (row === boxHeight - 2) {
1183
+ // Event description
1184
+ const desc = eventDesc.length > pickerWidth - 4 ? eventDesc.slice(0, pickerWidth - 7) + '...' : eventDesc;
1185
+ const padding = ' '.repeat(Math.max(0, pickerWidth - desc.length - 4));
1186
+ process.stdout.write(`\x1b[38;5;240m│\x1b[0m \x1b[38;5;246m${desc}\x1b[0m${padding} \x1b[38;5;240m│\x1b[0m`);
1107
1187
  } else if (row === boxHeight - 1) {
1108
1188
  // Bottom border
1109
1189
  process.stdout.write(`\x1b[38;5;240m└${'─'.repeat(pickerWidth - 2)}┘\x1b[0m`);
@@ -1194,7 +1274,7 @@ async function browse() {
1194
1274
  let controls;
1195
1275
  if (state.pickerMode) {
1196
1276
  controls = PLUGIN_INSTALLED
1197
- ? ' \x1b[38;5;240m↑/↓ select event enter play ctrl+s save ←/esc close\x1b[0m'
1277
+ ? ' \x1b[38;5;240m↑/↓ select enter play ctrl+s save d unassign ←/esc close\x1b[0m'
1198
1278
  : ' \x1b[38;5;240m↑/↓ select event enter play ←/esc close \x1b[38;5;208m(save disabled)\x1b[0m';
1199
1279
  } else {
1200
1280
  controls = ' \x1b[38;5;240m↑/↓ navigate ←/→ tags / search s assign enter play q quit\x1b[0m';
@@ -1846,20 +1926,30 @@ async function browse() {
1846
1926
  }
1847
1927
  return;
1848
1928
  } else if (key === '\u0013') {
1849
- // Ctrl+S - save assignment
1929
+ // Ctrl+S - save assignment (stay in picker mode to assign more)
1850
1930
  const config = loadCurrentConfig();
1851
1931
  const event = availableEvents[state.pickerSelectedEvent];
1852
1932
 
1853
1933
  if (!config.sounds) config.sounds = {};
1854
- config.sounds[event] = state.pickerSound.id;
1934
+ config.sounds[event] = state.pickerSound.name; // Save friendly name, not ID
1855
1935
 
1856
1936
  // Save to config file
1857
- const saved = saveConfig(config);
1937
+ saveConfig(config);
1858
1938
 
1859
- if (saved) {
1860
- state.pickerMode = false;
1861
- state.pickerSound = null;
1939
+ // Stay in picker mode, just show it was saved
1940
+ render();
1941
+ return;
1942
+ } else if (key === 'd') {
1943
+ // 'd' - unassign/delete sound from selected event
1944
+ const config = loadCurrentConfig();
1945
+ const event = availableEvents[state.pickerSelectedEvent];
1946
+
1947
+ if (config.sounds && config.sounds[event]) {
1948
+ delete config.sounds[event];
1949
+ saveConfig(config);
1862
1950
  }
1951
+
1952
+ // Stay in picker mode
1863
1953
  render();
1864
1954
  return;
1865
1955
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-plugin-boops",
3
- "version": "2.2.0",
3
+ "version": "2.3.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",