@shendeguize/dsh-agent-sidecar 0.1.0 → 0.2.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 (58) hide show
  1. package/README.md +78 -11
  2. package/lib/client.js +4708 -2932
  3. package/lib/client.js.map +1 -1
  4. package/package.json +3 -1
  5. package/src/client/analysis/AnalysisPanel.tsx +19 -27
  6. package/src/client/analysis/analysis.module.css +36 -97
  7. package/src/client/board/Board.tsx +255 -48
  8. package/src/client/board/board.module.css +113 -92
  9. package/src/client/board/logic.ts +99 -21
  10. package/src/client/board/project-view-logic.ts +27 -34
  11. package/src/client/board/project-view.module.css +46 -83
  12. package/src/client/board/project-view.tsx +50 -8
  13. package/src/client/board/strings.ts +70 -85
  14. package/src/client/commands.ts +30 -15
  15. package/src/client/controller.ts +27 -7
  16. package/src/client/detail/SessionDetail.tsx +234 -37
  17. package/src/client/detail/detail.module.css +100 -153
  18. package/src/client/detail/logic.ts +220 -29
  19. package/src/client/detail/strings.ts +66 -77
  20. package/src/client/detail-glue.ts +33 -0
  21. package/src/client/detail-view.module.css +43 -35
  22. package/src/client/detail-view.tsx +138 -118
  23. package/src/client/dsh-tools/LineageTree.tsx +22 -13
  24. package/src/client/dsh-tools/SearchPanel.tsx +18 -11
  25. package/src/client/dsh-tools/dsh-tools.module.css +53 -140
  26. package/src/client/dsh-tools/strings.ts +42 -77
  27. package/src/client/index.ts +285 -124
  28. package/src/client/inject/InjectPanel.tsx +81 -61
  29. package/src/client/inject/inject.module.css +97 -197
  30. package/src/client/inject/logic.ts +38 -0
  31. package/src/client/lifecycle/handoff.ts +83 -0
  32. package/src/client/locales/en.ts +91 -4
  33. package/src/client/locales/host.ts +131 -0
  34. package/src/client/locales/index.ts +196 -8
  35. package/src/client/locales/react.ts +10 -0
  36. package/src/client/locales/view.ts +38 -0
  37. package/src/client/locales/zh.ts +200 -125
  38. package/src/client/mount.tsx +75 -41
  39. package/src/client/navigation/CenterOverlay.tsx +40 -0
  40. package/src/client/navigation/center-overlay.module.css +51 -0
  41. package/src/client/navigation/center.ts +45 -0
  42. package/src/client/navigation/modal-isolation.ts +152 -0
  43. package/src/client/navigation/modal-surface-anchor.ts +72 -0
  44. package/src/client/navigation/sidebar-entry.module.css +73 -0
  45. package/src/client/navigation/sidebar-entry.ts +309 -0
  46. package/src/client/primitives/StaticPill.tsx +21 -0
  47. package/src/client/settings-card.module.css +35 -119
  48. package/src/client/settings-card.tsx +31 -153
  49. package/src/client/settings-fields.tsx +131 -0
  50. package/src/client/sidebar/SidebarTab.tsx +156 -0
  51. package/src/client/sidebar/model.ts +65 -0
  52. package/src/client/sidebar/sidebar-tab.module.css +118 -0
  53. package/src/client/sidebar-tab.tsx +46 -320
  54. package/src/client/theme/agsc.module.css +31 -0
  55. package/src/client/theme/parts.ts +56 -0
  56. package/src/client/ui-integration.ts +86 -0
  57. package/src/client/widget.tsx +42 -16
  58. package/src/client/inject/overlay.module.css +0 -22
