@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
@@ -8,28 +8,21 @@
8
8
  * {@link SidecarLocaleDomain}. `settings.*` is owned by the settings card
9
9
  * (T2.3); `inject.*` by the inject panel (T4.5, src/client/inject/);
10
10
  * `board.*` by the board-tab chrome (view switcher); `detail.*` /
11
- * `dshtools.*` / `project.*` mirror the M3 module tables (see below);
11
+ * `dshtools.*` / `project.*` by their corresponding feature surfaces;
12
12
  * `analysis.*` is owned by the analysis panel (T5.10b); `command.*` by the
13
13
  * `/sidecar` slash command (T4.6, re-exported via ./command.ts);
14
- * `sidebar.*` by the optional better-sidebar mini tab (T6.3,
15
- * src/client/sidebar-tab.tsx) — keeping the prefixes in one flat namespace
16
- * avoids cross-task collisions.
14
+ * `sidebar.*` by the plain-DOM Agent Center entry and optional better-sidebar
15
+ * mini tab (T6.3) — keeping the prefixes in one flat namespace avoids
16
+ * cross-task collisions.
17
17
  *
18
- * M3 unification (T5.10b): the component-local tables `detail/strings.ts`,
19
- * `dsh-tools/strings.ts` and `PROJECT_VIEW_STRINGS` stay the rendering
20
- * source for their components (decoupling kept), and this table REFERENCES
21
- * them entry-by-entry so `t()` covers every M3 string with zero copy drift
22
- * (parity is pinned by test/locales.test.ts). en translations for those
23
- * domains live in ./en.ts (the module tables are zh-only).
18
+ * This main dictionary is the source of truth for shipped Chinese copy.
19
+ * Feature-local modules expose dynamic, view-shaped facades over these flat
20
+ * keys; this locale core never depends back on UI modules.
24
21
  *
25
22
  * Copy sources: the schemastery `.description()` strings in src/config.ts
26
23
  * (the authoritative zh copy for each field) and design doc §4.a/§5.3/§6/§8.
27
24
  */
28
25
 
29
- import { DETAIL_STRINGS } from '../detail/strings.ts'
30
- import { DSH_TOOLS_STRINGS } from '../dsh-tools/strings.ts'
31
- import { PROJECT_VIEW_STRINGS } from '../board/project-view-logic.ts'
32
-
33
26
  /** Locale key domains (one owner surface per domain, see module doc). */
34
27
  export type SidecarLocaleDomain =
35
28
  | 'settings'
@@ -42,10 +35,6 @@ export type SidecarLocaleDomain =
42
35
  | 'command'
43
36
  | 'sidebar'
44
37
 
