Sitar118 Claude Sonnet 4.6 commited on
Commit
a21f3f0
·
1 Parent(s): 8208bb7

task: Sprint 6 — channel dropdown + multi-instance registry + list_instances

Browse files

Full spec: channel name ComboBox in Editor, auto-port via bind_to_any_port,
/tmp/pluginbridge-registry.json, new list_instances MCP tool (6th tool),
get_analysis prefixed with channel name.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (1) hide show
  1. SYNC.md +186 -1
SYNC.md CHANGED
@@ -18,7 +18,192 @@
18
  > Claude Code will test on Mac and answer before you write anything.
19
 
20
  ```
21
- STATUS: ANSWERED (2026-05-17)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
  ANSWER: Use Option B. Here is the exact implementation — verified by reading the code.
24
 
 
18
  > Claude Code will test on Mac and answer before you write anything.
19
 
20
  ```
21
+ STATUS: TASK READY — Sprint 6
22
+
23
+ TASK: Channel name dropdown + multi-instance registry + list_instances MCP tool
24
+
25
+ ─────────────────────────────────────────────────────────────
26
+ GOAL (producer workflow):
27
+ 1. Producer loads PluginBridge on Vocal Bus track
28
+ → Selects "Vocal Bus" from dropdown in plugin UI
29
+ → Port auto-assigned, invisible to producer
30
+ 2. Producer loads PluginBridge on Drum Bus track
31
+ → Selects "Drum Bus" from dropdown
32
+ 3. Claude: list_instances() → ["Vocal Bus", "Drum Bus"]
33
+ ─────────────────────────────────────────────────────────────
34
+
35
+ ── PART 1: Constants.h ──────────────────────────────────────
36
+ Add one constant:
37
+ static const char* kRegistryPath = "/tmp/pluginbridge-registry.json";
38
+
39
+ ── PART 2: New file — InstanceRegistry.h (header-only) ──────
40
+ No .cpp needed — header only. No CMakeLists change needed.
41
+ Place at: Source/Plugin/InstanceRegistry.h
42
+
43
+ Implement:
44
+ namespace InstanceRegistry {
45
+ // Write {channelName: port} to registry (atomic: write tmp → rename)
46
+ // Read existing file first, update entry, write back
47
+ void registerInstance(const std::string& channelName, int port);
48
+
49
+ // Remove channelName entry from registry
50
+ void unregisterInstance(const std::string& channelName);
51
+
52
+ // Read all entries → returns map of {name: port}
53
+ std::map<std::string, int> readAll();
54
+ }
55
+
56
+ Threading: use write-to-temp + rename for atomic writes (POSIX rename is atomic).
57
+ Use nlohmann json (already in libs/). Use std::ofstream / std::ifstream.
58
+ Handle missing file gracefully (registerInstance creates it).
59
+
60
+ ── PART 3: McpServer.h / McpServer.cpp ─────────────────────
61
+ CHANGE constructor:
62
+ // Before: McpServer(int port, HelperConnection& helper)
63
+ // After: McpServer(HelperConnection& helper) ← no port param
64
+ // Port is always auto-assigned via bind_to_any_port
65
+
66
+ CHANGE start() — use bind_to_any_port (VERIFIED in httplib.h line 1730):
67
+ // Instead of: server->listen("127.0.0.1", port)
68
+ // Use:
69
+ port = server->bind_to_any_port("127.0.0.1"); // OS assigns, returns actual port
70
+ server->listen_after_bind(); // blocks, runs server
71
+
72
+ ADD channelNameCallback (same pattern as getAnalysisCallback):
73
+ std::function<std::string()> channelNameCallback;
74
+
75
+ ADD list_instances tool to tools/list response and tools/call dispatch:
76
+ {
77
+ "name": "list_instances",
78
+ "description": "List all PluginBridge instances currently loaded in the DAW. Returns channel names.",
79
+ "inputSchema": { "type": "object", "properties": {} }
80
+ }
81
+
82
+ Implementation of list_instances:
83
+ auto registry = InstanceRegistry::readAll();
84
+ json names = json::array();
85
+ for (auto& [name, port] : registry) names.push_back(name);
86
+ return names.dump();
87
+
88
+ UPDATE get_analysis to prefix channel name:
89
+ // Before: "-19.5 LUFS | TP:-17.1 | ..."
90
+ // After: "[Vocal Bus] -19.5 LUFS | TP:-17.1 | ..."
91
+ std::string channel = channelNameCallback ? channelNameCallback() : "Unknown";
92
+ resultText = "[" + channel + "] " + getAnalysisCallback();
93
+
94
+ ── PART 4: PluginBridgeProcessor.h / .cpp ──────────────────
95
+ ADD member:
96
+ juce::String channelName { "Unnamed" };
97
+
98
+ ADD public methods:
99
+ void setChannelName(const juce::String& name);
100
+ juce::String getChannelName() const { return channelName; }
101
+
102
+ CHANGE McpServer construction (Constructor):
103
+ // Before: mcpServer(std::make_unique<McpServer>(PluginBridgeConstants::kDefaultMcpPort, helper))
104
+ // After: mcpServer(std::make_unique<McpServer>(helper))
105
+
106
+ AFTER mcpServer->start() — register to registry:
107
+ InstanceRegistry::registerInstance(channelName.toStdString(), mcpServer->getPort());
108
+
109
+ ADD channelNameCallback wiring (next to getAnalysisCallback):
110
+ mcpServer->channelNameCallback = [this]() { return channelName.toStdString(); };
111
+
112
+ IN setChannelName():
113
+ channelName = name;
114
+ // Re-register with new name (unregister old, register new)
115
+ InstanceRegistry::unregisterInstance(channelName.toStdString());
116
+ InstanceRegistry::registerInstance(name.toStdString(), mcpServer->getPort());
117
+
118
+ IN Destructor — unregister:
119
+ InstanceRegistry::unregisterInstance(channelName.toStdString());
120
+
121
+ UPDATE getStateInformation — save channelName:
122
+ state.setProperty("channelName", channelName, nullptr);
123
+
124
+ UPDATE setStateInformation — restore channelName:
125
+ channelName = state.getProperty("channelName", "Unnamed").toString();
126
+
127
+ ── PART 5: PluginBridgeEditor.h / .mm ── (AUTHORIZED this sprint)
128
+ ADD to PluginBridgeEditor.h:
129
+ juce::ComboBox channelDropdown;
130
+
131
+ IN PluginBridgeEditor constructor (.mm):
132
+ Populate channelDropdown with this EXACT list using addSectionHeading() + addItem():
133
+
134
+ ── Vocals ──
135
+ Lead Vocal (id:1), Backing Vocal (id:2), Adlibs (id:3)
136
+ ── Drums ──
137
+ Kick (id:4), Snare (id:5), Hi-Hat (id:6), Overheads (id:7), Percussion (id:8)
138
+ ── Bass ──
139
+ Bass / 808 (id:9), Bass Guitar (id:10), Sub Bass (id:11), Synth Bass (id:12)
140
+ ── Keys & Guitar ──
141
+ Piano / Keys (id:13), Acoustic Guitar (id:14), Electric Guitar (id:15)
142
+ ── Synths ──
143
+ Synth Lead (id:16), Synth Pad / Pluck (id:17)
144
+ ── Strings & Brass ──
145
+ Strings (id:18), Brass / Woodwind (id:19), Orchestral (id:20)
146
+ ── FX ──
147
+ Reverb / Delay (id:21), Foley / Ambient (id:22)
148
+ ── Buses ──
149
+ Vocal Bus (id:23), Drum Bus (id:24), Guitar Bus (id:25), Synth Bus (id:26),
150
+ Music Bus (id:27), Instrument Bus (id:28), Master Bus / Mix Bus (id:29)
151
+ ── Custom ──
152
+ Custom... (id:30)
153
+
154
+ channelDropdown.onChange = [this]() {
155
+ auto text = channelDropdown.getText();
156
+ if (text == "Custom...") {
157
+ // Show AlertWindow to enter custom name
158
+ juce::AlertWindow::showInputBoxAsync("Channel Name", "Enter channel name:", "",
159
+ processorRef.getChannelName(), nullptr,
160
+ [this](const juce::String& result) {
161
+ if (result.isNotEmpty()) {
162
+ channelDropdown.setText(result, juce::dontSendNotification);
163
+ processorRef.setChannelName(result);
164
+ }
165
+ });
166
+ } else {
167
+ processorRef.setChannelName(text);
168
+ }
169
+ };
170
+
171
+ // Set initial value from processor state
172
+ channelDropdown.setText(processorRef.getChannelName(), juce::dontSendNotification);
173
+
174
+ addAndMakeVisible(channelDropdown);
175
+
176
+ IN resized():
177
+ // Place channelDropdown in the top bar — 160px wide, left side
178
+ // Existing top bar is the area above the picker/plugin content
179
+ // Example (adjust to fit actual layout):
180
+ channelDropdown.setBounds(4, 4, 160, 22);
181
+
182
+ ── SCOPE ────────────────────────────────────────────────────
183
+ TOUCH:
184
+ Constants.h
185
+ Source/Plugin/InstanceRegistry.h ← NEW (header-only, no CMakeLists needed)
186
+ Source/Plugin/McpServer.h / .cpp
187
+ Source/Plugin/PluginBridgeProcessor.h / .cpp
188
+ Source/Plugin/PluginBridgeEditor.h / .mm ← AUTHORIZED this sprint only
189
+
190
+ DO NOT TOUCH:
191
+ PluginPickerComponent.mm ← LOCKED
192
+ HelperPluginHost.mm ← LOCKED
193
+ HelperConnection.cpp ← LOCKED
194
+ CMakeLists.txt ← no new .cpp files, no change needed
195
+
196
+ ── VERIFIED APIs ────────────────────────────────────────────
197
+ httplib bind_to_any_port("127.0.0.1") → returns int port ✅ (libs/httplib.h line 1730)
198
+ httplib listen_after_bind() ✅ (libs/httplib.h line 1731)
199
+ juce::ComboBox::addSectionHeading(text) ✅
200
+ juce::ComboBox::addItem(text, id) ✅
201
+ juce::AlertWindow::showInputBoxAsync(...) ✅
202
+ nlohmann json already in libs/ ✅
203
+ POSIX rename() for atomic file write ✅
204
+
205
+ STATUS (2026-05-17): Sprint 5 answer — prepareToPlay verified safe. See below.
206
+ ```
207
 
208
  ANSWER: Use Option B. Here is the exact implementation — verified by reading the code.
209