@@ -1,42 +1,32 @@
1
1
  /**
2
- * Agent Sidecar browser client half (T2.4 full integration + S5/T4.9
3
- * injection & command wiring).
2
+ * Agent Sidecar browser composition root.
4
3
  *
5
- * Mounts the completed modules into dsh Web on three slot seats plus one
6
- * service contribution:
4
+ * Builds the controller and narrow UI ports, then mounts:
7
5
  *
8
- * - `conversation.view` (list slot, session scope): the cross-agent board
9
- * as a "Sidecar" tab, order 30 — the official "multiple views per
10
- * session" seat (registration shape follows the S0 skeleton and the
11
- * dsh-project-kanban precedent). Selecting a session card opens the
12
- * inject panel (T4.5) as a modal overlay: onPrepare/onExecute ride the
13
- * action transport via inject-glue.ts, the capability bit comes from the
14
- * state snapshot, and `inject.default-mode` is adopted late from the
15
- * settings scope (seat 3) — 'queue', the schema default, until then;
6
+ * - `shell.overlay` (list slot, root scope): the first-class Agent Center,
7
+ * reachable with or without an active conversation;
8
+ * - `conversation.view` (list slot, session scope): a second entry to the
9
+ * same cross-agent board and its project/detail routes;
10
+ * - a self-healing Agent Center row in the host sidebar;
16
11
  * - `sidebar.footer.action` (list slot, root scope): the connection-dot
17
- * footer widget (slot declared by dsh-client-ui-sidebar's footer rail;
18
- * type merge imported from its installed d.ts);
12
+ * footer widget;
19
13
  * - `settings.plugin.item` (keyed slot, root scope): the settings card,
20
- * keyed by the host-side settings namespace 'agent-sidecar' (the
21
- * configurable-plugins tab dispatches one card per HOST-SERVED
22
- * namespace — the host half registers it via `ctx.settings`);
23
- * - `/sidecar` slash command (T4.6): registered lazily on the `commandUi`
24
- * service via registerSidecarCommand — a composition without the
25
- * slash-menu runtime simply never gains the command, and a duplicate
26
- * registration (double apply) degrades to a logged no-op;
27
- * - optional better-sidebar mini tab (T6.3): parked on the lazily-injected
28
- * `betterSidebar` service via mountSidebarTab (optional peer, runtime
29
- * duck-typed probe — never imported); not installed = silent skip.
14
+ * keyed by the host-side namespace `agent-sidecar`;
15
+ * - the lazily registered `/sidecar` command and optional better-sidebar
16
+ * mini tab.
30
17
  *
31
18
  * Lifecycle contract:
32
19
  * - every resource rides `ctx.effect`: the data stream + visibilitychange
33
20
  * listener, the injected `<style data-plugin>` tags (tsdown's CSS-module
34
- * loader injects them at factory execution once per materialization —
35
- * so a keeper effect caches their text and restores them when a
36
- * re-apply of the same materialized module follows an unload);
37
- * - apply-guard idempotency: each mount checks the slot ledger for an
38
- * entry with this plugin's id/key before registering, so a duplicate
39
- * apply never double-registers (same cell + same priority would throw);
21
+ * loader resets a current-bundle manifest and injects them at factory
22
+ * execution once per materialization so a keeper effect prunes stale
23
+ * tags and restores authoritative manifest text when a re-apply of the
24
+ * same materialized module follows an unload);
25
+ * - the optional Host locale bridge rides a `locale`-injected child fiber;
26
+ * without that service the local table stays on zh, while every independent
27
+ * React root subscribes once and refreshes its complete descendant tree;
28
+ * - HMR handoff: each slot waits briefly while an overlapping old fiber owns
29
+ * this plugin's id/key, then registers the new component closure;
40
30
  * - graceful degradation: every mount is individually try/catch-ed —
41
31
  * a failing seat logs and is skipped, never taking the GUI down;
42
32
  * - the settings card waits on the optional `settingsScope` service via
@@ -47,6 +37,7 @@
47
37
  import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
48
38
  // Type-only slot-contract merges into SlotMap (erased at runtime):
49
39
  import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
40
+ import type {} from '@deepseek-ai/dsh-client-ui-layout/client'
50
41
  import type {} from '@deepseek-ai/dsh-client-ui-sidebar/client'
51
42
  import type {} from '@deepseek-ai/dsh-client-ui-settings-plugins/client'
52
43
  import type { SettingsScope, SettingsScopeSpec } from '@deepseek-ai/dsh-client-runtime/client'
@@ -54,19 +45,38 @@ import { registerSidecarCommand } from './commands.ts'
54
45
  import { PLUGIN_ID, SidecarController } from './controller.ts'
55
46
  import { createInjectActions } from './inject-glue.ts'
56
47
  import type { InjectMode } from './inject/logic.ts'
57
- import { createBoardTab, createFooterWidget, createSettingsCardEntry } from './mount.tsx'
48
+ import {
49
+ acquireWithHandoff,
50
+ isRegistrationCollision,
51
+ } from './lifecycle/handoff.ts'
52
+ import type { HostLocalePort } from './locales/host.ts'
53
+ import { attachHostLocale, subscribeLocale, t } from './locales/index.ts'
54
+ import {
55
+ createBoardTab,
56
+ createCenterOverlay,
57
+ createFooterWidget,
58
+ createSettingsCardEntry,
59
+ } from './mount.tsx'
60
+ import { createCenterNavigation } from './navigation/center.ts'
61
+ import {
62
+ mountSidebarEntry,
63
+ type SidebarEntryCopyPort,
64
+ } from './navigation/sidebar-entry.ts'
58
65
  import { mountSidebarTab } from './sidebar-tab.tsx'
59
- import { createDefaultIntegration, type SidecarUiIntegration } from './detail-view.tsx'
66
+ import { createDefaultIntegration, type BoardUiPort } from './ui-integration.ts'
60
67
  import type { SidecarConfigView } from './settings-glue.ts'
61
68
 
62
69
  export const name = 'agent-sidecar'
63
70
 
64
- /** The slot registry is the only hard dependency; settingsScope is lazy. */
71
+ /** The slot registry is the only hard dependency; settingsScope and locale are lazy. */
65
72
  export const inject = ['slots']
