@suifei/dsh-ai-team 0.0.0-stage → 0.3.4

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 (84) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/LICENSE +21 -0
  3. package/README.md +226 -2
  4. package/client.js +2901 -0
  5. package/cordis.patch.yml +31 -0
  6. package/icon.svg +27 -0
  7. package/index.js +893 -0
  8. package/lib/domain/errors.js +36 -0
  9. package/lib/domain/ids.js +26 -0
  10. package/lib/domain/inbox.js +87 -0
  11. package/lib/domain/member-config.js +168 -0
  12. package/lib/domain/member-profile.js +53 -0
  13. package/lib/domain/policy.js +102 -0
  14. package/lib/domain/task.js +309 -0
  15. package/lib/domain/thread.js +54 -0
  16. package/lib/host/fault-adapter.js +50 -0
  17. package/lib/host/member-tools.js +225 -0
  18. package/lib/host/permission-guard.js +28 -0
  19. package/lib/host/project-registry.js +68 -0
  20. package/lib/host/project-runtime.js +73 -0
  21. package/lib/host/service.js +810 -0
  22. package/lib/host/views.js +213 -0
  23. package/lib/host/waker.js +177 -0
  24. package/lib/ledger/checkpoint.js +395 -0
  25. package/lib/ledger/events.js +398 -0
  26. package/lib/ledger/fold.js +991 -0
  27. package/lib/ledger/writer.js +983 -0
  28. package/lib/runtime/failure-policy.js +113 -0
  29. package/lib/runtime/host-agent.js +101 -0
  30. package/lib/runtime/member-runtime.js +286 -0
  31. package/lib/runtime/skill-catalog.js +27 -0
  32. package/lib/runtime/skill-loader.js +26 -0
  33. package/lib/runtime/skill-provider.js +121 -0
  34. package/lib/vendor/skillopt/LICENSE +21 -0
  35. package/lib/vendor/skillopt/requirements.txt +31 -0
  36. package/lib/vendor/skillopt/scripts/sleep.py +119 -0
  37. package/lib/vendor/skillopt/skillopt_sleep/__init__.py +20 -0
  38. package/lib/vendor/skillopt/skillopt_sleep/__main__.py +942 -0
  39. package/lib/vendor/skillopt/skillopt_sleep/adapters/__init__.py +1 -0
  40. package/lib/vendor/skillopt/skillopt_sleep/adapters/superpowers.py +1379 -0
  41. package/lib/vendor/skillopt/skillopt_sleep/backend.py +2844 -0
  42. package/lib/vendor/skillopt/skillopt_sleep/budget.py +75 -0
  43. package/lib/vendor/skillopt/skillopt_sleep/config.py +257 -0
  44. package/lib/vendor/skillopt/skillopt_sleep/consolidate.py +558 -0
  45. package/lib/vendor/skillopt/skillopt_sleep/cycle.py +1270 -0
  46. package/lib/vendor/skillopt/skillopt_sleep/dream.py +173 -0
  47. package/lib/vendor/skillopt/skillopt_sleep/evalkit.py +718 -0
  48. package/lib/vendor/skillopt/skillopt_sleep/evidence.py +143 -0
  49. package/lib/vendor/skillopt/skillopt_sleep/experiments/__init__.py +1 -0
  50. package/lib/vendor/skillopt/skillopt_sleep/experiments/gbrain_bench.py +119 -0
  51. package/lib/vendor/skillopt/skillopt_sleep/experiments/personas.py +86 -0
  52. package/lib/vendor/skillopt/skillopt_sleep/experiments/report.py +132 -0
  53. package/lib/vendor/skillopt/skillopt_sleep/experiments/run_experiment.py +178 -0
  54. package/lib/vendor/skillopt/skillopt_sleep/experiments/run_gbrain.py +209 -0
  55. package/lib/vendor/skillopt/skillopt_sleep/experiments/run_transfer.py +155 -0
  56. package/lib/vendor/skillopt/skillopt_sleep/experiments/sweep.py +164 -0
  57. package/lib/vendor/skillopt/skillopt_sleep/gate.py +50 -0
  58. package/lib/vendor/skillopt/skillopt_sleep/handoff_backend.py +173 -0
  59. package/lib/vendor/skillopt/skillopt_sleep/harvest.py +691 -0
  60. package/lib/vendor/skillopt/skillopt_sleep/harvest_codex.py +260 -0
  61. package/lib/vendor/skillopt/skillopt_sleep/harvest_copilot.py +443 -0
  62. package/lib/vendor/skillopt/skillopt_sleep/harvest_copilot_cli.py +244 -0
  63. package/lib/vendor/skillopt/skillopt_sleep/harvest_cursor.py +316 -0
  64. package/lib/vendor/skillopt/skillopt_sleep/harvest_opencode.py +364 -0
  65. package/lib/vendor/skillopt/skillopt_sleep/harvest_pi.py +311 -0
  66. package/lib/vendor/skillopt/skillopt_sleep/harvest_sources.py +135 -0
  67. package/lib/vendor/skillopt/skillopt_sleep/judges.py +344 -0
  68. package/lib/vendor/skillopt/skillopt_sleep/llm_miner.py +187 -0
  69. package/lib/vendor/skillopt/skillopt_sleep/memory.py +166 -0
  70. package/lib/vendor/skillopt/skillopt_sleep/mine.py +427 -0
  71. package/lib/vendor/skillopt/skillopt_sleep/multi_skill.py +190 -0
  72. package/lib/vendor/skillopt/skillopt_sleep/prompts.py +250 -0
  73. package/lib/vendor/skillopt/skillopt_sleep/replay.py +211 -0
  74. package/lib/vendor/skillopt/skillopt_sleep/rollout.py +184 -0
  75. package/lib/vendor/skillopt/skillopt_sleep/scheduler.py +286 -0
  76. package/lib/vendor/skillopt/skillopt_sleep/skill_resolver.py +244 -0
  77. package/lib/vendor/skillopt/skillopt_sleep/slow_update.py +141 -0
  78. package/lib/vendor/skillopt/skillopt_sleep/staging.py +3285 -0
  79. package/lib/vendor/skillopt/skillopt_sleep/state.py +283 -0
  80. package/lib/vendor/skillopt/skillopt_sleep/tasks_file.py +100 -0
  81. package/lib/vendor/skillopt/skillopt_sleep/types.py +227 -0
  82. package/lib/vendor/skillopt/src/index.js +394 -0
  83. package/package.json +62 -4
  84. package/skills/skillopt-sleep/SKILL.md +121 -0
