dsh-cursor-dev 0.0.0-stage → 0.11.1

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/lib/client.js ADDED
@@ -0,0 +1,626 @@
1
+ /**
2
+ * The Cursor panel, in the seat a user actually looks in.
3
+ *
4
+ * This is the plugin's CLIENT half. The host half (`lib/index.js`) serves the
5
+ * dashboard at `/plugins/dsh-cursor-dev/`; this half puts that same page in a
6
+ * **Settings page**, registered in the native `settings.section` slot — the
7
+ * place a user looks for "what is this thing doing and what is it set to".
8
+ *
9
+ * It is written by hand rather than built: a DSH client plugin is loaded through
10
+ * `window.__ModuleLoader__.load({ id, factory })`, whose `factory` receives a
11
+ * CommonJS `require` for the shared runtime modules. There is no JSX and no
12
+ * bundler step, which keeps the plugin installable straight from this repository.
13
+ *
14
+ * `ctx.inject` waits for a service, so load order against the settings host does
15
+ * not matter and neither service is a hard dependency: without them the plugin
16
+ * still loads, it just has no page.
17
+ *
18
+ * The panel probes the host route before mounting the frame. A frame that cannot
19
+ * load shows nothing useful, so the probe's own answer — reachable, or which HTTP
20
+ * status came back — is rendered instead of an empty pane.
21
+ *
22
+ * The panel speaks Chinese by default and English on request, following the app
23
+ * locale when it can read it: `ctx.locale.getLocale().active` is passed to the
24
+ * dashboard as `?lang=`, and the dashboard keeps its own switch from then on.
25
+ */
26
+
27
+ window.__ModuleLoader__.load({
28
+ id: 'dsh-cursor-dev',
29
+ factory: (require) => {
30
+ var module = { exports: {} };
31
+ var exports = module.exports;
32
+
33
+ const react = require('react');
34
+ const h = react.createElement;
35
+
36
+ const DASHBOARD_URL = '/plugins/dsh-cursor-dev/';
37
+ const STATUS_URL = '/plugins/dsh-cursor-dev/status';
38
+ const SETTINGS_ID = 'dsh-cursor-dev';
39
+
40
+ const CARD = {
41
+ padding: '16px 18px',
42
+ font: '13px/1.6 -apple-system, "PingFang SC", "Segoe UI", system-ui, sans-serif',
43
+ color: 'inherit',
44
+ };
45
+ const CODE = {
46
+ display: 'block',
47
+ marginTop: '10px',
48
+ padding: '10px 12px',
49
+ maxHeight: '220px',
50
+ overflow: 'auto',
51
+ whiteSpace: 'pre-wrap',
52
+ wordBreak: 'break-all',
53
+ font: '12px/1.5 ui-monospace, SFMono-Regular, Consolas, monospace',
54
+ background: 'rgba(127,127,127,.12)',
55
+ borderRadius: '8px',
56
+ };
57
+ const BUTTON = {
58
+ marginTop: '12px',
59
+ padding: '6px 14px',
60
+ font: 'inherit',
61
+ color: 'inherit',
62
+ background: 'rgba(127,127,127,.16)',
63
+ border: '1px solid rgba(127,127,127,.28)',
64
+ borderRadius: '8px',
65
+ cursor: 'pointer',
66
+ };
67
+ const TITLE = {
68
+ font: '600 14px/1.4 -apple-system, "PingFang SC", "Segoe UI", system-ui, sans-serif',
69
+ color: 'inherit',
70
+ };
71
+ const SUBTITLE = {
72
+ font: '12px/1.6 -apple-system, "PingFang SC", "Segoe UI", system-ui, sans-serif',
73
+ opacity: 0.75,
74
+ };
75
+ const CHIP = {
76
+ display: 'inline-flex',
77
+ alignItems: 'center',
78
+ gap: '6px',
79
+ height: '24px',
80
+ padding: '0 8px',
81
+ font: '12px/1 -apple-system, "PingFang SC", "Segoe UI", system-ui, sans-serif',
82
+ color: 'inherit',
83
+ background: 'rgba(127,127,127,.10)',
84
+ border: '1px solid rgba(127,127,127,.22)',
85
+ borderRadius: '12px',
86
+ cursor: 'pointer',
87
+ whiteSpace: 'nowrap',
88
+ };
89
+ const DOT = (live) => ({
90
+ width: '6px',
91
+ height: '6px',
92
+ borderRadius: '50%',
93
+ background: live ? 'var(--dsh-accent, #6d8cff)' : 'rgba(127,127,127,.55)',
94
+ boxShadow: live ? '0 0 0 3px rgba(109,140,255,.18)' : 'none',
95
+ });
96
+ const OVERLAY = {
97
+ position: 'fixed',
98
+ top: '52px',
99
+ right: '16px',
100
+ width: 'min(560px, calc(100vw - 32px))',
101
+ height: 'min(72vh, 640px)',
102
+ display: 'flex',
103
+ flexDirection: 'column',
104
+ gap: '8px',
105
+ padding: '10px 12px 12px',
106
+ background: 'var(--dsh-surface, #1e1e24)',
107
+ color: 'inherit',
108
+ border: '1px solid rgba(127,127,127,.28)',
109
+ borderRadius: '12px',
110
+ boxShadow: '0 18px 48px rgba(0,0,0,.4)',
111
+ zIndex: 90,
112
+ };
113
+ const OVERLAY_HEAD = {
114
+ display: 'flex',
115
+ alignItems: 'center',
116
+ gap: '10px',
117
+ font: '600 13px/1.4 -apple-system, "PingFang SC", "Segoe UI", system-ui, sans-serif',
118
+ };
119
+ const OVERLAY_LINK = {
120
+ marginLeft: 'auto',
121
+ font: '12px/1.4 -apple-system, "PingFang SC", "Segoe UI", system-ui, sans-serif',
122
+ color: 'inherit',
123
+ opacity: 0.7,
124
+ textDecoration: 'none',
125
+ };
126
+ const OVERLAY_CLOSE = {
127
+ padding: '2px 8px',
128
+ font: 'inherit',
129
+ color: 'inherit',
130
+ background: 'transparent',
131
+ border: '1px solid rgba(127,127,127,.28)',
132
+ borderRadius: '8px',
133
+ cursor: 'pointer',
134
+ };
135
+
136
+ /** The locale service, once (and if) it is available. */
137
+ let localeService;
138
+
139
+ /** `zh` unless the app is running in English. */
140
+ function langOf() {
141
+ try {
142
+ const active = localeService?.getLocale?.()?.active;
143
+ if (active === 'en' || active === 'zh') return active;
144
+ } catch {
145
+ // An unavailable or throwing locale service must not break a render.
146
+ }
147
+ const tag = typeof document === 'undefined' ? '' : String(document.documentElement?.lang ?? '');
148
+ return tag.toLowerCase().startsWith('en') ? 'en' : 'zh';
149
+ }
150
+
151
+ /** The host route is unreachable: say which answer came back instead of a blank pane. */
152
+ function PanelOffline(props) {
153
+ const { status, text, onRetry, lang } = props;
154
+ const zh = lang !== 'en';
155
+ const headline = status === 0
156
+ ? (zh ? '连不上宿主路由' : 'the host route could not be reached')
157
+ : (zh ? `宿主路由返回 HTTP ${status}` : `the host route answered HTTP ${status}`);
158
+ return h(
159
+ 'div',
160
+ { style: CARD },
161
+ h('div', { style: { fontWeight: 600 } }, zh ? 'Cursor 面板暂时不可用' : 'The Cursor panel is unavailable'),
162
+ h('div', { style: { marginTop: 6, opacity: 0.75 } }, headline),
163
+ h(
164
+ 'div',
165
+ { style: { marginTop: 10, opacity: 0.75 } },
166
+ zh
167
+ ? `宿主插件 dsh-cursor-dev 已启用,但它没有应答 ${STATUS_URL}。`
168
+ : `The host plugin dsh-cursor-dev is enabled but did not answer ${STATUS_URL}.`,
169
+ ),
170
+ h('code', { style: CODE }, text || '(empty response body)'),
171
+ h('button', { type: 'button', style: BUTTON, onClick: onRetry }, zh ? '重试' : 'Retry'),
172
+ );
173
+ }
174
+
175
+ /**
176
+ * The CLI is missing or signed out, so the plugin cannot do anything yet.
177
+ *
178
+ * These are the two walls every new user hits, and from inside the panel
179
+ * they look identical to a broken plugin. The card states which one it is
180
+ * and prints the exact command that clears it, instead of leaving a pane
181
+ * of dashes to interpret.
182
+ */
183
+ function SetupCard(props) {
184
+ const { kind, setup, detail, lang, onRetry } = props;
185
+ const zh = lang !== 'en';
186
+ const missing = kind === 'missing';
187
+ const windows = setup?.install?.win32 || "irm 'https://cursor.com/install?win32=true' | iex";
188
+ const unix = setup?.install?.unix || 'curl https://cursor.com/install -fsS | bash';
189
+ const login = setup?.login || 'agent login';
190
+ const verify = setup?.verify || 'agent status --format json';
191
+ const installArg = zh ? '安装命令(Windows PowerShell):' : 'Install (Windows PowerShell):';
192
+ const unixArg = zh ? 'macOS / Linux 用:' : 'macOS / Linux:';
193
+ const loginArg = zh ? '然后在终端里登录(会打开浏览器授权):' : 'Then sign in from a terminal (it opens the browser):';
194
+ const verifyArg = zh ? `确认状态:${verify}` : `Confirm with ${verify}.`;
195
+ return h(
196
+ 'div',
197
+ { style: CARD },
198
+ h(
199
+ 'div',
200
+ { style: { fontWeight: 600 } },
201
+ missing
202
+ ? (zh ? '还没找到 Cursor CLI' : 'The Cursor CLI is not installed yet')
203
+ : (zh ? 'Cursor CLI 还没登录' : 'The Cursor CLI is not signed in yet'),
204
+ ),
205
+ h(
206
+ 'div',
207
+ { style: { marginTop: 6, opacity: 0.75 } },
208
+ missing
209
+ ? (zh
210
+ ? '这个插件靠本机的 Cursor CLI 干活,所以先把 CLI 装上;装完回到这里点重试。'
211
+ : 'This plugin does its work through the local Cursor CLI, so install it first, then retry here.')
212
+ : (zh
213
+ ? '插件已经找到 CLI,但它还没登录 Cursor 账号,所以拿不到额度、也开不了会话。'
214
+ : 'The CLI was found but is not signed in, so there is no quota to report and no session to open.'),
215
+ ),
216
+ missing
217
+ ? h('code', { style: CODE }, `${installArg}\n${windows}\n\n${unixArg}\n${unix}\n\n${loginArg}\n${login}`)
218
+ : h('code', { style: CODE }, `${loginArg}\n${login}\n\n${verifyArg}`),
219
+ detail
220
+ ? h('div', { style: { marginTop: 8, opacity: 0.6 } }, detail)
221
+ : null,
222
+ h('button', { type: 'button', style: BUTTON, onClick: onRetry }, zh ? '装好 / 登录了,重试' : 'Retry now'),
223
+ );
224
+ }
225
+
226
+ /**
227
+ * The host's dashboard, same-origin, filling the pane.
228
+ *
229
+ * The page is a plain server-rendered document with its own styling and
230
+ * its own polling, so the component stays this small and the panel keeps
231
+ * working while the conversation agent is busy.
232
+ */
233
+ function HostFrame(props) {
234
+ const { title, lang, style } = props;
235
+ const [probe, setProbe] = react.useState(null);
236
+ const [attempt, setAttempt] = react.useState(0);
237
+
238
+ react.useEffect(() => {
239
+ let alive = true;
240
+ const controller = typeof AbortController === 'function' ? new AbortController() : undefined;
241
+ // Retrying means "I just installed it / logged in" — ask the host
242
+ // for a fresh sign-in check instead of its one-minute cache.
243
+ const url = attempt === 0 ? STATUS_URL : `${STATUS_URL}?auth=refresh`;
244
+ fetch(url, {
245
+ headers: { accept: 'application/json' },
246
+ signal: controller?.signal,
247
+ })
248
+ .then(async (response) => {
249
+ const text = await response.text();
250
+ if (alive) {
251
+ setProbe({ ok: response.ok, status: response.status, text: text.slice(0, 4000) });
252
+ }
253
+ })
254
+ .catch((error) => {
255
+ if (alive) {
256
+ setProbe({ ok: false, status: 0, text: String((error && error.message) || error) });
257
+ }
258
+ });
259
+ return () => {
260
+ alive = false;
261
+ controller?.abort();
262
+ };
263
+ }, [attempt]);
264
+
265
+ const retry = () => {
266
+ setProbe(null);
267
+ setAttempt((n) => n + 1);
268
+ };
269
+
270
+ if (probe === null) {
271
+ return h('div', { style: CARD }, lang === 'en' ? 'Connecting to the Cursor panel…' : '正在连接 Cursor 面板…');
272
+ }
273
+ if (!probe.ok) {
274
+ return h(PanelOffline, {
275
+ status: probe.status,
276
+ text: probe.text,
277
+ lang,
278
+ onRetry: retry,
279
+ });
280
+ }
281
+ let status;
282
+ try {
283
+ status = JSON.parse(probe.text);
284
+ }
285
+ catch {
286
+ status = undefined;
287
+ }
288
+ if (status !== undefined && status !== null && typeof status === 'object') {
289
+ if (status.agent !== undefined && status.agent?.found !== true) {
290
+ return h(SetupCard, { kind: 'missing', setup: status.setup, detail: status.auth?.error, lang, onRetry: retry });
291
+ }
292
+ if (status.auth !== undefined && status.auth?.authenticated !== true) {
293
+ return h(SetupCard, { kind: 'signed-out', setup: status.setup, detail: status.auth?.error, lang, onRetry: retry });
294
+ }
295
+ }
296
+ return h('iframe', {
297
+ src: `${DASHBOARD_URL}?lang=${lang === 'en' ? 'en' : 'zh'}`,
298
+ title,
299
+ style: Object.assign({
300
+ width: '100%',
301
+ height: '100%',
302
+ border: '0',
303
+ display: 'block',
304
+ background: 'transparent',
305
+ }, style),
306
+ });
307
+ }
308
+
309
+ /**
310
+ * Open/closed state of the header panel, shared by two seats.
311
+ *
312
+ * The chip lives in the conversation header (`conversation.session.header.utilities`),
313
+ * the panel in the frame-wide overlay (`shell.overlay`) — different React
314
+ * trees, so the state is module-level with a tiny subscription instead of
315
+ * a context that neither seat can reach.
316
+ */
317
+ let panelOpen = false;
318
+ const panelListeners = new Set();
319
+
320
+ /** Open or close the header panel, and tell every seat that is showing it. */
321
+ function setPanelOpen(next) {
322
+ panelOpen = next;
323
+ for (const listener of [...panelListeners]) {
324
+ try {
325
+ listener(next);
326
+ }
327
+ catch {
328
+ // A broken listener must not swallow the toggle.
329
+ }
330
+ }
331
+ }
332
+
333
+ /** Subscribe one component to the shared open state. */
334
+ function usePanelOpen() {
335
+ const [open, setOpen] = react.useState(panelOpen);
336
+ react.useEffect(() => {
337
+ panelListeners.add(setOpen);
338
+ setOpen(panelOpen);
339
+ return () => {
340
+ panelListeners.delete(setOpen);
341
+ };
342
+ }, []);
343
+ return open;
344
+ }
345
+
346
+ /**
347
+ * What the header chip shows: how many Cursor sessions are live, and on
348
+ * which model.
349
+ *
350
+ * Polled from `/status` rather than pushed, because the chip is rendered
351
+ * inside the conversation header where a stale "0 sessions" would be
352
+ * worse than one small request every 15 s. The poll stops with the
353
+ * component, so a session that is not open costs nothing.
354
+ */
355
+ function useLiveSessions() {
356
+ const [state, setState] = react.useState({ ready: false, count: 0, model: '', found: true });
357
+ react.useEffect(() => {
358
+ let alive = true;
359
+ const tick = () => {
360
+ fetch(STATUS_URL, { headers: { accept: 'application/json' } })
361
+ .then((response) => response.text())
362
+ .then((body) => {
363
+ if (!alive)
364
+ return;
365
+ let data;
366
+ try {
367
+ data = JSON.parse(body);
368
+ }
369
+ catch {
370
+ data = undefined;
371
+ }
372
+ const rows = data !== null && typeof data === 'object' && Array.isArray(data.live) ? data.live : [];
373
+ setState({
374
+ ready: true,
375
+ count: rows.length,
376
+ model: rows.length > 0 && typeof rows[0].model === 'string' ? rows[0].model : '',
377
+ found: data?.agent?.found === true,
378
+ });
379
+ })
380
+ .catch(() => {
381
+ if (alive)
382
+ setState((previous) => ({ ...previous, ready: true }));
383
+ });
384
+ };
385
+ tick();
386
+ const timer = setInterval(tick, 15000);
387
+ return () => {
388
+ alive = false;
389
+ clearInterval(timer);
390
+ };
391
+ }, []);
392
+ return state;
393
+ }
394
+
395
+ /**
396
+ * The header chip: the Cursor child agent's live session count, and the
397
+ * way into its panel.
398
+ *
399
+ * This is the seat the conversation itself offers (next to the parent's
400
+ * own subagent chips), so "how do I watch the child" has an answer that
401
+ * does not require knowing about Settings or a URL.
402
+ */
403
+ function CursorChip() {
404
+ const zh = langOf() !== 'en';
405
+ // Subscribe before polling: the shared open state is what the click
406
+ // below toggles, and the panel listens for the same change.
407
+ const open = usePanelOpen();
408
+ const live = useLiveSessions();
409
+ const suffix = live.count > 0 ? ` · ${live.count}` : '';
410
+ const title = live.model !== ''
411
+ ? (zh ? `Cursor 子智能体 · 当前模型 ${live.model}` : `Cursor child agent · ${live.model}`)
412
+ : (zh ? 'Cursor 子智能体:会话、模型与额度' : 'Cursor child agent: sessions, model and quota');
413
+ return h(
414
+ 'button',
415
+ {
416
+ type: 'button',
417
+ title,
418
+ onClick: () => setPanelOpen(!open),
419
+ style: CHIP,
420
+ },
421
+ h('span', { style: DOT(live.count > 0) }),
422
+ `Cursor${suffix}`,
423
+ );
424
+ }
425
+
426
+ /**
427
+ * The panel the chip opens, in the frame-wide overlay so it is never
428
+ * clipped by the header's own layout.
429
+ */
430
+ function CursorOverlay() {
431
+ const zh = langOf() !== 'en';
432
+ const open = usePanelOpen();
433
+ if (!open)
434
+ return null;
435
+ return h(
436
+ 'div',
437
+ { style: OVERLAY },
438
+ h(
439
+ 'div',
440
+ { style: OVERLAY_HEAD },
441
+ h('span', null, zh ? 'Cursor 子智能体' : 'Cursor child agent'),
442
+ h(
443
+ 'a',
444
+ {
445
+ href: `${DASHBOARD_URL}?lang=${zh ? 'zh' : 'en'}`,
446
+ target: '_blank',
447
+ rel: 'noreferrer',
448
+ style: OVERLAY_LINK,
449
+ },
450
+ zh ? '在新标签页打开' : 'open in a tab',
451
+ ),
452
+ h('button', { type: 'button', onClick: () => setPanelOpen(false), style: OVERLAY_CLOSE }, '✕'),
453
+ ),
454
+ h(
455
+ 'div',
456
+ { style: { flex: '1 1 auto', minHeight: '0' } },
457
+ h(HostFrame, { title: 'Cursor', lang: zh ? 'zh' : 'en' }),
458
+ ),
459
+ );
460
+ }
461
+
462
+ /**
463
+ * The Settings page.
464
+ *
465
+ * Settings is the seat a user looks in when they want the whole picture:
466
+ * quota, the model picker, and every conversation with its step
467
+ * timeline. The header chip opens the same dashboard in a floating
468
+ * panel, for the moment-to-moment question.
469
+ */
470
+ function CursorSettings() {
471
+ const lang = langOf();
472
+ const zh = lang !== 'en';
473
+ return h(
474
+ 'div',
475
+ {
476
+ style: {
477
+ display: 'flex',
478
+ flexDirection: 'column',
479
+ gap: '10px',
480
+ height: '100%',
481
+ minHeight: '0',
482
+ padding: '2px',
483
+ },
484
+ },
485
+ h('div', { style: TITLE }, zh ? 'Cursor 子智能体' : 'Cursor child agent'),
486
+ h(
487
+ 'div',
488
+ { style: SUBTITLE },
489
+ zh
490
+ ? '额度、cursor_code 使用的模型、以及子智能体跑过的每一次会话(步骤轨迹可展开)。'
491
+ : 'Quota, the model cursor_code uses, and every conversation the child ran (unfold a key for its step timeline).',
492
+ ),
493
+ h(
494
+ // The host wraps this section in an auto-height box, so a
495
+ // percentage height never resolves and the frame silently
496
+ // falls back to the browser's 150px default — that is all
497
+ // the user ever saw of the dashboard. A viewport-sized
498
+ // frame is definite no matter what the slot does, and the
499
+ // host's own scroll container absorbs any overflow.
500
+ 'div',
501
+ { style: { flex: '1 1 auto', height: 'min(72vh, 640px)', minHeight: '320px', overflow: 'hidden' } },
502
+ h(HostFrame, { title: 'Cursor', lang }),
503
+ ),
504
+ );
505
+ }
506
+
507
+ /**
508
+ * The dashboard's jump into a mirrored child conversation.
509
+ *
510
+ * The dashboard is served by the host half and rendered in an iframe, so
511
+ * it cannot navigate the app itself. It posts the address the subagent
512
+ * tree itself uses — `{parentSessionId, childSessionId, mode}` — and this
513
+ * half performs the opening, because `uiWorkspace.openSession` is a
514
+ * client-side service.
515
+ *
516
+ * The message is only trusted when it comes from this origin and carries
517
+ * the three non-empty strings the address is made of; anything else is
518
+ * ignored rather than forwarded to the app.
519
+ */
520
+ function bridgeChildOpens(ctx) {
521
+ const uiWorkspace = ctx.get('uiWorkspace');
522
+ // A host without the workspace service simply has no jump to offer;
523
+ // the dashboard knows that too and keeps opening the step timeline.
524
+ if (uiWorkspace === undefined || typeof uiWorkspace.openSession !== 'function')
525
+ return undefined;
526
+ const onMessage = (event) => {
527
+ if (event.origin !== window.location.origin)
528
+ return;
529
+ const data = event.data;
530
+ if (data === null || typeof data !== 'object' || data.type !== 'dsh-cursor-dev:open-child')
531
+ return;
532
+ const childSessionId = data.childSessionId;
533
+ const parentSessionId = data.parentSessionId;
534
+ if (typeof childSessionId !== 'string' || childSessionId === '')
535
+ return;
536
+ // The panel floats above the conversation; leaving it open would
537
+ // hide the very session it just opened.
538
+ setPanelOpen(false);
539
+ try {
540
+ uiWorkspace.openSession({
541
+ parentSessionId: typeof parentSessionId === 'string' ? parentSessionId : '',
542
+ childSessionId,
543
+ mode: typeof data.mode === 'string' && data.mode !== '' ? data.mode : 'one-shot',
544
+ });
545
+ }
546
+ catch (error) {
547
+ console.warn('[dsh-cursor-dev] opening a child conversation failed:', error);
548
+ }
549
+ };
550
+ window.addEventListener('message', onMessage);
551
+ return () => window.removeEventListener('message', onMessage);
552
+ }
553
+
554
+ function apply(ctx) {
555
+ // Optional services: a seat is registered only when its host exists,
556
+ // and none of them may fail the plugin load.
557
+ ctx.inject(['locale'], (injected) => {
558
+ localeService = injected.get('locale');
559
+ return undefined;
560
+ });
561
+
562
+ const seats = [];
563
+
564
+ // The jump from the dashboard to a child conversation. It needs the
565
+ // workspace service, and it must survive the panel being opened from
566
+ // either seat, so it is mounted once here rather than per component.
567
+ const bridgeSeat = ctx.inject(['uiWorkspace'], (injected) => {
568
+ const release = bridgeChildOpens(injected);
569
+ if (release !== undefined)
570
+ seats.push(release);
571
+ return undefined;
572
+ });
573
+
574
+ const slotsSeat = ctx.inject(['slots'], (injected) => {
575
+ const slots = injected.get('slots');
576
+ if (slots === undefined || typeof slots.register !== 'function' || typeof slots.inject !== 'function') {
577
+ console.warn('[dsh-cursor-dev] the slots service is unavailable; the Cursor seats are not registered.');
578
+ return undefined;
579
+ }
580
+ const seat = (slotName, order, label, component) => {
581
+ const release = slots.inject(slotName, () => slots.register({
582
+ name: slotName,
583
+ id: SETTINGS_ID,
584
+ order,
585
+ label: () => label(),
586
+ }, component));
587
+ seats.push(release);
588
+ return release;
589
+ };
590
+ seat('settings.section', 62, () => (langOf() === 'en' ? 'Cursor agent' : 'Cursor 子智能体'), CursorSettings);
591
+ // The conversation header: the child agent's sessions, one chip
592
+ // away from the conversation that spawned them.
593
+ seat('conversation.session.header.utilities', 40, () => (langOf() === 'en' ? 'Cursor sessions' : 'Cursor 会话'), CursorChip);
594
+ // And the floating panel it opens, in the frame-wide overlay so
595
+ // the header never clips it.
596
+ seat('shell.overlay', 90, () => 'Cursor', CursorOverlay);
597
+ return undefined;
598
+ });
599
+
600
+ ctx.effect(() => () => {
601
+ for (const release of seats) {
602
+ try {
603
+ release?.();
604
+ }
605
+ catch (error) {
606
+ console.warn('[dsh-cursor-dev] releasing a Cursor seat failed:', error);
607
+ }
608
+ }
609
+ seats.length = 0;
610
+ for (const [name, seat] of [['the child-open bridge', bridgeSeat], ['the Cursor seats', slotsSeat]]) {
611
+ try {
612
+ seat?.();
613
+ }
614
+ catch (error) {
615
+ console.warn(`[dsh-cursor-dev] releasing ${name} failed:`, error);
616
+ }
617
+ }
618
+ });
619
+ }
620
+
621
+ exports.name = 'dsh-cursor-dev-client';
622
+ exports.apply = apply;
623
+ exports.inject = [];
624
+ return module.exports;
625
+ },
626
+ });