66
73
 
67
74
  /** Entry id (list slots) and cell key (settings keyed slot) in one. */
68
75
  const ENTRY_ID = 'agent-sidecar'
69
76
 
77
+ /** Distinct list-seat id for the frame-wide Agent Center surface. */
78
+ const CENTER_ENTRY_ID = 'agent-sidecar-center'
79
+
70
80
  /** Host-side settings namespace (host half registers it via ctx.settings). */
71
81
  const SETTINGS_NAMESPACE = 'agent-sidecar'
72
82
 
@@ -80,56 +90,146 @@ interface SettingsScopeBinderFace {
80
90
  bind<T>(spec: SettingsScopeSpec<T>): SettingsScope<T>
81
91
  }
82
92
 
83
- /** The three slot seats this plugin occupies (all merged into SlotMap). */
84
- type SidecarSlot = 'conversation.view' | 'sidebar.footer.action' | 'settings.plugin.item'
93
+ /** Structural Cordis face keeps the unpublished locale package out of runtime imports. */
94
+ interface LocaleInjectContextFace {
95
+ inject(deps: string[], callback: (ctx: unknown) => void): unknown
96
+ }
97
+
98
+ /** The four slot seats this plugin occupies (all merged into SlotMap). */
99
+ type SidecarSlot =
100
+ | 'shell.overlay'
101
+ | 'conversation.view'
102
+ | 'sidebar.footer.action'
103
+ | 'settings.plugin.item'
85
104
 
86
105
  /**
87
106
  * Apply-guard: whether the slot ledger already holds this plugin's entry
88
- * (list slots match by `id`, the keyed settings slot by `key`; both use
89
- * the same 'agent-sidecar' token). `entries()` answers [] for undeclared
90
- * slots, and the check runs inside the deferred `slots.inject` callback —
91
- * i.e. at actual registration time, when the ledger is authoritative.
107
+ * (list slots match their seat-specific `id`, the keyed settings slot by
108
+ * namespace `key`). `entries()` answers [] for undeclared slots, and the
109
+ * check runs inside the deferred `slots.inject` callback — i.e. at actual
110
+ * registration time, when the ledger is authoritative.
92
111
  */
93
112
  function hasOwnEntry(ctx: ClientContext, slot: SidecarSlot): boolean {
113
+ const entryId = slot === 'shell.overlay' ? CENTER_ENTRY_ID : ENTRY_ID
94
114
  return ctx.slots
95
115
  .entries(slot)
96
- .some((entry) => entry.options.id === ENTRY_ID || entry.options.key === SETTINGS_NAMESPACE)
116
+ .some((entry) =>
117
+ entry.options.id === entryId ||
118
+ (slot === 'settings.plugin.item' && entry.options.key === SETTINGS_NAMESPACE))
119
+ }
120
+
121
+ /**
122
+ * Lease the optional Host locale service for this injected child fiber.
123
+ * The locale core arbitrates overlapping fibers that share one service.
124
+ */
125
+ function mountHostLocale(ctx: ClientContext): void {
126
+ const mount = ctx as unknown as LocaleInjectContextFace
127
+ mount.inject(['locale'], (injected) => {
128
+ const lctx = injected as ClientContext & HostLocalePort
129
+ lctx.effect(
130
+ () => attachHostLocale(lctx),
131
+ 'agent-sidecar: host locale bridge',
132
+ )
133
+ })
97
134
  }
