dsh-cc-mcp 0.2.1 → 0.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.
@@ -0,0 +1,1080 @@
1
+ window.__ModuleLoader__.load({ id: "dsh-cc-mcp", factory: (require) => {
2
+ 'use strict'
3
+ var module = { exports: {} }; var exports = module.exports;
4
+
5
+ // dsh-cc-mcp — browser half (the `/mcp` management panel).
6
+ //
7
+ // Plain DOM, no imports, no require, no build step, no top-level await: this
8
+ // file is a CLASSIC script that several bundles get concatenated into, so the
9
+ // single top-level statement above is this whole file and every declaration
10
+ // lives inside the factory.
11
+ //
12
+ // Transport: same-origin POSTs to the plugin's own host route (`/cc-mcp/*`,
13
+ // registered by the host half on the `webServer` service). There is no
14
+ // host→browser push, so the panel pulls (`state`) and refreshes after every
15
+ // mutation, and the session self-check polls `check` a bounded number of
16
+ // times.
17
+
18
+ var PLUGIN_ID = 'dsh-cc-mcp'
19
+ var CHANNEL = '/cc-mcp'
20
+ var STYLE_TAG_ID = 'dsh-cc-mcp/panel.css'
21
+ var CHECK_RETRY_DELAYS = [1000, 3000, 4000]
22
+ var TOAST_TTL_MS = 6000
23
+ var REFETCH_INTERVAL_MS = 700
24
+ var REFETCH_CAP_MS = 20000
25
+
26
+ // ─── module state ─────────────────────────────────────────────────────────
27
+ // Every one of these is written only from inside apply()/its deferred
28
+ // injections and cleared by the matching disposer, so an unload leaves no
29
+ // DOM node, timer, or captured caller behind.
30
+
31
+ var styleEl = null
32
+ var overlayRoot = null
33
+ var toastItems = null
34
+ var toastRoot = null
35
+ var panel = null
36
+ var sessionsScope = null
37
+ var sessionUnsub = null
38
+ var checkSessionId = null
39
+ var pendingCheck = null
40
+ var checkRetryTimer = null
41
+ var checkRetryIndex = 0
42
+ var bindSeq = 0
43
+
44
+ // ─── tiny helpers ─────────────────────────────────────────────────────────
45
+
46
+ function warn(message, error) {
47
+ try {
48
+ var text = error === undefined ? message : message + ': ' + String((error && error.message) || error)
49
+ if (typeof console !== 'undefined' && console !== null && typeof console.warn === 'function') console.warn(text)
50
+ } catch (ignored) { /* logging must never break the caller */ }
51
+ }
52
+
53
+ function isDom() {
54
+ return typeof document !== 'undefined' && document !== null && typeof document.createElement === 'function'
55
+ }
56
+
57
+ function isRecord(value) {
58
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
59
+ }
60
+
61
+ /** Always a string — snapshot fields come off the wire and are not trusted. */
62
+ function str(value, fallback) {
63
+ if (typeof value === 'string' && value !== '') return value
64
+ if (typeof value === 'number' && isFinite(value)) return String(value)
65
+ return fallback === undefined ? '' : fallback
66
+ }
67
+
68
+ function el(tag, className, text) {
69
+ var node = document.createElement(tag)
70
+ if (className) node.className = className
71
+ if (text !== undefined && text !== null && text !== '') node.textContent = text
72
+ return node
73
+ }
74
+
75
+ function clear(node) {
76
+ if (!node) return
77
+ while (node.firstChild) node.removeChild(node.firstChild)
78
+ }
79
+
80
+ // ─── transport ────────────────────────────────────────────────────────────
81
+
82
+ function transportFailure(code, message) {
83
+ return { ok: false, error: { code: code, message: message, details: {} } }
84
+ }
85
+
86
+ function toEnvelope(thrown) {
87
+ var message = (thrown && thrown.message) || thrown
88
+ return transportFailure('cc-mcp/transport', String(message === undefined || message === null ? 'transport failed' : message))
89
+ }
90
+
91
+ /**
92
+ * One panel call to the host's plugin-owned route. Never throws and never
93
+ * rejects: a missing transport, a failed request, or a malformed envelope all
94
+ * become the same `{ ok: false, error }` shape the views already render.
95
+ */
96
+ function api(endpoint, payload) {
97
+ try {
98
+ if (typeof fetch !== 'function') {
99
+ return Promise.resolve(transportFailure('cc-mcp/no-transport', 'fetch is unavailable in this shell'))
100
+ }
101
+ return fetch(CHANNEL + '/' + endpoint, {
102
+ method: 'POST',
103
+ headers: { 'content-type': 'application/json', 'x-cc-mcp': '1' },
104
+ body: JSON.stringify(payload === undefined || payload === null ? {} : payload),
105
+ credentials: 'same-origin',
106
+ }).then(
107
+ function (response) {
108
+ return response.json().then(function (result) {
109
+ if (isRecord(result) && result.ok === true) return { ok: true, value: result.value }
110
+ if (isRecord(result) && result.ok === false && isRecord(result.error)) {
111
+ return {
112
+ ok: false,
113
+ error: {
114
+ code: str(result.error.code, 'cc-mcp/internal'),
115
+ message: str(result.error.message, 'cc-mcp request failed'),
116
+ details: isRecord(result.error.details) ? result.error.details : {},
117
+ },
118
+ }
119
+ }
120
+ return transportFailure('cc-mcp/envelope', 'malformed result envelope from ' + endpoint)
121
+ }, function () {
122
+ return transportFailure('cc-mcp/envelope', 'HTTP ' + response.status + ' without a JSON envelope')
123
+ })
124
+ },
125
+ toEnvelope,
126
+ )
127
+ } catch (error) {
128
+ return Promise.resolve(toEnvelope(error))
129
+ }
130
+ }
131
+
132
+ function codeOf(response) {
133
+ return (response && response.error && response.error.code) || 'cc-mcp/internal'
134
+ }
135
+
136
+ function errorOf(response, fallback) {
137
+ var message = response && response.error && response.error.message
138
+ return str(message, fallback || 'request failed')
139
+ }
140
+
141
+ function entryOf(response) {
142
+ var value = response && response.value
143
+ return isRecord(value) && isRecord(value.entry) ? value.entry : null
144
+ }
145
+
146
+ function entryKey(entry) {
147
+ if (!isRecord(entry)) return ''
148
+ return str(entry.key) || (str(entry.scope) + ':' + str(entry.serverName))
149
+ }
150
+
151
+ function isDangerCode(code) {
152
+ return String(code || '').indexOf('no-transport') !== -1 || String(code || '').indexOf('transport') !== -1
153
+ }
154
+
155
+ // ─── styles ───────────────────────────────────────────────────────────────
156
+
157
+ var STYLES = [
158
+ '.ccmcp-overlay{position:fixed;inset:0;z-index:1100;display:flex;align-items:center;justify-content:center;padding:24px;pointer-events:none}',
159
+ '.ccmcp-backdrop{position:absolute;inset:0;background:var(--dsw-alias-bg-mask-1,rgb(0 0 0 / 24%));pointer-events:auto}',
160
+ '.ccmcp-panel{position:relative;pointer-events:auto;width:620px;max-width:100%;max-height:70vh;display:flex;flex-direction:column;overflow:hidden;border-radius:16px;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#0f1115);border:1px solid var(--dsw-alias-border-l1,rgb(0 0 0 / 10%));box-shadow:var(--dsw-elevation-prominent,0 12px 32px rgb(0 0 0 / 18%));font-family:var(--dsw-font-family,system-ui,-apple-system,"Segoe UI",sans-serif);font-size:13px;line-height:20px}',
161
+ '.ccmcp-head{display:flex;align-items:flex-start;gap:12px;padding:16px 16px 12px;flex:0 0 auto}',
162
+ '.ccmcp-head-main{flex:1 1 auto;min-width:0}',
163
+ '.ccmcp-title{margin:0;font-size:15px;line-height:22px;font-weight:600;color:var(--dsw-alias-label-primary,#0f1115)}',
164
+ '.ccmcp-subtitle{margin:2px 0 0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#61666b);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
165
+ '.ccmcp-head-actions{display:flex;align-items:center;gap:8px;flex:0 0 auto}',
166
+ '.ccmcp-iconbtn{appearance:none;border:0;background:transparent;color:var(--dsw-alias-label-secondary,#61666b);width:28px;height:28px;border-radius:8px;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;font-size:14px;line-height:1}',
167
+ '.ccmcp-iconbtn:hover{background:var(--dsw-alias-interactive-bg-hover,rgb(38 49 72 / 6%));color:var(--dsw-alias-label-primary,#0f1115)}',
168
+ '.ccmcp-backbtn{appearance:none;border:0;background:transparent;color:var(--dsw-alias-label-secondary,#61666b);font:inherit;font-size:12px;padding:4px 8px;margin:0 0 0 -8px;border-radius:8px;cursor:pointer;align-self:flex-start}',
169
+ '.ccmcp-backbtn:hover{background:var(--dsw-alias-interactive-bg-hover,rgb(38 49 72 / 6%));color:var(--dsw-alias-label-primary,#0f1115)}',
170
+ '.ccmcp-body{flex:1 1 auto;min-height:0;overflow:auto;padding:0 8px 8px}',
171
+ '.ccmcp-row{display:flex;align-items:center;gap:10px;width:100%;text-align:left;appearance:none;border:0;background:transparent;font:inherit;color:inherit;padding:8px;border-radius:10px;cursor:pointer}',
172
+ '.ccmcp-row:hover{background:var(--dsw-alias-interactive-bg-hover,rgb(38 49 72 / 6%))}',
173
+ '.ccmcp-glyph{flex:0 0 auto;width:28px;height:28px;border-radius:9px;display:inline-flex;align-items:center;justify-content:center;font-size:12px;font-weight:600;line-height:1;background:var(--dsw-alias-bg-module-platform,rgb(235 238 242));color:var(--dsw-alias-label-secondary,#61666b);text-transform:uppercase}',
174
+ '.ccmcp-name{flex:1 1 auto;min-width:0;font-weight:500;color:var(--dsw-alias-label-primary,#0f1115);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
175
+ '.ccmcp-type{flex:0 0 auto;font-size:12px;color:var(--dsw-alias-label-primary,#0f1115);white-space:nowrap}',
176
+ '.ccmcp-type-transport{color:var(--dsw-alias-label-secondary,#61666b)}',
177
+ '.ccmcp-status{flex:0 0 auto;display:flex;align-items:center;justify-content:flex-end;min-width:88px}',
178
+ '.ccmcp-status-muted{font-size:12px;color:var(--dsw-alias-label-secondary,#61666b)}',
179
+ '.ccmcp-btn{appearance:none;font:inherit;font-size:12px;line-height:16px;padding:4px 12px;border-radius:8px;cursor:pointer;border:1px solid var(--dsw-alias-border-l2,rgb(0 0 0 / 10%));background:var(--dsw-alias-button-elevated-fill,#fff);color:var(--dsw-alias-label-primary,#0f1115)}',
180
+ '.ccmcp-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgb(38 49 72 / 6%))}',
181
+ '.ccmcp-btn:disabled{cursor:default;opacity:.6}',
182
+ '.ccmcp-spinner{display:inline-block;width:14px;height:14px;border-radius:50%;border:1.5px solid var(--dsw-alias-border-l2,rgb(0 0 0 / 10%));border-top-color:var(--dsw-alias-label-secondary,#61666b);animation:ccmcp-spin .7s linear infinite}',
183
+ '.ccmcp-spinner-lg{width:18px;height:18px;border-width:2px}',
184
+ '.ccmcp-spin-row{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--dsw-alias-label-secondary,#61666b);padding:2px 0 10px}',
185
+ '.ccmcp-detail-head{display:flex;align-items:center;gap:10px;width:100%}',
186
+ '.ccmcp-detail-head .ccmcp-name{font-size:14px}',
187
+ '.ccmcp-statusline{display:flex;align-items:center;gap:6px;padding:12px 16px 0;font-size:13px}',
188
+ '.ccmcp-statusline-ready{color:var(--dsw-alias-state-success-primary,#12b76a)}',
189
+ '.ccmcp-statusline-error{color:var(--dsw-alias-state-error-primary,#ec1313)}',
190
+ '.ccmcp-statusline-muted{color:var(--dsw-alias-label-secondary,#61666b)}',
191
+ '.ccmcp-statusline-detail{font-weight:400;color:var(--dsw-alias-label-secondary,#61666b);overflow-wrap:anywhere}',
192
+ '.ccmcp-source{margin:4px 0 0;padding:0 16px;font-size:12px;color:var(--dsw-alias-label-secondary,#61666b);overflow-wrap:anywhere}',
193
+ '.ccmcp-tools{margin:12px 16px 16px;border:1px solid var(--dsw-alias-border-l1,rgb(0 0 0 / 10%));border-radius:10px;overflow:hidden}',
194
+ '.ccmcp-tools-head{padding:8px 12px;border-bottom:1px solid var(--dsw-alias-border-l1,rgb(0 0 0 / 10%));font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary,#61666b)}',
195
+ '.ccmcp-tools-body{max-height:280px;overflow:auto}',
196
+ '.ccmcp-tool{display:grid;grid-template-columns:minmax(0,1fr);gap:2px;padding:8px 12px;border-bottom:1px solid var(--dsw-alias-border-l1,rgb(0 0 0 / 10%))}',
197
+ '.ccmcp-tool:last-child{border-bottom:0}',
198
+ '.ccmcp-tool-name{font-family:var(--ds-font-family-code,ui-monospace,SFMono-Regular,Menlo,Consolas,monospace);font-size:12px;color:var(--dsw-alias-label-primary,#0f1115);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
199
+ '.ccmcp-tool-desc{margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#61666b);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;overflow-wrap:anywhere}',
200
+ '.ccmcp-tools-muted .ccmcp-tool-name,.ccmcp-tools-muted .ccmcp-tool-desc{opacity:.55}',
201
+ '.ccmcp-empty{padding:32px 16px;text-align:center;color:var(--dsw-alias-label-secondary,#61666b)}',
202
+ '.ccmcp-empty-text{margin:0 0 10px}',
203
+ '.ccmcp-toast-root{position:fixed;right:16px;bottom:16px;z-index:1120;display:flex;flex-direction:column;gap:8px;max-width:360px;pointer-events:none}',
204
+ '.ccmcp-toast{pointer-events:auto;cursor:pointer;display:flex;flex-direction:column;gap:2px;padding:10px 12px;border-radius:12px;background:var(--dsw-alias-bg-layer-2,#fff);border:1px solid var(--dsw-alias-border-l1,rgb(0 0 0 / 10%));box-shadow:var(--dsw-elevation-prominent,0 12px 32px rgb(0 0 0 / 18%));font-family:var(--dsw-font-family,system-ui,-apple-system,"Segoe UI",sans-serif)}',
205
+ '.ccmcp-toast-title{font-size:12px;font-weight:600;line-height:18px;color:var(--dsw-alias-label-primary,#0f1115)}',
206
+ '.ccmcp-toast-danger .ccmcp-toast-title{color:var(--dsw-alias-state-error-primary,#ec1313)}',
207
+ '.ccmcp-toast-body{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#61666b);overflow-wrap:anywhere}',
208
+ '@keyframes ccmcp-spin{to{transform:rotate(360deg)}}',
209
+ '@media (prefers-reduced-motion:reduce){.ccmcp-spinner{animation-duration:2s}}',
210
+ ].join('')
211
+
212
+ function installStyles() {
213
+ if (!isDom()) return null
214
+ try {
215
+ var existing = document.querySelector('style[data-plugin-css="' + STYLE_TAG_ID + '"]')
216
+ if (existing) return existing
217
+ var tag = document.createElement('style')
218
+ tag.setAttribute('data-plugin', PLUGIN_ID)
219
+ tag.setAttribute('data-plugin-css', STYLE_TAG_ID)
220
+ tag.textContent = STYLES
221
+ document.head.appendChild(tag)
222
+ return tag
223
+ } catch (error) {
224
+ warn('style install failed', error)
225
+ return null
226
+ }
227
+ }
228
+
229
+ // ─── toasts ───────────────────────────────────────────────────────────────
230
+
231
+ function closeToast(item) {
232
+ try {
233
+ if (item && item.timer !== null) clearTimeout(item.timer)
234
+ if (item && item.node && item.node.parentNode) item.node.parentNode.removeChild(item.node)
235
+ if (toastItems && item) toastItems.delete(item)
236
+ if (toastItems && toastItems.size === 0 && toastRoot && toastRoot.parentNode) {
237
+ toastRoot.parentNode.removeChild(toastRoot)
238
+ }
239
+ } catch (error) {
240
+ warn('toast dismiss failed', error)
241
+ }
242
+ }
243
+
244
+ function ensureToastRoot() {
245
+ if (!isDom()) return null
246
+ if (toastRoot && toastRoot.parentNode) return toastRoot
247
+ toastRoot = el('div', 'ccmcp-toast-root')
248
+ toastRoot.setAttribute('role', 'status')
249
+ document.body.appendChild(toastRoot)
250
+ toastItems = new Set()
251
+ return toastRoot
252
+ }
253
+
254
+ /** One bottom-right, auto-dismissing toast; click dismisses it early. */
255
+ function toast(title, body, tone) {
256
+ if (!isDom()) return null
257
+ try {
258
+ var root = ensureToastRoot()
259
+ if (!root || !toastItems) return null
260
+ var node = el('div', 'ccmcp-toast' + (tone === 'danger' ? ' ccmcp-toast-danger' : ''))
261
+ node.setAttribute('role', 'alert')
262
+ node.appendChild(el('div', 'ccmcp-toast-title', str(title, 'MCP')))
263
+ if (body) node.appendChild(el('div', 'ccmcp-toast-body', str(body)))
264
+ var item = { node: node, timer: null }
265
+ node.addEventListener('click', function () { closeToast(item) })
266
+ root.appendChild(node)
267
+ toastItems.add(item)
268
+ item.timer = setTimeout(function () { closeToast(item) }, TOAST_TTL_MS)
269
+ return item
270
+ } catch (error) {
271
+ warn('toast failed', error)
272
+ return null
273
+ }
274
+ }
275
+
276
+ function toastFailure(serverName, error) {
277
+ toast('MCP server failed to connect', '\u201C' + str(serverName, 'unknown server') + '\u201D \u2014 ' + str(error, 'connect failed'), 'danger')
278
+ }
279
+
280
+ function toastFailures(failures) {
281
+ if (!Array.isArray(failures)) return
282
+ for (var i = 0; i < failures.length; i += 1) {
283
+ var failure = failures[i]
284
+ if (!isRecord(failure)) continue
285
+ toastFailure(failure.serverName, failure.error)
286
+ }
287
+ }
288
+
289
+ function clearToasts() {
290
+ try {
291
+ if (toastItems) {
292
+ var items = []
293
+ toastItems.forEach(function (item) { items.push(item) })
294
+ for (var i = 0; i < items.length; i += 1) closeToast(items[i])
295
+ }
296
+ toastItems = null
297
+ if (toastRoot && toastRoot.parentNode) toastRoot.parentNode.removeChild(toastRoot)
298
+ toastRoot = null
299
+ } catch (error) {
300
+ warn('toast cleanup failed', error)
301
+ }
302
+ }
303
+
304
+ // ─── panel state ──────────────────────────────────────────────────────────
305
+
306
+ function closePanel() {
307
+ try {
308
+ if (panel && panel.refetchTimer !== null) clearTimeout(panel.refetchTimer)
309
+ if (overlayRoot && overlayRoot.parentNode) overlayRoot.parentNode.removeChild(overlayRoot)
310
+ overlayRoot = null
311
+ if (panel && panel.escape) {
312
+ document.removeEventListener('keydown', panel.escape, true)
313
+ panel.escape = null
314
+ }
315
+ panel = null
316
+ } catch (error) {
317
+ warn('panel close failed', error)
318
+ panel = null
319
+ overlayRoot = null
320
+ }
321
+ }
322
+
323
+ function setText(node, text) {
324
+ if (node && node.textContent !== text) node.textContent = text
325
+ }
326
+
327
+ /** A fresh element for one render pass; render() swaps it into the slot. */
328
+ function toolRow(tool) {
329
+ var row = el('div', 'ccmcp-tool')
330
+ var rawName = isRecord(tool) ? (str(tool.name) || str(tool.rawName)) : ''
331
+ var name = el('div', 'ccmcp-tool-name', rawName)
332
+ if (rawName) name.title = rawName
333
+ row.appendChild(name)
334
+ var description = isRecord(tool) ? str(tool.description) : ''
335
+ if (description) {
336
+ var desc = el('p', 'ccmcp-tool-desc', description)
337
+ desc.title = description
338
+ row.appendChild(desc)
339
+ }
340
+ return row
341
+ }
342
+
343
+ function toolList(entry) {
344
+ var tools = Array.isArray(entry.tools) ? entry.tools : []
345
+ var box = el('div', 'ccmcp-tools' + (entry.disabled === true ? ' ccmcp-tools-muted' : ''))
346
+ box.appendChild(el('div', 'ccmcp-tools-head', 'Tools (' + tools.length + ')'))
347
+ var body = el('div', 'ccmcp-tools-body')
348
+ if (tools.length === 0) {
349
+ body.appendChild(el('div', 'ccmcp-empty', 'This server reported no tools'))
350
+ } else {
351
+ for (var i = 0; i < tools.length; i += 1) body.appendChild(toolRow(tools[i]))
352
+ }
353
+ box.appendChild(body)
354
+ return box
355
+ }
356
+
357
+ // ─── render ───────────────────────────────────────────────────────────────
358
+
359
+ function render() {
360
+ var current = panel
361
+ if (!current || !current.body || !current.subtitle) return
362
+ try {
363
+ var snapshot = current.snapshot
364
+ if (current.view === 'detail') renderDetail(current, snapshot)
365
+ else renderList(current, snapshot)
366
+ } catch (error) {
367
+ warn('panel render failed', error)
368
+ }
369
+ }
370
+
371
+ /** A row the host has not settled yet keeps its spinner across re-renders. */
372
+ function pendingEntry(current, entry) {
373
+ return current.checkingKeys[entryKey(entry)] === true
374
+ }
375
+
376
+ function renderList(current, snapshot) {
377
+ var servers = snapshot && Array.isArray(snapshot.servers) ? snapshot.servers : []
378
+ var known = snapshot ? snapshot.known !== false : false
379
+ var projectRoot = snapshot ? str(snapshot.projectRoot) : ''
380
+ setText(current.subtitle, current.loading ? 'Loading\u2026' : (projectRoot || 'No project root resolved'))
381
+ clear(current.body)
382
+
383
+ if (current.loading) {
384
+ current.body.appendChild(spinRow('Loading servers\u2026'))
385
+ return
386
+ }
387
+ if (!snapshot && current.error) {
388
+ current.body.appendChild(emptyState(current.error, true))
389
+ return
390
+ }
391
+ if (!known || servers.length === 0) {
392
+ current.body.appendChild(emptyState('No MCP servers configured for this project', true))
393
+ return
394
+ }
395
+ for (var i = 0; i < servers.length; i += 1) {
396
+ if (isRecord(servers[i])) current.body.appendChild(serverRow(current, servers[i]))
397
+ }
398
+ }
399
+
400
+ function emptyState(text, retry) {
401
+ var box = el('div', 'ccmcp-empty')
402
+ box.appendChild(el('p', 'ccmcp-empty-text', text))
403
+ if (retry) {
404
+ var button = el('button', 'ccmcp-btn', 'Retry')
405
+ button.type = 'button'
406
+ button.setAttribute('data-role', 'retry')
407
+ button.addEventListener('click', function () { void refreshPanel() })
408
+ box.appendChild(button)
409
+ }
410
+ return box
411
+ }
412
+
413
+ function spinRow(text) {
414
+ var row = el('div', 'ccmcp-spin-row')
415
+ row.appendChild(el('span', 'ccmcp-spinner'))
416
+ row.appendChild(el('span', null, text))
417
+ return row
418
+ }
419
+
420
+ function markBusy(button, busy) {
421
+ clear(button)
422
+ if (busy) {
423
+ button.appendChild(el('span', 'ccmcp-spinner'))
424
+ button.setAttribute('aria-busy', 'true')
425
+ }
426
+ button.disabled = busy === true
427
+ }
428
+
429
+ function scopeInfo(entry) {
430
+ var scope = str(entry.scope)
431
+ if (scope === 'plugin' || (!scope && str(entry.pluginName))) {
432
+ return { label: 'Plugin', plugin: str(entry.pluginName) || 'plugin' }
433
+ }
434
+ if (scope === 'project') return { label: 'Project', plugin: '' }
435
+ return { label: scope ? scope.charAt(0).toUpperCase() + scope.slice(1) : 'Server', plugin: '' }
436
+ }
437
+
438
+ function typeCell(entry) {
439
+ var cell = el('div', 'ccmcp-type')
440
+ var info = scopeInfo(entry)
441
+ cell.appendChild(el('span', null, info.label))
442
+ var transport = str(entry.transport, 'stdio')
443
+ cell.appendChild(el('span', 'ccmcp-type-transport', ' \u00B7 ' + transport))
444
+ return cell
445
+ }
446
+
447
+ function serverRow(current, entry) {
448
+ var row = el('button', 'ccmcp-row')
449
+ row.type = 'button'
450
+ row.setAttribute('data-role', 'row')
451
+ row.setAttribute('data-key', entryKey(entry))
452
+ row.appendChild(el('span', 'ccmcp-glyph', firstLetter(entry.serverName)))
453
+ row.appendChild(el('span', 'ccmcp-name', str(entry.serverName, 'unnamed server')))
454
+ row.appendChild(typeCell(entry))
455
+ row.appendChild(statusCell(current, entry))
456
+ row.addEventListener('click', function () { openDetail(entryKey(entry)) })
457
+ return row
458
+ }
459
+
460
+ function firstLetter(serverName) {
461
+ var name = str(serverName)
462
+ return name ? name.charAt(0) : '?'
463
+ }
464
+
465
+ /** The status cell owns its own buttons, so a click here must not open the row. */
466
+ function statusCell(current, entry) {
467
+ var cell = el('div', 'ccmcp-status')
468
+ cell.setAttribute('data-role', 'status')
469
+ cell.addEventListener('click', function (event) { event.stopPropagation() })
470
+
471
+ if (pendingEntry(current, entry)) {
472
+ cell.appendChild(el('span', 'ccmcp-spinner'))
473
+ return cell
474
+ }
475
+ var status = str(entry.status, 'checking')
476
+ if (status === 'ready') {
477
+ var check = el('button', 'ccmcp-iconbtn')
478
+ check.type = 'button'
479
+ check.setAttribute('data-role', 'status-ready')
480
+ check.title = 'Re-check this server'
481
+ check.setAttribute('aria-label', 'Re-check ' + str(entry.serverName, 'server'))
482
+ check.textContent = '\u2713'
483
+ check.addEventListener('click', function (event) {
484
+ event.stopPropagation()
485
+ markBusy(check, true)
486
+ void reconnect(current, entryKey(entry), function () { markBusy(check, false) })
487
+ })
488
+ cell.appendChild(check)
489
+ return cell
490
+ }
491
+ if (status === 'error') {
492
+ var connect = el('button', 'ccmcp-btn', 'Connect')
493
+ connect.type = 'button'
494
+ connect.setAttribute('data-role', 'status-error')
495
+ connect.addEventListener('click', function (event) {
496
+ event.stopPropagation()
497
+ markBusy(connect, true)
498
+ void reconnect(current, entryKey(entry), function () {
499
+ markBusy(connect, false)
500
+ /* The panel re-renders with a fresh button, so only restore the label
501
+ * while this exact node is still the one on screen. */
502
+ if (connect.isConnected !== false) connect.textContent = 'Connect'
503
+ })
504
+ })
505
+ cell.appendChild(connect)
506
+ return cell
507
+ }
508
+ if (status === 'disabled') {
509
+ cell.appendChild(el('span', 'ccmcp-status-muted', 'Disabled'))
510
+ return cell
511
+ }
512
+ if (status === 'skipped') {
513
+ cell.appendChild(el('span', 'ccmcp-status-muted', 'Host'))
514
+ return cell
515
+ }
516
+ cell.appendChild(el('span', 'ccmcp-spinner'))
517
+ return cell
518
+ }
519
+
520
+ function renderDetail(current, snapshot) {
521
+ var servers = snapshot && Array.isArray(snapshot.servers) ? snapshot.servers : []
522
+ var projectRoot = snapshot ? str(snapshot.projectRoot) : ''
523
+ setText(current.subtitle, projectRoot || 'No project root resolved')
524
+ var entry = null
525
+ for (var i = 0; i < servers.length; i += 1) {
526
+ if (entryKey(servers[i]) === current.selectedKey) { entry = servers[i]; break }
527
+ }
528
+ clear(current.body)
529
+ if (!entry) {
530
+ current.view = 'list'
531
+ renderList(current, snapshot)
532
+ return
533
+ }
534
+ current.body.appendChild(detailHead(current, entry))
535
+ current.body.appendChild(statusLine(current, entry))
536
+
537
+ if (!current.checking) {
538
+ var info = scopeInfo(entry)
539
+ var source = str(entry.source)
540
+ var sourceLine
541
+ if (str(entry.scope) === 'host') {
542
+ /* A host row is the profile's own dsh-mcp-client instance: this plugin
543
+ * lists it and can hide it here, but the host owns the connection. */
544
+ sourceLine = source || 'Host MCP client row in the profile config'
545
+ } else if (info.plugin) {
546
+ sourceLine = 'Plugin ' + info.plugin + ' at ' + (source || 'unknown path')
547
+ } else {
548
+ sourceLine = 'Configured in ' + (source || 'an unknown source')
549
+ }
550
+ current.body.appendChild(el('p', 'ccmcp-source', sourceLine))
551
+ } else {
552
+ current.body.appendChild(spinRow('Checking\u2026'))
553
+ }
554
+ current.body.appendChild(toolList(entry))
555
+ }
556
+
557
+ function detailHead(current, entry) {
558
+ var head = el('div', 'ccmcp-head')
559
+ head.style.flexDirection = 'column'
560
+ head.style.gap = '6px'
561
+ var back = el('button', 'ccmcp-backbtn', '\u2190 All servers')
562
+ back.type = 'button'
563
+ back.setAttribute('data-role', 'back')
564
+ back.addEventListener('click', function () {
565
+ if (panel !== current) return
566
+ current.view = 'list'
567
+ current.selectedKey = ''
568
+ render()
569
+ })
570
+ head.appendChild(back)
571
+
572
+ var row = el('div', 'ccmcp-detail-head')
573
+ var main = el('div', 'ccmcp-head-main')
574
+ var titleRow = el('div', 'ccmcp-detail-head')
575
+ titleRow.appendChild(el('span', 'ccmcp-glyph', firstLetter(entry.serverName)))
576
+ titleRow.appendChild(el('span', 'ccmcp-name', str(entry.serverName, 'unnamed server')))
577
+ titleRow.appendChild(el('span', 'ccmcp-type-transport', str(entry.transport, 'stdio')))
578
+ main.appendChild(titleRow)
579
+ row.appendChild(main)
580
+ row.appendChild(toggleButton(current, entry))
581
+ head.appendChild(row)
582
+ return head
583
+ }
584
+
585
+ function toggleButton(current, entry) {
586
+ var actions = el('div', 'ccmcp-head-actions')
587
+ if (str(entry.status) === 'skipped') return actions
588
+ var disabled = entry.disabled === true
589
+ var button = el('button', 'ccmcp-btn', disabled ? 'Enable' : 'Disable')
590
+ button.type = 'button'
591
+ button.setAttribute('data-role', 'toggle')
592
+ button.addEventListener('click', function () {
593
+ markBusy(button, true)
594
+ void toggleServer(current, entryKey(entry), !disabled, function () {
595
+ markBusy(button, false)
596
+ if (button.isConnected !== false) button.textContent = disabled ? 'Enable' : 'Disable'
597
+ })
598
+ })
599
+ actions.appendChild(button)
600
+ return actions
601
+ }
602
+
603
+ function statusLine(current, entry) {
604
+ var status = pendingEntry(current, entry) ? 'checking' : str(entry.status, 'checking')
605
+ var line = el('div', 'ccmcp-statusline')
606
+ line.setAttribute('data-role', 'statusline')
607
+ if (status === 'ready') {
608
+ line.className += ' ccmcp-statusline-ready'
609
+ var suffix = ''
610
+ if (str(entry.scope) === 'host') {
611
+ /* Say who holds the connection: the host's own row, or this plugin
612
+ * after Connect adopted a row that exposed nothing. */
613
+ suffix = entry.managed === true ? ' \u00B7 adopted for this workspace' : ' \u00B7 connected by the host'
614
+ }
615
+ line.appendChild(el('span', null, 'Connected \u00B7 ' + (Number(entry.toolCount) || 0) + ' tools' + suffix))
616
+ return line
617
+ }
618
+ if (status === 'error') {
619
+ line.className += ' ccmcp-statusline-error'
620
+ line.appendChild(el('span', null, 'Disconnected'))
621
+ var detail = str(entry.error, 'the server did not respond')
622
+ line.appendChild(el('span', 'ccmcp-statusline-detail', '\u2014 ' + detail))
623
+ return line
624
+ }
625
+ if (status === 'disabled') {
626
+ line.className += ' ccmcp-statusline-muted'
627
+ line.appendChild(el('span', null, 'Disabled \u2014 tools are not in the model context'))
628
+ return line
629
+ }
630
+ if (status === 'skipped') {
631
+ line.className += ' ccmcp-statusline-muted'
632
+ line.appendChild(el('span', null, 'Provided by a host MCP client'))
633
+ return line
634
+ }
635
+ line.className += ' ccmcp-statusline-muted'
636
+ line.appendChild(el('span', 'ccmcp-spinner'))
637
+ line.appendChild(el('span', null, 'Checking\u2026'))
638
+ return line
639
+ }
640
+
641
+ // ─── data access ──────────────────────────────────────────────────────────
642
+
643
+ function entryFor(key) {
644
+ if (!panel || !panel.entries) return null
645
+ return panel.entries[key] || null
646
+ }
647
+
648
+ /** Overlay a live entry (post-connect / post-disable) onto the snapshot in place. */
649
+ function reshape(snapshot, overrides) {
650
+ if (!isRecord(snapshot)) return snapshot
651
+ var servers = Array.isArray(snapshot.servers) ? snapshot.servers.slice() : []
652
+ var index = {}
653
+ var i
654
+ for (i = 0; i < servers.length; i += 1) index[entryKey(servers[i])] = i
655
+ var keys = isRecord(overrides) ? Object.keys(overrides) : []
656
+ for (i = 0; i < keys.length; i += 1) {
657
+ var key = keys[i]
658
+ var entry = overrides[key]
659
+ var at = index[key]
660
+ if (at === undefined) {
661
+ servers.push(entry)
662
+ index[key] = servers.length - 1
663
+ } else {
664
+ servers[at] = entry
665
+ }
666
+ }
667
+ var next = {}
668
+ var names = Object.keys(snapshot)
669
+ for (i = 0; i < names.length; i += 1) next[names[i]] = snapshot[names[i]]
670
+ next.servers = servers
671
+ return next
672
+ }
673
+
674
+ function fetchSnapshot() {
675
+ var current = panel
676
+ if (!current) return Promise.resolve(null)
677
+ return api('state', { sessionId: current.sessionId }).then(function (response) {
678
+ if (panel !== current) return null
679
+ if (response.ok !== true) {
680
+ current.snapshot = null
681
+ current.error = errorOf(response, 'Could not read MCP state')
682
+ current.loading = false
683
+ syncPending()
684
+ return null
685
+ }
686
+ current.snapshot = reshape(response.value, current.entries)
687
+ current.error = ''
688
+ current.loading = false
689
+ syncPending()
690
+ return current.snapshot
691
+ })
692
+ }
693
+
694
+ /**
695
+ * `state` never blocks, so the host's start-up self-check can surface rows as
696
+ * `checking`. While any row is in flight, refetch on a timer until the list
697
+ * settles — bounded, so a check that never finishes cannot poll forever.
698
+ */
699
+ function syncPending() {
700
+ var current = panel
701
+ if (!current) return
702
+ var servers = current.snapshot && Array.isArray(current.snapshot.servers) ? current.snapshot.servers : []
703
+ var live = {}
704
+ for (var i = 0; i < servers.length; i += 1) {
705
+ var entry = servers[i]
706
+ var key = entryKey(entry)
707
+ if (key && str(entry.status) === 'checking') live[key] = true
708
+ }
709
+ var keys = Object.keys(current.checkingKeys)
710
+ for (i = 0; i < keys.length; i += 1) {
711
+ if (!live[keys[i]]) {
712
+ current.checkingKeys[keys[i]] = false
713
+ delete current.checkingKeys[keys[i]]
714
+ }
715
+ }
716
+ var liveKeys = Object.keys(live)
717
+ for (i = 0; i < liveKeys.length; i += 1) current.checkingKeys[liveKeys[i]] = true
718
+
719
+ if (liveKeys.length > 0) {
720
+ if (current.refetchUntil === 0) current.refetchUntil = Date.now() + REFETCH_CAP_MS
721
+ if (current.refetchTimer === null) {
722
+ current.refetchTimer = setTimeout(function () {
723
+ if (panel !== current) return
724
+ current.refetchTimer = null
725
+ if (Date.now() > current.refetchUntil) return
726
+ void refreshPanel()
727
+ }, REFETCH_INTERVAL_MS)
728
+ }
729
+ return
730
+ }
731
+ if (current.refetchTimer !== null) {
732
+ clearTimeout(current.refetchTimer)
733
+ current.refetchTimer = null
734
+ }
735
+ current.refetchUntil = 0
736
+ }
737
+
738
+ /** Full refresh: re-pull the snapshot, then re-render whatever view is open. */
739
+ function refreshPanel() {
740
+ var current = panel
741
+ if (!current) return Promise.resolve(null)
742
+ return fetchSnapshot().then(function (snapshot) {
743
+ if (panel !== current) return snapshot
744
+ if (snapshot && current.snapshot) current.snapshot = reshape(current.snapshot, current.entries)
745
+ render()
746
+ return snapshot
747
+ })
748
+ }
749
+
750
+ function recordEntry(entry) {
751
+ if (!panel || !isRecord(entry)) return
752
+ var key = entryKey(entry)
753
+ if (key) panel.entries[key] = entry
754
+ }
755
+
756
+ function reconnect(current, key, done) {
757
+ if (panel !== current || !key) { if (done) done(); return Promise.resolve(null) }
758
+ var previous = entryFor(key)
759
+ current.checking = true
760
+ current.entries[key] = mergeEntry(previous, { status: 'checking', error: null })
761
+ render()
762
+ return api('connect', { sessionId: current.sessionId, key: key }).then(function (response) {
763
+ var entry = entryOf(response)
764
+ if (panel !== current) { if (done) done(); return null }
765
+ if (entry) {
766
+ recordEntry(entry)
767
+ if (str(entry.status) === 'error') toastFailure(entry.serverName, entry.error)
768
+ } else {
769
+ current.entries[key] = mergeEntry(previous, {
770
+ status: 'error',
771
+ error: errorOf(response, 'connect failed'),
772
+ })
773
+ toastFailure(previous && previous.serverName, errorOf(response, 'connect failed'))
774
+ }
775
+ current.checking = false
776
+ return refreshPanel().then(function () { if (done) done(); return entry })
777
+ }, function (error) {
778
+ if (panel === current) {
779
+ current.checking = false
780
+ current.entries[key] = mergeEntry(previous, { status: 'error', error: String((error && error.message) || error) })
781
+ }
782
+ toastFailure(previous && previous.serverName, (error && error.message) || error)
783
+ if (done) done()
784
+ return null
785
+ })
786
+ }
787
+
788
+ function mergeEntry(previous, patch) {
789
+ var next = {}
790
+ var names = isRecord(previous) ? Object.keys(previous) : []
791
+ for (var i = 0; i < names.length; i += 1) next[names[i]] = previous[names[i]]
792
+ var keys = Object.keys(patch)
793
+ for (var j = 0; j < keys.length; j += 1) next[keys[j]] = patch[keys[j]]
794
+ return next
795
+ }
796
+
797
+ function toggleServer(current, key, disabled, done) {
798
+ if (panel !== current || !key) { if (done) done(); return Promise.resolve(null) }
799
+ var previous = entryFor(key)
800
+ return api('disable', { sessionId: current.sessionId, key: key, disabled: disabled }).then(function (response) {
801
+ var entry = entryOf(response)
802
+ if (panel !== current) { if (done) done(); return null }
803
+ if (entry) {
804
+ recordEntry(entry)
805
+ } else {
806
+ toast('MCP server action failed', str(previous && previous.serverName, key) + ' \u2014 ' + errorOf(response, 'the request failed'), 'danger')
807
+ }
808
+ return refreshPanel().then(function () { if (done) done(); return entry })
809
+ }, function (error) {
810
+ toast('MCP server action failed', str(previous && previous.serverName, key) + ' \u2014 ' + String((error && error.message) || error), 'danger')
811
+ if (done) done()
812
+ return null
813
+ })
814
+ }
815
+
816
+ // ─── panel lifecycle ──────────────────────────────────────────────────────
817
+
818
+ function openPanel(sessionId) {
819
+ if (!isDom()) return
820
+ try {
821
+ closePanel()
822
+ var session = str(sessionId)
823
+ if (!session) {
824
+ toast('MCP panel unavailable', 'No conversation is open, so there is no project to inspect.', 'danger')
825
+ return
826
+ }
827
+ panel = {
828
+ sessionId: session,
829
+ view: 'list',
830
+ selectedKey: '',
831
+ snapshot: null,
832
+ error: '',
833
+ entries: {},
834
+ checkingKeys: {},
835
+ loading: true,
836
+ checking: false,
837
+ refetchTimer: null,
838
+ refetchUntil: 0,
839
+ escape: null,
840
+ body: null,
841
+ subtitle: null,
842
+ }
843
+ var current = panel
844
+
845
+ var overlay = el('div', 'ccmcp-overlay')
846
+ overlay.setAttribute('data-plugin', PLUGIN_ID)
847
+ var backdrop = el('div', 'ccmcp-backdrop')
848
+ backdrop.setAttribute('data-role', 'backdrop')
849
+ backdrop.addEventListener('click', function () { if (panel === current) closePanel() })
850
+ overlay.appendChild(backdrop)
851
+
852
+ var card = el('div', 'ccmcp-panel')
853
+ card.setAttribute('role', 'dialog')
854
+ card.setAttribute('aria-label', 'MCP servers')
855
+
856
+ var head = el('div', 'ccmcp-head')
857
+ var headMain = el('div', 'ccmcp-head-main')
858
+ headMain.appendChild(el('h2', 'ccmcp-title', 'MCP servers'))
859
+ var subtitle = el('p', 'ccmcp-subtitle', 'Loading\u2026')
860
+ headMain.appendChild(subtitle)
861
+ head.appendChild(headMain)
862
+
863
+ var headActions = el('div', 'ccmcp-head-actions')
864
+ var close = el('button', 'ccmcp-iconbtn', '\u2715')
865
+ close.type = 'button'
866
+ close.setAttribute('data-role', 'close')
867
+ close.setAttribute('aria-label', 'Close MCP servers')
868
+ close.addEventListener('click', function () { if (panel === current) closePanel() })
869
+ headActions.appendChild(close)
870
+ head.appendChild(headActions)
871
+
872
+ var body = el('div', 'ccmcp-body')
873
+ card.appendChild(head)
874
+ card.appendChild(body)
875
+ overlay.appendChild(card)
876
+ document.body.appendChild(overlay)
877
+
878
+ overlayRoot = overlay
879
+ current.body = body
880
+ current.subtitle = subtitle
881
+
882
+ /* Escape closes from anywhere in the page, and survives the conversation
883
+ * re-rendering: it is a document listener, not a component one. */
884
+ current.escape = function (event) {
885
+ if (event && event.key === 'Escape' && panel === current) {
886
+ event.stopPropagation()
887
+ closePanel()
888
+ }
889
+ }
890
+ document.addEventListener('keydown', current.escape, true)
891
+
892
+ render()
893
+ void refreshPanel()
894
+ } catch (error) {
895
+ warn('panel open failed', error)
896
+ closePanel()
897
+ }
898
+ }
899
+
900
+ function openDetail(key) {
901
+ if (!panel || !key) return
902
+ panel.selectedKey = key
903
+ panel.view = 'detail'
904
+ render()
905
+ }
906
+
907
+ // ─── session self-check ───────────────────────────────────────────────────
908
+
909
+ function clearCheckRetry() {
910
+ if (checkRetryTimer !== null) {
911
+ clearTimeout(checkRetryTimer)
912
+ checkRetryTimer = null
913
+ }
914
+ }
915
+
916
+ function scheduleCheck(sessionId, delay) {
917
+ clearCheckRetry()
918
+ checkRetryTimer = setTimeout(function () {
919
+ checkRetryTimer = null
920
+ runCheck(sessionId, false)
921
+ }, delay)
922
+ }
923
+
924
+ /**
925
+ * Fire-and-forget `GET /cc-mcp/check` for the current session. Bounded: at
926
+ * most a couple of retries while the host reports `checked: false`, and one
927
+ * pass per failure so a stuck check can never tight-loop.
928
+ */
929
+ function runCheck(sessionId, isRetry) {
930
+ var id = str(sessionId)
931
+ if (!id) return
932
+ if (!isRetry && checkSessionId === id) return
933
+ if (pendingCheck === id) return
934
+ checkSessionId = id
935
+ pendingCheck = id
936
+ api('check', { sessionId: id }).then(function (response) {
937
+ if (pendingCheck === id) pendingCheck = null
938
+ if (checkSessionId !== id) return
939
+ if (response.ok !== true) {
940
+ if (isRetry) warn('self-check failed: ' + errorOf(response, codeOf(response)))
941
+ return
942
+ }
943
+ var value = isRecord(response.value) ? response.value : {}
944
+ if (value.checked === true) {
945
+ toastFailures(value.failures)
946
+ checkRetryIndex = 0
947
+ return
948
+ }
949
+ if (checkRetryIndex < CHECK_RETRY_DELAYS.length) {
950
+ scheduleCheck(id, CHECK_RETRY_DELAYS[checkRetryIndex])
951
+ checkRetryIndex += 1
952
+ }
953
+ }, function () {
954
+ if (pendingCheck === id) pendingCheck = null
955
+ checkRetryIndex = 0
956
+ })
957
+ }
958
+
959
+ function bindSessionsInject() {
960
+ if (sessionUnsub) return
961
+ if (!sessionsScope || !sessionsScope.sessions || !sessionsScope.sessions.list) return
962
+ var list = sessionsScope.sessions.list
963
+ if (typeof list.subscribe !== 'function' || typeof list.getSnapshot !== 'function') return
964
+ var last = null
965
+ var observer = function () {
966
+ var snapshot = null
967
+ try { snapshot = list.getSnapshot() } catch (error) { warn('session snapshot read failed', error); return }
968
+ var current = isRecord(snapshot) ? str(snapshot.current) : ''
969
+ if (current === last) return
970
+ last = current
971
+ checkRetryIndex = 0
972
+ clearCheckRetry()
973
+ if (current) runCheck(current, false)
974
+ }
975
+ sessionUnsub = list.subscribe(observer)
976
+ observer()
977
+ }
978
+
979
+ // ─── plugin ───────────────────────────────────────────────────────────────
980
+
981
+ function injectCmds(ctx) {
982
+ if (!ctx || typeof ctx.inject !== 'function') return
983
+ ctx.inject(['commandUi'], function (scope) {
984
+ try {
985
+ /* Resolve the service eagerly: if the scope cannot see it, the deferred
986
+ * injection firing is a no-op rather than a half-registered decoration. */
987
+ var commandUi = scope.commandUi
988
+ if (!commandUi || typeof commandUi.decorate !== 'function') return
989
+ scope.effect(function () {
990
+ return commandUi.decorate({
991
+ name: 'mcp',
992
+ available: function () { return true },
993
+ ui: {
994
+ kind: 'action',
995
+ run: function (session) {
996
+ try {
997
+ openPanel(session && session.sessionId)
998
+ } catch (error) {
999
+ warn('/mcp decoration failed', error)
1000
+ }
1001
+ },
1002
+ },
1003
+ })
1004
+ }, 'cc-mcp: /mcp')
1005
+ } catch (error) {
1006
+ warn('/mcp decoration failed', error)
1007
+ }
1008
+ })
1009
+ }
1010
+
1011
+ function injectSessions(ctx) {
1012
+ var seq = ++bindSeq
1013
+ if (!ctx || typeof ctx.inject !== 'function') return
1014
+ ctx.inject(['sessions'], function (scope) {
1015
+ try {
1016
+ sessionsScope = scope
1017
+ var applied = scope.effect(function () {
1018
+ bindSessionsInject()
1019
+ return function () {
1020
+ sessionsScope = null
1021
+ if (sessionUnsub) {
1022
+ try { sessionUnsub() } catch (error) { warn('session unsubscribe failed', error) }
1023
+ sessionUnsub = null
1024
+ }
1025
+ }
1026
+ }, 'cc-mcp: session self-check')
1027
+ /* Belt and braces: if the plugin was disposed while this registration
1028
+ * was still in flight, cordis suppresses the effect — release the
1029
+ * subscription ourselves rather than leave it dangling. */
1030
+ if (seq !== bindSeq && typeof applied === 'function') {
1031
+ try { applied() } catch (error) { warn('session self-check teardown failed', error) }
1032
+ }
1033
+ } catch (error) {
1034
+ warn('session self-check unavailable', error)
1035
+ }
1036
+ })
1037
+ }
1038
+
1039
+ function apply(ctx) {
1040
+ try {
1041
+ styleEl = installStyles()
1042
+ if (ctx && typeof ctx.effect === 'function') {
1043
+ ctx.effect(function () {
1044
+ return function () {
1045
+ try { closePanel() } catch (error) { warn('panel teardown failed', error) }
1046
+ try { clearToasts() } catch (error) { warn('toast teardown failed', error) }
1047
+ clearCheckRetry()
1048
+ bindSeq += 1
1049
+ if (sessionUnsub) {
1050
+ try { sessionUnsub() } catch (error) { warn('session unsubscribe failed', error) }
1051
+ }
1052
+ sessionUnsub = null
1053
+ sessionsScope = null
1054
+ checkSessionId = null
1055
+ pendingCheck = null
1056
+ checkRetryIndex = 0
1057
+ if (styleEl && styleEl.parentNode) styleEl.parentNode.removeChild(styleEl)
1058
+ styleEl = null
1059
+ }
1060
+ }, 'cc-mcp: panel lifecycle')
1061
+ }
1062
+ injectCmds(ctx)
1063
+ injectSessions(ctx)
1064
+ } catch (error) {
1065
+ /* apply() must never throw: a throw fails the fibre and the whole GUI
1066
+ * page boot. */
1067
+ warn('apply failed', error)
1068
+ }
1069
+ }
1070
+
1071
+ module.exports = {
1072
+ name: 'dsh-cc-mcp-client',
1073
+ inject: [],
1074
+ apply: apply,
1075
+ /* Programmatic face of the same transport the panel uses; every call
1076
+ * resolves to a result envelope and never throws. */
1077
+ api: api,
1078
+ }
1079
+ return module.exports
1080
+ } });