@suifei/dsh-ai-team 0.0.0-stage → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +98 -0
- package/LICENSE +21 -0
- package/README.md +38 -2
- package/client.js +2901 -0
- package/cordis.patch.yml +31 -0
- package/icon.svg +27 -0
- package/index.js +893 -0
- package/lib/domain/errors.js +36 -0
- package/lib/domain/ids.js +26 -0
- package/lib/domain/inbox.js +87 -0
- package/lib/domain/member-config.js +168 -0
- package/lib/domain/member-profile.js +53 -0
- package/lib/domain/policy.js +102 -0
- package/lib/domain/task.js +309 -0
- package/lib/domain/thread.js +54 -0
- package/lib/host/fault-adapter.js +50 -0
- package/lib/host/member-tools.js +225 -0
- package/lib/host/permission-guard.js +28 -0
- package/lib/host/project-registry.js +68 -0
- package/lib/host/project-runtime.js +73 -0
- package/lib/host/service.js +810 -0
- package/lib/host/views.js +213 -0
- package/lib/host/waker.js +177 -0
- package/lib/ledger/checkpoint.js +395 -0
- package/lib/ledger/events.js +398 -0
- package/lib/ledger/fold.js +991 -0
- package/lib/ledger/writer.js +983 -0
- package/lib/runtime/failure-policy.js +113 -0
- package/lib/runtime/host-agent.js +101 -0
- package/lib/runtime/member-runtime.js +286 -0
- package/lib/runtime/skill-catalog.js +27 -0
- package/lib/runtime/skill-loader.js +26 -0
- package/lib/runtime/skill-provider.js +121 -0
- package/lib/vendor/skillopt/LICENSE +21 -0
- package/lib/vendor/skillopt/requirements.txt +31 -0
- package/lib/vendor/skillopt/scripts/sleep.py +119 -0
- package/lib/vendor/skillopt/skillopt_sleep/__init__.py +20 -0
- package/lib/vendor/skillopt/skillopt_sleep/__main__.py +942 -0
- package/lib/vendor/skillopt/skillopt_sleep/adapters/__init__.py +1 -0
- package/lib/vendor/skillopt/skillopt_sleep/adapters/superpowers.py +1379 -0
- package/lib/vendor/skillopt/skillopt_sleep/backend.py +2844 -0
- package/lib/vendor/skillopt/skillopt_sleep/budget.py +75 -0
- package/lib/vendor/skillopt/skillopt_sleep/config.py +257 -0
- package/lib/vendor/skillopt/skillopt_sleep/consolidate.py +558 -0
- package/lib/vendor/skillopt/skillopt_sleep/cycle.py +1270 -0
- package/lib/vendor/skillopt/skillopt_sleep/dream.py +173 -0
- package/lib/vendor/skillopt/skillopt_sleep/evalkit.py +718 -0
- package/lib/vendor/skillopt/skillopt_sleep/evidence.py +143 -0
- package/lib/vendor/skillopt/skillopt_sleep/experiments/__init__.py +1 -0
- package/lib/vendor/skillopt/skillopt_sleep/experiments/gbrain_bench.py +119 -0
- package/lib/vendor/skillopt/skillopt_sleep/experiments/personas.py +86 -0
- package/lib/vendor/skillopt/skillopt_sleep/experiments/report.py +132 -0
- package/lib/vendor/skillopt/skillopt_sleep/experiments/run_experiment.py +178 -0
- package/lib/vendor/skillopt/skillopt_sleep/experiments/run_gbrain.py +209 -0
- package/lib/vendor/skillopt/skillopt_sleep/experiments/run_transfer.py +155 -0
- package/lib/vendor/skillopt/skillopt_sleep/experiments/sweep.py +164 -0
- package/lib/vendor/skillopt/skillopt_sleep/gate.py +50 -0
- package/lib/vendor/skillopt/skillopt_sleep/handoff_backend.py +173 -0
- package/lib/vendor/skillopt/skillopt_sleep/harvest.py +691 -0
- package/lib/vendor/skillopt/skillopt_sleep/harvest_codex.py +260 -0
- package/lib/vendor/skillopt/skillopt_sleep/harvest_copilot.py +443 -0
- package/lib/vendor/skillopt/skillopt_sleep/harvest_copilot_cli.py +244 -0
- package/lib/vendor/skillopt/skillopt_sleep/harvest_cursor.py +316 -0
- package/lib/vendor/skillopt/skillopt_sleep/harvest_opencode.py +364 -0
- package/lib/vendor/skillopt/skillopt_sleep/harvest_pi.py +311 -0
- package/lib/vendor/skillopt/skillopt_sleep/harvest_sources.py +135 -0
- package/lib/vendor/skillopt/skillopt_sleep/judges.py +344 -0
- package/lib/vendor/skillopt/skillopt_sleep/llm_miner.py +187 -0
- package/lib/vendor/skillopt/skillopt_sleep/memory.py +166 -0
- package/lib/vendor/skillopt/skillopt_sleep/mine.py +427 -0
- package/lib/vendor/skillopt/skillopt_sleep/multi_skill.py +190 -0
- package/lib/vendor/skillopt/skillopt_sleep/prompts.py +250 -0
- package/lib/vendor/skillopt/skillopt_sleep/replay.py +211 -0
- package/lib/vendor/skillopt/skillopt_sleep/rollout.py +184 -0
- package/lib/vendor/skillopt/skillopt_sleep/scheduler.py +286 -0
- package/lib/vendor/skillopt/skillopt_sleep/skill_resolver.py +244 -0
- package/lib/vendor/skillopt/skillopt_sleep/slow_update.py +141 -0
- package/lib/vendor/skillopt/skillopt_sleep/staging.py +3285 -0
- package/lib/vendor/skillopt/skillopt_sleep/state.py +283 -0
- package/lib/vendor/skillopt/skillopt_sleep/tasks_file.py +100 -0
- package/lib/vendor/skillopt/skillopt_sleep/types.py +227 -0
- package/lib/vendor/skillopt/src/index.js +394 -0
- package/package.json +62 -4
- 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
|
+
});
|