98
135
 
99
136
  // ---------------------------------------------------------------------------
100
137
  // Style lifecycle.
101
138
  // ---------------------------------------------------------------------------
102
139
 
140
+ const STYLE_OWNER = Symbol.for('@shendeguize/dsh-agent-sidecar/style-owner')
141
+ const STYLE_MANIFEST = Symbol.for('@shendeguize/dsh-agent-sidecar/style-manifest')
142
+ const STYLE_GENERATION = Symbol.for('@shendeguize/dsh-agent-sidecar/style-generation')
143
+
144
+ type StyleGlobals = Record<PropertyKey, unknown>
145
+
146
+ /**
147
+ * A fallback only for a sequential re-apply of this exact materialization.
148
+ * A new intro installs a new generation object, so it can never inherit an
149
+ * earlier materialization's cached CSS.
150
+ */
151
+ let cachedStyleManifest:
152
+ | { generation: object; styles: ReadonlyMap<string, string> }
153
+ | undefined
154
+
155
+ function setStyleOwner(tag: HTMLStyleElement, owner: object): void {
156
+ const sharedTag = tag as unknown as Record<PropertyKey, unknown>
157
+ sharedTag[STYLE_OWNER] = owner
158
+ }
159
+
160
+ function isStyleOwner(tag: Element, owner: object): boolean {
161
+ return (tag as unknown as Record<PropertyKey, unknown>)[STYLE_OWNER] === owner
162
+ }
163
+
103
164
  /**
104
- * Style text per data-plugin-css tag id, cached at module scope so it
105
- * survives unload re-apply cycles of one materialized module (the CSS
106
- * factory body only runs once per materialization).
165
+ * Freeze the current materialization's CSS before another bundle can reset
166
+ * it. A present Map is authoritative, including when it is empty.
107
167
  */
108
- const styleTextCache = new Map<string, string>()
168
+ function snapshotStyleManifest(globals: StyleGlobals): ReadonlyMap<string, string> {
169
+ const generation = globals[STYLE_GENERATION]
170
+ const manifest = globals[STYLE_MANIFEST]
171
+ if (manifest instanceof Map) {
172
+ const styles = new Map<string, string>()
173
+ for (const [tagId, cssText] of manifest) {
174
+ if (typeof tagId === 'string' && typeof cssText === 'string') {
175
+ styles.set(tagId, cssText)
176
+ }
177
+ }
178
+ if (typeof generation === 'object' && generation !== null) {
179
+ cachedStyleManifest = { generation, styles }
180
+ }
181
+ return styles
182
+ }
183
+ if (
184
+ typeof generation === 'object' &&
185
+ generation !== null &&
186
+ cachedStyleManifest?.generation === generation
187
+ ) {
188
+ return new Map(cachedStyleManifest.styles)
189
+ }
190
+ return new Map()
191
+ }
109
192
 
110
193
  /**
111
- * Keeper effect for the tsdown-injected `<style data-plugin>` tags: cache
112
- * their text, restore any tag a previous unload removed, and remove all of
113
- * this plugin's tags on dispose.
194
+ * Keeper effect for the tsdown-injected `<style data-plugin>` tags. Ownership
195
+ * lives on the DOM node through Symbol.for so the latest HMR fiber wins. The
196
+ * current bundle manifest is authoritative: plugin tags for CSS modules
197
+ * absent from it are stale and removed.
114
198
  */