45
- const D = DETAIL_STRINGS
46
- const Q = DSH_TOOLS_STRINGS
47
- const P = PROJECT_VIEW_STRINGS
48
-
49
38
  export const zh = {
50
39
  // ── card chrome ────────────────────────────────────────────────────────
51
40
  'settings.cardTitle': 'Agent Sidecar',
@@ -107,7 +96,7 @@ export const zh = {
107
96
  'settings.sectionInject': '消息注入',
108
97
  'settings.injectEnabledLabel': '启用注入',
109
98
  'settings.injectEnabledHint':
110
- '关闭时看板隐藏全部注入入口,写接口在服务端同步拒绝。',
99
+ '关闭后注入面板为只读禁用态,写接口在服务端同步拒绝。',
111
100
  'settings.injectDefaultModeLabel': '默认注入模式',
112
101
  'settings.injectDefaultModeHint': '注入面板打开时预选的模式。',
113
102
  'settings.injectModeQueue': 'queue(排队下一轮)',
@@ -179,6 +168,7 @@ export const zh = {
179
168
  '结果未知:消息可能已投递。请勿重试;请前往目标会话核对后再决定下一步。',
180
169
  'inject.resultReplayed': '幂等重放:返回的是此前同一请求的结果,未发生二次注入。',
181
170
  'inject.reprepare': '重新准备',
171
+ 'inject.observeListen': '开启监听观察反应',
182
172
 
183
173
  // ── inject panel: error vocabulary (gateway + transport) ─────────────
184
174
  'inject.errInjectDisabled': '注入功能已在服务端关闭;请在设置中开启注入。',
@@ -198,120 +188,203 @@ export const zh = {
198
188
  'inject.errParse': '服务端响应无法解析。',
199
189
  'inject.errGeneric': '请求失败({code})。',
200
190
 
201
- // ── board tab chrome: main-view switcher (T5.10b, design §5.1) ─────────
191
+ // ── board tab chrome + session board (design §5.1 / §5.3) ──────────────
202
192
  'board.viewBoard': '会话看板',
203
193
  'board.viewProjects': '项目视图',
194
+ 'board.status.working': '工作中',
195
+ 'board.status.waiting': '等待中',
196
+ 'board.status.idle': '空闲',
197
+ 'board.status.dead': '已结束',
198
+ 'board.status.unknown': '未知',
199
+ 'board.attention.gap': '事件缺口',
200
+ 'board.daemon.probe': '探测中',
201
+ 'board.daemon.adopted': '已连接 · 领养',
202
+ 'board.daemon.defer': '等待系统服务',
203
+ 'board.daemon.reprobe': '重新探测中',
204
+ 'board.daemon.hosting': '正在启动',
205
+ 'board.daemon.hosted': '已连接 · 托管',
206
+ 'board.daemon.backoff': '重启退避中',
207
+ 'board.daemon.failed': '离线',
208
+ 'board.stream.ok': '实时流正常',
209
+ 'board.stream.degraded': '实时流重连中',
210
+ 'board.stream.unknown': '实时流未建立',
211
+ 'board.banner.daemonFailed': 'sidecar 离线:看板显示最后一次快照,数据不再更新',
212
+ 'board.banner.streamDegraded': '实时流重连中,数据可能滞后',
213
+ 'board.empty.daemonFailedTitle': 'sidecar 已离线',
214
+ 'board.empty.daemonFailedHint':
215
+ 'daemon 连续启动失败已熔断。可在设置卡重试,或手动运行 agent-sidecar daemon start 后等待自动领养。',
216
+ 'board.empty.daemonDeferTitle': '等待系统服务拉起 daemon',
217
+ 'board.empty.daemonDeferHint':
218
+ '检测到 LaunchAgent 托管,插件不会自行启动 daemon;服务拉起后看板会自动出数。',
219
+ 'board.empty.filteredTitle': '当前过滤条件下没有会话',
220
+ 'board.empty.filteredHint': '试试放宽时间窗、取消状态过滤,或打开「显示已结束」。',
221
+ 'board.empty.noSessionsTitle': '暂无被观测的会话',
222
+ 'board.empty.noSessionsHint':
223
+ '本机 agent(claude / codex / cursor / dsh …)开始工作后会自动出现在这里。',
224
+ 'board.topbar.title': 'Sidecar 多 agent 看板',
225
+ 'board.topbar.refresh': '刷新',
226
+ 'board.topbar.refreshing': '刷新中…',
227
+ 'board.topbar.refreshTitle': '手动拉取最新快照',
228
+ 'board.topbar.refreshFailed': '手动刷新失败,看板仍显示原有快照',
229
+ 'board.topbar.dismiss': '知道了',
230
+ 'board.topbar.showDead': '显示已结束',
231
+ 'board.topbar.timeWindow': '时间窗',
232
+ 'board.topbar.countWorking': '{n} 工作中',
233
+ 'board.topbar.countWaiting': '{n} 等待中',
234
+ 'board.topbar.countTotal': '共 {n} 个会话',
235
+ 'board.topbar.filterByStatusTitle': '只看「{label}」的会话',
236
+ 'board.topbar.clearStatusFilterTitle': '取消状态过滤,显示全部会话',
237
+ 'board.group.collapseTitle': '收起该分组',
238
+ 'board.group.expandTitle': '展开该分组',
239
+ 'board.group.showAll': '展开全部 {n} 个会话',
240
+ 'board.group.showLess': '只看前 {n} 个',
241
+ 'board.card.noEvent': '暂无事件',
242
+ 'board.card.untitled': '(无标题)',
243
+ 'board.card.observedDisclaimer': '状态为从持久化数据推断的观察值,可能滞后',
244
+ 'board.card.observedValue': '观察值: {status}',
245
+ 'board.card.lastReconcile': '最近对账: {time}',
246
+ 'board.card.neverReconciled': '尚未对账',
247
+ 'board.card.copyId': '点击复制完整会话 id',
248
+ 'board.card.copied': '已复制',
249
+ 'board.time.justNow': '刚刚',
250
+ 'board.time.minutesAgo': '{n} 分钟前',
251
+ 'board.time.hoursAgo': '{n} 小时前',
252
+ 'board.timeWindow.hours': '{n} 小时',
253
+ 'board.timeWindow.days': '{n} 天',
254
+ 'board.groupCount': '{n} 个会话',
255
+ 'board.unknownProject': '未知项目',
256
+ 'board.widget.label': 'Sidecar',
257
+ 'board.widget.connection.ok': '已连接',
258
+ 'board.widget.connection.degraded': '连接不稳定',
259
+ 'board.widget.connection.off': '离线',
260
+ 'board.widget.working': '{n} 个会话工作中',
204
261
 
205
- // ── session detail (T5.3 detail/strings.ts, referenced verbatim) ───────
206
- 'detail.header.close': D.header.close,
207
- 'detail.header.listenOn': D.header.listenOn,
208
- 'detail.header.listenOff': D.header.listenOff,
209
- 'detail.header.listenHint': D.header.listenHint,
210
- 'detail.header.untitled': D.header.untitled,
211
- 'detail.header.unknownProject': D.header.unknownProject,
212
- 'detail.header.observedDisclaimer': D.header.observedDisclaimer,
213
- 'detail.status.working': D.status.working,
214
- 'detail.status.waiting': D.status.waiting,
215
- 'detail.status.idle': D.status.idle,
216
- 'detail.status.dead': D.status.dead,
217
- 'detail.status.unknown': D.status.unknown,
218
- 'detail.sources.title': D.sources.title,
219
- 'detail.sources.dshLive': D.sources.dshLive,
220
- 'detail.sources.dshCold': D.sources.dshCold,
221
- 'detail.sources.sidecarReplay': D.sources.sidecarReplay,
222
- 'detail.sources.sidecarBuffer': D.sources.sidecarBuffer,
223
- 'detail.sources.none': D.sources.none,
224
- 'detail.kind.user': D.kind.user,
225
- 'detail.kind.assistant': D.kind.assistant,
226
- 'detail.kind.thinking': D.kind.thinking,
227
- 'detail.kind.toolCall': D.kind.toolCall,
228
- 'detail.kind.toolResult': D.kind.toolResult,
229
- 'detail.kind.turn': D.kind.turn,
230
- 'detail.kind.step': D.kind.step,
231
- 'detail.kind.error': D.kind.error,
232
- 'detail.kind.other': D.kind.other,
233
- 'detail.gap.label': D.gap.label,
234
- 'detail.timeline.loadMore': D.timeline.loadMore,
235
- 'detail.timeline.loadingMore': D.timeline.loadingMore,
236
- 'detail.timeline.noMore': D.timeline.noMore,
237
- 'detail.timeline.expand': D.timeline.expand,
238
- 'detail.timeline.collapse': D.timeline.collapse,
239
- 'detail.timeline.newBadge': D.timeline.newBadge,
240
- 'detail.timeline.seq': D.timeline.seq,
241
- 'detail.timeline.hiddenNotice': D.timeline.hiddenNotice,
242
- 'detail.timeline.showAll': D.timeline.showAll,
243
- 'detail.states.loadingTitle': D.states.loadingTitle,
244
- 'detail.states.emptyTitle': D.states.emptyTitle,
245
- 'detail.states.emptyHint': D.states.emptyHint,
246
- 'detail.states.errorTitle': D.states.errorTitle,
247
- 'detail.states.errorFallback': D.states.errorFallback,
248
- 'detail.states.errors.session_not_found': D.states.errors.session_not_found,
249
- 'detail.states.errors.invalid_cursor': D.states.errors.invalid_cursor,
250
- 'detail.states.errors.fusion_not_wired': D.states.errors.fusion_not_wired,
251
- 'detail.states.errors.network_error': D.states.errors.network_error,
252
- 'detail.states.errors.request_timeout': D.states.errors.request_timeout,
253
- 'detail.time.justNow': D.time.justNow,
254
- 'detail.time.minutesAgo': D.time.minutesAgo,
255
- 'detail.time.hoursAgo': D.time.hoursAgo,
256
- 'detail.time.daysAgo': D.time.daysAgo,
262
+ // ── session detail (T5.3) ──────────────────────────────────────────────
263
+ 'detail.header.close': '返回看板',
264
+ 'detail.header.listenOn': '监听中',
265
+ 'detail.header.listenOff': '监听',
266
+ 'detail.header.listenHint': '开启后新事件将实时追加并高亮',
267
+ 'detail.header.refresh': '刷新',
268
+ 'detail.header.refreshing': '刷新中…',
269
+ 'detail.header.refreshHint': '手动拉取最新时间线窗口',
270
+ 'detail.header.copyIdTitle': '点击复制会话 ID',
271
+ 'detail.header.copied': '已复制',
272
+ 'detail.header.untitled': '(无标题)',
273
+ 'detail.header.unknownProject': '未知项目',
274
+ 'detail.header.observedDisclaimer': '状态为从持久化数据推断的观察值,可能滞后',
275
+ 'detail.status.working': '工作中',
276
+ 'detail.status.waiting': '等待中',
277
+ 'detail.status.idle': '空闲',
278
+ 'detail.status.dead': '已结束',
279
+ 'detail.status.unknown': '未知',
280
+ 'detail.sources.title': '数据来源',
281
+ 'detail.sources.dshLive': 'dsh 实时',
282
+ 'detail.sources.dshCold': 'dsh 冷读',
283
+ 'detail.sources.sidecarReplay': 'sidecar 重放',
284
+ 'detail.sources.sidecarBuffer': 'sidecar 缓冲',
285
+ 'detail.sources.none': '来源未知',
286
+ 'detail.kind.user': '用户消息',
287
+ 'detail.kind.assistant': '助手回复',
288
+ 'detail.kind.thinking': '思考',
289
+ 'detail.kind.toolCall': '工具调用',
290
+ 'detail.kind.toolResult': '工具结果',
291
+ 'detail.kind.turn': '回合',
292
+ 'detail.kind.step': '步骤',
293
+ 'detail.kind.error': '错误',
294
+ 'detail.kind.other': '事件',
295
+ 'detail.gap.label': '缺口:可能有 {n} 条事件未捕获(256 队列上限或未持久化)',
296
+ 'detail.filter.conversation': '只看对话',
297
+ 'detail.filter.all': '全部事件',
298
+ 'detail.filter.hiddenNotice': '已隐藏 {n} 条协议事件',
299
+ 'detail.timeline.loadMore': '加载更多历史',
300
+ 'detail.timeline.loadingMore': '加载中…',
301
+ 'detail.timeline.noMore': '已到时间线起点',
302
+ 'detail.timeline.expand': '展开',
303
+ 'detail.timeline.collapse': '收起',
304
+ 'detail.timeline.newBadge': '新',
305
+ 'detail.timeline.seq': 'seq {n}',
306
+ 'detail.timeline.hiddenNotice': '为保持流畅,较早的 {n} 条已折叠',
307
+ 'detail.timeline.showAll': '全部显示',
308
+ 'detail.timeline.chunkRun': '{n} 个流式分块',
309
+ 'detail.states.loadingTitle': '正在加载时间线…',
310
+ 'detail.states.emptyTitle': '暂无事件',
311
+ 'detail.states.emptyHint': '该会话还没有可展示的规范化事件。',
312
+ 'detail.states.errorTitle': '时间线加载失败',
313
+ 'detail.states.errorFallback': '错误码:{reason}',
314
+ 'detail.states.errors.session_not_found': '会话不存在或已不可见',
315
+ 'detail.states.errors.invalid_cursor': '分页游标无效,请重新打开详情',
316
+ 'detail.states.errors.fusion_not_wired': '当前 host 未启用时间线能力',
317
+ 'detail.states.errors.network_error': '网络错误,无法联系 dsh host',
318
+ 'detail.states.errors.request_timeout': '请求超时',
319
+ 'detail.time.justNow': '刚刚',
320
+ 'detail.time.minutesAgo': '{n} 分钟前',
321
+ 'detail.time.hoursAgo': '{n} 小时前',
322
+ 'detail.time.daysAgo': '{n} 天前',
257
323
 
258
324
  // ── session detail: integration chrome (T5.10b literals) ───────────────
259
325
  'detail.actions.inject': '注入',
260
326
  'detail.actions.analyze': 'AI 分析',
261
327
  'detail.actions.analyzeDisabledHint': '在设置中开启「启用 AI 旁路分析」后可用',
328
+ 'detail.tools.title': '谱系与检索',
329
+ 'detail.tools.show': '展开',
330
+ 'detail.tools.hide': '收起',
262
331
 
263
- // ── dsh deep-query tools (T5.4 dsh-tools/strings.ts, referenced) ───────
264
- 'dshtools.lineage.title': Q.lineage.title,
265
- 'dshtools.lineage.loading': Q.lineage.loading,
266
- 'dshtools.lineage.error': Q.lineage.error,
267
- 'dshtools.lineage.empty': Q.lineage.empty,
268
- 'dshtools.lineage.currentBadge': Q.lineage.currentBadge,
269
- 'dshtools.lineage.liveBadge': Q.lineage.liveBadge,
270
- 'dshtools.lineage.notPersistedBadge': Q.lineage.notPersistedBadge,
271
- 'dshtools.lineage.role.ancestor': Q.lineage.role.ancestor,
272
- 'dshtools.lineage.role.target': Q.lineage.role.target,
273
- 'dshtools.lineage.role.descendant': Q.lineage.role.descendant,
274
- 'dshtools.lineage.jumpTitle': Q.lineage.jumpTitle,
275
- 'dshtools.lineage.currentTitle': Q.lineage.currentTitle,
276
- 'dshtools.lineage.expand': Q.lineage.expand,
277
- 'dshtools.lineage.collapse': Q.lineage.collapse,
278
- 'dshtools.lineage.nodeCount': Q.lineage.nodeCount,
279
- 'dshtools.lineage.incompleteWithId': Q.lineage.incompleteWithId,
280
- 'dshtools.lineage.incomplete': Q.lineage.incomplete,
281
- 'dshtools.lineage.degrade.notDshTitle': Q.lineage.degrade.notDshTitle,
282
- 'dshtools.lineage.degrade.notDshBody': Q.lineage.degrade.notDshBody,
283
- 'dshtools.lineage.degrade.queryUnavailableTitle': Q.lineage.degrade.queryUnavailableTitle,
284
- 'dshtools.lineage.degrade.queryUnavailableBody': Q.lineage.degrade.queryUnavailableBody,
285
- 'dshtools.lineage.degrade.traceFailedTitle': Q.lineage.degrade.traceFailedTitle,
286
- 'dshtools.lineage.degrade.traceFailedBody': Q.lineage.degrade.traceFailedBody,
287
- 'dshtools.lineage.degrade.unknownTitle': Q.lineage.degrade.unknownTitle,
288
- 'dshtools.lineage.degrade.unknownBody': Q.lineage.degrade.unknownBody,
289
- 'dshtools.search.title': Q.search.title,
290
- 'dshtools.search.placeholder': Q.search.placeholder,
291
- 'dshtools.search.submit': Q.search.submit,
292
- 'dshtools.search.loading': Q.search.loading,
293
- 'dshtools.search.error': Q.search.error,
294
- 'dshtools.search.empty': Q.search.empty,
295
- 'dshtools.search.filterOnlyNotice': Q.search.filterOnlyNotice,
296
- 'dshtools.search.projectFilter': Q.search.projectFilter,
297
- 'dshtools.search.matchedBy.full-text': Q.search.matchedBy['full-text'],
298
- 'dshtools.search.matchedBy.title': Q.search.matchedBy.title,
299
- 'dshtools.search.matchedBy.project': Q.search.matchedBy.project,
300
- 'dshtools.search.matchedBy.other': Q.search.matchedBy.other,
301
- 'dshtools.search.untitled': Q.search.untitled,
332
+ // ── dsh deep-query tools (T5.4) ────────────────────────────────────────
333
+ 'dshtools.lineage.title': '会话谱系',
334
+ 'dshtools.lineage.loading': '谱系加载中…',
335
+ 'dshtools.lineage.error': '谱系加载失败',
336
+ 'dshtools.lineage.empty': '暂无谱系数据',
337
+ 'dshtools.lineage.currentBadge': '当前会话',
338
+ 'dshtools.lineage.liveBadge': '运行中',
339
+ 'dshtools.lineage.notPersistedBadge': '未持久化',
340
+ 'dshtools.lineage.role.ancestor': '祖先',
341
+ 'dshtools.lineage.role.target': '目标',
342
+ 'dshtools.lineage.role.descendant': '子会话',
343
+ 'dshtools.lineage.jumpTitle': '跳转到该会话',
344
+ 'dshtools.lineage.currentTitle': '正在查看该会话',
345
+ 'dshtools.lineage.expand': '展开',
346
+ 'dshtools.lineage.collapse': '折叠',
347
+ 'dshtools.lineage.nodeCount': '{n} 个会话',
348
+ 'dshtools.lineage.incompleteWithId': '谱系不完整:父会话 {id} 无法解析',
349
+ 'dshtools.lineage.incomplete': '谱系不完整:部分父链无法解析',
350
+ 'dshtools.lineage.degrade.notDshTitle': '谱系/溯源为 dsh 会话专属',
351
+ 'dshtools.lineage.degrade.notDshBody': '当前会话来自外部 agent,dsh 的谱系与溯源能力不适用。',
352
+ 'dshtools.lineage.degrade.queryUnavailableTitle': 'dsh 谱系服务不可用',
353
+ 'dshtools.lineage.degrade.queryUnavailableBody':
354
+ '当前 dsh 组合未挂载 sessionQuery,谱系与溯源暂不可用。',
355
+ 'dshtools.lineage.degrade.traceFailedTitle': '谱系追溯失败',
356
+ 'dshtools.lineage.degrade.traceFailedBody': 'dsh 无法解析该会话的谱系。',
357
+ 'dshtools.lineage.degrade.unknownTitle': '谱系不可用',
358
+ 'dshtools.lineage.degrade.unknownBody': '后端报告谱系不可用(原因: {reason})。',
359
+ 'dshtools.search.title': '会话检索',
360
+ 'dshtools.search.placeholder': '检索会话(标题 / 项目 / 全文)',
361
+ 'dshtools.search.submit': '检索',
362
+ 'dshtools.search.loading': '检索中…',
363
+ 'dshtools.search.error': '检索失败',
364
+ 'dshtools.search.empty': '没有匹配的会话',
365
+ 'dshtools.search.filterOnlyNotice': 'dsh 全文检索不可用,已降级为标题/项目过滤',
366
+ 'dshtools.search.projectFilter': '项目过滤: {project}',
367
+ 'dshtools.search.matchedBy.full-text': '全文',
368
+ 'dshtools.search.matchedBy.title': '标题',
369
+ 'dshtools.search.matchedBy.project': '项目',
370
+ 'dshtools.search.matchedBy.other': '其他',
371
+ 'dshtools.search.untitled': '(无标题)',
302
372
 
303
- // ── project correlation view (T5.5 PROJECT_VIEW_STRINGS, referenced) ───
304
- 'project.title': P.title,
305
- 'project.summary': P.summary,
306
- 'project.crossAgent': P.crossAgent,
307
- 'project.sessionCount': P.sessionCount,
308
- 'project.lastActive': P.lastActive,
309
- 'project.liveChip': P.liveChip,
310
- 'project.untitled': P.untitled,
311
- 'project.empty.title': P.empty.title,
312
- 'project.empty.hint': P.empty.hint,
313
- 'project.loading': P.loading,
314
- 'project.errorTitle': P.errorTitle,
373
+ // ── project correlation view (T5.5) ────────────────────────────────────
374
+ 'project.title': '项目关联',
375
+ 'project.summary': '{projects} 个项目 · {sessions} 个会话',
376
+ 'project.crossAgent': '{n} 种 agent',
377
+ 'project.sessionCount': '{n} 个会话',
378
+ 'project.lastActive': '最近活跃 {time}',
379
+ 'project.liveChip': '实时',
380
+ 'project.untitled': '(无标题)',
381
+ 'project.showAllSessions': '展开全部 {n} 个会话',
382
+ 'project.showLessSessions': '只看前 {n} 个',
383
+ 'project.empty.title': '暂无项目关联',
384
+ 'project.empty.hint':
385
+ '时间窗内没有跨 agent 的项目活动;agent 在某个项目目录下开始工作后会出现在这里。',
386
+ 'project.loading': '正在加载项目关联…',
387
+ 'project.errorTitle': '项目关联加载失败',
315
388
 
316
389
  // ── AI bypass analysis panel (T5.10b, design §4.e.3 / §5.1) ────────────
317
390
  'analysis.title': 'AI 分析',
@@ -371,7 +444,7 @@ export const zh = {
371
444
  'command.noSessions': '暂无被观测的会话',
372
445
  'command.unknownProject': '未知项目',
373
446
  'command.untitled': '(无标题)',
374
- 'command.truncated': '还有 {n} 个活跃会话未列出',
447
+ 'command.truncated': '还有 {n} 个会话未列出',
375
448
  'command.boardHint': '打开会话视图的「Sidecar」Tab 查看完整看板',
376
449
  'command.unreachable': 'sidecar 未连接',
377
450
  'command.unreachableHint':
@@ -387,7 +460,9 @@ export const zh = {
387
460
  'command.time.hoursAgo': '{n} 小时前',
388
461
  'command.time.daysAgo': '{n} 天前',
389
462
 
390
- // ── better-sidebar mini tab (T6.3, design §5.2 optional soft dep) ──────
463
+ // ── sidebar navigation (plain DOM + optional better-sidebar) ───────────
464
+ 'sidebar.centerEntryLabel': 'Agent 中心',
465
+ 'sidebar.centerEntryAria': '打开 Agent 中心',
391
466
  'sidebar.tabTitle': 'Sidecar',
392
467
  'sidebar.countsRow': '{working} 工作中 · {waiting} 等待中',
393
468
  'sidebar.recentTitle': '最近活跃',
@@ -1,12 +1,13 @@
1
1
  /**
2
- * Slot-facing React glue (T2.4): binds the {@link SidecarController} stores
3
- * to the three presentational modules via `useSyncExternalStore` and hands
4
- * back zero-prop components ready for slot registration. Factories close
5
- * over the controller so subscribe/getSnapshot identities stay stable
6
- * across renders (uSES resubscribes on identity change).
2
+ * React bindings for the board tab, footer widget, and settings card.
3
+ * Factories close over the controller so subscribe/getSnapshot identities
4
+ * stay stable across renders. The board depends only on {@link BoardUiPort}
5
+ * and passes its nested detail port to the detail container.
6
+ * Each exported root factory's top-level component subscribes once to the
7
+ * active locale, refreshing its complete descendant tree without leaf subscriptions.
7
8
  *
8
- * The settings card entry additionally owns the staged-edit lifecycle over
9
- * a bound `SettingsScope` (browser mirror of the host settings namespace):
9
+ * The settings card owns the staged-edit lifecycle over a bound
10
+ * `SettingsScope` (browser mirror of the host settings namespace):
10
11
  * resolved values come from the scope snapshot, edits stage locally, save
11
12
  * writes one complete top-level group per changed group (see
12
13
  * settings-glue.ts for the write-granularity rationale), and success is
@@ -25,13 +26,15 @@ import { SettingsCard, type SettingsCardValues, type SidecarDaemonStatus } from
25
26
  import { countWorking, deriveWidgetConnection } from './board/logic.ts'
26
27
  import type { BoardFilterState } from './board/logic.ts'
27
28
  import type { SidecarController, SidecarViewState } from './controller.ts'
28
- import type { InjectMode } from './inject/logic.ts'
29
- import type { InjectActions } from './inject-glue.ts'
30
- import { SidecarDetailView, type SidecarUiIntegration } from './detail-view.tsx'
29
+ import { SidecarDetailView } from './detail-view.tsx'
31
30
  import { findCardHint, type DetailHeaderHint } from './detail-glue.ts'
32
- import { findProjectSessionHint, type ProjectsStore } from './project-glue.ts'
31
+ import { findProjectSessionHint } from './project-glue.ts'
32
+ import type { BoardUiPort, ProjectsStorePort } from './ui-integration.ts'
33
33
  import { detailErrorText } from './detail/logic.ts'
34
34
  import { t } from './locales/index.ts'
35
+ import { useActiveLocale } from './locales/react.ts'
36
+ import { CenterOverlay } from './navigation/CenterOverlay.tsx'
37
+ import type { CenterNavigation, CenterNavigationStore } from './navigation/center.ts'
35
38
  import css from './detail-view.module.css'
36
39
  import {
37
40
  DEFAULT_CONFIG_VIEW,
@@ -41,21 +44,6 @@ import {
41
44
  type SidecarConfigView,
42
45
  } from './settings-glue.ts'
43
46
 
44
- /**
45
- * What the board tab needs to host the inject panel (S5 wiring, T4.9).
46
- * Since T5.10b the panel opens from the detail view's 注入 button; this
47
- * shape rides {@link SidecarUiIntegration.inject} unchanged.
48
- */
49
- export interface BoardInjectIntegration {
50
- /** onPrepare/onExecute over the action transport (inject-glue.ts). */
51
- actions: InjectActions
52
- /**
53
- * Late-bound `inject.default-mode` reader: the settings scope resolves
54
- * after the tab mounts, so the value is read at panel-open time.
55
- */
56
- getDefaultMode: () => InjectMode
57
- }
58
-
59
47
  /** Board-tab main views (detail is an overlay route on top of either). */
60
48
  type MainView = 'board' | 'projects'
61
49
 
@@ -65,7 +53,7 @@ type MainView = 'board' | 'projects'
65
53
  */
66
54
  function ProjectsContainer(props: {
67
55
  controller: SidecarController
68
- store: ProjectsStore
56
+ store: ProjectsStorePort
69
57
  onSelectSession: (sessionId: string) => void
70
58
  }): ReactElement {
71
59
  const { controller, store } = props
@@ -85,28 +73,27 @@ function ProjectsContainer(props: {
85
73
  }
86
74
 
87
75
  /**
88
- * Cross-agent board tab (the "Sidecar" conversation tab), since T5.10b the
89
- * shell of the whole M3 information architecture (design §5.1):
76
+ * Cross-agent board content and its project/detail routes:
90
77
  *
91
78
  * - view 1: the session board, with a 「会话看板 / 项目视图」 switcher
92
79
  * (ProjectView over `GET projects`);
93
80
  * - view 2: clicking a session card in EITHER view routes to the full-tab
94
81
  * session-detail view (timeline + 注入 + AI 分析 + dsh 谱系/检索);
95
82
  * detail-internal jumps (lineage nodes, search hits) re-route in place;
96
- * - view 3: the M2 inject panel opens as a modal from the detail view.
83
+ * - view 3: the inject panel opens as a modal from the detail view.
97
84
  *
98
85
  * Without an integration the board renders read-only and inert (no detail
99
- * routing) — the M1 degradation posture.
86
+ * routing).
100
87
  */
101
- export function createBoardTab(
88
+ function createBoardContent(
102
89
  controller: SidecarController,
103
- integration?: SidecarUiIntegration,
90
+ integration?: BoardUiPort,
104
91
  ): () => ReactElement {
105
92
  const subscribe = (cb: () => void): (() => void) => controller.subscribe(cb)
106
93
  const getState = (): SidecarViewState => controller.getState()
107
94
  const getFilters = (): BoardFilterState => controller.getFilters()
108
95
 
109
- return function SidecarBoardTab(): ReactElement {
96
+ return function BoardContent(): ReactElement {
110
97
  // Third argument (server snapshot) keeps the components renderable under
111
98
  // react-dom/server (DOM-level verification harness); same source.
112
99
  const state = useSyncExternalStore(subscribe, getState, getState)
@@ -115,9 +102,9 @@ export function createBoardTab(
115
102
  const [detail, setDetail] = useState<{ id: string; hint: DetailHeaderHint | null } | null>(
116
103
  null,
117
104
  )
118
- // One ProjectsStore per tab mount, created lazily with the integration
105
+ // One project store per tab mount, created lazily with the integration
119
106
  // seam; state survives board↔projects↔detail switches within the tab.
120
- const [projectsStore] = useState<ProjectsStore | null>(
107
+ const [projectsStore] = useState<ProjectsStorePort | null>(
121
108
  () => integration?.createProjectsStore() ?? null,
122
109
  )
123
110
  useEffect(() => () => { projectsStore?.dispose() }, [projectsStore])
@@ -140,7 +127,7 @@ export function createBoardTab(
140
127
  sessionId={detail.id}
141
128
  hint={detail.hint}
142
129
  controller={controller}
143
- integration={integration}
130
+ integration={integration.detail}
144
131
  onClose={() => { setDetail(null) }}
145
132
  onSelectSession={openDetail}
146
133
  />
@@ -154,6 +141,7 @@ export function createBoardTab(
154
141
  type="button"
155
142
  className={css['switcherButton']}
156
143
  data-active={mainView === 'board' || undefined}
144
+ aria-pressed={mainView === 'board'}
157
145
  onClick={() => { setMainView('board') }}
158
146
  >
159
147
  {t('board.viewBoard')}
@@ -162,6 +150,7 @@ export function createBoardTab(
162
150
  type="button"
163
151
  className={css['switcherButton']}
164
152
  data-active={mainView === 'projects' || undefined}
153
+ aria-pressed={mainView === 'projects'}
165
154
  onClick={() => { setMainView('projects') }}
166
155
  >
167
156
  {t('board.viewProjects')}
@@ -186,9 +175,9 @@ export function createBoardTab(
186
175
  onFiltersChange={(next) => {
187
176
  controller.setFilters(next)
188
177
  }}
189
- onRefresh={() => {
190
- void controller.refresh()
191
- }}
178
+ // Hand the promise through so the board can render the
179
+ // in-flight state and a failure notice (UX-07).
180
+ onRefresh={() => controller.refresh()}
192
181
  onSelectSession={openDetail}
193
182
  />
194
183
  )}
@@ -197,16 +186,60 @@ export function createBoardTab(
197
186
  }
198
187
  }
199
188
 
189
+ /** Bind board content to its independent React root and locale subscription. */
190
+ export function createBoardTab(
191
+ controller: SidecarController,
192
+ integration?: BoardUiPort,
193
+ ): () => ReactElement {
194
+ const BoardContent = createBoardContent(controller, integration)
195
+ return function SidecarBoardTab(): ReactElement {
196
+ useActiveLocale()
197
+ return <BoardContent />
198
+ }
199
+ }
200
+
201
+ /** Bind the shared navigation source to the shell overlay and existing board. */
202
+ export function createCenterOverlay(
203
+ controller: SidecarController,
204
+ integration: BoardUiPort,
205
+ navigation: CenterNavigationStore,
206
+ ): () => ReactElement {
207
+ const BoardContent = createBoardContent(controller, integration)
208
+ return function SidecarCenterOverlay(): ReactElement {
209
+ useActiveLocale()
210
+ const open = useSyncExternalStore(
211
+ navigation.subscribe,
212
+ navigation.getSnapshot,
213
+ navigation.getSnapshot,
214
+ )
215
+ return (
216
+ <CenterOverlay
217
+ open={open}
218
+ onClose={navigation.close}
219
+ title={t('board.topbar.title')}
220
+ closeLabel={t('inject.close')}
221
+ >
222
+ {open ? <BoardContent /> : null}
223
+ </CenterOverlay>
224
+ )
225
+ }
226
+ }
227
+
200
228
  /** Footer connection dot + working counter bound to the controller. */
201
- export function createFooterWidget(controller: SidecarController): () => ReactElement {
229
+ export function createFooterWidget(
230
+ controller: SidecarController,
231
+ onOpen: CenterNavigation,
232
+ ): () => ReactElement {
202
233
  const subscribe = (cb: () => void): (() => void) => controller.subscribe(cb)
203
234
  const getState = (): SidecarViewState => controller.getState()
204
235
  return function SidecarFooterWidget(): ReactElement {
236
+ useActiveLocale()
205
237
  const state = useSyncExternalStore(subscribe, getState, getState)
206
238
  return (
207
239
  <SidecarWidget
208
240
  connection={deriveWidgetConnection(state.daemonState, state.streamHealth)}
209
241
  workingCount={countWorking(state.sessions)}
242
+ onOpen={onOpen}
210
243
  />
211
244
  )
212
245
  }
@@ -230,6 +263,7 @@ export function createSettingsCardEntry(
230
263
  const getSnapshot = (): ReturnType<typeof scope.getSnapshot> => scope.getSnapshot()
231
264
 
232
265
  return function SidecarSettingsCardEntry(): ReactElement {
266
+ useActiveLocale()
233
267
  const snapshot = useSyncExternalStore(subscribeScope, getSnapshot, getSnapshot)
234
268
  const state = useSyncExternalStore(subscribeState, getState, getState)
235
269
  const [staged, setStaged] = useState<Partial<SettingsCardValues>>({})