package/client.js ADDED
@@ -0,0 +1,2901 @@
1
+ /**
2
+ * dsh-ai-team — Client half (M2.5 workbench).
3
+ *
4
+ * A three-pane project workbench rendered inside the DSH `main` slot:
5
+ * left = page nav + Project tree + Agents
6
+ * center= project header + 主对话 / 看板 / 态势 tabs + composer
7
+ * right = Task Thread panel (discussion, acceptance, done receipt)
8
+ *
9
+ * UI references: docs/04 §4–§7/§10, docs/appendix/D §2–§3, docs/07 §4–§6.
10
+ * This is the M2.5 workbench client: it reads
11
+ * `/ai-team/api/bootstrap`, falls back to embedded demo data, and every
12
+ * write action only produces local feedback. The registration contract
13
+ * (`sidebar.panellist` id `ai-team`, `main` key `ai-team`) is unchanged.
14
+ *
15
+ * Colors come from `--dsw-alias-*` only; radii/font steps from host tokens.
16
+ */
17
+ window.__ModuleLoader__.load({
18
+ id: '@suifei/dsh-ai-team',
19
+ factory(require) {
20
+ const React = require('react');
21
+ const h = React.createElement;
22
+ const NS = 'agentTeam';
23
+ const PKG = '@suifei/dsh-ai-team';
24
+
25
+ const zh = {
26
+ panel: '文渊阁',
27
+ back: '对话',
28
+ inbox: '收件箱',
29
+ board: '任务看板',
30
+ projects: '项目',
31
+ agents: 'Agents',
32
+ main: '主对话',
33
+ boardTab: '看板',
34
+ tabSituation: '态势',
35
+ tabsAria: '项目视图',
36
+ composer: '写一条消息…',
37
+ taskPlaceholder: '一句话任务,可稍后补完判据',
38
+ discuss: '讨论',
39
+ taskMode: '任务',
40
+ send: '发送',
41
+ createTask: '创建 Task',
42
+ enterHint: 'Ctrl/⌘ + Enter 发送',
43
+ thread: '讨论',
44
+ closeThread: '关闭讨论',
45
+ follow: '关注',
46
+ unfollow: '取消关注',
47
+ convert: '转为 Task',
48
+ reply: '回复此 Thread…',
49
+ replyHint: '将基于 #{seq} 发送',
50
+ replyHintNew: '将新建此 Task 的 Thread',
51
+ owner: '负责人',
52
+ executor: '执行',
53
+ unassigned: '未指派',
54
+ blocked: '受阻',
55
+ dependsOn: '依赖',
56
+ replyCount: '{n} 条回复',
57
+ stPending: '待处理',
58
+ stInProgress: '进行中',
59
+ stPendingAcceptance: '待验收',
60
+ stDone: '已完成',
61
+ stCancelled: '已取消',
62
+ agentRunning: '运行中',
63
+ agentIdle: '空闲',
64
+ agentDefaultModel: '默认模型',
65
+ agentFailure: 'LLM 失败:{category} · 第 {n} 次',
66
+ agentFailureShort: '失败',
67
+ agentProvisioning: '正在准备记忆',
68
+ agentFailed: '失败',
69
+ agentRetired: '已退役',
70
+ groupCreated: '新建 {n} 个任务',
71
+ groupBlocked: '{n} 项有依赖',
72
+ groupMore: '展开其余 {n} 项',
73
+ groupLess: '收起其余 {n} 项',
74
+ fromDiscussion: '由讨论升格',
75
+ loading: '加载中…',
76
+ fallbackTitle: '演示数据',
77
+ fallbackBody: 'bootstrap 接口(/ai-team)不可达,已使用内置样例;发送、关注、验收只做本地反馈,不写账本。',
78
+ spike: 'AI Team',
79
+ spikeTitle: '文渊阁 AI Team 工作台:修书、校勘、纂辑;方向、验收与账本。',
80
+ navAria: '页面导航',
81
+ menu: '目录',
82
+ menuAria: '打开项目与成员目录',
83
+ projectsAria: '项目',
84
+ agentsAria: '成员',
85
+ streamAria: '主对话',
86
+ boardAria: '任务看板',
87
+ situationAria: '项目态势',
88
+ threadAria: '任务讨论',
89
+ composerAria: '写一条消息',
90
+ modeAria: '发送模式',
91
+ columnAria: '「{title}」,{n} 项',
92
+ emptyStreamTitle: '说一句话开工',
93
+ emptyStreamBody: '例如:把这次回归里失败的三条都修了。',
94
+ emptyStreamExample: '把这次回归里失败的三条都修了',
95
+ fillExample: '填入示例',
96
+ emptyBoardTitle: '还没有任务',
97
+ emptyBoardBody: '在讨论里说一句,或把一条消息转为 Task。',
98
+ emptyBoardAction: '回到主对话',
99
+ inboxTitle: '收件箱',
100
+ inboxGoal: '需要你的决策与失败,都在这里',
101
+ inboxEmptyTitle: '没有需要你处理的事',
102
+ inboxEmptyBody: '成员失败、需要决策或有人提到你时,会出现在这里。',
103
+ inboxDecisionTitle: '需要你决策',
104
+ inboxItemTitle: '收件箱消息',
105
+ inboxCompletedTitle: 'Task 已完成:{title}',
106
+ inboxOpenTask: '打开 Task',
107
+ inboxAck: '知道了',
108
+ inboxAcked: '已标记处理。',
109
+ inboxEmptyAction: '看任务看板',
110
+ projectEmptyTitle: '还没有项目',
111
+ projectEmptyBody: '创建项目属于 S-01;M1 之后接入。',
112
+ emptyColumn: '暂无',
113
+ expandDone: '展开全部',
114
+ collapseDone: '收起',
115
+ threadEmptyTitle: '还没有消息',
116
+ threadEmptyBody: '先写下完成判据,再开始执行。',
117
+ acceptance: '验收',
118
+ candidate: '候选验收',
119
+ acceptEligible: '你从未认领过该任务,且不是执行者,所以可以由你验收。',
120
+ acceptSteward: '验收请求投给:你(未设 steward,Human 兜底)。',
121
+ irreversibleNote: '此接受后不可撤回(合并 / 发布 / 对外交付)。',
122
+ irreversibleCheck: '我已确认影响,接受后不可撤回',
123
+ acceptCta: '接受并标记完成',
124
+ acceptIrreversibleCta: '接受且不可撤回',
125
+ requestChanges: '要求修改',
126
+ resultSummary: '结果摘要',
127
+ evidence: '证据',
128
+ noEvidence: '还没有证据',
129
+ noSummary: '执行者未提交结果摘要',
130
+ diff: '差异',
131
+ noDiff: '未提供差异',
132
+ taskActions: '任务动作',
133
+ mockOnly: '仅演示,不写账本',
134
+ actCancel: '取消任务',
135
+ actReopen: '重开',
136
+ actReassign: '改派…',
137
+ cancelPrompt: '取消理由(可留空)',
138
+ cancelDone: '已取消 #{n}。',
139
+ reopenPrompt: '重开理由(可留空)',
140
+ reopenDone: '已重开 #{n}。',
141
+ deleteConfirm: '删除只允许从未认领的 pending Task,且只留 tombstone。确定?',
142
+ deleteDone: '已删除 #{n}(tombstone)。',
143
+ reassignPrompt: '把 accountable owner 改派给(输入名字):',
144
+ reassignDone: '已把 #{n} 改派给 {name}。',
145
+ reassignNoTarget: '没有可改派的活跃成员。',
146
+ actHandoff: '交接…',
147
+ actDelete: '删除',
148
+ actionMock: 'spike:{action} 需要二次确认并写账本,命令接入后启用。',
149
+ convertTitle: '转为 Task',
150
+ titleLabel: '标题',
151
+ convertNamePlaceholder: '一句话说清要做什么',
152
+ convertDod: '完成判据',
153
+ convertDodPlaceholder: '怎样算完成:可复核的结果或证据',
154
+ convertContext: '上下文:引用 #{n} 的 {m} 条消息',
155
+ convertCancel: '取消',
156
+ convertSubmit: '创建 Task',
157
+ convertMock: 'spike:将从 #{n} 创建 Task,并带上下文引用(命令未接入)。',
158
+ conflictTitle: '你的回复基于较早的 Thread,已退回',
159
+ conflictNew: 'Thread 已到 #{head}(新增 {n} 条)',
160
+ conflictContinue: '基于最新继续写',
161
+ conflictContext: '查看我当时的上下文',
162
+ conflictContinueMock: 'spike:已把发送基线更新到 #{head}(不写账本)。',
163
+ conflictContextMock: 'spike:将展示你在 #{base} 时的上下文(命令未接入)。',
164
+ replyMock: 'spike:回复将基于 #{seq} 发送并写入账本(命令未接入)。',
165
+ replyMockNew: 'spike:将新建 Thread 并写入回复(命令未接入)。',
166
+ replySent: '已写入 Thread #{seq}。',
167
+ replyFailed: '回复失败:{message}',
168
+ acceptNote: '工作台验收:已核对 Thread 与证据。',
169
+ acceptDone: '已验收 #{n}。',
170
+ acceptFailed: '验收失败:{message}',
171
+ changesNeedNote: '先把返工理由写在回复框,再点退回。',
172
+ changesDone: '已退回 #{n},要求返工。',
173
+ changesFailed: '退回失败:{message}',
174
+ followMock: '已关注 #{n},写入账本。',
175
+ unfollowMock: '已取消关注 #{n};完成、受阻或需要你决定时仍会出现在看板。',
176
+ acceptMock: 'spike:接受 #{n} 将写验收记录并标记完成(命令未接入)。',
177
+ changesMock: 'spike:退回需要至少 10 字原因并写账本(命令未接入)。',
178
+ projectSwitchMock: 'spike:项目切换只更新视图标题;按项目读取账本在 M1 接入。',
179
+ archivedMock: 'spike:归档项目只读;取消归档在 M3 接入。',
180
+ agentMock: 'spike:成员详情(打开会话 / 发消息 / 记忆与技能)在 M3 接入。',
181
+ agentOpen: '查看 {name} 的详情',
182
+ moreActions: '更多操作',
183
+ newProject: '新建项目',
184
+ addAgent: '添加成员',
185
+ newProjectMock: 'spike:新建项目属于 S-01 流程,M1 之后接入。',
186
+ newProjectTitle: '新建项目',
187
+ projectName: '项目名',
188
+ projectGoal: '目标',
189
+ projectCreate: '创建项目',
190
+ projectCreated: '已创建项目 {name}。',
191
+ projectSwitched: '已切换项目。',
192
+ addAgentMock: 'spike:添加成员在 M1 之后接入。',
193
+ addAgentTitle: '添加成员',
194
+ memberName: '名字',
195
+ memberRole: '职责',
196
+ memberPrompt: '初始指令(可选)',
197
+ memberPromptHint: '例如:任务 #1 已待验收,读 tasks 后独立验收。',
198
+ memberPreset: '运行配方',
199
+ presetStandard: '标准模式',
200
+ presetPtc: 'PTC 模式',
201
+ presetMinimal: '极简模式',
202
+ presetCordis: '创造模式',
203
+ memberProvider: 'Provider',
204
+ memberModel: '模型',
205
+ memberOwner: '设为 Project owner',
206
+ memberCreate: '创建并激活',
207
+ memberAdding: '正在创建…',
208
+ memberAdded: '成员已创建并激活。',
209
+ cancel: '取消',
210
+ memberProfileTitle: '成员资料',
211
+ memberProfileTitleName: '成员资料 · {name}',
212
+ memberProfileMeta: '{status} · 资料 v{version} · 蒸馏 v{distill}',
213
+ memberPersonality: '个性 / 工作方式',
214
+ memberSkills: '技能',
215
+ memberNotes: '最近备忘',
216
+ memberMaxTokens: 'Max tokens',
217
+ qaRequired: '需要 QA 验收',
218
+ qaChecklist: 'QA 检查清单',
219
+ notePlaceholder: '加一条备忘…',
220
+ skillPlaceholder: '加一个技能,如 nuwa-skill',
221
+ messagePlaceholder: '给这位成员发私信…',
222
+ sendMessage: '发私信',
223
+ messageSent: '私信已发送并唤醒成员。',
224
+ addNote: '加备忘',
225
+ addSkill: '加技能',
226
+ retry: '重试',
227
+ retried: '已重试并重新激活。',
228
+ save: '保存',
229
+ saving: '保存中…',
230
+ redistill: '重新蒸馏',
231
+ leaveTeam: '离开团队',
232
+ leaveConfirm: '离开团队后成员会进入人才库,遗留任务会列出来等待接手。确定?',
233
+ leftTeam: '已进入人才库;遗留任务 {n} 张,等待接手。',
234
+ leftTeamHanded: '已进入人才库;{n} 张遗留任务已交给继承人。',
235
+ successor: '遗留工作交给',
236
+ noSuccessor: '暂不指定(留在看板等待接手)',
237
+ recruited: '已从人才库重新招募。',
238
+ talentPoolCount: '人才库 {n}',
239
+ talentPool: '人才库',
240
+ recruit: '重新招募',
241
+ profileSaved: '成员资料已保存,下一次唤醒生效。',
242
+ profileDistilled: '已重新蒸馏并记录新版本。',
243
+ conversationMock: 'spike:宿主没有可用的 layout.selectPanel(conversation)。',
244
+ discussSent: '已发送给 Project owner。',
245
+ taskSent: '已创建 Task。',
246
+ sendFailed: '发送失败:{message}',
247
+ inboxUnread: '收件箱,{n} 条未读',
248
+ dismiss: '关闭提示',
249
+ retry: '重试',
250
+ crashTitle: '工作台渲染失败',
251
+ noProject: '未选择项目',
252
+ archivedCount: '已归档 {n}',
253
+ q1Title: '需要你',
254
+ q2Title: '团队在做',
255
+ q3Title: '等待受阻',
256
+ q4Title: '最近完成',
257
+ q1Def: '现在哪件事非你不可?',
258
+ q2Def: '团队现在在推进什么?',
259
+ q3Def: '卡在哪、在等谁?',
260
+ q4Def: '最近成了什么?',
261
+ q1Empty: '没有需要你的事',
262
+ q2Empty: '团队当前空闲',
263
+ q3Empty: '没有卡住的事',
264
+ q4Empty: '还没有完成',
265
+ viewAll: '查看全部',
266
+ quadrantMeta: 'Host 投影 · 事件流已连接({time})',
267
+ q4Total: '最近 {n} 条 / 共 {m} 条',
268
+ catReview: '待验收',
269
+ catIrreversible: '不可撤回',
270
+ catDecision: '决策',
271
+ catOwnerBlocked: '负责人处理',
272
+ catInProgress: '进行中',
273
+ catHandoff: '等交接',
274
+ catBlocked: '受阻',
275
+ catStalled: '停滞',
276
+ catDone: '已完成',
277
+ waitOn: '等 {ref}',
278
+ waitDependency: '依赖未完成',
279
+ decisionMock: 'spike:决策项需要在收件箱页处理(M1 之后接入),当前只在态势里展示。',
280
+ quadrantMissing: 'spike:该条目对应的 Task 不在当前 bootstrap 数据里。',
281
+ filterLabel: '筛选:{name}',
282
+ filterClear: '清除筛选',
283
+ filterInboxExtra: '另有 {n} 条收件箱决策项',
284
+ filteredEmptyTitle: '这个筛选下没有任务',
285
+ filteredEmptyBody: '清除筛选后可看到全部任务。',
286
+ itemCount: '{n} 项',
287
+ doneTitle: '完成信息',
288
+ doneAt: '完成于 {time}',
289
+ acceptedBy: '验收人 {name}',
290
+ evidenceCount: '证据 {n} 条',
291
+ doneNoInfo: '完成信息未记录',
292
+ };
293
+ const en = {
294
+ panel: 'Wenyuan Pavilion',
295
+ back: 'Chat',
296
+ inbox: 'Inbox',
297
+ board: 'All tasks',
298
+ projects: 'Projects',
299
+ agents: 'Agents',
300
+ main: 'Main',
301
+ boardTab: 'Board',
302
+ tabSituation: 'Situation',
303
+ tabsAria: 'Project views',
304
+ composer: 'Write a message…',
305
+ taskPlaceholder: 'A one-line task; add the definition of done later',
306
+ discuss: 'Discuss',
307
+ taskMode: 'Task',
308
+ send: 'Send',
309
+ createTask: 'Create task',
310
+ enterHint: 'Ctrl/⌘ + Enter to send',
311
+ thread: 'Discussion',
312
+ closeThread: 'Close discussion',
313
+ follow: 'Follow',
314
+ unfollow: 'Unfollow',
315
+ convert: 'Turn into task',
316
+ reply: 'Reply to this thread…',
317
+ replyHint: 'Will send on #{seq}',
318
+ replyHintNew: 'Will start a new thread',
319
+ owner: 'Owner',
320
+ executor: 'Runs',
321
+ unassigned: 'Unassigned',
322
+ blocked: 'Blocked',
323
+ dependsOn: 'depends on',
324
+ replyCount: '{n} replies',
325
+ stPending: 'To do',
326
+ stInProgress: 'In progress',
327
+ stPendingAcceptance: 'To accept',
328
+ stDone: 'Done',
329
+ stCancelled: 'Cancelled',
330
+ agentRunning: 'Running',
331
+ agentIdle: 'Idle',
332
+ agentDefaultModel: 'Default model',
333
+ agentFailure: 'LLM failure: {category} · attempt {n}',
334
+ agentFailureShort: 'failed',
335
+ agentProvisioning: 'Preparing memory',
336
+ agentFailed: 'Failed',
337
+ agentRetired: 'Retired',
338
+ groupCreated: '{n} tasks created',
339
+ groupBlocked: '{n} with dependencies',
340
+ groupMore: 'Show {n} more',
341
+ groupLess: 'Collapse {n}',
342
+ fromDiscussion: 'Promoted from discussion',
343
+ loading: 'Loading…',
344
+ fallbackTitle: 'Demo data',
345
+ fallbackBody: 'The bootstrap endpoint (/ai-team) is unreachable, so the embedded sample is shown. Sending, following and acceptance only give local feedback; nothing is written to the ledger.',
346
+ spike: 'AI Team',
347
+ spikeTitle: 'Wenyuan Pavilion AI Team workbench: compile, collate, edit; direction, acceptance and ledger.',
348
+ navAria: 'Page navigation',
349
+ menu: 'Menu',
350
+ menuAria: 'Open the project and member menu',
351
+ projectsAria: 'Projects',
352
+ agentsAria: 'Agents',
353
+ streamAria: 'Main stream',
354
+ boardAria: 'Task board',
355
+ situationAria: 'Project situation',
356
+ threadAria: 'Task discussion',
357
+ composerAria: 'Write a message',
358
+ modeAria: 'Send mode',
359
+ columnAria: '“{title}”, {n} items',
360
+ emptyStreamTitle: 'Say what needs doing',
361
+ emptyStreamBody: 'For example: fix the three failing regressions.',
362
+ emptyStreamExample: 'Fix the three failing regressions from this run',
363
+ fillExample: 'Use this example',
364
+ emptyBoardTitle: 'No tasks yet',
365
+ emptyBoardBody: 'Say it in the discussion, or turn a message into a task.',
366
+ emptyBoardAction: 'Back to main',
367
+ inboxTitle: 'Inbox',
368
+ inboxGoal: 'Decisions and failures that need you land here',
369
+ inboxEmptyTitle: 'Nothing needs you',
370
+ inboxEmptyBody: 'Member failures, decisions and mentions land here.',
371
+ inboxDecisionTitle: 'Decision required',
372
+ inboxItemTitle: 'Inbox message',
373
+ inboxCompletedTitle: 'Task completed: {title}',
374
+ inboxOpenTask: 'Open Task',
375
+ inboxAck: 'Acknowledge',
376
+ inboxAcked: 'Marked handled.',
377
+ inboxEmptyAction: 'Open the board',
378
+ projectEmptyTitle: 'No projects yet',
379
+ projectEmptyBody: 'Project creation belongs to S-01; it lands after M1.',
380
+ emptyColumn: 'None',
381
+ expandDone: 'Show all',
382
+ collapseDone: 'Collapse',
383
+ threadEmptyTitle: 'No messages yet',
384
+ threadEmptyBody: 'Write the definition of done before starting.',
385
+ acceptance: 'Acceptance',
386
+ candidate: 'Candidate',
387
+ acceptEligible: 'You never claimed this task and are not its executor, so acceptance is yours.',
388
+ acceptSteward: 'Acceptance request went to: you (no steward set; Human fallback).',
389
+ irreversibleNote: 'This acceptance cannot be undone (merge, release, external delivery).',
390
+ irreversibleCheck: 'I understand; this cannot be undone',
391
+ acceptCta: 'Accept and mark done',
392
+ acceptIrreversibleCta: 'Accept, no undo',
393
+ requestChanges: 'Request changes',
394
+ resultSummary: 'Result',
395
+ evidence: 'Evidence',
396
+ noEvidence: 'No evidence yet',
397
+ noSummary: 'The executor submitted no result summary',
398
+ diff: 'Diff',
399
+ noDiff: 'No diff provided',
400
+ taskActions: 'Task actions',
401
+ mockOnly: 'Demo only; nothing is written',
402
+ actCancel: 'Cancel task',
403
+ actReopen: 'Reopen',
404
+ actReassign: 'Reassign…',
405
+ cancelPrompt: 'Cancellation reason (optional)',
406
+ cancelDone: 'Cancelled #{n}.',
407
+ reopenPrompt: 'Reopen reason (optional)',
408
+ reopenDone: 'Reopened #{n}.',
409
+ deleteConfirm: 'Only a never-claimed pending Task can be deleted; it becomes a tombstone. Continue?',
410
+ deleteDone: 'Deleted #{n} (tombstone).',
411
+ reassignPrompt: 'Reassign the accountable owner to (name):',
412
+ reassignDone: 'Reassigned #{n} to {name}.',
413
+ reassignNoTarget: 'No active member is available for reassignment.',
414
+ actHandoff: 'Hand off…',
415
+ actDelete: 'Delete',
416
+ actionMock: 'spike: {action} needs confirmation and a ledger write; enabled when commands land.',
417
+ convertTitle: 'Turn into task',
418
+ titleLabel: 'Title',
419
+ convertNamePlaceholder: 'One line about what needs doing',
420
+ convertDod: 'Definition of done',
421
+ convertDodPlaceholder: 'What counts as done: a verifiable result or evidence',
422
+ convertContext: 'Context: {m} messages from #{n}',
423
+ convertCancel: 'Cancel',
424
+ convertSubmit: 'Create task',
425
+ convertMock: 'spike: will create a task from #{n} with context refs (commands not wired).',
426
+ conflictTitle: 'Your reply was based on an older thread and was returned',
427
+ conflictNew: 'Thread is at #{head} ({n} new)',
428
+ conflictContinue: 'Continue from latest',
429
+ conflictContext: 'See what I had',
430
+ conflictContinueMock: 'spike: send base moved to #{head} (nothing written).',
431
+ conflictContextMock: 'spike: will show your context at #{base} (commands not wired).',
432
+ replyMock: 'spike: reply will be sent on #{seq} and written to the ledger (commands not wired).',
433
+ replyMockNew: 'spike: will start a thread and write the reply (commands not wired).',
434
+ replySent: 'Written to Thread #{seq}.',
435
+ replyFailed: 'Reply failed: {message}',
436
+ acceptNote: 'Workbench acceptance: Thread and evidence reviewed.',
437
+ acceptDone: 'Accepted #{n}.',
438
+ acceptFailed: 'Accept failed: {message}',
439
+ changesNeedNote: 'Write the change request in the reply box first.',
440
+ changesDone: 'Changes requested on #{n}.',
441
+ changesFailed: 'Change request failed: {message}',
442
+ followMock: 'Following #{n}; written to the ledger.',
443
+ unfollowMock: 'Unfollowed #{n}; completion, blockage or decisions still appear on the board.',
444
+ acceptMock: 'spike: accepting #{n} will write an acceptance record and mark it done (commands not wired).',
445
+ changesMock: 'spike: requesting changes needs at least 10 characters and a ledger write (commands not wired).',
446
+ projectSwitchMock: 'spike: switching projects only updates the header; per-project ledger reads land with M1.',
447
+ archivedMock: 'spike: archived projects are read-only; unarchive lands with M3.',
448
+ agentMock: 'spike: member detail (open session / message / memory & skills) lands with M3.',
449
+ agentOpen: 'Open details for {name}',
450
+ moreActions: 'More actions',
451
+ newProject: 'New project',
452
+ addAgent: 'Add member',
453
+ newProjectMock: 'spike: project creation belongs to S-01 and lands after M1.',
454
+ newProjectTitle: 'New project',
455
+ projectName: 'Project name',
456
+ projectGoal: 'Goal',
457
+ projectCreate: 'Create project',
458
+ projectCreated: 'Created project {name}.',
459
+ projectSwitched: 'Switched project.',
460
+ addAgentMock: 'spike: adding members lands after M1.',
461
+ addAgentTitle: 'Add member',
462
+ memberName: 'Name',
463
+ memberRole: 'Role',
464
+ memberPrompt: 'Initial instruction (optional)',
465
+ memberPromptHint: 'e.g. Task #1 is awaiting acceptance; read tasks and review it independently.',
466
+ memberPreset: 'Recipe',
467
+ presetStandard: 'Standard',
468
+ presetPtc: 'PTC',
469
+ presetMinimal: 'Minimal',
470
+ presetCordis: 'Creative',
471
+ memberProvider: 'Provider',
472
+ memberModel: 'Model',
473
+ memberOwner: 'Make Project owner',
474
+ memberCreate: 'Create and activate',
475
+ memberAdding: 'Creating…',
476
+ memberAdded: 'Member created and activated.',
477
+ cancel: 'Cancel',
478
+ memberProfileTitle: 'Member profile',
479
+ memberProfileTitleName: 'Member profile · {name}',
480
+ memberProfileMeta: '{status} · profile v{version} · distill v{distill}',
481
+ memberPersonality: 'Personality / working style',
482
+ memberSkills: 'Skills',
483
+ memberNotes: 'Recent notes',
484
+ memberMaxTokens: 'Max tokens',
485
+ qaRequired: 'Require QA review',
486
+ qaChecklist: 'QA checklist',
487
+ notePlaceholder: 'Add a note…',
488
+ skillPlaceholder: 'Add a skill, e.g. nuwa-skill',
489
+ messagePlaceholder: 'Message this member…',
490
+ sendMessage: 'Send message',
491
+ messageSent: 'Message sent and member woken.',
492
+ addNote: 'Add note',
493
+ addSkill: 'Add skill',
494
+ retry: 'Retry',
495
+ retried: 'Retried and re-activated.',
496
+ save: 'Save',
497
+ saving: 'Saving…',
498
+ redistill: 'Re-distill',
499
+ leaveTeam: 'Leave team',
500
+ leaveConfirm: 'Leaving moves the member to the talent pool; unfinished tasks will be listed for takeover. Continue?',
501
+ leftTeam: 'Moved to the talent pool; {n} unfinished task(s) await takeover.',
502
+ leftTeamHanded: 'Moved to the talent pool; {n} unfinished task(s) handed to the successor.',
503
+ successor: 'Hand leftover work to',
504
+ noSuccessor: 'No successor now (leave it on the board)',
505
+ recruited: 'Recruited from the talent pool.',
506
+ talentPoolCount: 'Talent pool {n}',
507
+ talentPool: 'Talent pool',
508
+ recruit: 'Recruit',
509
+ profileSaved: 'Profile saved; effective on the next wake.',
510
+ profileDistilled: 'Re-distilled and recorded as a new version.',
511
+ conversationMock: 'spike: this host has no working layout.selectPanel(conversation).',
512
+ discussSent: 'Sent to the Project owner.',
513
+ taskSent: 'Task created.',
514
+ sendFailed: 'Send failed: {message}',
515
+ inboxUnread: 'Inbox, {n} unread',
516
+ dismiss: 'Dismiss',
517
+ retry: 'Retry',
518
+ crashTitle: 'Workbench failed to render',
519
+ noProject: 'No project selected',
520
+ archivedCount: 'Archived {n}',
521
+ q1Title: 'Needs you',
522
+ q2Title: 'Team is doing',
523
+ q3Title: 'Waiting / blocked',
524
+ q4Title: 'Recently done',
525
+ q1Def: 'What can only you do right now?',
526
+ q2Def: 'What is the team moving forward?',
527
+ q3Def: 'Where is it stuck, and on whom?',
528
+ q4Def: 'What got done recently?',
529
+ q1Empty: 'Nothing needs you',
530
+ q2Empty: 'The team is idle',
531
+ q3Empty: 'Nothing is stuck',
532
+ q4Empty: 'Nothing completed yet',
533
+ viewAll: 'View all',
534
+ quadrantMeta: 'Host projection · event stream connected ({time})',
535
+ q4Total: 'Recent {n} / {m} total',
536
+ catReview: 'To accept',
537
+ catIrreversible: 'Irreversible',
538
+ catDecision: 'Decision',
539
+ catOwnerBlocked: 'Owner action',
540
+ catInProgress: 'In progress',
541
+ catHandoff: 'Handoff pending',
542
+ catBlocked: 'Blocked',
543
+ catStalled: 'Stalled',
544
+ catDone: 'Done',
545
+ waitOn: 'waiting on {ref}',
546
+ waitDependency: 'dependency unfinished',
547
+ decisionMock: 'spike: decisions are handled on the inbox page (after M1); for now they only show in Situation.',
548
+ quadrantMissing: 'spike: the Task behind this item is not in the current bootstrap data.',
549
+ filterLabel: 'Filter: {name}',
550
+ filterClear: 'Clear filter',
551
+ filterInboxExtra: '{n} inbox decision item(s) not on the board',
552
+ filteredEmptyTitle: 'No tasks under this filter',
553
+ filteredEmptyBody: 'Clear the filter to see all tasks.',
554
+ itemCount: '{n} items',
555
+ doneTitle: 'Completion',
556
+ doneAt: 'Done at {time}',
557
+ acceptedBy: 'Accepted by {name}',
558
+ evidenceCount: '{n} evidence items',
559
+ doneNoInfo: 'No completion details recorded',
560
+ };
561
+
562
+ const MOCK = (() => {
563
+ const now = Date.now();
564
+ const ago = (min) => now - min * 60_000;
565
+ const thread11 = {
566
+ taskId: 'tsk_11',
567
+ headSeq: 6,
568
+ baseSeq: 4,
569
+ entries: [
570
+ { seq: 1, author: '沈舟', at: ago(18), body: '先复现,再判断是不是同一个根因。' },
571
+ { seq: 2, author: '林岚', at: ago(16), body: '我这边跑到第二条就复现了,日志在 /tmp/regression-2.log。' },
572
+ { seq: 3, author: '沈舟', at: ago(14), body: '收到,把第三条也跑一遍,我加锁修。' },
573
+ { seq: 4, author: '林岚', at: ago(11), body: 'upload.ts:88 的锁范围扩大后,第三条不再失败。' },
574
+ { seq: 5, author: '沈舟', at: ago(7), body: '我认领 #11,先把修复提上来。' },
575
+ { seq: 6, author: '沈舟', at: ago(5), body: '已提交待验收:3 条用例全绿,回归 214 条通过。' },
576
+ ],
577
+ changes: [
578
+ { who: '沈舟', what: '认领修复', seq: 5 },
579
+ { who: '沈舟', what: '提交待验收', seq: 6 },
580
+ ],
581
+ };
582
+ return {
583
+ projects: [
584
+ { id: 'prj_main', name: 'Main-Dev', goal: '把这次回归清干净', owner: '沈舟', active: true },
585
+ { id: 'prj_bug', name: 'Bug-Check-Fix', goal: '排查 issue 与后续修复', owner: '沈舟' },
586
+ { id: 'prj_ui', name: 'UI-UX-AX', goal: '工作台体验与可访问性', owner: '顾言' },
587
+ { id: 'prj_old', name: 'Old-Spike', goal: '已结束的第一阶段验证', owner: '顾言', archived: true },
588
+ ],
589
+ agents: [
590
+ { id: 'mem_shenzhou', name: '沈舟', role: '架构 · 管验收标准', status: 'running' },
591
+ { id: 'mem_linlan', name: '林岚', role: '研究 · 复跑门禁', status: 'idle' },
592
+ { id: 'mem_chenmo', name: '陈默', role: '产品 · 验收', status: 'idle' },
593
+ { id: 'mem_guyan', name: '顾言', role: 'UI/UX · 工作台体验', status: 'idle' },
594
+ { id: 'mem_xiaoman', name: '小满', role: '开发 · 正在准备记忆', status: 'provisioning' },
595
+ { id: 'mem_laozhou', name: '老周', role: '已退役(历史保留)', status: 'retired' },
596
+ ],
597
+ inbox: [
598
+ { id: 'inbox_d1', kind: 'inbox', category: 'decision', title: '决策:首页文案选 A 还是 B', waitingSince: ago(25), read: false },
599
+ { id: 'inbox_m1', kind: 'inbox', category: 'mention', title: '林岚在 #16 提到了你', waitingSince: ago(12), read: false },
600
+ ],
601
+ stream: [
602
+ { id: 'str_1', seq: 1, kind: 'human_message', at: ago(15), author: { kind: 'human', name: '你' }, body: '把这次回归里失败的三条都修了,谁该看谁看。' },
603
+ {
604
+ id: 'str_2', seq: 2, kind: 'task_card', at: ago(14), author: { kind: 'agent', name: '沈舟' },
605
+ body: '拆成 5 项,2 项互相依赖。我先复现,其余并行。',
606
+ taskIds: ['tsk_11', 'tsk_12', 'tsk_13', 'tsk_14', 'tsk_15'],
607
+ },
608
+ {
609
+ id: 'str_3', seq: 3, kind: 'task_card', at: ago(6), author: { kind: 'agent', name: '林岚' },
610
+ body: '两条已提交待验收,一条已完成;验收前先看证据。',
611
+ taskIds: ['tsk_16', 'tsk_17', 'tsk_18'],
612
+ },
613
+ ],
614
+ tasks: [
615
+ { id: 'tsk_11', number: 11, title: '复现三条失败的用例,界定影响面与不修什么', status: 'in_progress', owner: '沈舟', assignee: '沈舟', replies: 2, following: true, blockedBy: [], originRefs: ['str_1'], updatedAt: ago(8), summary: '先复现,再判断是不是同一个根因;不修与本次无关的 2 条 flaky。' },
616
+ { id: 'tsk_12', number: 12, title: '登录超时:先加锁再断言,见 upload.ts:88', status: 'pending', owner: '沈舟', assignee: null, replies: 0, following: true, blockedBy: [], originRefs: ['str_2'], updatedAt: ago(11) },
617
+ { id: 'tsk_13', number: 13, title: '上传 413:前端表单与后端限制不一致', status: 'pending', owner: '沈舟', assignee: null, replies: 0, following: true, blockedBy: ['tsk_12'], originRefs: ['str_2'], updatedAt: ago(30) },
618
+ { id: 'tsk_14', number: 14, title: '回归门禁:复跑 build 与 browser 8/8', status: 'in_progress', owner: '林岚', assignee: '林岚', replies: 1, following: false, blockedBy: [], originRefs: ['str_2'], updatedAt: ago(20) },
619
+ { id: 'tsk_15', number: 15, title: '把修法写成可复核的验收证据', status: 'in_progress', owner: '陈默', assignee: '陈默', replies: 0, following: false, blockedBy: [], originRefs: ['str_2'], updatedAt: ago(30) },
620
+ {
621
+ id: 'tsk_16', number: 16, title: '上传 413:前后端限制对齐后复验', status: 'pending_acceptance', owner: '沈舟', assignee: '沈舟', replies: 3, following: true, blockedBy: [], originRefs: ['str_3'], updatedAt: ago(5),
622
+ submission: { summary: '三条用例已修,回归全绿。', evidence: [{ label: 'test/report.xml', ref: 'a1f3' }, { label: 'pnpm test', ref: '214 passed' }], diffStat: '3 files · +48 −12' },
623
+ },
624
+ {
625
+ id: 'tsk_17', number: 17, title: '发布前确认:测试环境 v0.2', status: 'pending_acceptance', owner: '顾言', assignee: '顾言', replies: 2, following: false, blockedBy: [], originRefs: ['str_3'], updatedAt: ago(3), irreversible: true,
626
+ submission: { summary: '已发布 v0.2 到测试环境,对外可见。', evidence: [{ label: 'release-note.md', ref: 'draft' }], diffStat: '2 files · +6 −2' },
627
+ },
628
+ { id: 'tsk_18', number: 18, title: '看板已完成列展开控件的可见性判断修正', status: 'done', owner: '林岚', assignee: '林岚', replies: 1, following: false, blockedBy: [], originRefs: ['str_3'], updatedAt: ago(5), completedAt: ago(5), acceptedBy: '沈舟', evidenceCount: 2 },
629
+ { id: 'tsk_19', number: 19, title: '工作台窄容器三档截图与横向滚动检查', status: 'in_progress', owner: '顾言', assignee: '顾言', replies: 0, following: false, blockedBy: [], updatedAt: ago(40) },
630
+ { id: 'tsk_20', number: 20, title: '把回归脚本的失败日志整理成证据', status: 'pending', owner: '沈舟', assignee: '沈舟', handoffTo: '简', replies: 0, following: false, blockedBy: [], updatedAt: ago(10) },
631
+ { id: 'tsk_21', number: 21, title: '修复上传 413 的前端限制', status: 'done', owner: '沈舟', assignee: '沈舟', replies: 0, following: false, blockedBy: [], updatedAt: ago(60), completedAt: ago(60), acceptedBy: '你', evidenceCount: 1 },
632
+ { id: 'tsk_22', number: 22, title: '回归门禁复跑 8/8', status: 'done', owner: '陈默', assignee: '陈默', replies: 0, following: false, blockedBy: [], updatedAt: ago(120), completedAt: ago(120), acceptedBy: '你', evidenceCount: 3 },
633
+ { id: 'tsk_23', number: 23, title: 'Thread 引用与复制动作补齐', status: 'done', owner: '林岚', assignee: '林岚', replies: 0, following: false, blockedBy: [], updatedAt: ago(1440), completedAt: ago(1440), acceptedBy: '沈舟', evidenceCount: 0 },
634
+ { id: 'tsk_24', number: 24, title: '工作台三栏骨架首次安装验证', status: 'done', owner: '沈舟', assignee: '沈舟', replies: 0, following: false, blockedBy: [], updatedAt: ago(2880), completedAt: ago(2880), acceptedBy: '你', evidenceCount: 1 },
635
+ ],
636
+ threads: {
637
+ tsk_11: thread11,
638
+ tsk_16: {
639
+ taskId: 'tsk_16', headSeq: 3,
640
+ entries: [
641
+ { seq: 1, author: '林岚', at: ago(12), body: '三条用例已修,回归全绿。' },
642
+ { seq: 2, author: '沈舟', at: ago(8), body: '已提交待验收,证据见 test/report.xml。' },
643
+ { seq: 3, author: '林岚', at: ago(6), body: '复跑 build 与 browser 8/8 通过。' },
644
+ ],
645
+ },
646
+ tsk_17: {
647
+ taskId: 'tsk_17', headSeq: 2,
648
+ entries: [
649
+ { seq: 1, author: '顾言', at: ago(10), body: '测试环境已发布 v0.2,对外可见。' },
650
+ { seq: 2, author: '顾言', at: ago(4), body: '发布动作不可撤回,请确认影响。' },
651
+ ],
652
+ },
653
+ tsk_18: {
654
+ taskId: 'tsk_18', headSeq: 1,
655
+ entries: [{ seq: 1, author: '沈舟', at: ago(6), body: '看板已完成列展开逻辑修好了。' }],
656
+ },
657
+ },
658
+ };
659
+ })();
660
+
661
+ const STATUS = {
662
+ pending: { label: 'stPending', tone: 'idle' },
663
+ in_progress: { label: 'stInProgress', tone: 'business' },
664
+ pending_acceptance: { label: 'stPendingAcceptance', tone: 'warn' },
665
+ done: { label: 'stDone', tone: 'success' },
666
+ cancelled: { label: 'stCancelled', tone: 'muted' },
667
+ };
668
+ const AGENT_STATUS = {
669
+ running: 'agentRunning',
670
+ idle: 'agentIdle',
671
+ provisioning: 'agentProvisioning',
672
+ failed: 'agentFailed',
673
+ retired: 'agentRetired',
674
+ };
675
+ const QUADRANTS = [
676
+ { key: 'q1', name: 'q1Title', def: 'q1Def', empty: 'q1Empty', tone: 'warn', icon: 'alert' },
677
+ { key: 'q2', name: 'q2Title', def: 'q2Def', empty: 'q2Empty', tone: 'business', icon: 'dot' },
678
+ { key: 'q3', name: 'q3Title', def: 'q3Def', empty: 'q3Empty', tone: 'warn', icon: 'block' },
679
+ { key: 'q4', name: 'q4Title', def: 'q4Def', empty: 'q4Empty', tone: 'success', icon: 'check' },
680
+ ];
681
+ const CATEGORY_LABEL = {
682
+ review: 'catReview',
683
+ irreversible: 'catIrreversible',
684
+ decision: 'catDecision',
685
+ owner_blocked: 'catOwnerBlocked',
686
+ in_progress: 'catInProgress',
687
+ handoff: 'catHandoff',
688
+ blocked: 'catBlocked',
689
+ stalled: 'catStalled',
690
+ done: 'catDone',
691
+ };
692
+ const Q1_ORDER = { irreversible: 0, review: 1, decision: 2, owner_blocked: 3 };
693
+ const TABS = ['main', 'board', 'situation'];
694
+
695
+ const cx = (...parts) => parts.filter(Boolean).join(' ');
696
+ const tf = (t, key, vars) => String(t(key)).replace(/\{(\w+)\}/g, (match, name) => (
697
+ vars && Object.prototype.hasOwnProperty.call(vars, name) ? String(vars[name]) : match
698
+ ));
699
+ const taskNo = (task) => (task && task.number !== undefined && task.number !== null ? task.number : task?.id ?? '');
700
+ const fmtTime = (at) => {
701
+ if (!at) return '';
702
+ const d = new Date(at);
703
+ if (Number.isNaN(d.getTime())) return '';
704
+ try { return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } catch { return ''; }
705
+ };
706
+ const fmtDateTime = (at) => {
707
+ if (!at) return '';
708
+ const d = new Date(at);
709
+ if (Number.isNaN(d.getTime())) return '';
710
+ try { return d.toLocaleString([], { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); } catch { return ''; }
711
+ };
712
+ const iso = (at) => {
713
+ if (!at) return undefined;
714
+ const d = new Date(at);
715
+ return Number.isNaN(d.getTime()) ? undefined : d.toISOString();
716
+ };
717
+ const firstSentence = (text) => {
718
+ const value = String(text ?? '').trim();
719
+ const hit = value.match(/^[^。!?!?\n]+/);
720
+ return (hit ? hit[0] : value).trim();
721
+ };
722
+ const statusMeta = (status) => STATUS[status] ?? STATUS.pending;
723
+ const agentFor = (data, name) => (data?.agents ?? []).find((agent) => agent.name === name) ?? null;
724
+ const threadFor = (data, taskId) => {
725
+ if (!data || !taskId) return null;
726
+ if (data.threads && data.threads[taskId]) return data.threads[taskId];
727
+ if (data.thread && data.thread.taskId === taskId) return data.thread;
728
+ return null;
729
+ };
730
+ const blockedRefs = (task, tasks) => (task?.blockedBy ?? []).map((id) => {
731
+ const dep = (tasks ?? []).find((item) => item.id === id);
732
+ return '#' + taskNo(dep ?? { id });
733
+ });
734
+ const normalizePayload = (payload) => {
735
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return null;
736
+ return {
737
+ ...payload,
738
+ projects: Array.isArray(payload.projects) ? payload.projects : [],
739
+ agents: Array.isArray(payload.agents) ? payload.agents : [],
740
+ stream: Array.isArray(payload.stream) ? payload.stream : [],
741
+ tasks: Array.isArray(payload.tasks) ? payload.tasks : [],
742
+ inbox: Array.isArray(payload.inbox) ? payload.inbox : [],
743
+ };
744
+ };
745
+
746
+ /** Derive the read-only quadrants projection from the bootstrap DTO (spike fallback). */
747
+ function deriveQuadrants(data) {
748
+ const direct = data?.quadrants;
749
+ if (direct && direct.q1 && direct.q2 && direct.q3 && direct.q4) return direct;
750
+ const tasks = data?.tasks ?? [];
751
+ const inbox = data?.inbox ?? [];
752
+ const q1 = [];
753
+ const q2 = [];
754
+ const q3 = [];
755
+ const q4 = [];
756
+ const used = new Set();
757
+ const take = (bucket, item) => {
758
+ if (!item || used.has(item.id)) return;
759
+ used.add(item.id);
760
+ bucket.push(item);
761
+ };
762
+ const itemOf = (task, category) => ({
763
+ id: task.id,
764
+ kind: 'task',
765
+ category,
766
+ title: task.title,
767
+ taskId: task.id,
768
+ taskNumber: task.number,
769
+ ownerName: task.owner,
770
+ assigneeName: task.assignee,
771
+ waitingSince: task.updatedAt,
772
+ doneAt: task.completedAt,
773
+ evidenceCount: task.evidenceCount ?? task.submission?.evidence?.length ?? 0,
774
+ blockedBy: task.blockedBy,
775
+ handoffTo: task.handoffTo,
776
+ acceptedBy: task.acceptedBy,
777
+ });
778
+ tasks.forEach((task) => {
779
+ if (task.status === 'pending_acceptance') take(q1, itemOf(task, task.irreversible ? 'irreversible' : 'review'));
780
+ });
781
+ tasks.forEach((task) => {
782
+ if (task.owner === '你' && task.status === 'pending' && (task.blockedBy?.length || task.handoffTo)) {
783
+ take(q1, itemOf(task, 'owner_blocked'));
784
+ }
785
+ });
786
+ inbox.forEach((item) => {
787
+ if (item.category === 'decision' && !used.has(item.id)) {
788
+ used.add(item.id);
789
+ q1.push({
790
+ id: item.id,
791
+ kind: 'inbox',
792
+ category: 'decision',
793
+ title: item.title,
794
+ taskId: item.taskId,
795
+ taskNumber: item.taskNumber,
796
+ waitingSince: item.waitingSince,
797
+ });
798
+ }
799
+ });
800
+ tasks.forEach((task) => {
801
+ if (task.status === 'in_progress') take(q2, itemOf(task, 'in_progress'));
802
+ });
803
+ tasks.forEach((task) => {
804
+ if (task.status !== 'pending' || used.has(task.id)) return;
805
+ if (task.handoffTo) take(q3, itemOf(task, 'handoff'));
806
+ else if (task.blockedBy?.length) take(q3, itemOf(task, 'blocked'));
807
+ });
808
+ tasks.forEach((task) => {
809
+ if (task.status === 'done') take(q4, itemOf(task, 'done'));
810
+ });
811
+ q1.sort((a, b) => (Q1_ORDER[a.category] ?? 9) - (Q1_ORDER[b.category] ?? 9) || (a.waitingSince ?? 0) - (b.waitingSince ?? 0));
812
+ q2.sort((a, b) => (b.waitingSince ?? 0) - (a.waitingSince ?? 0));
813
+ q3.sort((a, b) => (a.waitingSince ?? 0) - (b.waitingSince ?? 0));
814
+ q4.sort((a, b) => (b.doneAt ?? 0) - (a.doneAt ?? 0));
815
+ return {
816
+ rev: data?.rev ?? 0,
817
+ generatedAt: data?.generatedAt ?? Date.now(),
818
+ q1: { count: q1.length, items: q1.slice(0, 5) },
819
+ q2: { count: q2.length, items: q2.slice(0, 5) },
820
+ q3: { count: q3.length, items: q3.slice(0, 5) },
821
+ q4: { count: Math.min(q4.length, 20), items: q4.slice(0, 5), total: q4.length },
822
+ };
823
+ }
824
+
825
+ /** Only the four board columns; `blocked` stays a badge, never a column. */
826
+ function matchesFilter(task, filter) {
827
+ if (filter === 'q1') return task.status === 'pending_acceptance' || (task.owner === '你' && task.status === 'pending' && (task.blockedBy?.length || task.handoffTo));
828
+ if (filter === 'q2') return task.status === 'in_progress';
829
+ if (filter === 'q3') return task.status === 'pending' && (task.blockedBy?.length || task.handoffTo);
830
+ if (filter === 'q4') return task.status === 'done';
831
+ return true;
832
+ }
833
+
834
+ const CSS = `
835
+ .at-root { height: 100%; min-height: 0; display: flex; flex-direction: column; position: relative; overflow: hidden; color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-base); font-family: var(--dsw-font-family); font-size: 13px; line-height: 21px; container-type: inline-size; container-name: at-wb; }
836
+ .at-root *, .at-root *::before, .at-root *::after { box-sizing: border-box; }
837
+ .at-root :focus-visible { outline: 2px solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); outline-offset: 2px; }
838
+ .at-top { height: 40px; flex: 0 0 auto; display: flex; align-items: center; gap: 8px; min-width: 0; padding: 0 12px; border-bottom: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-1); }
839
+ .at-brand { color: var(--dsw-alias-label-primary); font-size: 12px; font-weight: 500; white-space: nowrap; }
840
+ .at-spike { display: inline-flex; align-items: center; height: 20px; padding: 0 6px; border: 1px solid var(--dsw-alias-border-l3); border-radius: var(--dsw-radius-xs); color: var(--dsw-alias-label-tertiary); font-size: 10px; line-height: 16px; letter-spacing: .04em; text-transform: uppercase; white-space: nowrap; }
841
+ .at-topproject { margin-left: auto; min-width: 0; color: var(--dsw-alias-label-tertiary); font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
842
+ .at-mobile { display: none; position: relative; min-width: 0; }
843
+ .at-mobilesummary { display: inline-flex; align-items: center; gap: 6px; min-height: 28px; padding: 0 8px; border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--dsw-radius-sm); color: var(--dsw-alias-label-secondary); font-size: 11px; cursor: pointer; list-style: none; }
844
+ .at-mobilesummary::-webkit-details-marker { display: none; }
845
+ .at-mobilepop { position: absolute; top: calc(100% + 6px); left: 0; z-index: 30; width: min(320px, calc(100vw - 32px)); width: min(320px, calc(100cqw - 16px)); max-height: 70vh; overflow-y: auto; overflow-x: hidden; padding: 8px; border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--dsw-radius-md); background: var(--dsw-alias-bg-layer-1); box-shadow: var(--dsw-elevation-panel, 0 8px 24px var(--dsw-alias-bg-mask-2)); }
846
+ .at-grid { flex: 1; min-height: 0; min-width: 0; position: relative; display: grid; grid-template-columns: 248px minmax(0, 1fr); }
847
+ .at-grid.at-withthread { grid-template-columns: 248px minmax(0, 1fr) 384px; }
848
+ .at-left { min-width: 0; min-height: 0; overflow-y: auto; overflow-x: hidden; padding: 8px 8px 12px; border-right: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-base); scrollbar-width: thin; scrollbar-color: var(--dsw-alias-scrollbar-bg-l1) transparent; }
849
+ .at-center { min-width: 0; min-height: 0; display: flex; flex-direction: column; background: var(--dsw-alias-bg-base); container-type: inline-size; container-name: at-center; }
850
+ .at-right { min-width: 0; min-height: 0; display: flex; flex-direction: column; border-left: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-1); }
851
+ .at-nav { display: flex; flex-direction: column; gap: 1px; }
852
+ .at-navrow { width: 100%; min-height: 32px; display: flex; align-items: center; gap: 8px; padding: 0 8px; border: 0; border-radius: var(--dsw-radius-sm); background: transparent; color: var(--dsw-alias-label-secondary); font: inherit; font-size: 12px; line-height: 18px; text-align: left; cursor: pointer; }
853
+ .at-navrow:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
854
+ .at-navrow.at-on { background: var(--dsw-alias-button-ghost-active-fill); color: var(--dsw-alias-label-primary); }
855
+ .at-count { margin-left: auto; color: var(--dsw-alias-label-caption); font-size: 11px; font-variant-numeric: tabular-nums; }
856
+ .at-h { display: flex; align-items: center; gap: 6px; min-height: 28px; padding: 12px 6px 4px; color: var(--dsw-alias-label-caption); font-size: 11px; font-weight: 500; letter-spacing: .04em; text-transform: uppercase; }
857
+ .at-h > span:first-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
858
+ .at-mini { margin-left: auto; width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: var(--dsw-radius-sm); background: transparent; color: var(--dsw-alias-label-tertiary); cursor: pointer; }
859
+ .at-mini:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
860
+ .at-tree { display: flex; flex-direction: column; gap: 1px; }
861
+ .at-prow { min-height: 32px; display: flex; align-items: center; gap: 6px; padding: 0 10px; border-radius: var(--dsw-radius-sm); color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; cursor: pointer; }
862
+ .at-prow:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
863
+ .at-prow.at-on { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-interactive-bg-hover); box-shadow: inset 2px 0 0 var(--dsw-alias-state-business-primary); }
864
+ .at-hash { flex: 0 0 auto; color: var(--dsw-alias-label-tertiary); }
865
+ .at-prowtitle { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
866
+ .at-archived { margin-top: 4px; }
867
+ .at-archived > summary { min-height: 28px; display: flex; align-items: center; padding: 0 8px; border-radius: var(--dsw-radius-sm); color: var(--dsw-alias-label-caption); font-size: 11px; cursor: pointer; list-style: none; }
868
+ .at-archived > summary::-webkit-details-marker { display: none; }
869
+ .at-archived > summary:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-secondary); }
870
+ .at-agent { min-height: 44px; display: flex; align-items: center; gap: 8px; padding: 4px 6px; border-radius: var(--dsw-radius-sm); }
871
+ .at-agent:hover { background: var(--dsw-alias-interactive-bg-hover); }
872
+ .at-dot { flex: 0 0 auto; width: 6px; height: 6px; border-radius: 999px; background: var(--dsw-alias-state-idle-primary); }
873
+ .at-dot-running { background: var(--dsw-alias-state-business-primary); }
874
+ .at-dot-provisioning { background: transparent; border: 1.5px solid var(--dsw-alias-state-idle-primary); }
875
+ .at-dot-failed { background: var(--dsw-alias-state-error-primary); }
876
+ .at-dot-retired { background: var(--dsw-alias-label-caption); opacity: .5; }
877
+ .at-agentmeta { min-width: 0; flex: 1; display: flex; flex-direction: column; }
878
+ .at-agentname { align-self: flex-start; max-width: 100%; padding: 0; border: 0; background: transparent; color: var(--dsw-alias-label-primary); font: inherit; font-size: 13px; line-height: 18px; text-align: left; cursor: pointer; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
879
+ .at-agentname:hover { text-decoration: underline; }
880
+ .at-agentrole { min-width: 0; color: var(--dsw-alias-label-caption); font-size: 11px; line-height: 16px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
881
+ .at-inboxitem { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 11px 13px; border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--dsw-radius-sm); background: var(--dsw-alias-bg-layer-2); }
882
+ .at-inboxitem.at-inboxdone { opacity: 0.62; }
883
+ .at-inboxbody { min-width: 0; flex: 1; }
884
+ .at-inboxtitle { color: var(--dsw-alias-label-primary); font-size: 13px; line-height: 20px; }
885
+ .at-inboxmeta { color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 16px; }
886
+ .at-inboxstate { color: var(--dsw-alias-label-caption); font-size: 10.5px; }
887
+ .at-inboxactions { display: flex; align-items: center; gap: 6px; flex: 0 0 auto; }
888
+ .at-qacheck { display: inline-flex; align-items: center; gap: 4px; color: var(--dsw-alias-label-tertiary); font-size: 11px; white-space: nowrap; }
889
+ .at-qacheck input { accent-color: var(--dsw-alias-state-business-primary); }
890
+ .at-qabadge { display: inline-flex; align-items: center; margin-left: 5px; padding: 0 5px; border: 1px solid var(--dsw-alias-state-business-primary); border-radius: 999px; color: var(--dsw-alias-state-business-primary); font-size: 9px; line-height: 15px; vertical-align: 1px; }
891
+ .at-talented { opacity: 0.82; }
892
+ .at-btn-small { min-height: 24px; padding: 0 8px; font-size: 11px; }
893
+ .at-agentconfig { min-width: 0; display: flex; gap: 4px; align-items: center; color: var(--dsw-alias-label-tertiary); font-size: 10.5px; line-height: 15px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
894
+ .at-agentfail { flex: none; color: var(--dsw-alias-state-warn-primary); }
895
+ .at-agentmore { flex: 0 0 auto; width: 28px; height: 28px; border: 0; border-radius: var(--dsw-radius-sm); background: transparent; color: var(--dsw-alias-label-tertiary); font: inherit; font-size: 13px; cursor: pointer; }
896
+ .at-agentmore:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
897
+ .at-sr { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0; }
898
+ .at-chead { flex: 0 0 auto; padding: 14px 18px 0; }
899
+ .at-title { display: flex; align-items: baseline; gap: 8px; min-width: 0; color: var(--dsw-alias-label-primary); font-size: 16px; font-weight: 600; line-height: 22px; letter-spacing: -.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
900
+ .at-goal { min-width: 0; margin-top: 2px; color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
901
+ .at-tabs { display: flex; gap: 4px; margin-top: 8px; border-bottom: 1px solid var(--dsw-alias-border-l1); }
902
+ .at-tab { min-height: 36px; display: inline-flex; align-items: center; gap: 6px; padding: 0 12px; border: 0; border-bottom: 2px solid transparent; background: transparent; color: var(--dsw-alias-label-tertiary); font: inherit; font-size: 13px; font-weight: 500; cursor: pointer; }
903
+ .at-tab:hover { color: var(--dsw-alias-label-primary); }
904
+ .at-tab.at-on { color: var(--dsw-alias-label-primary); border-bottom-color: var(--dsw-alias-state-business-primary); }
905
+ .at-tabcount { color: var(--dsw-alias-label-caption); font-size: 11px; font-weight: 400; font-variant-numeric: tabular-nums; }
906
+ .at-notewrap { flex: 0 0 auto; width: 100%; max-width: 760px; margin: 0 auto; padding: 8px 18px 0; }
907
+ .at-note { display: flex; align-items: flex-start; gap: 8px; padding: 6px 8px; border-left: 2px solid var(--dsw-alias-state-business-primary); border-radius: var(--dsw-radius-sm); background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-secondary); font-size: 11px; line-height: 16px; }
908
+ .at-notetext { min-width: 0; flex: 1; overflow-wrap: anywhere; }
909
+ .at-noteclose { flex: 0 0 auto; width: 24px; height: 24px; display: grid; place-items: center; margin: -4px -4px -4px 0; border: 0; border-radius: var(--dsw-radius-sm); background: transparent; color: var(--dsw-alias-label-tertiary); cursor: pointer; }
910
+ .at-noteclose:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
911
+ .at-banner { flex: 0 0 auto; margin: 8px 18px 0; padding: 6px 8px; border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--dsw-radius-sm); background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-secondary); font-size: 11px; line-height: 16px; }
912
+ .at-banner b { color: var(--dsw-alias-state-warn-label); font-weight: 500; }
913
+ .at-stream { flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; padding: 16px 18px 8px; scrollbar-width: thin; scrollbar-color: var(--dsw-alias-scrollbar-bg-l1) transparent; }
914
+ .at-streaminner { width: 100%; max-width: 760px; margin: 0 auto; display: flex; flex-direction: column; gap: 16px; }
915
+ .at-msg { display: flex; align-items: flex-start; gap: 8px; min-width: 0; }
916
+ .at-avatar { flex: 0 0 auto; width: 24px; height: 24px; display: grid; place-items: center; border: 1px solid var(--dsw-alias-border-l2); border-radius: 999px; background: var(--dsw-alias-bg-layer-3); color: var(--dsw-alias-label-secondary); font-size: 10px; font-weight: 500; }
917
+ .at-avatar-human { border-color: transparent; background: var(--dsw-alias-button-primary-fill); color: var(--dsw-alias-label-primary-inverted); }
918
+ .at-msgb { min-width: 0; flex: 1; }
919
+ .at-msgh { display: flex; align-items: baseline; flex-wrap: wrap; gap: 6px; color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; }
920
+ .at-msgh b { color: var(--dsw-alias-label-primary); font-weight: 500; }
921
+ .at-role { color: var(--dsw-alias-label-caption); font-size: 11px; }
922
+ .at-msgh time { color: var(--dsw-alias-label-caption); font-size: 11px; font-variant-numeric: tabular-nums; }
923
+ .at-msgt { max-width: 66ch; margin-top: 2px; color: var(--dsw-alias-label-secondary); font-size: 13px; line-height: 21px; white-space: pre-wrap; overflow-wrap: anywhere; }
924
+ .at-msg-human .at-msgt { color: var(--dsw-alias-label-primary); }
925
+ .at-group { min-width: 0; margin-top: 8px; overflow: hidden; border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--dsw-radius-md); background: var(--dsw-alias-bg-layer-2); }
926
+ .at-grouphead { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; padding: 8px 12px; border-bottom: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 16px; }
927
+ .at-groupicon { display: inline-flex; color: var(--dsw-alias-label-tertiary); }
928
+ .at-grouptitle { color: var(--dsw-alias-label-secondary); font-size: 12px; font-weight: 500; }
929
+ .at-groupsummary { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
930
+ .at-card { min-width: 0; display: flex; flex-direction: column; gap: 6px; cursor: pointer; }
931
+ .at-card-rich { min-height: 96px; padding: 11px 13px; }
932
+ .at-card-board { width: 100%; padding: 11px 13px; border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--dsw-radius-sm); background: var(--dsw-alias-bg-layer-1); }
933
+ .at-card-compact { min-height: 40px; padding: 6px 10px; }
934
+ .at-card:hover { background: var(--dsw-alias-interactive-bg-hover); }
935
+ .at-card.at-sel { background: var(--dsw-alias-interactive-bg-hover); box-shadow: inset 0 0 0 1px var(--dsw-alias-border-l4); }
936
+ .at-card:focus-visible { outline-offset: -2px; }
937
+ .at-group .at-card + .at-card { border-top: 1px solid var(--dsw-alias-border-l1); }
938
+ .at-cardtop { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; min-width: 0; }
939
+ .at-origin { display: inline-flex; color: var(--dsw-alias-label-caption); }
940
+ .at-num { color: var(--dsw-alias-label-secondary); font-size: 12px; font-weight: 500; line-height: 18px; white-space: nowrap; font-variant-numeric: tabular-nums; }
941
+ .at-pill { display: inline-flex; align-items: center; gap: 4px; height: 20px; padding: 0 6px; border-radius: var(--dsw-radius-xs); background: var(--dsw-alias-bg-module-platform); color: var(--dsw-alias-label-tertiary); font-size: 11px; font-weight: 500; line-height: 16px; white-space: nowrap; }
942
+ .at-pill-md { height: 24px; font-size: 12px; }
943
+ .at-pill.at-tone-business { background: var(--dsw-alias-state-business-tertiary); color: var(--dsw-alias-state-business-primary); }
944
+ .at-pill.at-tone-warn { padding-left: 4px; border-left: 2px solid var(--dsw-alias-state-warn-primary); background: var(--dsw-alias-state-warn-tertiary); color: var(--dsw-alias-state-warn-label); }
945
+ .at-pill.at-tone-success { background: var(--dsw-alias-state-success-tertiary); color: var(--dsw-alias-state-success-primary); }
946
+ .at-pill.at-tone-muted { border: 1px solid var(--dsw-alias-border-l3); background: transparent; color: var(--dsw-alias-label-caption); }
947
+ .at-blocked { display: inline-flex; align-items: center; gap: 4px; height: 20px; padding: 0 6px; border-radius: var(--dsw-radius-xs); background: var(--dsw-alias-state-warn-tertiary); color: var(--dsw-alias-state-warn-label); font-size: 11px; font-weight: 500; line-height: 16px; white-space: nowrap; }
948
+ .at-dep { color: var(--dsw-alias-state-warn-label); font-weight: 400; }
949
+ .at-replies { margin-left: auto; color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 16px; white-space: nowrap; font-variant-numeric: tabular-nums; }
950
+ .at-ct { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; color: var(--dsw-alias-label-primary); font-size: 13px; font-weight: 500; line-height: 21px; overflow-wrap: anywhere; }
951
+ .at-card-board .at-ct { font-size: 13px; }
952
+ .at-cs { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; overflow-wrap: anywhere; }
953
+ .at-cm { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; min-width: 0; color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 16px; }
954
+ .at-owner, .at-exec { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
955
+ .at-sep { color: var(--dsw-alias-label-caption); }
956
+ .at-miniavatar { width: 16px; height: 16px; display: inline-grid; place-items: center; margin-right: 4px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 999px; background: var(--dsw-alias-bg-layer-3); color: var(--dsw-alias-label-secondary); font-size: 10px; vertical-align: -3px; }
957
+ .at-follow { flex: 0 0 auto; min-height: 24px; display: inline-flex; align-items: center; gap: 4px; margin-left: auto; padding: 0 6px; border: 1px solid transparent; border-radius: var(--dsw-radius-sm); background: transparent; color: var(--dsw-alias-label-tertiary); font: inherit; font-size: 11px; line-height: 16px; cursor: pointer; }
958
+ .at-follow:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
959
+ .at-follow.at-follow-on { color: var(--dsw-alias-state-business-primary); }
960
+ .at-follow.at-follow-compact { padding: 0; width: 24px; justify-content: center; }
961
+ .at-groupmore { width: 100%; display: flex; flex-direction: column; gap: 2px; padding: 6px 12px; border: 0; border-top: 1px solid var(--dsw-alias-border-l1); background: transparent; color: var(--dsw-alias-label-tertiary); font: inherit; font-size: 11px; line-height: 16px; text-align: left; cursor: pointer; }
962
+ .at-groupmore:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
963
+ .at-morelabel { color: var(--dsw-alias-label-secondary); }
964
+ .at-morelist { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-caption); }
965
+ .at-composer { flex: 0 0 auto; padding: 8px 18px 14px; }
966
+ .at-composerinner { width: 100%; max-width: 760px; margin: 0 auto; }
967
+ .at-box { min-width: 0; padding: 8px 9px; border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--dsw-radius-md); background: var(--dsw-alias-bg-layer-2); }
968
+ .at-box:focus-within { border-color: var(--dsw-alias-state-business-primary); box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-state-business-primary) 20%, transparent); }
969
+ .at-box.at-taskmode { border-color: color-mix(in srgb, var(--dsw-alias-state-business-primary) 20%, var(--dsw-alias-border-l2)); }
970
+ .at-box textarea { display: block; width: 100%; min-height: 38px; max-height: 160px; resize: none; padding: 2px; border: 0; background: transparent; color: var(--dsw-alias-label-primary); font: inherit; font-size: 13px; line-height: 21px; }
971
+ .at-barrow { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 6px; }
972
+ .at-mode { display: inline-flex; overflow: hidden; border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--dsw-radius-sm); }
973
+ .at-mode button { min-height: 24px; padding: 0 8px; border: 0; background: transparent; color: var(--dsw-alias-label-tertiary); font: inherit; font-size: 11px; line-height: 16px; cursor: pointer; }
974
+ .at-mode button[aria-pressed="true"] { background: var(--dsw-alias-button-ghost-active-fill); color: var(--dsw-alias-label-primary); }
975
+ .at-hint { min-width: 0; color: var(--dsw-alias-label-caption); font-size: 10px; line-height: 16px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
976
+ .at-send { margin-left: auto; min-height: 28px; padding: 0 12px; border: 1px solid transparent; border-radius: var(--dsw-radius-sm); background: var(--dsw-alias-button-primary-fill); color: var(--dsw-alias-label-primary-inverted); font: inherit; font-size: 12px; line-height: 18px; cursor: pointer; white-space: nowrap; }
977
+ .at-send:hover { background: var(--dsw-alias-button-primary-hover); }
978
+ .at-send:disabled { border-color: var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-module-platform); color: var(--dsw-alias-label-caption); cursor: not-allowed; }
979
+ .at-boardwrap { flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; padding: 12px 18px 16px; scrollbar-width: thin; scrollbar-color: var(--dsw-alias-scrollbar-bg-l1) transparent; }
980
+ .at-boardbar { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin: 0 0 8px; padding: 6px 8px; border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--dsw-radius-sm); background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 16px; }
981
+ .at-filterchip { color: var(--dsw-alias-label-primary); font-weight: 500; }
982
+ .at-filternote { color: var(--dsw-alias-label-caption); }
983
+ .at-filterclear { margin-left: auto; }
984
+ .at-board { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; align-items: start; min-width: 0; }
985
+ .at-col { min-width: 0; display: flex; flex-direction: column; gap: 8px; padding: 8px; border-radius: var(--dsw-radius-md); background: var(--dsw-alias-bg-module-platform); }
986
+ .at-colhead { min-height: 40px; display: flex; align-items: center; gap: 8px; padding: 0 2px; }
987
+ .at-coltitle { color: var(--dsw-alias-label-primary); font-size: 13px; font-weight: 500; line-height: 18px; white-space: nowrap; }
988
+ .at-colcount { color: var(--dsw-alias-label-caption); font-size: 11px; line-height: 16px; font-variant-numeric: tabular-nums; }
989
+ .at-col-pending_acceptance .at-colhead { padding-bottom: 8px; border-bottom: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 20%, transparent); }
990
+ .at-coltoggle { margin-left: auto; min-height: 24px; padding: 0 4px; border: 0; background: transparent; color: var(--dsw-alias-label-tertiary); font: inherit; font-size: 11px; cursor: pointer; white-space: nowrap; }
991
+ .at-coltoggle:hover { color: var(--dsw-alias-label-primary); }
992
+ .at-colempty { padding: 4px; color: var(--dsw-alias-label-caption); font-size: 11px; line-height: 16px; }
993
+ .at-situation { flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; padding: 12px 18px 16px; scrollbar-width: thin; scrollbar-color: var(--dsw-alias-scrollbar-bg-l1) transparent; }
994
+ .at-situationinner { min-width: 0; }
995
+ .at-qmeta { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 8px; color: var(--dsw-alias-label-caption); font-size: 10px; line-height: 16px; }
996
+ .at-qmeta > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
997
+ .at-quadrants { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; align-items: stretch; min-width: 0; }
998
+ .at-quad { --at-qaccent: var(--dsw-alias-state-business-primary); min-width: 0; display: flex; flex-direction: column; gap: 6px; padding: 12px; border: 1px solid var(--dsw-alias-border-l1); border-left: 2px solid var(--at-qaccent); border-radius: var(--dsw-radius-md); background: var(--dsw-alias-bg-layer-2); }
999
+ .at-quad-q1 { --at-qaccent: var(--dsw-alias-state-warn-primary); }
1000
+ .at-quad-q2 { --at-qaccent: var(--dsw-alias-state-business-primary); }
1001
+ .at-quad-q3 { --at-qaccent: var(--dsw-alias-state-warn-primary); }
1002
+ .at-quad-q4 { --at-qaccent: var(--dsw-alias-state-success-primary); }
1003
+ .at-quadhead { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
1004
+ .at-qname { color: var(--dsw-alias-label-primary); font-size: 13px; font-weight: 500; line-height: 18px; white-space: nowrap; }
1005
+ .at-qcount { margin-left: auto; color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; font-variant-numeric: tabular-nums; }
1006
+ .at-qdef { color: var(--dsw-alias-label-caption); font-size: 11px; line-height: 16px; }
1007
+ .at-qtotal { color: var(--dsw-alias-label-caption); font-size: 10px; line-height: 16px; }
1008
+ .at-qlist { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
1009
+ .at-qitem { width: 100%; min-width: 0; display: flex; align-items: flex-start; gap: 6px; margin: 0 -4px; padding: 5px 4px; border: 0; border-radius: var(--dsw-radius-sm); background: transparent; color: var(--dsw-alias-label-secondary); font: inherit; text-align: left; cursor: pointer; }
1010
+ .at-qitem:hover { background: var(--dsw-alias-interactive-bg-hover); }
1011
+ .at-qitem:focus-visible { outline-offset: -2px; }
1012
+ .at-qitemicon { flex: 0 0 auto; display: inline-flex; margin-top: 2px; color: var(--at-qaccent); }
1013
+ .at-qitembody { min-width: 0; flex: 1; display: flex; flex-direction: column; }
1014
+ .at-qtitle { min-width: 0; color: var(--dsw-alias-label-primary); font-size: 12px; line-height: 18px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1015
+ .at-qitemmeta { min-width: 0; color: var(--dsw-alias-label-tertiary); font-size: 10px; line-height: 16px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1016
+ .at-qempty { padding: 4px; color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; }
1017
+ .at-qfoot { margin-top: auto; padding-top: 4px; }
1018
+ .at-qmore { min-height: 24px; padding: 0; border: 0; background: transparent; color: var(--dsw-alias-link); font: inherit; font-size: 11px; line-height: 16px; cursor: pointer; }
1019
+ .at-qmore:hover { text-decoration: underline; }
1020
+ .at-qmore:disabled { color: var(--dsw-alias-label-caption); cursor: default; text-decoration: none; }
1021
+ .at-empty { display: flex; flex-direction: column; gap: 6px; justify-content: center; max-width: 520px; margin: 0 auto; padding: 24px; text-align: center; }
1022
+ .at-empty-compact { align-items: flex-start; margin: 0; padding: 12px; text-align: left; }
1023
+ .at-emptyicon { display: inline-flex; align-self: center; color: var(--dsw-alias-label-caption); }
1024
+ .at-empty-compact .at-emptyicon { align-self: flex-start; }
1025
+ .at-emptytitle { color: var(--dsw-alias-label-primary); font-size: 14px; font-weight: 500; line-height: 22px; }
1026
+ .at-emptybody { max-width: 46ch; color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; }
1027
+ .at-empty .at-btn { align-self: center; margin-top: 8px; }
1028
+ .at-empty-compact .at-btn { align-self: flex-start; }
1029
+ .at-btn { min-height: 32px; display: inline-flex; align-items: center; justify-content: center; padding: 0 14px; border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--dsw-radius-sm); background: var(--dsw-alias-button-elevated-fill); color: var(--dsw-alias-label-primary); font: inherit; font-size: 12px; line-height: 18px; cursor: pointer; white-space: nowrap; }
1030
+ .at-btn:hover { background: var(--dsw-alias-interactive-bg-hover); }
1031
+ .at-btn-primary { border-color: transparent; background: var(--dsw-alias-button-primary-fill); color: var(--dsw-alias-label-primary-inverted); }
1032
+ .at-btn-primary:hover { background: var(--dsw-alias-button-primary-hover); }
1033
+ .at-btn:disabled { border-color: var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-module-platform); color: var(--dsw-alias-label-caption); cursor: not-allowed; }
1034
+ .at-btn-quiet { min-height: 28px; padding: 0 8px; border-color: transparent; background: transparent; color: var(--dsw-alias-label-secondary); font-size: 11px; }
1035
+ .at-btn-quiet:hover { background: var(--dsw-alias-interactive-bg-hover); }
1036
+ .at-rhead { flex: 0 0 auto; min-height: 44px; display: flex; align-items: center; gap: 6px; padding: 8px 10px; border-bottom: 1px solid var(--dsw-alias-border-l1); }
1037
+ .at-rh { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 1px; }
1038
+ .at-rhmeta { color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 16px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1039
+ .at-rhtitle { color: var(--dsw-alias-label-primary); font-size: 12px; font-weight: 500; line-height: 18px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1040
+ .at-rhead .at-follow { margin-left: 0; }
1041
+ .at-rbtn { min-height: 28px; display: inline-flex; align-items: center; justify-content: center; padding: 0 8px; border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--dsw-radius-sm); background: transparent; color: var(--dsw-alias-label-secondary); font: inherit; font-size: 11px; line-height: 16px; white-space: nowrap; cursor: pointer; }
1042
+ .at-rbtn:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
1043
+ .at-iconbtn { width: 28px; padding: 0; }
1044
+ .at-rnote { flex: 0 0 auto; display: flex; align-items: flex-start; gap: 8px; margin: 8px 10px 0; padding: 6px 8px; border-left: 2px solid var(--dsw-alias-state-business-primary); border-radius: var(--dsw-radius-sm); background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-secondary); font-size: 11px; line-height: 16px; }
1045
+ .at-rbody { flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; padding: 0 14px 12px; scrollbar-width: thin; scrollbar-color: var(--dsw-alias-scrollbar-bg-l1) transparent; }
1046
+ .at-tmsgs { display: flex; flex-direction: column; }
1047
+ .at-tmsg { padding: 10px 0; border-bottom: 1px solid var(--dsw-alias-border-l1); }
1048
+ .at-tmsg:last-child { border-bottom: 0; }
1049
+ .at-tmh { display: flex; align-items: baseline; flex-wrap: wrap; gap: 6px; color: var(--dsw-alias-label-caption); font-size: 11px; line-height: 16px; }
1050
+ .at-tmh b { color: var(--dsw-alias-label-secondary); font-weight: 500; }
1051
+ .at-tseq { color: var(--dsw-alias-label-caption); font-variant-numeric: tabular-nums; }
1052
+ .at-tmt { max-width: 66ch; margin-top: 2px; color: var(--dsw-alias-label-secondary); font-size: 13px; line-height: 21px; white-space: pre-wrap; overflow-wrap: anywhere; }
1053
+ .at-rfoot { flex: 0 0 auto; padding: 8px 10px 10px; border-top: 1px solid var(--dsw-alias-border-l1); }
1054
+ .at-rbox textarea { min-height: 48px; }
1055
+ .at-conflict { margin: 8px 0; padding: 10px 12px; border-left: 2px solid var(--dsw-alias-state-warn-primary); border-radius: var(--dsw-radius-sm); background: var(--dsw-alias-bg-layer-2); }
1056
+ .at-conflicttitle { color: var(--dsw-alias-state-warn-label); font-size: 12px; font-weight: 500; line-height: 18px; }
1057
+ .at-conflictnew { margin-top: 2px; color: var(--dsw-alias-label-secondary); font-size: 11px; line-height: 16px; }
1058
+ .at-conflictlist { margin: 4px 0 0; padding: 0; list-style: none; color: var(--dsw-alias-label-secondary); font-size: 11px; line-height: 18px; }
1059
+ .at-conflictactions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
1060
+ .at-accept { margin: 12px 0 0; padding: 14px; border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--dsw-radius-md); background: var(--dsw-alias-bg-layer-2); display: flex; flex-direction: column; gap: 12px; }
1061
+ .at-accepthead { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
1062
+ .at-accepttitle { color: var(--dsw-alias-label-primary); font-size: 14px; font-weight: 500; line-height: 22px; }
1063
+ .at-candidate { display: inline-flex; align-items: center; height: 20px; padding: 0 6px; border-radius: var(--dsw-radius-xs); background: var(--dsw-alias-state-warn-tertiary); color: var(--dsw-alias-state-warn-label); font-size: 10px; line-height: 16px; white-space: nowrap; }
1064
+ .at-kv { display: flex; flex-wrap: wrap; gap: 4px 8px; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; }
1065
+ .at-kvlabel { flex: 0 0 64px; color: var(--dsw-alias-label-caption); font-size: 11px; line-height: 18px; }
1066
+ .at-kvbody { min-width: 0; flex: 1 1 180px; }
1067
+ .at-evidence { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 2px; }
1068
+ .at-evidence li { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; min-width: 0; font-size: 12px; line-height: 18px; }
1069
+ .at-evidence code, .at-context code { padding: 1px 4px; border-radius: var(--dsw-radius-xs); background: var(--dsw-alias-markdown-code-block); color: var(--dsw-alias-label-secondary); font-family: var(--dsw-font-family); font-size: 11px; }
1070
+ .at-elig, .at-steward { margin: 0; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; }
1071
+ .at-steward { color: var(--dsw-alias-label-tertiary); font-size: 11px; }
1072
+ .at-irrev { display: flex; flex-direction: column; gap: 6px; padding: 8px; border-radius: var(--dsw-radius-sm); background: var(--dsw-alias-state-warn-tertiary); color: var(--dsw-alias-state-warn-label); font-size: 11px; line-height: 16px; }
1073
+ .at-irrev p { margin: 0; }
1074
+ .at-check { display: flex; align-items: flex-start; gap: 8px; color: var(--dsw-alias-label-primary); font-size: 12px; line-height: 18px; cursor: pointer; }
1075
+ .at-check input { flex: 0 0 auto; width: 16px; height: 16px; margin: 1px 0 0; accent-color: var(--dsw-alias-state-warn-primary); }
1076
+ .at-acceptactions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
1077
+ .at-actions { margin: 12px 0; padding-top: 12px; border-top: 1px solid var(--dsw-alias-border-l1); }
1078
+ .at-actionhead { display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px; }
1079
+ .at-actiontitle { color: var(--dsw-alias-label-secondary); font-size: 12px; font-weight: 500; line-height: 18px; }
1080
+ .at-mocktag { color: var(--dsw-alias-label-caption); font-size: 10px; line-height: 16px; }
1081
+ .at-actionrow { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
1082
+ .at-done { margin: 12px 0 0; padding: 12px; border-left: 2px solid var(--dsw-alias-state-success-primary); border-radius: var(--dsw-radius-sm); background: var(--dsw-alias-bg-layer-2); display: flex; flex-direction: column; gap: 4px; }
1083
+ .at-donehead { display: flex; align-items: center; gap: 6px; color: var(--dsw-alias-state-success-primary); font-size: 12px; font-weight: 500; line-height: 18px; }
1084
+ .at-donemeta { display: flex; flex-wrap: wrap; gap: 4px 10px; color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 16px; }
1085
+ .at-donetext { margin: 2px 0 0; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; }
1086
+ .at-convert { flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; padding: 14px; display: flex; flex-direction: column; gap: 12px; }
1087
+ .at-converthead { color: var(--dsw-alias-label-primary); font-size: 14px; font-weight: 500; line-height: 22px; }
1088
+ .at-field { display: flex; flex-direction: column; gap: 4px; min-width: 0; color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 16px; }
1089
+ .at-field input, .at-field textarea, .at-field select { width: 100%; padding: 6px 8px; border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--dsw-radius-sm); background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary); font: inherit; font-size: 12px; line-height: 18px; }
1090
+ .at-field textarea { min-height: 64px; resize: vertical; }
1091
+ .at-fieldrow { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
1092
+ .at-modalback { position: fixed; inset: 0; z-index: 80; display: flex; align-items: center; justify-content: center; padding: 20px; background: color-mix(in srgb, var(--dsw-alias-bg-base) 72%, transparent); }
1093
+ .at-modal { width: min(420px, 100%); max-height: 86vh; overflow-y: auto; display: flex; flex-direction: column; gap: 10px; padding: 16px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 12px; background: var(--dsw-alias-bg-layer-2); }
1094
+ .at-modaltitle { margin: 0 0 2px; font-size: 15px; font-weight: 600; color: var(--dsw-alias-label-primary); }
1095
+ .at-modalmeta { color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 16px; }
1096
+ .at-check { display: flex; align-items: center; gap: 6px; color: var(--dsw-alias-label-secondary); font-size: 12px; }
1097
+ .at-check input { accent-color: var(--dsw-alias-state-business-primary); }
1098
+ .at-modalerror { color: var(--dsw-alias-state-error-primary); font-size: 11.5px; line-height: 16px; }
1099
+ .at-inlineadd { display: flex; align-items: center; gap: 6px; }
1100
+ .at-inlineadd input { flex: 1; min-width: 0; padding: 6px 8px; border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--dsw-radius-sm); background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary); font: inherit; font-size: 12px; }
1101
+ .at-modalactions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; margin-top: 2px; }
1102
+ .at-fieldrow .at-staticfield { min-width: 0; padding: 6px 8px; border: 1px solid var(--dsw-alias-border-l1); border-radius: var(--dsw-radius-sm); background: var(--dsw-alias-bg-module-platform); color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1103
+ .at-context { color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 16px; }
1104
+ .at-convertfoot { display: flex; justify-content: flex-end; gap: 8px; }
1105
+ .at-skel { background: var(--dsw-alias-bg-skeleton); border-radius: var(--dsw-radius-sm); }
1106
+ .at-skel-line { height: 12px; margin-bottom: 8px; }
1107
+ .at-skel-title { height: 20px; width: 40%; margin: 4px 0 10px; }
1108
+ .at-skel-msg { height: 56px; margin-bottom: 12px; }
1109
+ .at-skel-card { height: 88px; margin-top: 12px; }
1110
+ .at-skel-agent { height: 40px; margin-bottom: 6px; }
1111
+ .at-skel-col { height: 120px; }
1112
+ .at-crash { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; padding: 24px; color: var(--dsw-alias-state-error-primary); text-align: center; }
1113
+ .at-crashtitle { font-size: 14px; font-weight: 500; line-height: 22px; }
1114
+ .at-crashbody { max-width: 60ch; margin: 0; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; white-space: pre-wrap; overflow-wrap: anywhere; }
1115
+ @container at-center (max-width: 759px) {
1116
+ .at-board { grid-template-columns: repeat(2, minmax(0, 1fr)); }
1117
+ }
1118
+ @container at-center (max-width: 519px) {
1119
+ .at-board { grid-template-columns: minmax(0, 1fr); }
1120
+ }
1121
+ @container at-wb (max-width: 1119px) {
1122
+ .at-grid, .at-grid.at-withthread { grid-template-columns: 200px minmax(0, 1fr); }
1123
+ .at-right { position: absolute; top: 0; right: 0; bottom: 0; z-index: 10; width: min(420px, 100%); border-left: 1px solid var(--dsw-alias-border-l1); box-shadow: var(--dsw-elevation-panel, 0 8px 24px var(--dsw-alias-bg-mask-2)); }
1124
+ .at-boardwrap { padding: 12px; }
1125
+ .at-board { gap: 8px; }
1126
+ }
1127
+ @container at-wb (max-width: 959px) {
1128
+ .at-grid, .at-grid.at-withthread { grid-template-columns: 200px minmax(0, 1fr); }
1129
+ .at-right { top: 0; right: 0; bottom: 0; width: 100%; max-width: none; border-left: 0; }
1130
+ .at-board { grid-template-columns: minmax(0, 1fr); }
1131
+ .at-quadrants { grid-template-columns: minmax(0, 1fr); }
1132
+ .at-chead { padding: 10px 12px 0; }
1133
+ .at-stream { padding: 12px 12px 8px; }
1134
+ .at-notewrap { padding: 8px 12px 0; }
1135
+ .at-banner { margin: 8px 12px 0; }
1136
+ .at-composer { padding: 8px 12px 12px; }
1137
+ .at-situation { padding: 12px 12px 16px; }
1138
+ }
1139
+ @container at-wb (max-width: 719px) {
1140
+ .at-grid, .at-grid.at-withthread { grid-template-columns: minmax(0, 1fr); }
1141
+ .at-left { display: none; }
1142
+ .at-mobile { display: block; }
1143
+ .at-top { padding: 0 8px; gap: 6px; }
1144
+ .at-topproject { display: none; }
1145
+ .at-agentmore, .at-agentname { min-height: 28px; }
1146
+ .at-quadrants { grid-template-columns: minmax(0, 1fr); }
1147
+ }
1148
+ @container at-wb (max-width: 420px) {
1149
+ .at-rh { flex-basis: 100%; }
1150
+ .at-rhead { flex-wrap: wrap; }
1151
+ .at-hint { display: none; }
1152
+ .at-send { margin-left: auto; }
1153
+ }
1154
+ @media (prefers-reduced-motion: reduce) {
1155
+ .at-root *, .at-root *::before, .at-root *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; }
1156
+ }
1157
+ `;
1158
+
1159
+ function Ico({ name, size = 14 }) {
1160
+ const common = {
1161
+ width: size,
1162
+ height: size,
1163
+ viewBox: '0 0 16 16',
1164
+ fill: 'none',
1165
+ stroke: 'currentColor',
1166
+ strokeWidth: 1.5,
1167
+ strokeLinecap: 'round',
1168
+ strokeLinejoin: 'round',
1169
+ 'aria-hidden': 'true',
1170
+ focusable: 'false',
1171
+ };
1172
+ if (name === 'back') return h('svg', common, h('path', { d: 'M10 3 5 8l5 5' }));
1173
+ if (name === 'inbox') return h('svg', common, h('path', { d: 'M2.5 9.5h3l1 1.5h3l1-1.5h3' }), h('path', { d: 'M2.5 9.5V4.5a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v5' }));
1174
+ if (name === 'board') return h('svg', common, h('rect', { x: 2.5, y: 3.5, width: 11, height: 9, rx: 1.5 }), h('path', { d: 'M6 3.5v9M10 3.5v9' }));
1175
+ if (name === 'plus') return h('svg', common, h('path', { d: 'M8 3v10M3 8h10' }));
1176
+ if (name === 'menu') return h('svg', common, h('path', { d: 'M3 4.5h10M3 8h10M3 11.5h10' }));
1177
+ if (name === 'bell') return h('svg', common, h('path', { d: 'M4.5 7a3.5 3.5 0 0 1 7 0v3l1 1.5h-9L4.5 10V7Z' }), h('path', { d: 'M6.5 13a1.5 1.5 0 0 0 3 0' }));
1178
+ if (name === 'bellOff') return h('svg', common, h('path', { d: 'M4.5 7a3.5 3.5 0 0 1 5.5-3' }), h('path', { d: 'M6.5 13a1.5 1.5 0 0 0 3 0' }), h('path', { d: 'M4.5 10l-1 1.5h9L11.5 10V8.5' }), h('path', { d: 'M3 3l10 10' }));
1179
+ if (name === 'convert') return h('svg', common, h('path', { d: 'M4 12 12 4M6.5 4H12v5.5' }));
1180
+ if (name === 'close') return h('svg', common, h('path', { d: 'M4 4l8 8M12 4l-8 8' }));
1181
+ if (name === 'reply') return h('svg', common, h('path', { d: 'M6 4 2.5 7.5 6 11' }), h('path', { d: 'M2.5 7.5h6a4 4 0 0 1 4 4v.5' }));
1182
+ if (name === 'block') return h('svg', common, h('circle', { cx: 8, cy: 8, r: 5.5 }), h('path', { d: 'M4.5 11.5l7-7' }));
1183
+ if (name === 'check') return h('svg', common, h('path', { d: 'M3 8.5l3 3 7-7' }));
1184
+ if (name === 'alert') return h('svg', common, h('path', { d: 'M8 2.5 14 13H2L8 2.5Z' }), h('path', { d: 'M8 6.5v3M8 11.5v.5' }));
1185
+ if (name === 'dot') return h('svg', common, h('circle', { cx: 8, cy: 8, r: 3.5, fill: 'currentColor', stroke: 'none' }));
1186
+ if (name === 'layers') return h('svg', common, h('path', { d: 'M8 2.5 14 6 8 9.5 2 6Z' }), h('path', { d: 'M2 9.5 8 13l6-3.5' }));
1187
+ if (name === 'chevron') return h('svg', common, h('path', { d: 'M6 3.5 10.5 8 6 12.5' }));
1188
+ if (name === 'loop') return h('svg', { width: 18, height: 18, viewBox: '0 0 32 32', fill: 'none', 'aria-hidden': 'true', focusable: 'false' }, h('rect', { x: 2.5, y: 5.5, width: 21, height: 21, rx: 6.5, stroke: 'currentColor', strokeWidth: 2.6 }), h('circle', { cx: 23.5, cy: 16, r: 4.8, fill: 'var(--dsw-alias-state-business-primary)' }));
1189
+ return null;
1190
+ }
1191
+
1192
+ function StatusGlyph({ status }) {
1193
+ const common = { width: 12, height: 12, viewBox: '0 0 12 12', fill: 'none', stroke: 'currentColor', strokeWidth: 1.4, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': 'true', focusable: 'false' };
1194
+ if (status === 'done') return h('svg', common, h('path', { d: 'M2.5 6.2l2.3 2.3 4.7-4.7' }));
1195
+ if (status === 'cancelled') return h('svg', common, h('circle', { cx: 6, cy: 6, r: 4.2 }), h('path', { d: 'M3 9 9 3' }));
1196
+ if (status === 'pending') return h('svg', common, h('circle', { cx: 6, cy: 6, r: 4.2 }));
1197
+ if (status === 'pending_acceptance') return h('svg', common, h('circle', { cx: 6, cy: 6, r: 4.2, fill: 'currentColor', stroke: 'none' }));
1198
+ return h('svg', common, h('circle', { cx: 6, cy: 6, r: 4.2, fill: 'currentColor', stroke: 'none' }));
1199
+ }
1200
+
1201
+ function StatusPill({ t, status, size }) {
1202
+ const meta = statusMeta(status);
1203
+ const label = t(meta.label);
1204
+ return h('span', { className: cx('at-pill', 'at-tone-' + meta.tone, size === 'md' && 'at-pill-md'), 'aria-label': label, title: label },
1205
+ h(StatusGlyph, { status }),
1206
+ h('span', null, label));
1207
+ }
1208
+
1209
+ function Avatar({ name, human }) {
1210
+ return h('span', { className: cx('at-avatar', human && 'at-avatar-human'), 'aria-hidden': 'true' }, String(name ?? '?').slice(0, 1));
1211
+ }
1212
+
1213
+ function FollowToggle({ t, taskNo: no, following, onToggle, compact }) {
1214
+ const label = following ? t('unfollow') : t('follow');
1215
+ return h('button', {
1216
+ className: cx('at-follow', following && 'at-follow-on', compact && 'at-follow-compact'),
1217
+ type: 'button',
1218
+ 'aria-pressed': Boolean(following),
1219
+ 'aria-label': label + ' #' + no,
1220
+ title: label,
1221
+ onClick: (event) => { event.stopPropagation(); onToggle(); },
1222
+ onKeyDown: (event) => { if (event.key === 'Enter' || event.key === ' ') event.stopPropagation(); },
1223
+ }, h(Ico, { name: following ? 'bell' : 'bellOff', size: 12 }), compact ? null : h('span', null, label));
1224
+ }
1225
+
1226
+ function TaskCard({ t, card, tasks, variant, selected, following, onOpen, onToggleFollow }) {
1227
+ const meta = statusMeta(card.status);
1228
+ const no = taskNo(card);
1229
+ const refs = blockedRefs(card, tasks);
1230
+ const fromDiscussion = Array.isArray(card.originRefs) && card.originRefs.length > 0;
1231
+ const ownerText = t('owner') + ' ' + (card.owner ?? t('unassigned'));
1232
+ const execText = card.assignee ? t('executor') + ' ' + card.assignee : t('unassigned');
1233
+ const replyLabel = tf(t, 'replyCount', { n: card.replies ?? 0 });
1234
+ const time = fmtTime(card.updatedAt);
1235
+ const aria = [
1236
+ 'Task #' + no,
1237
+ card.title,
1238
+ t(meta.label),
1239
+ refs.length ? t('blocked') + ' ' + t('dependsOn') + ' ' + refs.join('、') : '',
1240
+ ownerText,
1241
+ execText,
1242
+ replyLabel,
1243
+ ].filter(Boolean).join(',');
1244
+ const open = (event) => onOpen(card, event);
1245
+ const onKeyDown = (event) => {
1246
+ if (event.key === 'Enter' || event.key === ' ') {
1247
+ event.preventDefault();
1248
+ open(event);
1249
+ return;
1250
+ }
1251
+ if ((event.key === 'f' || event.key === 'F') && !event.ctrlKey && !event.metaKey && !event.altKey) {
1252
+ event.preventDefault();
1253
+ onToggleFollow(card);
1254
+ }
1255
+ };
1256
+ return h('div', {
1257
+ className: cx('at-card', 'at-card-' + variant, selected && 'at-sel'),
1258
+ role: 'button',
1259
+ tabIndex: 0,
1260
+ 'aria-label': aria,
1261
+ onClick: open,
1262
+ onKeyDown,
1263
+ },
1264
+ h('div', { className: 'at-cardtop' },
1265
+ fromDiscussion ? h('span', { className: 'at-origin', title: t('fromDiscussion'), 'aria-hidden': 'true' }, h(Ico, { name: 'reply', size: 12 })) : null,
1266
+ h('span', { className: 'at-num' }, 'Task #' + no),
1267
+ h(StatusPill, { t, status: card.status }),
1268
+ refs.length ? h('span', { className: 'at-blocked' }, h(Ico, { name: 'block', size: 12 }), t('blocked'), h('span', { className: 'at-dep' }, '· ' + t('dependsOn') + ' ' + refs.join('、'))) : null,
1269
+ h('span', { className: 'at-replies' }, replyLabel + (time ? ' · ' + time : '')),
1270
+ ),
1271
+ h('div', { className: 'at-ct', title: card.title }, card.title),
1272
+ variant === 'rich' && card.summary ? h('div', { className: 'at-cs' }, card.summary) : null,
1273
+ h('div', { className: 'at-cm' },
1274
+ h('span', { className: 'at-owner' }, ownerText),
1275
+ h('span', { className: 'at-sep', 'aria-hidden': 'true' }, '·'),
1276
+ h('span', { className: 'at-exec' }, card.assignee ? h('span', { className: 'at-miniavatar', 'aria-hidden': 'true' }, String(card.assignee).slice(0, 1)) : null, execText),
1277
+ h(FollowToggle, { t, taskNo: no, following, onToggle: () => onToggleFollow(card), compact: variant === 'compact' }),
1278
+ ));
1279
+ }
1280
+
1281
+ function TaskGroup({ t, cards, tasks, selected, follows, onOpen, onToggleFollow }) {
1282
+ const [open, setOpen] = React.useState(false);
1283
+ if (!cards.length) return null;
1284
+ const first = cards[0];
1285
+ const rest = cards.slice(1);
1286
+ const counts = {};
1287
+ cards.forEach((card) => { counts[card.status] = (counts[card.status] ?? 0) + 1; });
1288
+ const parts = ['in_progress', 'pending_acceptance', 'pending', 'done']
1289
+ .filter((status) => counts[status])
1290
+ .map((status) => t(statusMeta(status).label) + ' ' + counts[status]);
1291
+ const blockedCount = cards.filter((card) => (card.blockedBy ?? []).length > 0).length;
1292
+ const groupLabel = tf(t, 'groupCreated', { n: cards.length });
1293
+ const renderCard = (card) => h(TaskCard, {
1294
+ key: card.id,
1295
+ t,
1296
+ card,
1297
+ tasks,
1298
+ variant: 'rich',
1299
+ selected: selected === card.id,
1300
+ following: follows(card),
1301
+ onOpen,
1302
+ onToggleFollow,
1303
+ });
1304
+ return h('div', { className: 'at-group', role: 'group', 'aria-label': groupLabel },
1305
+ h('div', { className: 'at-grouphead' },
1306
+ h('span', { className: 'at-groupicon', 'aria-hidden': 'true' }, h(Ico, { name: 'layers' })),
1307
+ h('span', { className: 'at-grouptitle' }, groupLabel),
1308
+ h('span', { className: 'at-groupsummary' }, parts.join(' · ') + (blockedCount ? ' · ' + tf(t, 'groupBlocked', { n: blockedCount }) : '')),
1309
+ ),
1310
+ renderCard(first),
1311
+ !open && rest.length ? h('button', { className: 'at-groupmore', type: 'button', 'aria-expanded': false, onClick: () => setOpen(true) },
1312
+ h('span', { className: 'at-morelabel' }, tf(t, 'groupMore', { n: rest.length })),
1313
+ h('span', { className: 'at-morelist' }, rest.slice(0, 3).map((card) => '#' + taskNo(card) + ' ' + firstSentence(card.title)).join(' · '))) : null,
1314
+ open ? rest.map(renderCard) : null,
1315
+ open && rest.length ? h('button', { className: 'at-groupmore', type: 'button', 'aria-expanded': true, onClick: () => setOpen(false) },
1316
+ h('span', { className: 'at-morelabel' }, tf(t, 'groupLess', { n: rest.length }))) : null,
1317
+ );
1318
+ }
1319
+
1320
+ function StreamMessage({ t, item, data, tasks, selected, follows, onOpen, onToggleFollow }) {
1321
+ const author = item.author ?? {};
1322
+ const human = author.kind === 'human' || author.name === '你';
1323
+ const agent = agentFor(data, author.name);
1324
+ const cards = (item.taskIds ?? []).map((id) => tasks.find((task) => task.id === id)).filter(Boolean);
1325
+ return h('article', { className: cx('at-msg', human && 'at-msg-human') },
1326
+ h(Avatar, { name: author.name ?? (human ? '你' : '?'), human }),
1327
+ h('div', { className: 'at-msgb' },
1328
+ h('div', { className: 'at-msgh' },
1329
+ h('b', null, author.name ?? '—'),
1330
+ agent ? h('span', { className: 'at-role' }, agent.role) : null,
1331
+ h('time', { dateTime: iso(item.at) }, fmtTime(item.at)),
1332
+ ),
1333
+ h('div', { className: 'at-msgt' }, item.body),
1334
+ cards.length ? h(TaskGroup, { t, cards, tasks, selected, follows, onOpen, onToggleFollow }) : null,
1335
+ ));
1336
+ }
1337
+
1338
+ function EmptyState({ t, icon, title, body, action, compact }) {
1339
+ return h('div', { className: cx('at-empty', compact && 'at-empty-compact'), role: 'status' },
1340
+ icon ? h('span', { className: 'at-emptyicon', 'aria-hidden': 'true' }, h(Ico, { name: icon, size: 28 })) : null,
1341
+ h('div', { className: 'at-emptytitle' }, title),
1342
+ body ? h('div', { className: 'at-emptybody' }, body) : null,
1343
+ action ? h('button', { className: 'at-btn', type: 'button', onClick: action.onClick }, action.label) : null);
1344
+ }
1345
+
1346
+ function BoardColumn({ t, status, items, tasks, selected, follows, doneOpen, onToggleDone, onOpen, onToggleFollow }) {
1347
+ const meta = statusMeta(status);
1348
+ const isDone = status === 'done';
1349
+ const shown = isDone && !doneOpen ? [] : items;
1350
+ return h('section', { className: cx('at-col', 'at-col-' + status), role: 'group', 'aria-label': tf(t, 'columnAria', { title: t(meta.label), n: items.length }) },
1351
+ h('div', { className: 'at-colhead' },
1352
+ h('span', { className: 'at-coltitle' }, t(meta.label)),
1353
+ h('span', { className: 'at-colcount' }, items.length),
1354
+ isDone && items.length ? h('button', { className: 'at-coltoggle', type: 'button', 'aria-expanded': doneOpen, onClick: onToggleDone }, doneOpen ? t('collapseDone') : t('expandDone')) : null,
1355
+ ),
1356
+ !items.length ? h('div', { className: 'at-colempty' }, t('emptyColumn')) : null,
1357
+ shown.map((card) => h(TaskCard, {
1358
+ key: card.id,
1359
+ t,
1360
+ card,
1361
+ tasks,
1362
+ variant: 'board',
1363
+ selected: selected === card.id,
1364
+ following: follows(card),
1365
+ onOpen,
1366
+ onToggleFollow,
1367
+ })));
1368
+ }
1369
+
1370
+ function Board({ t, tasks, selected, follows, doneOpen, onToggleDone, onOpen, onToggleFollow, filter, onClearFilter, q1Extra, onBackMain }) {
1371
+ const filtered = filter === 'all' ? tasks : tasks.filter((task) => matchesFilter(task, filter));
1372
+ const filterName = filter === 'all' ? '' : t(filter === 'q1' ? 'q1Title' : filter === 'q2' ? 'q2Title' : filter === 'q3' ? 'q3Title' : 'q4Title');
1373
+ const bar = filter === 'all' ? null : h('div', { className: 'at-boardbar', role: 'status' },
1374
+ h('span', { className: 'at-filterchip' }, tf(t, 'filterLabel', { name: filterName }) + ' · ' + tf(t, 'itemCount', { n: filtered.length })),
1375
+ q1Extra > 0 ? h('span', { className: 'at-filternote' }, tf(t, 'filterInboxExtra', { n: q1Extra })) : null,
1376
+ h('button', { className: 'at-btn at-btn-quiet at-filterclear', type: 'button', onClick: onClearFilter }, t('filterClear')));
1377
+ if (!filtered.length) {
1378
+ return h('div', { className: 'at-boardwrap', role: 'tabpanel', id: 'at-panel-board', 'aria-labelledby': 'at-tab-board' },
1379
+ bar,
1380
+ filter === 'all'
1381
+ ? h(EmptyState, {
1382
+ t,
1383
+ icon: 'board',
1384
+ title: t('emptyBoardTitle'),
1385
+ body: t('emptyBoardBody'),
1386
+ action: { label: t('emptyBoardAction'), onClick: onBackMain },
1387
+ })
1388
+ : h(EmptyState, {
1389
+ t,
1390
+ icon: 'board',
1391
+ title: t('filteredEmptyTitle'),
1392
+ body: t('filteredEmptyBody'),
1393
+ action: { label: t('filterClear'), onClick: onClearFilter },
1394
+ }));
1395
+ }
1396
+ return h('div', { className: 'at-boardwrap', role: 'tabpanel', id: 'at-panel-board', 'aria-labelledby': 'at-tab-board' },
1397
+ bar,
1398
+ h('div', { className: 'at-board' }, ['pending', 'in_progress', 'pending_acceptance', 'done'].map((status) => h(BoardColumn, {
1399
+ key: status,
1400
+ t,
1401
+ status,
1402
+ items: filtered.filter((task) => task.status === status),
1403
+ tasks,
1404
+ selected,
1405
+ follows,
1406
+ doneOpen: doneOpen,
1407
+ onToggleDone,
1408
+ onOpen,
1409
+ onToggleFollow,
1410
+ }))));
1411
+ }
1412
+
1413
+ function ProjectRow({ t, project, active, onSelect }) {
1414
+ const onKeyDown = (event) => {
1415
+ if (event.key === 'Enter' || event.key === ' ') {
1416
+ event.preventDefault();
1417
+ onSelect(project);
1418
+ return;
1419
+ }
1420
+ if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;
1421
+ event.preventDefault();
1422
+ const nodes = Array.from(event.currentTarget.parentElement.querySelectorAll('[role="treeitem"]'));
1423
+ const index = nodes.indexOf(event.currentTarget);
1424
+ const next = nodes[index + (event.key === 'ArrowDown' ? 1 : -1)];
1425
+ if (next) next.focus();
1426
+ };
1427
+ return h('div', {
1428
+ className: cx('at-prow', active && 'at-on'),
1429
+ role: 'treeitem',
1430
+ 'aria-level': 1,
1431
+ 'aria-selected': Boolean(active),
1432
+ tabIndex: 0,
1433
+ title: project.name,
1434
+ onClick: () => onSelect(project),
1435
+ onKeyDown,
1436
+ },
1437
+ h('span', { className: 'at-hash', 'aria-hidden': 'true' }, '#'),
1438
+ h('span', { className: 'at-prowtitle' }, project.name));
1439
+ }
1440
+
1441
+ function AgentRow({ t, agent, onAgent }) {
1442
+ const statusText = t(AGENT_STATUS[agent.status] ?? 'agentIdle');
1443
+ const config = agent.config ?? null;
1444
+ const model = config?.model ? config.model : t('agentDefaultModel');
1445
+ const context = Number.isFinite(config?.contextWindow) ? ' · ctx ' + Math.round(config.contextWindow / 1000) + 'k' : '';
1446
+ const version = Number.isFinite(agent.configVersion) && agent.configVersion > 0 ? ' · v' + agent.configVersion : '';
1447
+ const failure = agent.status === 'failed' ? (agent.lastFailure ?? null) : null;
1448
+ return h('div', { className: 'at-agent', role: 'listitem' },
1449
+ h('span', { className: 'at-dot at-dot-' + (agent.status ?? 'idle'), 'aria-hidden': 'true' }),
1450
+ h('div', { className: 'at-agentmeta' },
1451
+ h('button', { className: 'at-agentname', type: 'button', onClick: () => onAgent(agent), title: agent.name }, agent.name),
1452
+ h('div', { className: 'at-agentrole', title: agent.role ?? '' },
1453
+ agent.role ?? '',
1454
+ agent.qa ? h('span', { className: 'at-qabadge' }, 'QA') : null),
1455
+ h('div', {
1456
+ className: 'at-agentconfig',
1457
+ title: [model + context + version, failure ? tf(t, 'agentFailure', { category: failure.category, n: failure.attempt }) : ''].filter(Boolean).join(' · '),
1458
+ },
1459
+ h('span', null, model + context + version),
1460
+ failure ? h('span', { className: 'at-agentfail' }, t('agentFailureShort')) : null,
1461
+ ),
1462
+ ),
1463
+ h('span', { className: 'at-sr' }, statusText),
1464
+ h('button', {
1465
+ className: 'at-agentmore',
1466
+ type: 'button',
1467
+ 'aria-label': tf(t, 'agentOpen', { name: agent.name }),
1468
+ title: t('moreActions'),
1469
+ onClick: () => onAgent(agent),
1470
+ }, '⋯'));
1471
+ }
1472
+
1473
+ function LeftPanel({ t, data, projectId, scope, tab, inboxUnread, onBack, onInbox, onBoard, onProject, onNewProject, onAgent, onAddAgent, onRecruit }) {
1474
+ const projects = data.projects.filter((project) => !project.archived);
1475
+ const activeAgents = data.agents.filter((agent) => agent.status !== 'retired');
1476
+ const talentAgents = data.agents.filter((agent) => agent.status === 'retired');
1477
+ const archived = data.projects.filter((project) => project.archived);
1478
+ const boardActive = scope === 'project' && tab === 'board';
1479
+ return h('div', null,
1480
+ h('nav', { className: 'at-nav', 'aria-label': t('navAria') },
1481
+ h('button', { className: 'at-navrow', type: 'button', onClick: onBack }, h(Ico, { name: 'back' }), h('span', null, t('back'))),
1482
+ h('button', {
1483
+ className: cx('at-navrow', scope === 'inbox' && 'at-on'),
1484
+ type: 'button',
1485
+ 'aria-current': scope === 'inbox' ? 'page' : undefined,
1486
+ 'aria-label': inboxUnread ? tf(t, 'inboxUnread', { n: inboxUnread }) : t('inbox'),
1487
+ onClick: onInbox,
1488
+ }, h(Ico, { name: 'inbox' }), h('span', null, t('inbox')), inboxUnread > 0 ? h('span', { className: 'at-count' }, inboxUnread > 99 ? '99+' : inboxUnread) : null),
1489
+ h('button', {
1490
+ className: cx('at-navrow', boardActive && 'at-on'),
1491
+ type: 'button',
1492
+ 'aria-current': boardActive ? 'page' : undefined,
1493
+ onClick: onBoard,
1494
+ }, h(Ico, { name: 'board' }), h('span', null, t('board'))),
1495
+ ),
1496
+ h('div', { className: 'at-h' },
1497
+ h('span', null, t('projects')),
1498
+ h('button', { className: 'at-mini', type: 'button', 'aria-label': t('newProject'), title: t('newProject'), onClick: onNewProject }, h(Ico, { name: 'plus', size: 12 })),
1499
+ ),
1500
+ projects.length
1501
+ ? h('div', { className: 'at-tree', role: 'tree', 'aria-label': t('projectsAria') }, projects.map((project) => h(ProjectRow, {
1502
+ key: project.id,
1503
+ t,
1504
+ project,
1505
+ active: scope === 'project' && project.id === projectId,
1506
+ onSelect: onProject,
1507
+ })))
1508
+ : h('div', { className: 'at-colempty' }, t('projectEmptyTitle')),
1509
+ archived.length ? h('details', { className: 'at-archived' },
1510
+ h('summary', null, tf(t, 'archivedCount', { n: archived.length })),
1511
+ h('div', { className: 'at-tree', role: 'group', 'aria-label': t('projectsAria') }, archived.map((project) => h(ProjectRow, {
1512
+ key: project.id,
1513
+ t,
1514
+ project,
1515
+ active: false,
1516
+ onSelect: onProject,
1517
+ })))) : null,
1518
+ h('div', { className: 'at-h' },
1519
+ h('span', null, t('agents')),
1520
+ h('button', { className: 'at-mini', type: 'button', 'aria-label': t('addAgent'), title: t('addAgent'), onClick: onAddAgent }, h(Ico, { name: 'plus', size: 12 })),
1521
+ ),
1522
+ activeAgents.length
1523
+ ? h('div', { role: 'list', 'aria-label': t('agentsAria') }, activeAgents.map((agent) => h(AgentRow, { key: agent.id, t, agent, onAgent })))
1524
+ : h('div', { className: 'at-colempty' }, t('emptyColumn')),
1525
+ talentAgents.length ? h('details', { className: 'at-archived' },
1526
+ h('summary', null, tf(t, 'talentPoolCount', { n: talentAgents.length })),
1527
+ h('div', { role: 'list', 'aria-label': t('talentPool') }, talentAgents.map((agent) => h('div', { key: agent.id, className: 'at-agent at-talented' },
1528
+ h('div', { className: 'at-agentmeta' },
1529
+ h('span', { className: 'at-agentname' }, agent.name),
1530
+ h('div', { className: 'at-agentrole' }, agent.role ?? '', agent.qa ? h('span', { className: 'at-qabadge' }, 'QA') : null)),
1531
+ h('button', { className: 'at-btn at-btn-small', type: 'button', onClick: () => onRecruit(agent) }, t('recruit')))))) : null);
1532
+ }
1533
+
1534
+ function ConvertForm({ t, project, task, thread, title, dod, onTitle, onDod, onCancel, onSubmit }) {
1535
+ const valid = Boolean(title.trim() && dod.trim());
1536
+ const onKeyDown = (event) => {
1537
+ if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
1538
+ event.preventDefault();
1539
+ if (valid) onSubmit();
1540
+ }
1541
+ };
1542
+ return h('form', {
1543
+ className: 'at-convert',
1544
+ role: 'dialog',
1545
+ 'aria-labelledby': 'at-convert-title',
1546
+ onSubmit: (event) => { event.preventDefault(); if (valid) onSubmit(); },
1547
+ onKeyDown,
1548
+ },
1549
+ h('div', { className: 'at-converthead', id: 'at-convert-title' }, t('convertTitle')),
1550
+ h('label', { className: 'at-field' },
1551
+ h('span', null, t('titleLabel')),
1552
+ h('input', { value: title, maxLength: 80, autoFocus: true, placeholder: t('convertNamePlaceholder'), onChange: (event) => onTitle(event.target.value) })),
1553
+ h('div', { className: 'at-fieldrow' },
1554
+ h('div', { className: 'at-field' }, h('span', null, t('owner')), h('div', { className: 'at-staticfield', title: project?.owner ?? t('unassigned') }, project?.owner ?? t('unassigned'))),
1555
+ h('div', { className: 'at-field' }, h('span', null, t('executor')), h('div', { className: 'at-staticfield' }, t('unassigned')))),
1556
+ h('label', { className: 'at-field' },
1557
+ h('span', null, t('convertDod')),
1558
+ h('textarea', { value: dod, rows: 3, placeholder: t('convertDodPlaceholder'), onChange: (event) => onDod(event.target.value) })),
1559
+ h('div', { className: 'at-context' }, tf(t, 'convertContext', { n: taskNo(task), m: thread?.entries?.length ?? 0 })),
1560
+ h('div', { className: 'at-convertfoot' },
1561
+ h('button', { className: 'at-btn', type: 'button', onClick: onCancel }, t('convertCancel')),
1562
+ h('button', { className: 'at-btn at-btn-primary', type: 'submit', disabled: !valid }, t('convertSubmit'))));
1563
+ }
1564
+
1565
+ function AcceptancePanel({ t, task, checked, onChecked, checks, onCheck, onAccept, onChanges }) {
1566
+ const submission = task.submission ?? null;
1567
+ const evidence = Array.isArray(submission?.evidence) ? submission.evidence : [];
1568
+ return h('section', { className: 'at-accept', 'aria-labelledby': 'at-accept-title' },
1569
+ h('div', { className: 'at-accepthead' },
1570
+ h('span', { className: 'at-accepttitle', id: 'at-accept-title' }, t('acceptance')),
1571
+ h('span', { className: 'at-candidate' }, t('candidate')),
1572
+ h(StatusPill, { t, status: 'pending_acceptance' })),
1573
+ h('div', { className: 'at-kv' },
1574
+ h('span', { className: 'at-kvlabel' }, t('resultSummary')),
1575
+ h('div', { className: 'at-kvbody' }, submission?.summary ?? t('noSummary'))),
1576
+ h('div', { className: 'at-kv' },
1577
+ h('span', { className: 'at-kvlabel' }, t('evidence')),
1578
+ h('div', { className: 'at-kvbody' }, evidence.length
1579
+ ? h('ul', { className: 'at-evidence' }, evidence.map((entry, index) => {
1580
+ const item = typeof entry === 'string' ? { label: entry, ref: '' } : entry;
1581
+ return h('li', { key: index }, h(Ico, { name: 'check', size: 12 }), h('span', null, item.label), item.ref ? h('code', null, item.ref) : null);
1582
+ }))
1583
+ : t('noEvidence'))),
1584
+ h('div', { className: 'at-kv' },
1585
+ h('span', { className: 'at-kvlabel' }, t('diff')),
1586
+ h('div', { className: 'at-kvbody' }, submission?.diffStat ?? t('noDiff'))),
1587
+ Array.isArray(task.qaChecklist) && task.qaChecklist.length > 0 ? h('div', { className: 'at-kv' },
1588
+ h('span', { className: 'at-kvlabel' }, t('qaChecklist')),
1589
+ h('div', { className: 'at-kvbody' }, task.qaChecklist.map((item, index) => h('label', { key: index, className: 'at-check' },
1590
+ h('input', {
1591
+ type: 'checkbox',
1592
+ checked: checks?.[index]?.passed === true,
1593
+ onChange: (event) => onCheck(index, event.target.checked),
1594
+ }),
1595
+ h('span', null, item))))) : null,
1596
+ h('p', { className: 'at-elig' }, t('acceptEligible')),
1597
+ h('p', { className: 'at-steward' }, t('acceptSteward')),
1598
+ task.irreversible ? h('div', { className: 'at-irrev' },
1599
+ h('p', null, t('irreversibleNote')),
1600
+ h('label', { className: 'at-check' },
1601
+ h('input', { type: 'checkbox', checked, onChange: (event) => onChecked(event.target.checked) }),
1602
+ h('span', null, t('irreversibleCheck')))) : null,
1603
+ h('div', { className: 'at-acceptactions' },
1604
+ h('button', { className: 'at-btn', type: 'button', onClick: onChanges }, t('requestChanges')),
1605
+ h('button', {
1606
+ className: 'at-btn at-btn-primary',
1607
+ type: 'button',
1608
+ disabled: Boolean(task.irreversible && !checked),
1609
+ onClick: onAccept,
1610
+ }, task.irreversible ? t('acceptIrreversibleCta') : t('acceptCta'))));
1611
+ }
1612
+
1613
+ function ConflictBanner({ t, thread, baseSeq, headSeq, onContinue, onContext }) {
1614
+ const changes = Array.isArray(thread?.changes) ? thread.changes.slice(0, 5) : [];
1615
+ return h('div', { className: 'at-conflict', role: 'alert' },
1616
+ h('div', { className: 'at-conflicttitle' }, t('conflictTitle')),
1617
+ h('div', { className: 'at-conflictnew' }, tf(t, 'conflictNew', { head: headSeq, n: headSeq - baseSeq })),
1618
+ changes.length ? h('ul', { className: 'at-conflictlist' }, changes.map((change) => h('li', { key: (change.seq ?? '') + ':' + (change.what ?? '') }, (change.who ?? '—') + ' #' + (change.seq ?? '') + ' ' + (change.what ?? '')))) : null,
1619
+ h('div', { className: 'at-conflictactions' },
1620
+ h('button', { className: 'at-btn at-btn-quiet', type: 'button', onClick: onContinue }, t('conflictContinue')),
1621
+ h('button', { className: 'at-btn at-btn-quiet', type: 'button', onClick: onContext }, t('conflictContext'))));
1622
+ }
1623
+
1624
+ function DoneSummary({ t, task }) {
1625
+ const time = fmtDateTime(task.completedAt ?? task.updatedAt);
1626
+ const evidence = task.evidenceCount ?? task.submission?.evidence?.length;
1627
+ return h('section', { className: 'at-done' },
1628
+ h('div', { className: 'at-donehead' }, h(Ico, { name: 'check', size: 14 }), h('span', null, t('doneTitle'))),
1629
+ h('div', { className: 'at-donemeta' },
1630
+ time ? h('span', null, tf(t, 'doneAt', { time })) : null,
1631
+ task.acceptedBy ? h('span', null, tf(t, 'acceptedBy', { name: task.acceptedBy })) : null,
1632
+ evidence !== undefined ? h('span', null, tf(t, 'evidenceCount', { n: evidence })) : null),
1633
+ !time && !task.acceptedBy && evidence === undefined ? h('div', { className: 'at-donemeta' }, t('doneNoInfo')) : null,
1634
+ task.submission?.summary ? h('p', { className: 'at-donetext' }, task.submission.summary) : null);
1635
+ }
1636
+
1637
+ function ThreadPanel({ t, task, thread, project, following, onToggleFollow, onClose, converting, convert, reply, onReplyChange, onReplySend, replyBase, onContinue, onContext, note, onDismissNote, acceptChecked, onAcceptChecked, qaChecks, onQaCheck, onAccept, onChanges, actions, panelRef, replyRef }) {
1638
+ const headSeq = thread?.headSeq ?? thread?.entries?.length ?? 0;
1639
+ const stale = replyBase !== null && replyBase !== undefined && headSeq > replyBase;
1640
+ const entries = Array.isArray(thread?.entries) ? thread.entries : [];
1641
+ return h('aside', { className: 'at-right', role: 'complementary', 'aria-label': t('threadAria'), tabIndex: -1, ref: panelRef },
1642
+ h('div', { className: 'at-rhead' },
1643
+ h('div', { className: 'at-rh' },
1644
+ h('div', { className: 'at-rhmeta', title: t('thread') + ' · #' + taskNo(task) }, t('thread') + ' · #' + taskNo(task)),
1645
+ h('div', { className: 'at-rhtitle', title: task.title }, task.title)),
1646
+ h(FollowToggle, { t, taskNo: taskNo(task), following, onToggle: onToggleFollow }),
1647
+ h('button', { className: 'at-rbtn', type: 'button', 'aria-expanded': Boolean(converting), onClick: convert.onOpen }, t('convert')),
1648
+ h('button', { className: 'at-rbtn at-iconbtn', type: 'button', 'aria-label': t('closeThread'), title: t('closeThread'), onClick: onClose }, h(Ico, { name: 'close', size: 14 }))),
1649
+ note ? h('div', { className: 'at-rnote', role: 'status' },
1650
+ h('span', { className: 'at-notetext' }, note),
1651
+ h('button', { className: 'at-noteclose', type: 'button', 'aria-label': t('dismiss'), title: t('dismiss'), onClick: onDismissNote }, h(Ico, { name: 'close', size: 12 }))) : null,
1652
+ converting
1653
+ ? h(ConvertForm, { t, project, task, thread, title: convert.title, dod: convert.dod, onTitle: convert.onTitle, onDod: convert.onDod, onCancel: convert.onCancel, onSubmit: convert.onSubmit })
1654
+ : h('div', { className: 'at-rbody' },
1655
+ stale ? h(ConflictBanner, { t, thread, baseSeq: replyBase, headSeq, onContinue, onContext }) : null,
1656
+ task.status === 'done' ? h(DoneSummary, { t, task }) : null,
1657
+ entries.length
1658
+ ? h('div', { className: 'at-tmsgs' }, entries.map((message, index) => h('div', { key: message.seq ?? index, className: 'at-tmsg' },
1659
+ h('div', { className: 'at-tmh' },
1660
+ h('b', null, message.author ?? '—'),
1661
+ h('span', { className: 'at-tseq' }, '#' + (message.seq ?? index + 1)),
1662
+ h('time', { dateTime: iso(message.at) }, fmtTime(message.at))),
1663
+ h('div', { className: 'at-tmt' }, message.body))))
1664
+ : h(EmptyState, { t, compact: true, title: t('threadEmptyTitle'), body: t('threadEmptyBody') }),
1665
+ task.status === 'pending_acceptance' ? h(AcceptancePanel, {
1666
+ t,
1667
+ task,
1668
+ checked: acceptChecked,
1669
+ onChecked: onAcceptChecked,
1670
+ checks: qaChecks,
1671
+ onCheck: onQaCheck,
1672
+ onAccept,
1673
+ onChanges,
1674
+ }) : null,
1675
+ actions.length ? h('section', { className: 'at-actions', 'aria-label': t('taskActions') },
1676
+ h('div', { className: 'at-actionhead' },
1677
+ h('span', { className: 'at-actiontitle' }, t('taskActions'))),
1678
+ h('div', { className: 'at-actionrow' }, actions.map((action) => h('button', {
1679
+ key: action.label,
1680
+ className: 'at-btn at-btn-quiet',
1681
+ type: 'button',
1682
+ onClick: action.onClick,
1683
+ }, action.label)))) : null),
1684
+ !converting ? h('div', { className: 'at-rfoot' },
1685
+ h('div', { className: 'at-box at-rbox' },
1686
+ h('textarea', {
1687
+ value: reply,
1688
+ rows: 2,
1689
+ ref: replyRef,
1690
+ placeholder: t('reply'),
1691
+ 'aria-label': t('reply'),
1692
+ onChange: (event) => onReplyChange(event.target.value),
1693
+ onKeyDown: (event) => {
1694
+ if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
1695
+ event.preventDefault();
1696
+ onReplySend();
1697
+ }
1698
+ },
1699
+ }),
1700
+ h('div', { className: 'at-barrow' },
1701
+ h('span', { className: 'at-hint' }, headSeq ? tf(t, 'replyHint', { seq: replyBase ?? headSeq }) : t('replyHintNew')),
1702
+ h('button', { className: 'at-send', type: 'button', disabled: !reply.trim(), onClick: onReplySend }, t('send'))))) : null);
1703
+ }
1704
+
1705
+ function Composer({ t, mode, draft, disabled, qaRequired, onQaRequired, onMode, onDraft, onSend }) {
1706
+ const send = () => { if (draft.trim() && !disabled) onSend(); };
1707
+ return h('div', { className: 'at-composer' },
1708
+ h('div', { className: 'at-composerinner' },
1709
+ h('div', { className: cx('at-box', 'at-composerbox', mode === 'task' && 'at-taskmode') },
1710
+ h('textarea', {
1711
+ value: draft,
1712
+ rows: 2,
1713
+ disabled,
1714
+ placeholder: mode === 'task' ? t('taskPlaceholder') : t('composer'),
1715
+ 'aria-label': t('composerAria'),
1716
+ onChange: (event) => onDraft(event.target.value),
1717
+ onKeyDown: (event) => {
1718
+ if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
1719
+ event.preventDefault();
1720
+ send();
1721
+ }
1722
+ },
1723
+ }),
1724
+ h('div', { className: 'at-barrow' },
1725
+ h('div', { className: 'at-mode', role: 'group', 'aria-label': t('modeAria') },
1726
+ h('button', { type: 'button', 'aria-pressed': mode === 'discuss', onClick: () => onMode('discuss') }, t('discuss')),
1727
+ h('button', { type: 'button', 'aria-pressed': mode === 'task', onClick: () => onMode('task') }, t('taskMode'))),
1728
+ mode === 'task' ? h('label', { className: 'at-qacheck' },
1729
+ h('input', { type: 'checkbox', checked: qaRequired, onChange: (event) => onQaRequired(event.target.checked) }),
1730
+ h('span', null, t('qaRequired'))) : null,
1731
+ h('span', { className: 'at-hint' }, t('enterHint')),
1732
+ h('button', { className: 'at-send', type: 'button', disabled: disabled || !draft.trim(), onClick: send }, mode === 'task' ? t('createTask') : t('send'))))));
1733
+ }
1734
+
1735
+ function QuadrantCard({ t, meta, quadrant, tasks, items, count, onItem, onViewAll }) {
1736
+ const shown = (Array.isArray(items) ? items : []).slice(0, 5);
1737
+ const title = t(quadrant.name);
1738
+ return h('section', { className: cx('at-quad', 'at-quad-' + quadrant.key), role: 'group', 'aria-label': title + ',' + tf(t, 'itemCount', { n: count }) },
1739
+ h('div', { className: 'at-quadhead' },
1740
+ h('span', { className: 'at-qname' }, title),
1741
+ h('span', { className: 'at-qcount' }, count)),
1742
+ h('div', { className: 'at-qdef' }, t(quadrant.def)),
1743
+ quadrant.key === 'q4' && meta && meta.total > meta.count ? h('div', { className: 'at-qtotal' }, tf(t, 'q4Total', { n: meta.count, m: meta.total })) : null,
1744
+ shown.length
1745
+ ? h('div', { className: 'at-qlist' }, shown.map((item) => {
1746
+ const blocked = (item.blockedBy ?? []).map((id) => {
1747
+ const dep = tasks.find((task) => task.id === id);
1748
+ return '#' + taskNo(dep ?? { id });
1749
+ });
1750
+ const category = t(CATEGORY_LABEL[item.category] ?? 'catBlocked');
1751
+ const parts = [category];
1752
+ if (quadrant.key === 'q3') {
1753
+ if (blocked.length) parts.push(tf(t, 'waitOn', { ref: blocked.join('、') }));
1754
+ else if (item.handoffTo) parts.push(tf(t, 'waitOn', { ref: '@' + item.handoffTo }));
1755
+ else parts.push(t('waitDependency'));
1756
+ } else if (quadrant.key === 'q4') {
1757
+ if (item.doneAt) parts.push(fmtTime(item.doneAt));
1758
+ if (item.acceptedBy) parts.push(tf(t, 'acceptedBy', { name: item.acceptedBy }));
1759
+ } else if (item.assigneeName) {
1760
+ parts.push(item.assigneeName);
1761
+ } else if (item.ownerName) {
1762
+ parts.push(item.ownerName);
1763
+ }
1764
+ const hasNumber = Number.isFinite(item.taskNumber);
1765
+ const itemTitle = (hasNumber ? '#' + item.taskNumber + ' ' : '') + (item.title ?? '');
1766
+ const onKeyDown = (event) => {
1767
+ if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;
1768
+ event.preventDefault();
1769
+ const nodes = Array.from(event.currentTarget.parentElement.querySelectorAll('[data-qitem]'));
1770
+ const index = nodes.indexOf(event.currentTarget);
1771
+ const next = nodes[index + (event.key === 'ArrowDown' ? 1 : -1)];
1772
+ if (next) next.focus();
1773
+ };
1774
+ return h('button', {
1775
+ key: item.id,
1776
+ className: 'at-qitem',
1777
+ type: 'button',
1778
+ 'data-qitem': 'true',
1779
+ title: itemTitle,
1780
+ onClick: (event) => onItem(item, event),
1781
+ onKeyDown,
1782
+ },
1783
+ h('span', { className: 'at-qitemicon', 'aria-hidden': 'true' }, h(Ico, { name: quadrant.icon, size: 12 })),
1784
+ h('span', { className: 'at-qitembody' },
1785
+ h('span', { className: 'at-qtitle' }, itemTitle),
1786
+ h('span', { className: 'at-qitemmeta' }, parts.join(' · '))));
1787
+ }))
1788
+ : h('div', { className: 'at-qempty' }, t(quadrant.empty)),
1789
+ h('div', { className: 'at-qfoot' },
1790
+ h('button', { className: 'at-qmore', type: 'button', disabled: count === 0, onClick: () => onViewAll(quadrant.key) }, t('viewAll'))));
1791
+ }
1792
+
1793
+ function Situation({ t, quadrants, tasks, onItem, onViewAll }) {
1794
+ return h('div', { className: 'at-situation', role: 'tabpanel', id: 'at-panel-situation', 'aria-labelledby': 'at-tab-situation' },
1795
+ h('div', { className: 'at-situationinner' },
1796
+ h('div', { className: 'at-qmeta' },
1797
+ h('span', null, tf(t, 'quadrantMeta', { time: fmtTime(quadrants.generatedAt) })),
1798
+ quadrants.rev ? h('span', null, 'rev ' + quadrants.rev) : null),
1799
+ h('div', { className: 'at-quadrants' }, QUADRANTS.map((quadrant) => {
1800
+ const block = quadrants[quadrant.key] ?? { count: 0, items: [] };
1801
+ return h(QuadrantCard, {
1802
+ key: quadrant.key,
1803
+ t,
1804
+ meta: block,
1805
+ quadrant,
1806
+ tasks,
1807
+ items: Array.isArray(block.items) ? block.items : [],
1808
+ count: block.count ?? (block.items?.length ?? 0),
1809
+ onItem,
1810
+ onViewAll,
1811
+ });
1812
+ }))));
1813
+ }
1814
+
1815
+ function Skeleton({ t }) {
1816
+ return h('div', { className: 'at-root', 'aria-busy': 'true' },
1817
+ h('div', { className: 'at-top' },
1818
+ h('span', { className: 'at-brand' }, t('panel')),
1819
+ h('span', { className: 'at-spike' }, t('spike')),
1820
+ h('span', { className: 'at-sr', role: 'status' }, t('loading'))),
1821
+ h('div', { className: 'at-grid' },
1822
+ h('aside', { className: 'at-left' }, [0, 1, 2, 3, 4, 5].map((index) => h('div', { key: index, className: 'at-skel at-skel-line' }))),
1823
+ h('main', { className: 'at-center' },
1824
+ h('div', { className: 'at-chead' }, h('div', { className: 'at-skel at-skel-title' })),
1825
+ h('div', { className: 'at-stream' },
1826
+ h('div', { className: 'at-streaminner' },
1827
+ h('div', { className: 'at-skel at-skel-msg' }),
1828
+ h('div', { className: 'at-skel at-skel-msg' }),
1829
+ h('div', { className: 'at-skel at-skel-card' }))))));
1830
+ }
1831
+
1832
+ class Boundary extends React.Component {
1833
+ constructor(props) { super(props); this.state = { error: null }; }
1834
+ static getDerivedStateFromError(error) { return { error }; }
1835
+ render() {
1836
+ if (this.state.error === null) return this.props.children;
1837
+ const t = typeof this.props.t === 'function' ? this.props.t : (key) => key;
1838
+ return h('div', { className: 'at-crash', role: 'alert' },
1839
+ h('div', { className: 'at-crashtitle' }, t('crashTitle')),
1840
+ h('pre', { className: 'at-crashbody' }, String(this.state.error?.message ?? this.state.error)),
1841
+ h('button', { className: 'at-btn at-btn-primary', type: 'button', onClick: () => this.setState({ error: null }) }, t('retry')));
1842
+ }
1843
+ }
1844
+
1845
+ function Workbench({ t, ctx }) {
1846
+ const [phase, setPhase] = React.useState('loading');
1847
+ const [data, setData] = React.useState(null);
1848
+ const [projectId, setProjectId] = React.useState('');
1849
+ const [scope, setScope] = React.useState('project');
1850
+ const [tab, setTab] = React.useState('main');
1851
+ const [selected, setSelected] = React.useState('');
1852
+ const [threadOpen, setThreadOpen] = React.useState(false);
1853
+ const [mode, setMode] = React.useState('discuss');
1854
+ const [qaTask, setQaTask] = React.useState(false);
1855
+ const [draft, setDraft] = React.useState('');
1856
+ const [note, setNote] = React.useState('');
1857
+ const [rightNote, setRightNote] = React.useState('');
1858
+ const [reply, setReply] = React.useState('');
1859
+ const [follows, setFollows] = React.useState({});
1860
+ const [doneOpen, setDoneOpen] = React.useState(false);
1861
+ const [boardFilter, setBoardFilter] = React.useState('all');
1862
+ const [converting, setConverting] = React.useState(false);
1863
+ const [cTitle, setCTitle] = React.useState('');
1864
+ const [cDod, setCDod] = React.useState('');
1865
+ const [acceptChecked, setAcceptChecked] = React.useState(false);
1866
+ const [qaChecks, setQaChecks] = React.useState([]);
1867
+ const [addOpen, setAddOpen] = React.useState(false);
1868
+ const [mName, setMName] = React.useState('');
1869
+ const [mRole, setMRole] = React.useState('');
1870
+ const [mPrompt, setMPrompt] = React.useState('');
1871
+ const [mProvider, setMProvider] = React.useState('');
1872
+ const [mModel, setMModel] = React.useState('');
1873
+ const [mOwner, setMOwner] = React.useState(false);
1874
+ const [mBusy, setMBusy] = React.useState(false);
1875
+ const [mError, setMError] = React.useState('');
1876
+ const [profileAgent, setProfileAgent] = React.useState(null);
1877
+ const [pName, setPName] = React.useState('');
1878
+ const [pRole, setPRole] = React.useState('');
1879
+ const [pPersonality, setPPersonality] = React.useState('');
1880
+ const [pPreset, setPPreset] = React.useState('standard');
1881
+ const [pProvider, setPProvider] = React.useState('');
1882
+ const [pModel, setPModel] = React.useState('');
1883
+ const [pMaxTokens, setPMaxTokens] = React.useState('');
1884
+ const [pNote, setPNote] = React.useState('');
1885
+ const [pSkill, setPSkill] = React.useState('');
1886
+ const [pMessage, setPMessage] = React.useState('');
1887
+ const [newOpen, setNewOpen] = React.useState(false);
1888
+ const [nName, setNName] = React.useState('');
1889
+ const [nGoal, setNGoal] = React.useState('');
1890
+ const [nBusy, setNBusy] = React.useState(false);
1891
+ const [nError, setNError] = React.useState('');
1892
+ const [pSuccessor, setPSuccessor] = React.useState('');
1893
+ const [pBusy, setPBusy] = React.useState(false);
1894
+ const [pError, setPError] = React.useState('');
1895
+ const [replyBase, setReplyBase] = React.useState(null);
1896
+ const rightRef = React.useRef(null);
1897
+ const replyRef = React.useRef(null);
1898
+ const triggerRef = React.useRef(null);
1899
+ const mobileRef = React.useRef(null);
1900
+
1901
+ React.useEffect(() => {
1902
+ let alive = true;
1903
+ const controller = typeof AbortController === 'function' ? new AbortController() : null;
1904
+ // The package is @suifei/dsh-ai-team; the Host prefix is /ai-team.
1905
+ const query = projectId ? '?project=' + encodeURIComponent(projectId) : '';
1906
+ const paths = ['/ai-team/api/bootstrap' + query];
1907
+ async function load() {
1908
+ let lastError = null;
1909
+ for (const path of paths) {
1910
+ try {
1911
+ const response = await fetch(path, {
1912
+ headers: { accept: 'application/json' },
1913
+ signal: controller ? controller.signal : undefined,
1914
+ });
1915
+ if (!response.ok) {
1916
+ lastError = new Error('HTTP ' + response.status + ' for ' + path);
1917
+ continue;
1918
+ }
1919
+ const next = normalizePayload(await response.json());
1920
+ if (!next) {
1921
+ lastError = new Error('invalid bootstrap payload from ' + path);
1922
+ continue;
1923
+ }
1924
+ if (!alive) return;
1925
+ setData(next);
1926
+ setPhase('ready');
1927
+ return;
1928
+ } catch (error) {
1929
+ lastError = error;
1930
+ if (!alive || (controller && controller.signal.aborted)) return;
1931
+ }
1932
+ }
1933
+ if (!alive) return;
1934
+ console.error(PKG + ': bootstrap unavailable, using embedded demo data', lastError);
1935
+ setData(MOCK);
1936
+ setPhase('fallback');
1937
+ }
1938
+ load();
1939
+ // Event-driven refresh only; the Host sends one frame per command and
1940
+ // no heartbeat/timer. EventSource reconnects on its own.
1941
+ const source = typeof EventSource === 'function'
1942
+ ? new EventSource('/ai-team/api/projects/' + encodeURIComponent(projectId || 'prj_main') + '/events')
1943
+ : null;
1944
+ if (source) source.addEventListener('change', () => { if (alive) load(); });
1945
+ return () => {
1946
+ alive = false;
1947
+ if (controller) controller.abort();
1948
+ if (source) source.close();
1949
+ };
1950
+ }, [projectId]);
1951
+
1952
+ React.useEffect(() => {
1953
+ if (!data) return;
1954
+ if (!data.projects.some((project) => project.id === projectId)) {
1955
+ const active = data.projects.find((project) => project.active);
1956
+ setProjectId(active?.id ?? data.projects[0]?.id ?? '');
1957
+ }
1958
+ if (selected && !data.tasks.some((task) => task.id === selected)) {
1959
+ setSelected(data.tasks[0]?.id ?? '');
1960
+ }
1961
+ if (!selected && data.tasks[0]) setSelected(data.tasks[0].id);
1962
+ }, [data, projectId, selected]);
1963
+
1964
+ React.useEffect(() => {
1965
+ const thread = threadFor(data, selected);
1966
+ setReplyBase(thread?.baseSeq ?? null);
1967
+ setAcceptChecked(false);
1968
+ const nextTask = data?.tasks?.find((task) => task.id === selected) ?? data?.tasks?.[0];
1969
+ setQaChecks((nextTask?.qaChecklist ?? []).map((item) => ({ item, passed: false })));
1970
+ setConverting(false);
1971
+ setCTitle('');
1972
+ setCDod('');
1973
+ setRightNote('');
1974
+ }, [selected, data]);
1975
+
1976
+ React.useEffect(() => {
1977
+ if (!threadOpen) return;
1978
+ const node = replyRef.current ?? rightRef.current;
1979
+ if (node && typeof node.focus === 'function') node.focus({ preventScroll: true });
1980
+ }, [threadOpen, selected]);
1981
+
1982
+ function closeThread() {
1983
+ setThreadOpen(false);
1984
+ setConverting(false);
1985
+ setRightNote('');
1986
+ const node = triggerRef.current;
1987
+ const focus = () => { try { if (node && node.isConnected) node.focus({ preventScroll: true }); } catch { /* ignore */ } };
1988
+ if (typeof requestAnimationFrame === 'function') requestAnimationFrame(focus); else focus();
1989
+ }
1990
+
1991
+ React.useEffect(() => {
1992
+ if (!threadOpen) return undefined;
1993
+ const onKey = (event) => {
1994
+ if (event.key !== 'Escape') return;
1995
+ event.preventDefault();
1996
+ event.stopPropagation();
1997
+ if (converting) { setConverting(false); return; }
1998
+ closeThread();
1999
+ };
2000
+ window.addEventListener('keydown', onKey);
2001
+ return () => window.removeEventListener('keydown', onKey);
2002
+ }, [threadOpen, converting]);
2003
+
2004
+ if (phase === 'loading' || !data) return h(Skeleton, { t });
2005
+
2006
+ const project = data.projects.find((item) => item.id === projectId) ?? data.projects[0] ?? null;
2007
+ const tasks = data.tasks;
2008
+ const boardTaskCount = tasks.filter((task) => ['pending', 'in_progress', 'pending_acceptance', 'done'].includes(task.status)).length;
2009
+ const selectedTask = tasks.find((task) => task.id === selected) ?? tasks[0] ?? null;
2010
+ const thread = threadFor(data, selectedTask?.id);
2011
+ const quadrants = deriveQuadrants(data);
2012
+ const inboxUnread = Number.isFinite(data.inboxUnread)
2013
+ ? data.inboxUnread
2014
+ : (data.inbox ?? []).filter((item) => !item.read).length;
2015
+ const q1Extra = (quadrants.q1.items ?? []).filter((item) => item.kind === 'inbox').length;
2016
+ const threadHead = thread?.headSeq ?? thread?.entries?.length ?? 0;
2017
+ const followingOf = (card) => (Object.prototype.hasOwnProperty.call(follows, card.id) ? follows[card.id] : Boolean(card.following));
2018
+
2019
+ function say(text) { setNote(text); }
2020
+ function sayRight(text) { setRightNote(text); }
2021
+ async function toggleFollow(card) {
2022
+ if (!card) return;
2023
+ const next = !followingOf(card);
2024
+ const text = next ? tf(t, 'followMock', { n: taskNo(card) }) : tf(t, 'unfollowMock', { n: taskNo(card) });
2025
+ try {
2026
+ await runCommand('task.follow', { taskId: card.id, following: next });
2027
+ setFollows((prev) => ({ ...prev, [card.id]: next }));
2028
+ if (threadOpen && selectedTask && selectedTask.id === card.id) sayRight(text); else say(text);
2029
+ await reloadData();
2030
+ } catch (error) {
2031
+ if (threadOpen && selectedTask && selectedTask.id === card.id) sayRight(tf(t, 'sendFailed', { message: error?.message ?? String(error) }));
2032
+ else say(tf(t, 'sendFailed', { message: error?.message ?? String(error) }));
2033
+ }
2034
+ }
2035
+ function openTask(card, event) {
2036
+ if (!card) return;
2037
+ if (event && event.currentTarget) triggerRef.current = event.currentTarget;
2038
+ setSelected(card.id);
2039
+ setThreadOpen(true);
2040
+ setRightNote('');
2041
+ }
2042
+ function openTaskById(taskId, event) {
2043
+ if (typeof taskId !== 'string' || taskId === '') { say(t('quadrantMissing')); return; }
2044
+ const card = (Array.isArray(tasks) ? tasks : []).find((item) => item.id === taskId);
2045
+ if (!card) { say(t('quadrantMissing')); return; }
2046
+ triggerRef.current = event?.currentTarget ?? null;
2047
+ setSelected(card.id);
2048
+ setThreadOpen(true);
2049
+ setRightNote('');
2050
+ }
2051
+ function openQuadrantItem(item, event) {
2052
+ if (!item) return;
2053
+ if (item.taskId) {
2054
+ openTaskById(item.taskId, event);
2055
+ return;
2056
+ }
2057
+ if (item.kind === 'inbox') {
2058
+ closeMobile();
2059
+ setScope('inbox');
2060
+ setThreadOpen(false);
2061
+ setNote(t('inboxTitle'));
2062
+ return;
2063
+ }
2064
+ say(t('quadrantMissing'));
2065
+ }
2066
+ function viewQuadrant(key) {
2067
+ setBoardFilter(key);
2068
+ setTab('board');
2069
+ setNote('');
2070
+ if (key === 'q4') setDoneOpen(true);
2071
+ }
2072
+ function goConversation() {
2073
+ try {
2074
+ if (ctx?.layout && typeof ctx.layout.selectPanel === 'function') {
2075
+ ctx.layout.selectPanel('conversation');
2076
+ return;
2077
+ }
2078
+ } catch { /* host without layout */ }
2079
+ say(t('conversationMock'));
2080
+ }
2081
+ function closeMobile() {
2082
+ const node = mobileRef.current;
2083
+ if (node) node.open = false;
2084
+ }
2085
+ function switchInbox() {
2086
+ closeMobile();
2087
+ setScope('inbox');
2088
+ setThreadOpen(false);
2089
+ setNote('');
2090
+ }
2091
+ function switchBoard() {
2092
+ closeMobile();
2093
+ setScope('project');
2094
+ setTab('board');
2095
+ setBoardFilter('all');
2096
+ setNote('');
2097
+ }
2098
+ async function switchProject(next) {
2099
+ closeMobile();
2100
+ if (next.id === projectId && scope === 'project') return;
2101
+ try {
2102
+ await runCommand('project.activate', { projectId: next.id });
2103
+ setScope('project');
2104
+ setTab('main');
2105
+ setBoardFilter('all');
2106
+ setThreadOpen(false);
2107
+ setReply('');
2108
+ await reloadData(next.id);
2109
+ setProjectId(next.id);
2110
+ say(next.archived ? t('archivedMock') : t('projectSwitched'));
2111
+ } catch (error) {
2112
+ say(tf(t, 'sendFailed', { message: error?.message ?? String(error) }));
2113
+ }
2114
+ }
2115
+ function openNewProject() {
2116
+ setNName('');
2117
+ setNGoal('');
2118
+ setNError('');
2119
+ setNewOpen(true);
2120
+ }
2121
+ async function createProject() {
2122
+ if (!nName.trim() || nBusy) return;
2123
+ setNBusy(true);
2124
+ setNError('');
2125
+ try {
2126
+ const result = await runCommand('project.create', { name: nName.trim(), goal: nGoal.trim() });
2127
+ const next = result.project;
2128
+ setNewOpen(false);
2129
+ setScope('project');
2130
+ setTab('main');
2131
+ await reloadData(next.id);
2132
+ setProjectId(next.id);
2133
+ say(tf(t, 'projectCreated', { name: next.name }));
2134
+ } catch (error) {
2135
+ setNError(error?.message ?? String(error));
2136
+ } finally {
2137
+ setNBusy(false);
2138
+ }
2139
+ }
2140
+ function selectTab(next) {
2141
+ setTab(next);
2142
+ setNote('');
2143
+ }
2144
+ function tabKeyDown(event) {
2145
+ const keys = ['ArrowLeft', 'ArrowRight', 'Home', 'End'];
2146
+ if (!keys.includes(event.key)) return;
2147
+ event.preventDefault();
2148
+ const index = TABS.indexOf(tab);
2149
+ let next = null;
2150
+ if (event.key === 'Home') next = TABS[0];
2151
+ if (event.key === 'End') next = TABS[TABS.length - 1];
2152
+ if (event.key === 'ArrowRight') next = TABS[(index + 1) % TABS.length];
2153
+ if (event.key === 'ArrowLeft') next = TABS[(index + TABS.length - 1) % TABS.length];
2154
+ if (!next) return;
2155
+ setTab(next);
2156
+ const buttons = event.currentTarget.querySelectorAll('[role="tab"]');
2157
+ buttons.forEach((button) => { if (button.dataset.tab === next) button.focus(); });
2158
+ }
2159
+ async function reloadData(forProject = projectId) {
2160
+ const query = forProject ? '?project=' + encodeURIComponent(forProject) : '';
2161
+ const response = await fetch('/ai-team/api/bootstrap' + query, { headers: { accept: 'application/json' } });
2162
+ if (!response.ok) throw new Error('HTTP ' + response.status);
2163
+ const next = normalizePayload(await response.json());
2164
+ if (next) setData(next);
2165
+ }
2166
+ async function runCommand(type, payload, targetProject = projectId) {
2167
+ const target = targetProject || 'prj_main';
2168
+ const response = await fetch('/ai-team/api/projects/' + encodeURIComponent(target) + '/commands', {
2169
+ method: 'POST',
2170
+ headers: { 'content-type': 'application/json', accept: 'application/json' },
2171
+ body: JSON.stringify({ type, payload }),
2172
+ });
2173
+ const body = await response.json().catch(() => null);
2174
+ if (!response.ok) throw new Error(body?.error?.message ?? ('HTTP ' + response.status));
2175
+ return body;
2176
+ }
2177
+ function openAddAgent() {
2178
+ const defaults = data?.runtime?.defaultModel ?? {};
2179
+ setMName('');
2180
+ setMRole('');
2181
+ setMPrompt('');
2182
+ setMProvider(defaults.provider ?? '');
2183
+ setMModel(defaults.model ?? '');
2184
+ setMOwner((data?.agents ?? []).length === 0);
2185
+ setMError('');
2186
+ setAddOpen(true);
2187
+ }
2188
+ async function createAgent() {
2189
+ if (!mName.trim() || mBusy) return;
2190
+ setMBusy(true);
2191
+ setMError('');
2192
+ try {
2193
+ const added = await runCommand('member.add', {
2194
+ name: mName.trim(),
2195
+ role: mRole.trim(),
2196
+ kind: 'agent',
2197
+ status: 'provisioning',
2198
+ });
2199
+ const memberId = added.member.id;
2200
+ if (mProvider.trim() || mModel.trim()) {
2201
+ await runCommand('member.config.update', {
2202
+ memberId,
2203
+ patch: {
2204
+ ...(mProvider.trim() ? { provider: mProvider.trim() } : {}),
2205
+ ...(mModel.trim() ? { model: mModel.trim() } : {}),
2206
+ },
2207
+ reason: 'workbench add member',
2208
+ });
2209
+ }
2210
+ await runCommand('member.activate', {
2211
+ memberId,
2212
+ prompt: mPrompt.trim() || (mOwner
2213
+ ? '你是 Project owner。等待 Human 的 Main stream 消息;收到后用 team 工具 create 开卡并推进。不要修改无关文件。'
2214
+ : '你已加入 AI Team。等待 Team event;先读 tasks/inbox,再行动。不要修改无关文件。'),
2215
+ });
2216
+ if (mOwner) {
2217
+ await runCommand('project.owner.update', { ownerMemberId: memberId, reason: 'workbench add member' });
2218
+ }
2219
+ setAddOpen(false);
2220
+ say(t('memberAdded'));
2221
+ await reloadData();
2222
+ } catch (error) {
2223
+ setMError(error?.message ?? String(error));
2224
+ } finally {
2225
+ setMBusy(false);
2226
+ }
2227
+ }
2228
+ function openProfile(agent) {
2229
+ setProfileAgent(agent);
2230
+ setPName(agent.name ?? '');
2231
+ setPRole(agent.role ?? '');
2232
+ setPPersonality(agent.personality ?? '');
2233
+ setPPreset(agent.preset ?? agent.config?.preset ?? 'standard');
2234
+ setPProvider(agent.config?.provider ?? '');
2235
+ setPModel(agent.config?.model ?? '');
2236
+ setPMaxTokens(agent.config?.maxTokens ?? '');
2237
+ setPNote('');
2238
+ setPSkill('');
2239
+ setPMessage('');
2240
+ const successor = (data?.agents ?? []).find((candidate) => candidate.id !== agent.id && candidate.status !== 'retired');
2241
+ setPSuccessor(successor?.id ?? '');
2242
+ setPError('');
2243
+ }
2244
+ async function saveProfile() {
2245
+ if (!profileAgent || pBusy) return;
2246
+ setPBusy(true);
2247
+ setPError('');
2248
+ try {
2249
+ await runCommand('member.profile.update', {
2250
+ memberId: profileAgent.id,
2251
+ name: pName.trim(),
2252
+ role: pRole.trim(),
2253
+ personality: pPersonality,
2254
+ reason: 'workbench profile',
2255
+ });
2256
+ const config = profileAgent.config ?? {};
2257
+ const patch = {};
2258
+ if (pPreset !== (config.preset ?? profileAgent.preset ?? 'standard')) patch.preset = pPreset;
2259
+ if (pProvider.trim() !== (config.provider ?? '')) patch.provider = pProvider.trim();
2260
+ if (pModel.trim() !== (config.model ?? '')) patch.model = pModel.trim();
2261
+ const maxTokens = Number(pMaxTokens);
2262
+ if (Number.isSafeInteger(maxTokens) && maxTokens > 0 && maxTokens !== config.maxTokens) patch.maxTokens = maxTokens;
2263
+ if (Object.keys(patch).length > 0) {
2264
+ await runCommand('member.config.update', {
2265
+ memberId: profileAgent.id,
2266
+ patch,
2267
+ reason: 'workbench config',
2268
+ });
2269
+ }
2270
+ setProfileAgent(null);
2271
+ say(t('profileSaved'));
2272
+ await reloadData();
2273
+ } catch (error) {
2274
+ setPError(error?.message ?? String(error));
2275
+ } finally {
2276
+ setPBusy(false);
2277
+ }
2278
+ }
2279
+ async function sendMemberMessage() {
2280
+ if (!profileAgent || !pMessage.trim()) return;
2281
+ try {
2282
+ await runCommand('member.message', { memberId: profileAgent.id, body: pMessage.trim() });
2283
+ setPMessage('');
2284
+ say(t('messageSent'));
2285
+ await reloadData();
2286
+ } catch (error) {
2287
+ setPError(error?.message ?? String(error));
2288
+ }
2289
+ }
2290
+ async function addNote() {
2291
+ if (!profileAgent || !pNote.trim()) return;
2292
+ try {
2293
+ const result = await runCommand('member.note', { memberId: profileAgent.id, text: pNote.trim() });
2294
+ setPNote('');
2295
+ setProfileAgent(result.member);
2296
+ await reloadData();
2297
+ } catch (error) {
2298
+ setPError(error?.message ?? String(error));
2299
+ }
2300
+ }
2301
+ async function addSkill() {
2302
+ if (!profileAgent || !pSkill.trim()) return;
2303
+ try {
2304
+ const result = await runCommand('member.skill', { memberId: profileAgent.id, name: pSkill.trim() });
2305
+ setPSkill('');
2306
+ setProfileAgent(result.member);
2307
+ await reloadData();
2308
+ } catch (error) {
2309
+ setPError(error?.message ?? String(error));
2310
+ }
2311
+ }
2312
+ async function retryProfile() {
2313
+ if (!profileAgent || pBusy) return;
2314
+ setPBusy(true);
2315
+ setPError('');
2316
+ try {
2317
+ await runCommand('member.retry', { memberId: profileAgent.id });
2318
+ const result = await runCommand('member.activate', {
2319
+ memberId: profileAgent.id,
2320
+ prompt: '你已被重试。先读 tasks/inbox,继续未完成的工作;不要修改无关文件。',
2321
+ });
2322
+ setProfileAgent(result.member);
2323
+ say(t('retried'));
2324
+ await reloadData();
2325
+ } catch (error) {
2326
+ setPError(error?.message ?? String(error));
2327
+ } finally {
2328
+ setPBusy(false);
2329
+ }
2330
+ }
2331
+ async function distillProfile() {
2332
+ if (!profileAgent || pBusy) return;
2333
+ setPBusy(true);
2334
+ setPError('');
2335
+ try {
2336
+ const result = await runCommand('member.distill', {
2337
+ memberId: profileAgent.id,
2338
+ mode: 'model',
2339
+ reason: 'workbench re-distill',
2340
+ });
2341
+ setPPersonality(result.member.personality);
2342
+ setProfileAgent(result.member);
2343
+ say(t('profileDistilled'));
2344
+ await reloadData();
2345
+ } catch (error) {
2346
+ setPError(error?.message ?? String(error));
2347
+ } finally {
2348
+ setPBusy(false);
2349
+ }
2350
+ }
2351
+ async function leaveProfile() {
2352
+ if (!profileAgent || pBusy) return;
2353
+ if (typeof window.confirm === 'function' && !window.confirm(t('leaveConfirm'))) return;
2354
+ setPBusy(true);
2355
+ setPError('');
2356
+ try {
2357
+ const result = await runCommand('member.leave', { memberId: profileAgent.id, reason: 'workbench leave' });
2358
+ const unfinished = result.unfinishedTasks ?? [];
2359
+ let handed = 0;
2360
+ if (pSuccessor && unfinished.length > 0) {
2361
+ const taken = await runCommand('member.takeover', {
2362
+ fromMemberId: profileAgent.id,
2363
+ toMemberId: pSuccessor,
2364
+ reason: 'workbench successor',
2365
+ });
2366
+ handed = taken.tasks?.length ?? 0;
2367
+ }
2368
+ setProfileAgent(null);
2369
+ say(handed > 0 ? tf(t, 'leftTeamHanded', { n: handed }) : tf(t, 'leftTeam', { n: unfinished.length }));
2370
+ await reloadData();
2371
+ } catch (error) {
2372
+ setPError(error?.message ?? String(error));
2373
+ } finally {
2374
+ setPBusy(false);
2375
+ }
2376
+ }
2377
+ async function recruit(agent) {
2378
+ try {
2379
+ await runCommand('member.rejoin', { memberId: agent.id, reason: 'workbench recruit' });
2380
+ await runCommand('member.activate', {
2381
+ memberId: agent.id,
2382
+ prompt: '你从人才库回到团队。先读 tasks/inbox;若有遗留工作,由 Human/新 owner 决定是否接手。不要修改无关文件。',
2383
+ });
2384
+ say(t('recruited'));
2385
+ await reloadData();
2386
+ } catch (error) {
2387
+ say(tf(t, 'sendFailed', { message: error?.message ?? String(error) }));
2388
+ }
2389
+ }
2390
+ async function openInboxTask(item) {
2391
+ if (item.projectId && item.projectId !== projectId) {
2392
+ await switchProject({ id: item.projectId, name: item.projectName });
2393
+ }
2394
+ setScope('project');
2395
+ setSelected(item.ref?.taskId ?? '');
2396
+ setThreadOpen(true);
2397
+ setRightNote('');
2398
+ }
2399
+ async function ackInbox(item) {
2400
+ try {
2401
+ await runCommand('inbox.ack', { itemId: item.id, disposition: 'read' }, item.projectId ?? projectId);
2402
+ say(t('inboxAcked'));
2403
+ await reloadData();
2404
+ } catch (error) {
2405
+ say(tf(t, 'sendFailed', { message: error?.message ?? String(error) }));
2406
+ }
2407
+ }
2408
+ async function send() {
2409
+ const text = draft.trim();
2410
+ if (!text) return;
2411
+ try {
2412
+ if (mode === 'task') {
2413
+ await runCommand('task.create', { title: text, resultDefinition: text, qaRequired: qaTask });
2414
+ } else {
2415
+ await runCommand('stream.post', { body: text });
2416
+ }
2417
+ setDraft('');
2418
+ say(mode === 'task' ? t('taskSent') : t('discussSent'));
2419
+ await reloadData();
2420
+ } catch (error) {
2421
+ say(tf(t, 'sendFailed', { message: error?.message ?? String(error) }));
2422
+ }
2423
+ }
2424
+ async function sendReply() {
2425
+ const text = reply.trim();
2426
+ if (!text || !selectedTask) return;
2427
+ try {
2428
+ const result = await runCommand('thread.post', {
2429
+ taskId: selectedTask.id,
2430
+ baseSeq: replyBase ?? threadHead ?? 0,
2431
+ body: text,
2432
+ });
2433
+ setReply('');
2434
+ sayRight(tf(t, 'replySent', { seq: result.entry?.seq ?? '' }));
2435
+ await reloadData();
2436
+ } catch (error) {
2437
+ sayRight(tf(t, 'replyFailed', { message: error?.message ?? String(error) }));
2438
+ }
2439
+ }
2440
+ function openConvert() {
2441
+ if (converting) { setConverting(false); return; }
2442
+ if (!selectedTask) return;
2443
+ if (!cTitle.trim()) {
2444
+ const source = thread?.entries?.[0]?.body ?? selectedTask.title;
2445
+ setCTitle(firstSentence(source).slice(0, 40));
2446
+ }
2447
+ setConverting(true);
2448
+ }
2449
+ function submitConvert() {
2450
+ if (!selectedTask || !cTitle.trim() || !cDod.trim()) return;
2451
+ sayRight(tf(t, 'convertMock', { n: taskNo(selectedTask) }));
2452
+ setConverting(false);
2453
+ setCTitle('');
2454
+ setCDod('');
2455
+ }
2456
+ async function acceptTask() {
2457
+ if (!selectedTask) return;
2458
+ try {
2459
+ await runCommand('task.accept', {
2460
+ taskId: selectedTask.id,
2461
+ expectedRevision: selectedTask.revision,
2462
+ note: t('acceptNote'),
2463
+ confirmed: acceptChecked,
2464
+ checks: qaChecks,
2465
+ });
2466
+ sayRight(tf(t, 'acceptDone', { n: taskNo(selectedTask) }));
2467
+ await reloadData();
2468
+ } catch (error) {
2469
+ sayRight(tf(t, 'acceptFailed', { message: error?.message ?? String(error) }));
2470
+ }
2471
+ }
2472
+ async function requestChanges() {
2473
+ if (!selectedTask) return;
2474
+ const note = reply.trim();
2475
+ if (!note) {
2476
+ sayRight(t('changesNeedNote'));
2477
+ return;
2478
+ }
2479
+ try {
2480
+ await runCommand('task.request_changes', {
2481
+ taskId: selectedTask.id,
2482
+ expectedRevision: selectedTask.revision,
2483
+ note,
2484
+ });
2485
+ setReply('');
2486
+ sayRight(tf(t, 'changesDone', { n: taskNo(selectedTask) }));
2487
+ await reloadData();
2488
+ } catch (error) {
2489
+ sayRight(tf(t, 'changesFailed', { message: error?.message ?? String(error) }));
2490
+ }
2491
+ }
2492
+ async function reassignSelected() {
2493
+ if (!selectedTask) return;
2494
+ const options = (data?.agents ?? []).filter((agent) => agent.status !== 'retired');
2495
+ if (options.length === 0) {
2496
+ sayRight(t('reassignNoTarget'));
2497
+ return;
2498
+ }
2499
+ const answer = typeof window.prompt === 'function'
2500
+ ? window.prompt(t('reassignPrompt') + '\n' + options.map((agent) => agent.name).join('、'), options[0].name)
2501
+ : null;
2502
+ if (answer === null) return;
2503
+ const target = options.find((agent) => agent.name === answer.trim() || agent.id === answer.trim());
2504
+ if (target === undefined) {
2505
+ sayRight(t('reassignNoTarget'));
2506
+ return;
2507
+ }
2508
+ try {
2509
+ await runCommand('task.reassign', {
2510
+ taskId: selectedTask.id,
2511
+ expectedRevision: selectedTask.revision,
2512
+ ownerId: target.id,
2513
+ reason: 'workbench reassign',
2514
+ });
2515
+ sayRight(tf(t, 'reassignDone', { n: taskNo(selectedTask), name: target.name }));
2516
+ await reloadData();
2517
+ } catch (error) {
2518
+ sayRight(tf(t, 'sendFailed', { message: error?.message ?? String(error) }));
2519
+ }
2520
+ }
2521
+ async function cancelSelected() {
2522
+ if (!selectedTask) return;
2523
+ const reason = typeof window.prompt === 'function' ? window.prompt(t('cancelPrompt'), '') : null;
2524
+ if (reason === null) return;
2525
+ try {
2526
+ await runCommand('task.cancel', { taskId: selectedTask.id, expectedRevision: selectedTask.revision, reason });
2527
+ sayRight(tf(t, 'cancelDone', { n: taskNo(selectedTask) }));
2528
+ await reloadData();
2529
+ } catch (error) {
2530
+ sayRight(tf(t, 'sendFailed', { message: error?.message ?? String(error) }));
2531
+ }
2532
+ }
2533
+ async function reopenSelected() {
2534
+ if (!selectedTask) return;
2535
+ const reason = typeof window.prompt === 'function' ? window.prompt(t('reopenPrompt'), '') : null;
2536
+ if (reason === null) return;
2537
+ try {
2538
+ await runCommand('task.reopen', { taskId: selectedTask.id, expectedRevision: selectedTask.revision, reason });
2539
+ sayRight(tf(t, 'reopenDone', { n: taskNo(selectedTask) }));
2540
+ await reloadData();
2541
+ } catch (error) {
2542
+ sayRight(tf(t, 'sendFailed', { message: error?.message ?? String(error) }));
2543
+ }
2544
+ }
2545
+ async function deleteSelected() {
2546
+ if (!selectedTask) return;
2547
+ if (typeof window.confirm === 'function' && !window.confirm(t('deleteConfirm'))) return;
2548
+ try {
2549
+ await runCommand('task.delete', { taskId: selectedTask.id, expectedRevision: selectedTask.revision });
2550
+ sayRight(tf(t, 'deleteDone', { n: taskNo(selectedTask) }));
2551
+ await reloadData();
2552
+ } catch (error) {
2553
+ sayRight(tf(t, 'sendFailed', { message: error?.message ?? String(error) }));
2554
+ }
2555
+ }
2556
+ function continueFromLatest() {
2557
+ const latest = threadHead;
2558
+ setReplyBase(latest);
2559
+ sayRight(tf(t, 'conflictContinueMock', { head: latest }));
2560
+ }
2561
+ function contextAtBase() {
2562
+ sayRight(tf(t, 'conflictContextMock', { base: replyBase ?? 0 }));
2563
+ }
2564
+
2565
+ const actions = [];
2566
+ if (selectedTask) {
2567
+ const status = selectedTask.status;
2568
+ if (status !== 'done' && status !== 'cancelled') actions.push({ label: t('actCancel'), onClick: cancelSelected });
2569
+ if (status === 'done' || status === 'pending_acceptance') actions.push({ label: t('actReopen'), onClick: reopenSelected });
2570
+ if (status !== 'done' && status !== 'cancelled') actions.push({ label: t('actReassign'), onClick: reassignSelected });
2571
+ if (status === 'pending' && !selectedTask.assignee && !selectedTask.replies) actions.push({ label: t('actDelete'), onClick: deleteSelected });
2572
+ }
2573
+
2574
+ const inInbox = scope === 'inbox';
2575
+ const headerTitle = inInbox ? t('inboxTitle') : project ? '# ' + project.name : t('noProject');
2576
+ const headerGoal = inInbox ? t('inboxGoal') : (project?.goal ?? '');
2577
+ const unread = inboxUnread > 99 ? '99+' : inboxUnread;
2578
+
2579
+ const leftProps = {
2580
+ t,
2581
+ data,
2582
+ projectId,
2583
+ scope,
2584
+ tab,
2585
+ inboxUnread,
2586
+ onBack: () => { closeMobile(); goConversation(); },
2587
+ onInbox: switchInbox,
2588
+ onBoard: switchBoard,
2589
+ onProject: switchProject,
2590
+ onNewProject: () => { closeMobile(); openNewProject(); },
2591
+ onAgent: (agent) => { closeMobile(); openProfile(agent); },
2592
+ onAddAgent: () => { closeMobile(); openAddAgent(); },
2593
+ onRecruit: (agent) => { closeMobile(); recruit(agent); },
2594
+ };
2595
+
2596
+ const body = inInbox
2597
+ ? h('div', { className: 'at-stream', role: 'region', 'aria-label': t('inboxTitle') },
2598
+ h('div', { className: 'at-streaminner' },
2599
+ (data.inbox ?? []).length
2600
+ ? (data.inbox ?? []).map((item) => h('div', { key: item.id, className: cx('at-inboxitem', item.state !== 'unread' && 'at-inboxdone') },
2601
+ h('div', { className: 'at-inboxbody' },
2602
+ h('div', { className: 'at-inboxtitle' },
2603
+ item.type === 'decision_required'
2604
+ ? t('inboxDecisionTitle')
2605
+ : item.type === 'followed_task_update'
2606
+ ? tf(t, 'inboxCompletedTitle', { title: item.ref?.title ?? item.ref?.taskId ?? '' })
2607
+ : t('inboxItemTitle')),
2608
+ h('div', { className: 'at-inboxmeta' },
2609
+ item.projectName ? item.projectName + ' · ' : '',
2610
+ item.ref?.category ? item.ref.category + ' · ' : '',
2611
+ item.ref?.memberId ? item.ref.memberId : '',
2612
+ ' · ',
2613
+ fmtTime(item.createdAt)),
2614
+ item.state !== 'unread' ? h('div', { className: 'at-inboxstate' }, item.state) : null),
2615
+ h('div', { className: 'at-inboxactions' },
2616
+ item.ref?.taskId ? h('button', { className: 'at-btn at-btn-small', type: 'button', onClick: () => openInboxTask(item) }, t('inboxOpenTask')) : null,
2617
+ item.state === 'unread' ? h('button', { className: 'at-btn at-btn-small at-btn-primary', type: 'button', onClick: () => ackInbox(item) }, t('inboxAck')) : null)))
2618
+ : h(EmptyState, {
2619
+ t,
2620
+ icon: 'inbox',
2621
+ title: t('inboxEmptyTitle'),
2622
+ body: t('inboxEmptyBody'),
2623
+ action: { label: t('inboxEmptyAction'), onClick: switchBoard },
2624
+ })))
2625
+ : tab === 'board'
2626
+ ? h(Board, {
2627
+ t,
2628
+ tasks,
2629
+ selected,
2630
+ follows: followingOf,
2631
+ doneOpen,
2632
+ onToggleDone: () => setDoneOpen((value) => !value),
2633
+ onOpen: openTask,
2634
+ onToggleFollow: toggleFollow,
2635
+ filter: boardFilter,
2636
+ onClearFilter: () => setBoardFilter('all'),
2637
+ q1Extra,
2638
+ onBackMain: () => setTab('main'),
2639
+ })
2640
+ : tab === 'situation'
2641
+ ? h(Situation, { t, quadrants, tasks, onItem: openQuadrantItem, onViewAll: viewQuadrant })
2642
+ : h('div', { className: 'at-stream', role: 'tabpanel', id: 'at-panel-main', 'aria-labelledby': 'at-tab-main' },
2643
+ h('div', { className: 'at-streaminner', role: 'log', 'aria-live': 'polite', 'aria-label': t('streamAria') },
2644
+ data.stream.length
2645
+ ? data.stream.map((item) => h(StreamMessage, {
2646
+ key: item.id,
2647
+ t,
2648
+ item,
2649
+ data,
2650
+ tasks,
2651
+ selected,
2652
+ follows: followingOf,
2653
+ onOpen: openTask,
2654
+ onToggleFollow: toggleFollow,
2655
+ }))
2656
+ : h(EmptyState, {
2657
+ t,
2658
+ title: t('emptyStreamTitle'),
2659
+ body: t('emptyStreamBody'),
2660
+ action: { label: t('fillExample'), onClick: () => setDraft(t('emptyStreamExample')) },
2661
+ })));
2662
+
2663
+ return h('div', { className: 'at-root', role: 'region', 'aria-label': t('panel') },
2664
+ h('div', { className: 'at-top' },
2665
+ h('span', { className: 'at-brand' }, t('panel')),
2666
+ h('span', { className: 'at-spike', title: t('spikeTitle') }, t('spike')),
2667
+ h('details', { className: 'at-mobile', ref: mobileRef },
2668
+ h('summary', { className: 'at-mobilesummary', 'aria-label': t('menuAria') }, h(Ico, { name: 'menu', size: 14 }), h('span', null, t('menu'))),
2669
+ h('div', { className: 'at-mobilepop' }, h(LeftPanel, leftProps))),
2670
+ h('span', { className: 'at-topproject', title: headerTitle }, inInbox ? t('inboxTitle') + (unread ? ' · ' + unread : '') : (project?.name ?? ''))),
2671
+ h('div', { className: cx('at-grid', threadOpen && selectedTask && 'at-withthread') },
2672
+ h('aside', { className: 'at-left', role: 'region', 'aria-label': t('projectsAria') }, h(LeftPanel, leftProps)),
2673
+ h('main', { className: 'at-center' },
2674
+ h('header', { className: 'at-chead' },
2675
+ h('div', { className: 'at-title', title: headerTitle }, headerTitle),
2676
+ headerGoal ? h('div', { className: 'at-goal', title: headerGoal }, headerGoal) : null,
2677
+ inInbox ? null : h('div', { className: 'at-tabs', role: 'tablist', 'aria-label': t('tabsAria'), onKeyDown: tabKeyDown },
2678
+ h('button', {
2679
+ className: cx('at-tab', tab === 'main' && 'at-on'),
2680
+ type: 'button',
2681
+ role: 'tab',
2682
+ id: 'at-tab-main',
2683
+ 'data-tab': 'main',
2684
+ 'aria-selected': tab === 'main',
2685
+ 'aria-controls': 'at-panel-main',
2686
+ tabIndex: tab === 'main' ? 0 : -1,
2687
+ onClick: () => selectTab('main'),
2688
+ }, t('main')),
2689
+ h('button', {
2690
+ className: cx('at-tab', tab === 'board' && 'at-on'),
2691
+ type: 'button',
2692
+ role: 'tab',
2693
+ id: 'at-tab-board',
2694
+ 'data-tab': 'board',
2695
+ 'aria-selected': tab === 'board',
2696
+ 'aria-controls': 'at-panel-board',
2697
+ tabIndex: tab === 'board' ? 0 : -1,
2698
+ onClick: () => selectTab('board'),
2699
+ }, t('boardTab'), h('span', { className: 'at-tabcount' }, boardTaskCount)),
2700
+ h('button', {
2701
+ className: cx('at-tab', tab === 'situation' && 'at-on'),
2702
+ type: 'button',
2703
+ role: 'tab',
2704
+ id: 'at-tab-situation',
2705
+ 'data-tab': 'situation',
2706
+ 'aria-selected': tab === 'situation',
2707
+ 'aria-controls': 'at-panel-situation',
2708
+ tabIndex: tab === 'situation' ? 0 : -1,
2709
+ onClick: () => selectTab('situation'),
2710
+ }, t('tabSituation')))),
2711
+ phase === 'fallback' ? h('div', { className: 'at-banner', role: 'note' }, h('b', null, t('fallbackTitle') + ' · '), h('span', null, t('fallbackBody'))) : null,
2712
+ note ? h('div', { className: 'at-notewrap' },
2713
+ h('div', { className: 'at-note', role: 'status' },
2714
+ h('span', { className: 'at-notetext' }, note),
2715
+ h('button', { className: 'at-noteclose', type: 'button', 'aria-label': t('dismiss'), title: t('dismiss'), onClick: () => setNote('') }, h(Ico, { name: 'close', size: 12 })))) : null,
2716
+ body,
2717
+ !inInbox && tab === 'main' ? h(Composer, {
2718
+ t,
2719
+ mode,
2720
+ draft,
2721
+ disabled: !project,
2722
+ qaRequired: qaTask,
2723
+ onQaRequired: setQaTask,
2724
+ onMode: setMode,
2725
+ onDraft: setDraft,
2726
+ onSend: send,
2727
+ }) : null),
2728
+ threadOpen && selectedTask ? h(ThreadPanel, {
2729
+ t,
2730
+ task: selectedTask,
2731
+ thread,
2732
+ project,
2733
+ following: followingOf(selectedTask),
2734
+ onToggleFollow: () => toggleFollow(selectedTask),
2735
+ onClose: closeThread,
2736
+ converting,
2737
+ convert: {
2738
+ onOpen: openConvert,
2739
+ title: cTitle,
2740
+ dod: cDod,
2741
+ onTitle: setCTitle,
2742
+ onDod: setCDod,
2743
+ onCancel: () => setConverting(false),
2744
+ onSubmit: submitConvert,
2745
+ },
2746
+ reply,
2747
+ onReplyChange: setReply,
2748
+ onReplySend: sendReply,
2749
+ replyBase,
2750
+ onContinue: continueFromLatest,
2751
+ onContext: contextAtBase,
2752
+ note: rightNote,
2753
+ onDismissNote: () => setRightNote(''),
2754
+ acceptChecked,
2755
+ onAcceptChecked: setAcceptChecked,
2756
+ qaChecks,
2757
+ onQaCheck: (index, passed) => setQaChecks((prev) => prev.map((entry, i) => i === index ? { ...entry, passed } : entry)),
2758
+ onAccept: acceptTask,
2759
+ onChanges: requestChanges,
2760
+ actions,
2761
+ panelRef: rightRef,
2762
+ replyRef,
2763
+ }) : null,
2764
+ addOpen ? h('div', { className: 'at-modalback' },
2765
+ h('form', {
2766
+ className: 'at-modal',
2767
+ role: 'dialog',
2768
+ 'aria-modal': 'true',
2769
+ 'aria-label': t('addAgentTitle'),
2770
+ onSubmit: (event) => { event.preventDefault(); createAgent(); },
2771
+ },
2772
+ h('h2', { className: 'at-modaltitle' }, t('addAgentTitle')),
2773
+ h('label', { className: 'at-field' }, h('span', null, t('memberName')),
2774
+ h('input', { value: mName, onChange: (event) => setMName(event.target.value), autoFocus: true })),
2775
+ h('label', { className: 'at-field' }, h('span', null, t('memberRole')),
2776
+ h('input', { value: mRole, onChange: (event) => setMRole(event.target.value) })),
2777
+ h('label', { className: 'at-field' }, h('span', null, t('memberPrompt')),
2778
+ h('textarea', {
2779
+ value: mPrompt,
2780
+ onChange: (event) => setMPrompt(event.target.value),
2781
+ placeholder: t('memberPromptHint'),
2782
+ })),
2783
+ h('div', { className: 'at-fieldrow' },
2784
+ h('label', { className: 'at-field' }, h('span', null, t('memberProvider')),
2785
+ h('input', { value: mProvider, onChange: (event) => setMProvider(event.target.value), placeholder: 'deepseek-account' })),
2786
+ h('label', { className: 'at-field' }, h('span', null, t('memberModel')),
2787
+ h('input', { value: mModel, onChange: (event) => setMModel(event.target.value), placeholder: 'deepseek-flash' }))),
2788
+ h('label', { className: 'at-check' },
2789
+ h('input', { type: 'checkbox', checked: mOwner, onChange: (event) => setMOwner(event.target.checked) }),
2790
+ h('span', null, t('memberOwner'))),
2791
+ mError ? h('div', { className: 'at-modalerror', role: 'alert' }, mError) : null,
2792
+ h('div', { className: 'at-modalactions' },
2793
+ h('button', { className: 'at-btn', type: 'button', onClick: () => setAddOpen(false), disabled: mBusy }, t('cancel')),
2794
+ h('button', { className: 'at-btn at-btn-primary', type: 'submit', disabled: mBusy || !mName.trim() }, mBusy ? t('memberAdding') : t('memberCreate'))))) : null,
2795
+ profileAgent ? h('div', { className: 'at-modalback' },
2796
+ h('form', {
2797
+ className: 'at-modal',
2798
+ role: 'dialog',
2799
+ 'aria-modal': 'true',
2800
+ 'aria-label': t('memberProfileTitle'),
2801
+ onSubmit: (event) => { event.preventDefault(); saveProfile(); },
2802
+ },
2803
+ h('h2', { className: 'at-modaltitle' }, tf(t, 'memberProfileTitleName', { name: profileAgent.name })),
2804
+ h('div', { className: 'at-modalmeta' },
2805
+ tf(t, 'memberProfileMeta', {
2806
+ status: t(AGENT_STATUS[profileAgent.status] ?? 'agentIdle'),
2807
+ version: profileAgent.profileVersion ?? 1,
2808
+ distill: profileAgent.distillVersion ?? 0,
2809
+ })),
2810
+ (profileAgent.skills?.length || profileAgent.notes?.length)
2811
+ ? h('div', { className: 'at-modalmeta' },
2812
+ (profileAgent.skills?.length ? t('memberSkills') + ':' + profileAgent.skills.join('、') : ''),
2813
+ (profileAgent.skills?.length && profileAgent.notes?.length ? ' · ' : ''),
2814
+ (profileAgent.notes?.length ? t('memberNotes') + ':' + profileAgent.notes.slice(-3).join(';') : ''))
2815
+ : null,
2816
+ h('label', { className: 'at-field' }, h('span', null, t('memberName')),
2817
+ h('input', { value: pName, onChange: (event) => setPName(event.target.value), autoFocus: true })),
2818
+ h('label', { className: 'at-field' }, h('span', null, t('memberRole')),
2819
+ h('input', { value: pRole, onChange: (event) => setPRole(event.target.value) })),
2820
+ h('label', { className: 'at-field' }, h('span', null, t('memberPersonality')),
2821
+ h('textarea', { value: pPersonality, onChange: (event) => setPPersonality(event.target.value) })),
2822
+ h('div', { className: 'at-fieldrow' },
2823
+ h('label', { className: 'at-field' }, h('span', null, t('memberPreset')),
2824
+ h('select', { value: pPreset, onChange: (event) => setPPreset(event.target.value) },
2825
+ h('option', { value: 'standard' }, t('presetStandard')),
2826
+ h('option', { value: 'ptc' }, t('presetPtc')),
2827
+ h('option', { value: 'minimal' }, t('presetMinimal')),
2828
+ h('option', { value: 'cordis' }, t('presetCordis')))),
2829
+ h('label', { className: 'at-field' }, h('span', null, t('memberProvider')),
2830
+ h('input', { value: pProvider, onChange: (event) => setPProvider(event.target.value), placeholder: 'deepseek-account' })),
2831
+ h('label', { className: 'at-field' }, h('span', null, t('memberModel')),
2832
+ h('input', { value: pModel, onChange: (event) => setPModel(event.target.value), placeholder: 'deepseek-flash' })),
2833
+ h('label', { className: 'at-field' }, h('span', null, t('memberMaxTokens')),
2834
+ h('input', { value: pMaxTokens, onChange: (event) => setPMaxTokens(event.target.value), inputMode: 'numeric' }))),
2835
+ h('label', { className: 'at-field' }, h('span', null, t('successor')),
2836
+ h('select', {
2837
+ value: pSuccessor,
2838
+ onChange: (event) => setPSuccessor(event.target.value),
2839
+ },
2840
+ h('option', { value: '' }, t('noSuccessor')),
2841
+ (data?.agents ?? []).filter((candidate) => candidate.id !== profileAgent.id && candidate.status !== 'retired')
2842
+ .map((candidate) => h('option', { key: candidate.id, value: candidate.id }, candidate.name + (candidate.qa ? ' · QA' : ''))))),
2843
+ h('div', { className: 'at-inlineadd' },
2844
+ h('input', { value: pNote, onChange: (event) => setPNote(event.target.value), placeholder: t('notePlaceholder') }),
2845
+ h('button', { className: 'at-btn at-btn-small', type: 'button', onClick: addNote, disabled: !pNote.trim() }, t('addNote'))),
2846
+ h('div', { className: 'at-inlineadd' },
2847
+ h('input', { value: pSkill, onChange: (event) => setPSkill(event.target.value), placeholder: t('skillPlaceholder') }),
2848
+ h('button', { className: 'at-btn at-btn-small', type: 'button', onClick: addSkill, disabled: !pSkill.trim() }, t('addSkill'))),
2849
+ h('div', { className: 'at-inlineadd' },
2850
+ h('input', { value: pMessage, onChange: (event) => setPMessage(event.target.value), placeholder: t('messagePlaceholder') }),
2851
+ h('button', { className: 'at-btn at-btn-small', type: 'button', onClick: sendMemberMessage, disabled: !pMessage.trim() }, t('sendMessage'))),
2852
+ pError ? h('div', { className: 'at-modalerror', role: 'alert' }, pError) : null,
2853
+ h('div', { className: 'at-modalactions' },
2854
+ h('button', { className: 'at-btn', type: 'button', onClick: () => setProfileAgent(null), disabled: pBusy }, t('cancel')),
2855
+ h('button', { className: 'at-btn', type: 'button', onClick: leaveProfile, disabled: pBusy }, t('leaveTeam')),
2856
+ profileAgent.status === 'failed' ? h('button', { className: 'at-btn', type: 'button', onClick: retryProfile, disabled: pBusy }, t('retry')) : null,
2857
+ h('button', { className: 'at-btn', type: 'button', onClick: distillProfile, disabled: pBusy }, t('redistill')),
2858
+ h('button', { className: 'at-btn at-btn-primary', type: 'submit', disabled: pBusy || !pName.trim() }, pBusy ? t('saving') : t('save'))))) : null,
2859
+ newOpen ? h('div', { className: 'at-modalback' },
2860
+ h('form', {
2861
+ className: 'at-modal',
2862
+ role: 'dialog',
2863
+ 'aria-modal': 'true',
2864
+ 'aria-label': t('newProjectTitle'),
2865
+ onSubmit: (event) => { event.preventDefault(); createProject(); },
2866
+ },
2867
+ h('h2', { className: 'at-modaltitle' }, t('newProjectTitle')),
2868
+ h('label', { className: 'at-field' }, h('span', null, t('projectName')),
2869
+ h('input', { value: nName, onChange: (event) => setNName(event.target.value), autoFocus: true })),
2870
+ h('label', { className: 'at-field' }, h('span', null, t('projectGoal')),
2871
+ h('textarea', { value: nGoal, onChange: (event) => setNGoal(event.target.value) })),
2872
+ nError ? h('div', { className: 'at-modalerror', role: 'alert' }, nError) : null,
2873
+ h('div', { className: 'at-modalactions' },
2874
+ h('button', { className: 'at-btn', type: 'button', onClick: () => setNewOpen(false), disabled: nBusy }, t('cancel')),
2875
+ h('button', { className: 'at-btn at-btn-primary', type: 'submit', disabled: nBusy || !nName.trim() }, nBusy ? t('saving') : t('projectCreate'))))) : null));
2876
+ }
2877
+
2878
+ function PanelIcon() {
2879
+ return h('span', { style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: 'inherit' } }, h(Ico, { name: 'loop' }));
2880
+ }
2881
+
2882
+ function apply(ctx) {
2883
+ ctx.effect(() => {
2884
+ const tag = document.createElement('style');
2885
+ tag.dataset.plugin = PKG;
2886
+ tag.textContent = CSS;
2887
+ document.head.append(tag);
2888
+ return () => tag.remove();
2889
+ }, `${PKG}: styles`);
2890
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), `${PKG}: locale`);
2891
+ const t = ctx.locale.bind(NS);
2892
+ ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({
2893
+ name: 'sidebar.panellist', id: 'ai-team', order: 25, label: () => ctx.locale.bind(NS)('panel'),
2894
+ }, PanelIcon));
2895
+ ctx.slots.inject('main', () => ctx.slots.register({ name: 'main', key: 'ai-team' }, () => h(Boundary, { t }, h(Workbench, { t, ctx }))));
2896
+ console.log(`${PKG}: client workbench spike applied`);
2897
+ }
2898
+
2899
+ return { inject: ['slots', 'locale'], apply };
2900
+ },
2901
+ });