synabun 2026.4.20-beta.7 → 2026.4.20-beta.9
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.
|
@@ -99,6 +99,7 @@ let _primaryAgents = new Set();
|
|
|
99
99
|
let _sessions = [];
|
|
100
100
|
const _userMsgIds = new Set(); // track user messageIDs to filter echoed SSE events
|
|
101
101
|
const _partTypes = new Map(); // track partID → type (reasoning/text) from part.updated events
|
|
102
|
+
const _renderedAssistantMsgIds = new Set(); // prevents double-render when message.updated re-fires after finalize (Windows timing)
|
|
102
103
|
let _activeSendRequestId = null; // WS request id of the in-flight message:send (for abort cancellation)
|
|
103
104
|
let _renderSeq = 0; // guard against stale async renders overwriting fresh ones
|
|
104
105
|
let _activeQuestionToolId = null; // toolId of the currently active (unanswered) question card
|
|
@@ -1163,6 +1164,15 @@ export function handleSSEEvent(eventType, event) {
|
|
|
1163
1164
|
if (mid) _userMsgIds.add(mid);
|
|
1164
1165
|
break;
|
|
1165
1166
|
}
|
|
1167
|
+
// Guard: once we've rendered this assistant message's final payload, ignore
|
|
1168
|
+
// re-emitted message.updated events (OpenCode on Windows sometimes fires a
|
|
1169
|
+
// late duplicate after finalize, which would otherwise re-stream the whole
|
|
1170
|
+
// response into a brand-new assistant bubble).
|
|
1171
|
+
const assistantMid = info.id || event.messageID;
|
|
1172
|
+
if (assistantMid && _renderedAssistantMsgIds.has(assistantMid)) {
|
|
1173
|
+
update();
|
|
1174
|
+
break;
|
|
1175
|
+
}
|
|
1166
1176
|
// If no streaming element was built by prior deltas, render the
|
|
1167
1177
|
// assistant's content directly from the event (non-streaming path)
|
|
1168
1178
|
const hasStreaming = container.querySelector('.ocp-msg-assistant.streaming');
|
|
@@ -1176,6 +1186,9 @@ export function handleSSEEvent(eventType, event) {
|
|
|
1176
1186
|
removeThinking(container);
|
|
1177
1187
|
finalizeStreamingMessage(container);
|
|
1178
1188
|
_partTypes.clear();
|
|
1189
|
+
if (assistantMid && (explicitTerminal || info.tokens || info.usage)) {
|
|
1190
|
+
_renderedAssistantMsgIds.add(assistantMid);
|
|
1191
|
+
}
|
|
1179
1192
|
if (info.tokens || info.usage) {
|
|
1180
1193
|
const raw = info.tokens || info.usage || {};
|
|
1181
1194
|
tab.threadTokenUsage = normalizeThreadTokenUsage(raw);
|
|
@@ -1287,6 +1300,14 @@ export function handleSSEEvent(eventType, event) {
|
|
|
1287
1300
|
}
|
|
1288
1301
|
case 'message.completed': {
|
|
1289
1302
|
removeThinking(container);
|
|
1303
|
+
const completedMid = event.messageID || event.id;
|
|
1304
|
+
if (completedMid && _renderedAssistantMsgIds.has(completedMid)) {
|
|
1305
|
+
tab.running = false;
|
|
1306
|
+
tab.turnStartedAt = 0;
|
|
1307
|
+
setTurnStatus(tab);
|
|
1308
|
+
update();
|
|
1309
|
+
break;
|
|
1310
|
+
}
|
|
1290
1311
|
// Fallback: render content if no streaming element was built
|
|
1291
1312
|
const hasStreamingLegacy = container.querySelector('.ocp-msg-assistant.streaming');
|
|
1292
1313
|
renderAssistantPayload(container, event.content ?? event.parts ?? event.text ?? '', {
|
|
@@ -1294,6 +1315,7 @@ export function handleSSEEvent(eventType, event) {
|
|
|
1294
1315
|
});
|
|
1295
1316
|
finalizeStreamingMessage(container);
|
|
1296
1317
|
_partTypes.clear();
|
|
1318
|
+
if (completedMid) _renderedAssistantMsgIds.add(completedMid);
|
|
1297
1319
|
tab.running = false;
|
|
1298
1320
|
tab.turnStartedAt = 0;
|
|
1299
1321
|
setTurnStatus(tab);
|
|
@@ -1080,6 +1080,7 @@ function buildSetupTab(setupStatus) {
|
|
|
1080
1080
|
const claudeConnected = setupStatus.claude?.connected || false;
|
|
1081
1081
|
const geminiConnected = setupStatus.gemini?.connected || false;
|
|
1082
1082
|
const codexConnected = setupStatus.codex?.connected || false;
|
|
1083
|
+
const opencodeConnected = setupStatus.opencode?.connected || false;
|
|
1083
1084
|
|
|
1084
1085
|
const statusBadge = (on) => `<span class="setup-status-badge ${on ? 'active' : 'inactive'}">${on ? 'Connected' : 'Off'}</span>`;
|
|
1085
1086
|
|
|
@@ -1184,6 +1185,40 @@ function buildSetupTab(setupStatus) {
|
|
|
1184
1185
|
</div>
|
|
1185
1186
|
</div>
|
|
1186
1187
|
|
|
1188
|
+
<!-- OPENCODE -->
|
|
1189
|
+
<div class="iface-section collapsed" data-collapsible id="setup-opencode">
|
|
1190
|
+
<div class="gfx-group-title">
|
|
1191
|
+
<span style="display:flex;align-items:center;gap:8px">
|
|
1192
|
+
${CHEVRON_ICON}
|
|
1193
|
+
${OPENCODE_ICON}
|
|
1194
|
+
<span>OpenCode</span>
|
|
1195
|
+
${statusBadge(opencodeConnected)}
|
|
1196
|
+
</span>
|
|
1197
|
+
</div>
|
|
1198
|
+
<div class="cc-section-body">
|
|
1199
|
+
<div class="cc-integration-item${opencodeConnected ? ' enabled' : ''}" id="setup-opencode-mcp-row">
|
|
1200
|
+
<div class="cc-integration-info">
|
|
1201
|
+
<div class="cc-integration-label">SynaBun MCP</div>
|
|
1202
|
+
<div class="cc-integration-path" id="setup-opencode-mcp-status">${opencodeConnected ? 'Registered in ~/.config/opencode/config.json' : 'Not connected'}</div>
|
|
1203
|
+
</div>
|
|
1204
|
+
<button class="cc-toggle${opencodeConnected ? ' on' : ''}" id="setup-opencode-mcp-toggle"></button>
|
|
1205
|
+
</div>
|
|
1206
|
+
|
|
1207
|
+
<div style="margin-top:12px">
|
|
1208
|
+
<div class="cc-greeting-label" style="margin-bottom:4px">Manual Config <span style="color:var(--t-faint)">(~/.config/opencode/config.json)</span></div>
|
|
1209
|
+
<div class="cc-ruleset-preview" id="setup-opencode-config-preview" style="max-height:120px">Loading...</div>
|
|
1210
|
+
<button class="cc-copy-btn" id="setup-opencode-config-copy" style="margin-top:4px">${COPY_ICON} Copy JSON Config</button>
|
|
1211
|
+
</div>
|
|
1212
|
+
|
|
1213
|
+
<div style="margin-top:12px">
|
|
1214
|
+
<div class="cc-greeting-label" style="margin-bottom:4px">AGENTS.md Ruleset</div>
|
|
1215
|
+
<div class="cc-ruleset-preview" id="setup-opencode-ruleset-preview">Loading...</div>
|
|
1216
|
+
<button class="cc-copy-btn" id="setup-opencode-ruleset-copy" style="margin-top:4px">${COPY_ICON} Copy Ruleset</button>
|
|
1217
|
+
<div class="setup-hint">Paste into your project's <code>AGENTS.md</code></div>
|
|
1218
|
+
</div>
|
|
1219
|
+
</div>
|
|
1220
|
+
</div>
|
|
1221
|
+
|
|
1187
1222
|
<!-- COEXISTENCE RULES -->
|
|
1188
1223
|
<div class="iface-section collapsed" data-collapsible id="setup-coexistence">
|
|
1189
1224
|
<div class="gfx-group-title">
|
|
@@ -2855,7 +2890,7 @@ export async function openSettingsModal() {
|
|
|
2855
2890
|
let greetingConfig = { defaults: {}, projects: {}, global: {} };
|
|
2856
2891
|
let codexGreetingConfig = { enabled: true, defaults: {}, projects: {}, global: {} };
|
|
2857
2892
|
let opencodeGreetingConfig = { enabled: false, defaults: {}, projects: {}, global: {} };
|
|
2858
|
-
let setupStatus = { claude: {}, gemini: {}, codex: {}, paths: {} };
|
|
2893
|
+
let setupStatus = { claude: {}, gemini: {}, codex: {}, opencode: {}, paths: {} };
|
|
2859
2894
|
let cliConfig = {};
|
|
2860
2895
|
let toolPermissions = {};
|
|
2861
2896
|
let toolCategories = [];
|
|
@@ -2944,7 +2979,7 @@ export async function openSettingsModal() {
|
|
|
2944
2979
|
<nav class="settings-nav">
|
|
2945
2980
|
${buildNavHTML(variantTabs, {
|
|
2946
2981
|
server: settings.storage === 'sqlite' ? 'connected' : 'disconnected',
|
|
2947
|
-
setup: (setupStatus.claude?.connected || setupStatus.gemini?.connected || setupStatus.codex?.connected) ? 'connected' : 'disconnected',
|
|
2982
|
+
setup: (setupStatus.claude?.connected || setupStatus.gemini?.connected || setupStatus.codex?.connected || setupStatus.opencode?.connected) ? 'connected' : 'disconnected',
|
|
2948
2983
|
discord: discordConfig.botToken ? 'connected' : 'disconnected',
|
|
2949
2984
|
})}
|
|
2950
2985
|
</nav>
|
|
@@ -4989,6 +5024,38 @@ export async function openSettingsModal() {
|
|
|
4989
5024
|
}
|
|
4990
5025
|
wireRulesetPreview('codex', 'codex');
|
|
4991
5026
|
|
|
5027
|
+
// ── OpenCode ──
|
|
5028
|
+
wireSetupMcpToggle('opencode', '/api/opencode/mcp', '~/.config/opencode/config.json');
|
|
5029
|
+
// Config preview — OpenCode stores MCP servers under the "mcp" key of config.json
|
|
5030
|
+
{
|
|
5031
|
+
const preview = overlay.querySelector('#setup-opencode-config-preview');
|
|
5032
|
+
let cachedConfig = '';
|
|
5033
|
+
if (preview) {
|
|
5034
|
+
const mp = setupStatus.paths?.mcpIndexPath || '<path-to>/mcp-server/run.mjs';
|
|
5035
|
+
const ep = setupStatus.paths?.envPath || '<path-to>/synabun/.env';
|
|
5036
|
+
fetch('/api/opencode/mcp').then(r => r.json()).then(data => {
|
|
5037
|
+
if (data.ok && data.data && Object.keys(data.data).length) {
|
|
5038
|
+
cachedConfig = JSON.stringify({ mcp: data.data }, null, 2);
|
|
5039
|
+
preview.textContent = cachedConfig;
|
|
5040
|
+
} else if (data.ok) {
|
|
5041
|
+
cachedConfig = JSON.stringify({
|
|
5042
|
+
mcp: {
|
|
5043
|
+
SynaBun: {
|
|
5044
|
+
type: 'stdio',
|
|
5045
|
+
command: 'node',
|
|
5046
|
+
args: [mp],
|
|
5047
|
+
env: { DOTENV_PATH: ep },
|
|
5048
|
+
},
|
|
5049
|
+
},
|
|
5050
|
+
}, null, 2);
|
|
5051
|
+
preview.textContent = cachedConfig;
|
|
5052
|
+
} else { preview.textContent = 'Could not load config.'; }
|
|
5053
|
+
}).catch(() => { preview.textContent = 'Failed to load.'; });
|
|
5054
|
+
}
|
|
5055
|
+
wireCopyBtn('setup-opencode-config-copy', () => cachedConfig, 'Copy JSON Config');
|
|
5056
|
+
}
|
|
5057
|
+
wireRulesetPreview('opencode', 'generic');
|
|
5058
|
+
|
|
4992
5059
|
// ── Coexistence rules ──
|
|
4993
5060
|
wireRulesetPreview('coexistence', 'coexistence');
|
|
4994
5061
|
}
|
|
@@ -13887,7 +13887,17 @@ app.get('/api/setup/status', (req, res) => {
|
|
|
13887
13887
|
} catch {}
|
|
13888
13888
|
codex.cliCommand = `codex --full-auto mcp add -- node "${mcpIndexPath}"`;
|
|
13889
13889
|
|
|
13890
|
-
|
|
13890
|
+
// OpenCode
|
|
13891
|
+
let opencode = { connected: false };
|
|
13892
|
+
try {
|
|
13893
|
+
const ocPath = getOpencodeConfigPath();
|
|
13894
|
+
if (existsSync(ocPath)) {
|
|
13895
|
+
const oc = JSON.parse(readFileSync(ocPath, 'utf-8'));
|
|
13896
|
+
opencode.connected = !!(oc?.mcp && oc.mcp.SynaBun);
|
|
13897
|
+
}
|
|
13898
|
+
} catch {}
|
|
13899
|
+
|
|
13900
|
+
res.json({ ok: true, claude, gemini, codex, opencode, paths: { mcpIndexPath, envPath } });
|
|
13891
13901
|
} catch (err) {
|
|
13892
13902
|
res.status(500).json({ error: err.message });
|
|
13893
13903
|
}
|