115
- function keepStylesAlive(): () => void {
116
- if (typeof document === 'undefined') return () => {}
199
+ function keepStylesAlive(
200
+ documentRef: Document | undefined = typeof document === 'undefined' ? undefined : document,
201
+ globals: StyleGlobals = globalThis as unknown as StyleGlobals,
202
+ ): () => void {
203
+ if (documentRef === undefined) return () => {}
204
+ const manifest = snapshotStyleManifest(globals)
205
+ const owner = {}
117
206
  const ownTags = `style[data-plugin=${JSON.stringify(PLUGIN_ID)}]`
118
- for (const el of Array.from(document.querySelectorAll(ownTags))) {
119
- const key = (el as HTMLStyleElement).dataset['pluginCss']
120
- if (key !== undefined) styleTextCache.set(key, el.textContent ?? '')
207
+ for (const el of Array.from(documentRef.querySelectorAll(ownTags))) {
208
+ const tag = el as HTMLStyleElement
209
+ const key = tag.dataset['pluginCss']
210
+ const cssText = key === undefined ? undefined : manifest.get(key)
211
+ if (cssText === undefined) {
212
+ tag.remove()
213
+ continue
214
+ }
215
+ tag.textContent = cssText
216
+ setStyleOwner(tag, owner)
121
217
  }
122
- for (const [key, cssText] of styleTextCache) {
123
- if (document.querySelector(`style[data-plugin-css=${JSON.stringify(key)}]`) === null) {
124
- const tag = document.createElement('style')
218
+ for (const [key, cssText] of manifest) {
219
+ const selector = `${ownTags}[data-plugin-css=${JSON.stringify(key)}]`
220
+ if (documentRef.querySelector(selector) === null) {
221
+ const tag = documentRef.createElement('style')
125
222
  tag.dataset['plugin'] = PLUGIN_ID
126
223
  tag.dataset['pluginCss'] = key
127
224
  tag.textContent = cssText
128
- document.head.appendChild(tag)
225
+ setStyleOwner(tag, owner)
226
+ documentRef.head.appendChild(tag)
129
227
  }
130
228
  }
131
229
  return () => {
132
- for (const el of Array.from(document.querySelectorAll(ownTags))) el.remove()
230
+ for (const el of Array.from(documentRef.querySelectorAll(ownTags))) {
231
+ if (isStyleOwner(el, owner)) el.remove()
232
+ }
133
233
  }
134
234
  }
135
235
 
@@ -144,24 +244,35 @@ function keepStylesAlive(): () => void {
144
244
  */
145
245
  export function apply(ctx: ClientContext): void {
146
246
  const controller = new SidecarController()
247
+ const navigation = createCenterNavigation()
248
+ const openAgentCenter = navigation.open
249
+ const sidebarEntryCopy: SidebarEntryCopyPort = {
250
+ get label() { return t('sidebar.centerEntryLabel') },
251
+ get accessibilityLabel() { return t('sidebar.centerEntryAria') },
252
+ subscribe: subscribeLocale,
253
+ }
147
254
 
148
- // Inject integration (S5/T4.9): the detail view hosts the panel; a
149
- // delivered execute pulls one fresh snapshot so the board reflects the
150
- // injection promptly. The default mode is a mutable box because the
151
- // settings scope (seat 3) resolves after the board mounts — the reader
152
- // is late-bound and the box holds the schema default until then.
255
+ // Optional service: a pending inject fiber allocates no resources and keeps
256
+ // the module-owned zh fallback. A bridge failure never blocks other seats.
257
+ try {
258
+ mountHostLocale(ctx)
259
+ } catch {
260
+ // Optional locale injection must not block the remaining client seats.
261
+ }
262
+
263
+ // The detail view hosts injection. A delivered execute pulls one fresh
264
+ // snapshot so the board reflects it promptly. The default mode remains
265
+ // late-bound because settings resolve after the board mounts.
153
266
  const injectPrefs: { defaultMode: InjectMode } = { defaultMode: 'queue' }
154
267
 
155
- // Analysis capability (T5.10b): the state snapshot carries no analysis
156
- // bit, so the UI gate mirrors the live `analysis.enabled` setting the
157
- // same late-bound way (schema default false = fail-closed until the
158
- // scope resolves); the server 403 gate stays authoritative regardless.
268
+ // The state snapshot carries no analysis bit, so the UI mirrors the live
269
+ // `analysis.enabled` setting. False is fail-closed until settings resolve;
270
+ // the server's 403 gate remains authoritative.
159
271
  const analysisPrefs: { enabled: boolean } = { enabled: false }
160
272
 
161
- // M3 integration handed to the board tab (design §5.1): detail routing,
162
- // project view, dsh deep-query tools, and the analysis panel, each over
163
- // its glue store on the real transports.
164
- const uiIntegration: SidecarUiIntegration = createDefaultIntegration({
273
+ // Compose the production stores and detail capabilities behind the
274
+ // board's narrow port.
275
+ const uiIntegration: BoardUiPort = createDefaultIntegration({
165
276
  inject: {
166
277
  actions: createInjectActions({
167
278
  onDelivered: () => {
@@ -199,51 +310,96 @@ export function apply(ctx: ClientContext): void {
199
310
 
200
311
  ctx.effect(keepStylesAlive, 'agent-sidecar: injected styles')
201
312
 
202
- // Seat 1: cross-agent board as the "Sidecar" conversation tab since
203
- // T5.10b the shell of the M3 information architecture: board/project
204
- // switcher, card-click detail routing (timeline + inject + analysis +
205
- // dsh lineage/search), all riding the integration seams above.
313
+ // Seat 1: frame-wide Agent Center. Root scope keeps it reachable from an
314
+ // empty conversation, and the retained navigation snapshot makes early
315
+ // open requests visible as soon as the layout declares this official seat.
316
+ try {
317
+ const SidecarCenterOverlay = createCenterOverlay(controller, uiIntegration, navigation)
318
+ ctx.slots.inject('shell.overlay', () => acquireWithHandoff(
319
+ () => hasOwnEntry(ctx, 'shell.overlay')
320
+ ? undefined
321
+ : ctx.slots.register({
322
+ name: 'shell.overlay',
323
+ id: CENTER_ENTRY_ID,
324
+ order: 30,
325
+ }, SidecarCenterOverlay),
326
+ {
327
+ isCollision: isRegistrationCollision,
328
+ onError: (error) => {
329
+ console.error('agent-sidecar: center overlay registration failed', error)
330
+ },
331
+ onTimeout: () => {
332
+ console.error('agent-sidecar: center overlay handoff timed out')
333
+ },
334
+ },
335
+ ))
336
+ } catch (err) {
337
+ console.error('agent-sidecar: center overlay mount failed', err)
338
+ }
339
+
340
+ // Sidebar navigation shares the same narrow callback as the footer and
341
+ // `/sidecar`; this adapter owns only its host-side DOM placement.
342
+ try {
343
+ ctx.effect(
344
+ () => mountSidebarEntry(openAgentCenter, sidebarEntryCopy),
345
+ 'agent-sidecar: sidebar entry',
346
+ )
347
+ } catch (err) {
348
+ console.error('agent-sidecar: sidebar entry mount failed', err)
349
+ }
350
+
351
+ // Seat 2: cross-agent board as the "Sidecar" conversation tab.
206
352
  try {
207
353
  const SidecarBoardTab = createBoardTab(controller, uiIntegration)
208
- ctx.slots.inject('conversation.view', () => {
209
- if (hasOwnEntry(ctx, 'conversation.view')) return () => {}
210
- try {
211
- return ctx.slots.register({
212
- name: 'conversation.view',
213
- id: ENTRY_ID,
214
- order: 30,
215
- label: 'Sidecar',
216
- }, SidecarBoardTab)
217
- } catch (err) {
218
- console.error('agent-sidecar: board tab registration failed', err)
219
- return () => {}
220
- }
221
- })
354
+ ctx.slots.inject('conversation.view', () => acquireWithHandoff(
355
+ () => hasOwnEntry(ctx, 'conversation.view')
356
+ ? undefined
357
+ : ctx.slots.register({
358
+ name: 'conversation.view',
359
+ id: ENTRY_ID,
360
+ order: 30,
361
+ label: 'Sidecar',
362
+ }, SidecarBoardTab),
363
+ {
364
+ isCollision: isRegistrationCollision,
365
+ onError: (error) => {
366
+ console.error('agent-sidecar: board tab registration failed', error)
367
+ },
368
+ onTimeout: () => {
369
+ console.error('agent-sidecar: board tab handoff timed out')
370
+ },
371
+ },
372
+ ))
222
373
  } catch (err) {
223
374
  console.error('agent-sidecar: board tab mount failed', err)
224
375
  }
225
376
 
226
- // Seat 2: footer status widget.
377
+ // Seat 3: footer status widget.
227
378
  try {
228
- const SidecarFooterWidget = createFooterWidget(controller)
229
- ctx.slots.inject('sidebar.footer.action', () => {
230
- if (hasOwnEntry(ctx, 'sidebar.footer.action')) return () => {}
231
- try {
232
- return ctx.slots.register({
233
- name: 'sidebar.footer.action',
234
- id: ENTRY_ID,
235
- order: 30,
236
- }, SidecarFooterWidget)
237
- } catch (err) {
238
- console.error('agent-sidecar: footer widget registration failed', err)
239
- return () => {}
240
- }
241
- })
379
+ const SidecarFooterWidget = createFooterWidget(controller, openAgentCenter)
380
+ ctx.slots.inject('sidebar.footer.action', () => acquireWithHandoff(
381
+ () => hasOwnEntry(ctx, 'sidebar.footer.action')
382
+ ? undefined
383
+ : ctx.slots.register({
384
+ name: 'sidebar.footer.action',
385
+ id: ENTRY_ID,
386
+ order: 30,
387
+ }, SidecarFooterWidget),
388
+ {
389
+ isCollision: isRegistrationCollision,
390
+ onError: (error) => {
391
+ console.error('agent-sidecar: footer widget registration failed', error)
392
+ },
393
+ onTimeout: () => {
394
+ console.error('agent-sidecar: footer widget handoff timed out')
395
+ },
396
+ },
397
+ ))
242
398
  } catch (err) {
243
399
  console.error('agent-sidecar: footer widget mount failed', err)
244
400
  }
245
401
 
246
- // Seat 3: settings card, once the optional settingsScope service resolves.
402
+ // Seat 4: settings card, once the optional settingsScope service resolves.
247
403
  try {
248
404
  // The inject callback receives the base cordis Context; narrow once
249
405
  // (slots/effect are inherited, settingsScope is the resolved service).
@@ -271,18 +427,23 @@ export function apply(ctx: ClientContext): void {
271
427
  adoptDefaults()
272
428
 
273
429
  const SidecarSettingsCardEntry = createSettingsCardEntry(controller, scope)
274
- sctx.slots.inject('settings.plugin.item', () => {
275
- if (hasOwnEntry(sctx, 'settings.plugin.item')) return () => {}
276
- try {
277
- return sctx.slots.register({
278
- name: 'settings.plugin.item',
279
- key: SETTINGS_NAMESPACE,
280
- }, SidecarSettingsCardEntry)
281
- } catch (err) {
282
- console.error('agent-sidecar: settings card registration failed', err)
283
- return () => {}
284
- }
285
- })
430
+ sctx.slots.inject('settings.plugin.item', () => acquireWithHandoff(
431
+ () => hasOwnEntry(sctx, 'settings.plugin.item')
432
+ ? undefined
433
+ : sctx.slots.register({
434
+ name: 'settings.plugin.item',
435
+ key: SETTINGS_NAMESPACE,
436
+ }, SidecarSettingsCardEntry),
437
+ {
438
+ isCollision: isRegistrationCollision,
439
+ onError: (error) => {
440
+ console.error('agent-sidecar: settings card registration failed', error)
441
+ },
442
+ onTimeout: () => {
443
+ console.error('agent-sidecar: settings card handoff timed out')
444
+ },
445
+ },
446
+ ))
286
447
  } catch (err) {
287
448
  console.error('agent-sidecar: settings card mount failed', err)
288
449
  }
@@ -291,19 +452,19 @@ export function apply(ctx: ClientContext): void {
291
452
  console.error('agent-sidecar: settings scope injection failed', err)
292
453
  }
293
454
 
294
- // Seat 4: the `/sidecar` slash command, lazy on the commandUi service
295
- // (T4.6). registerSidecarCommand catches its own registration failures
296
- // (duplicate name on a double apply logs and no-ops); this try/catch
297
- // keeps the degradation posture uniform with the slot seats. Disposal
298
- // rides the injected fiber — unloading the plugin unregisters it.
455
+ // Seat 5: the `/sidecar` slash command, lazy on the commandUi service.
456
+ // registerSidecarCommand catches its own registration failures
457
+ // and owns the bounded overlap handoff; this try/catch keeps the
458
+ // degradation posture uniform with the slot seats. Disposal rides the
459
+ // injected fiber — unloading the plugin unregisters it.
299
460
  try {
300
- registerSidecarCommand(ctx)
461
+ registerSidecarCommand(ctx, { openCenter: openAgentCenter })
301
462
  } catch (err) {
302
463
  console.error('agent-sidecar: /sidecar command mount failed', err)
303
464
  }
304
465
 
305
- // Seat 5 (optional, T6.3): the better-sidebar mini tab, parked on the
306
- // lazily-injected `betterSidebar` service (optional peer; runtime
466
+ // Seat 6 (optional): the better-sidebar mini tab, parked on the
467
+ // lazily injected `betterSidebar` service (optional peer; runtime
307
468
  // duck-typed probe, never imported). Not installed → the fiber stays
308
469
  // pending forever: silent skip, zero resources. A probe/registration
309
470
  // failure degrades to a log and never touches the other seats.