@teamclaws/teamclaw 2026.3.26-2 → 2026.4.2-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +52 -8
  2. package/cli.mjs +538 -224
  3. package/index.ts +76 -27
  4. package/openclaw.plugin.json +53 -28
  5. package/package.json +5 -2
  6. package/skills/teamclaw/SKILL.md +213 -0
  7. package/skills/teamclaw/references/api-quick-ref.md +117 -0
  8. package/skills/teamclaw-setup/SKILL.md +81 -0
  9. package/skills/teamclaw-setup/references/install-modes.md +136 -0
  10. package/skills/teamclaw-setup/references/validation-checklist.md +73 -0
  11. package/src/config.ts +44 -16
  12. package/src/controller/controller-capacity.ts +2 -2
  13. package/src/controller/controller-service.ts +193 -47
  14. package/src/controller/controller-tools.ts +102 -2
  15. package/src/controller/delivery-report.ts +563 -0
  16. package/src/controller/http-server.ts +1907 -172
  17. package/src/controller/kickoff-orchestrator.ts +292 -0
  18. package/src/controller/managed-gateway-process.ts +330 -0
  19. package/src/controller/orchestration-manifest.ts +69 -1
  20. package/src/controller/preview-manager.ts +676 -0
  21. package/src/controller/prompt-injector.ts +116 -67
  22. package/src/controller/role-inference.ts +41 -0
  23. package/src/controller/websocket.ts +3 -1
  24. package/src/controller/worker-provisioning.ts +429 -74
  25. package/src/discovery.ts +1 -1
  26. package/src/git-collaboration.ts +198 -47
  27. package/src/identity.ts +12 -2
  28. package/src/interaction-contracts.ts +179 -3
  29. package/src/networking.ts +99 -0
  30. package/src/openclaw-workspace.ts +478 -11
  31. package/src/prompt-policy.ts +381 -0
  32. package/src/roles.ts +37 -36
  33. package/src/state.ts +40 -1
  34. package/src/task-executor.ts +282 -78
  35. package/src/types.ts +150 -7
  36. package/src/ui/app.js +1403 -175
  37. package/src/ui/assets/teamclaw-app-icon.png +0 -0
  38. package/src/ui/index.html +122 -40
  39. package/src/ui/style.css +829 -143
  40. package/src/worker/http-handler.ts +40 -4
  41. package/src/worker/prompt-injector.ts +9 -38
  42. package/src/worker/skill-installer.ts +2 -2
  43. package/src/worker/tools.ts +31 -5
  44. package/src/worker/worker-service.ts +49 -8
  45. package/src/workspace-browser.ts +20 -7
  46. package/src/controller/local-worker-manager.ts +0 -533
package/src/ui/app.js CHANGED
@@ -6,24 +6,316 @@
6
6
  let ws = null;
7
7
  let currentFilter = "all";
8
8
  let activeTab = "tasks";
9
- let teamState = { workers: [], tasks: [], controllerRuns: [], messages: [], clarifications: [] };
9
+ let teamState = { workers: [], tasks: [], controllerRuns: [], messages: [], clarifications: [], modelReadiness: null, externalWorkerInstall: null };
10
+ let selectedExternalWorkerRole = "developer";
11
+ let selectedExternalWorkerDiscoveryMode = "mdns";
12
+ let externalWorkerInstallVisible = false;
10
13
  let selectedTaskId = null;
11
14
  let selectedTaskDetail = null;
12
- let selectedTaskDetailTab = "overview";
13
- let followTaskOutput = true;
15
+ let selectedTaskDetailTab = "details";
16
+ let taskTimelineAutoFollow = true;
14
17
  let workspaceTree = [];
15
18
  let selectedWorkspacePath = null;
16
19
  let selectedWorkspaceFile = null;
17
20
  let selectedWorkspaceView = "source";
18
21
  let workspaceLoaded = false;
22
+ let clarificationPromptOpen = false;
23
+ let activeClarificationId = null;
24
+ let dismissedClarificationIds = [];
25
+ let reconnectTimer = null;
26
+ let reconnectAttempts = 0;
27
+ let isConnecting = false;
19
28
  const CONTROLLER_SESSION_STORAGE_KEY = "teamclaw.controllerSessionKey";
20
29
  const CONTROLLER_CONVERSATION_STORAGE_KEY = "teamclaw.controllerConversation";
30
+ const LANGUAGE_STORAGE_KEY = "teamclaw.ui.language";
21
31
  let controllerConversation = loadControllerConversation();
22
32
  let controllerCommandPending = false;
33
+ const initialUiState = parseInitialUiState();
34
+ let initialUiStateApplied = false;
35
+ let currentLanguage = loadLanguage();
36
+
37
+ const TRANSLATIONS = {
38
+ en: {
39
+ "action.refresh": "Refresh",
40
+ "action.close": "Close",
41
+ "action.dismiss": "Dismiss",
42
+ "action.copyCommand": "Copy command",
43
+ "action.copied": "Copied",
44
+ "sidebar.workers": "Workers",
45
+ "sidebar.roles": "Roles",
46
+ "tab.planning": "Planning",
47
+ "tab.tasks": "Tasks",
48
+ "tab.workspace": "Workspace",
49
+ "tab.clarifications": "Clarifications",
50
+ "tab.messages": "Messages",
51
+ "tab.manualTask": "Manual Task",
52
+ "planning.title": "Team Planning",
53
+ "planning.description": "Submit a requirement via the command bar below. Complex projects (3+ roles) will trigger a team kickoff meeting where each role assesses the requirement collaboratively.",
54
+ "planning.sessions": "Sessions",
55
+ "planning.requirement": "Requirement",
56
+ "planning.kickoff": "Team Kickoff Meeting",
57
+ "planning.controllerOutput": "Controller Output",
58
+ "planning.originalRequest": "Original Request",
59
+ "planning.requiredRoles": "Required Roles",
60
+ "planning.plannedTasks": "Planned Tasks",
61
+ "planning.deferredTasks": "Deferred Tasks",
62
+ "planning.clarificationsNeeded": "Clarifications Needed",
63
+ "planning.notes": "Notes",
64
+ "planning.noControllerOutput": "No controller output yet.",
65
+ "workspace.preview": "Preview",
66
+ "workspace.selectFile": "Select a file",
67
+ "workspace.openRaw": "Open Raw",
68
+ "workspace.source": "Source",
69
+ "workspace.files": "Files",
70
+ "messages.panelNote": "Controller activity is persisted here so you can follow requirement intake, orchestration, and follow-up runs from the web UI.",
71
+ "manualTask.note": "Raw human requirements should go to the controller conversation first. Use this form only for explicit manual task injection or testing.",
72
+ "manualTask.title": "Title",
73
+ "manualTask.description": "Description",
74
+ "manualTask.skills": "Recommended Skills",
75
+ "manualTask.priority": "Priority",
76
+ "manualTask.assignedRole": "Assigned Role",
77
+ "manualTask.autoAssign": "Auto-assign",
78
+ "manualTask.create": "Create Manual Task",
79
+ "manualTask.titlePlaceholder": "Task title...",
80
+ "manualTask.descriptionPlaceholder": "Execution-ready task description...",
81
+ "manualTask.skillsPlaceholder": "Comma-separated skill slugs, e.g. find-skills, ui-ux-pro-max",
82
+ "empty.noWorkers": "No workers connected",
83
+ "empty.noTasks": "No tasks yet",
84
+ "empty.noTasksWithStatus": "No tasks with status \"{status}\"",
85
+ "empty.noClarifications": "No clarification requests",
86
+ "empty.noControllerActivity": "No controller activity yet",
87
+ "empty.noMessages": "No messages yet",
88
+ "empty.noPlanningSessions": "No planning sessions yet",
89
+ "empty.noKickoffData": "No kickoff data",
90
+ "empty.workspaceLoading": "Workspace tree loading…",
91
+ "empty.noWorkspaceFiles": "No project files in the workspace yet.",
92
+ "empty.selectFileSource": "Select a file from the workspace tree to view its source.",
93
+ "empty.selectFilePreview": "Select a file from the workspace tree to preview Markdown or HTML output.",
94
+ "empty.selectTask": "Select a task",
95
+ "empty.taskDetail": "Select a task to inspect its execution details.",
96
+ "empty.taskMessages": "No messages on this task yet.",
97
+ "empty.taskHistory": "No execution history recorded yet.",
98
+ "empty.copiedControllerReply": "Controller finished without a textual reply.",
99
+ "runtime.title": "TeamClaw is installed but cannot work yet.",
100
+ "runtime.noModel": "No TeamClaw model is configured for this instance.",
101
+ "runtime.noAuth": "No usable OpenClaw auth profile was found for TeamClaw.",
102
+ "worker.add": "Add worker",
103
+ "worker.hide": "Hide worker command",
104
+ "worker.cardTitle": "Register a new external worker",
105
+ "worker.cardSubtitle": "Choose a role and discovery mode, then copy a one-line installer command for the target machine.",
106
+ "worker.role": "Role",
107
+ "worker.discovery": "Controller discovery",
108
+ "worker.discoveryMdns": "LAN auto-discovery (mDNS)",
109
+ "worker.discoveryManual": "Manual controller URL (LAN IP)",
110
+ "worker.recommendedUrl": "Recommended controller URL: ",
111
+ "filter.all": "All",
112
+ "filter.pending": "Pending",
113
+ "filter.assigned": "Assigned",
114
+ "filter.in_progress": "In Progress",
115
+ "filter.blocked": "Blocked",
116
+ "filter.completed": "Completed",
117
+ "filter.failed": "Failed",
118
+ "priority.low": "Low",
119
+ "priority.medium": "Medium",
120
+ "priority.high": "High",
121
+ "priority.critical": "Critical",
122
+ "detail.kicker": "Task Details",
123
+ "live.idle": "Idle",
124
+ "clarification.kicker": "Clarification needed",
125
+ "clarification.title": "Human input required"
126
+ },
127
+ zh: {
128
+ "action.refresh": "刷新",
129
+ "action.close": "关闭",
130
+ "action.dismiss": "稍后处理",
131
+ "action.copyCommand": "复制命令",
132
+ "action.copied": "已复制",
133
+ "sidebar.workers": "成员",
134
+ "sidebar.roles": "角色",
135
+ "tab.planning": "规划",
136
+ "tab.tasks": "任务",
137
+ "tab.workspace": "工作区",
138
+ "tab.clarifications": "澄清",
139
+ "tab.messages": "消息",
140
+ "tab.manualTask": "手动任务",
141
+ "planning.title": "团队规划",
142
+ "planning.description": "通过下方命令栏提交需求。复杂项目(3 个及以上角色)会触发团队 kickoff 会议,由各角色协作评估需求。",
143
+ "planning.sessions": "会话",
144
+ "planning.requirement": "需求",
145
+ "planning.kickoff": "团队 Kickoff 会议",
146
+ "planning.controllerOutput": "Controller 输出",
147
+ "planning.originalRequest": "原始请求",
148
+ "planning.requiredRoles": "所需角色",
149
+ "planning.plannedTasks": "计划任务",
150
+ "planning.deferredTasks": "延后任务",
151
+ "planning.clarificationsNeeded": "待澄清问题",
152
+ "planning.notes": "备注",
153
+ "planning.noControllerOutput": "暂无 controller 输出。",
154
+ "workspace.preview": "预览",
155
+ "workspace.selectFile": "选择文件",
156
+ "workspace.openRaw": "打开原始文件",
157
+ "workspace.source": "源码",
158
+ "workspace.files": "文件",
159
+ "messages.panelNote": "这里会保留 controller 活动,方便你在 Web UI 中跟踪需求 intake、编排过程和后续跟进。",
160
+ "manualTask.note": "原始人工需求应先发送到 controller 对话。这个表单仅用于明确的手动任务注入或测试。",
161
+ "manualTask.title": "标题",
162
+ "manualTask.description": "描述",
163
+ "manualTask.skills": "推荐技能",
164
+ "manualTask.priority": "优先级",
165
+ "manualTask.assignedRole": "指定角色",
166
+ "manualTask.autoAssign": "自动分配",
167
+ "manualTask.create": "创建手动任务",
168
+ "manualTask.titlePlaceholder": "任务标题...",
169
+ "manualTask.descriptionPlaceholder": "可直接执行的任务描述...",
170
+ "manualTask.skillsPlaceholder": "逗号分隔的 skill slug,例如 find-skills, ui-ux-pro-max",
171
+ "empty.noWorkers": "暂无已连接成员",
172
+ "empty.noTasks": "暂无任务",
173
+ "empty.noTasksWithStatus": "没有状态为“{status}”的任务",
174
+ "empty.noClarifications": "暂无澄清请求",
175
+ "empty.noControllerActivity": "暂无 controller 活动",
176
+ "empty.noMessages": "暂无消息",
177
+ "empty.noPlanningSessions": "暂无规划会话",
178
+ "empty.noKickoffData": "暂无 kickoff 数据",
179
+ "empty.workspaceLoading": "工作区树加载中…",
180
+ "empty.noWorkspaceFiles": "工作区中还没有项目文件。",
181
+ "empty.selectFileSource": "从工作区文件树选择文件以查看源码。",
182
+ "empty.selectFilePreview": "从工作区文件树选择文件以预览 Markdown 或 HTML 输出。",
183
+ "empty.selectTask": "选择一个任务",
184
+ "empty.taskDetail": "选择一个任务以查看执行细节。",
185
+ "empty.taskMessages": "该任务暂无消息。",
186
+ "empty.taskHistory": "暂无执行历史。",
187
+ "empty.copiedControllerReply": "Controller 已完成,但没有文本回复。",
188
+ "runtime.title": "TeamClaw 已安装,但当前还无法工作。",
189
+ "runtime.noModel": "当前实例还没有为 TeamClaw 配置模型。",
190
+ "runtime.noAuth": "未找到 TeamClaw 可用的 OpenClaw 认证配置。",
191
+ "worker.add": "添加 worker",
192
+ "worker.hide": "隐藏 worker 命令",
193
+ "worker.cardTitle": "注册新的外部 worker",
194
+ "worker.cardSubtitle": "选择角色和发现方式,然后复制目标机器可直接执行的一行安装命令。",
195
+ "worker.role": "角色",
196
+ "worker.discovery": "Controller 发现方式",
197
+ "worker.discoveryMdns": "局域网自动发现(mDNS)",
198
+ "worker.discoveryManual": "手动填写 controller 地址(局域网 IP)",
199
+ "worker.recommendedUrl": "推荐的 controller 地址:",
200
+ "filter.all": "全部",
201
+ "filter.pending": "待处理",
202
+ "filter.assigned": "已分配",
203
+ "filter.in_progress": "进行中",
204
+ "filter.blocked": "阻塞",
205
+ "filter.completed": "已完成",
206
+ "filter.failed": "失败",
207
+ "priority.low": "低",
208
+ "priority.medium": "中",
209
+ "priority.high": "高",
210
+ "priority.critical": "紧急",
211
+ "detail.kicker": "任务详情",
212
+ "live.idle": "空闲",
213
+ "clarification.kicker": "需要澄清",
214
+ "clarification.title": "需要人工输入"
215
+ }
216
+ };
23
217
 
24
218
  function $(selector) { return document.querySelector(selector); }
25
219
  function $$(selector) { return document.querySelectorAll(selector); }
26
220
 
221
+ function loadLanguage() {
222
+ try {
223
+ var stored = window.localStorage.getItem(LANGUAGE_STORAGE_KEY);
224
+ return stored === "zh" ? "zh" : "en";
225
+ } catch (_err) {
226
+ return "en";
227
+ }
228
+ }
229
+
230
+ function t(key, params) {
231
+ var template = (TRANSLATIONS[currentLanguage] && TRANSLATIONS[currentLanguage][key]) || TRANSLATIONS.en[key] || key;
232
+ return template.replace(/\{(\w+)\}/g, function (_match, name) {
233
+ return params && params[name] != null ? String(params[name]) : "";
234
+ });
235
+ }
236
+
237
+ function setLanguage(language) {
238
+ currentLanguage = language === "zh" ? "zh" : "en";
239
+ try {
240
+ window.localStorage.setItem(LANGUAGE_STORAGE_KEY, currentLanguage);
241
+ } catch (_err) {}
242
+ document.documentElement.lang = currentLanguage === "zh" ? "zh-CN" : "en";
243
+ applyStaticTranslations();
244
+ refreshAll();
245
+ }
246
+
247
+ function applyStaticTranslations() {
248
+ $$("[data-i18n]").forEach(function (element) {
249
+ var key = element.getAttribute("data-i18n");
250
+ if (key) {
251
+ element.textContent = t(key);
252
+ }
253
+ });
254
+ $$("[data-i18n-placeholder]").forEach(function (element) {
255
+ var key = element.getAttribute("data-i18n-placeholder");
256
+ if (key) {
257
+ element.setAttribute("placeholder", t(key));
258
+ }
259
+ });
260
+ var languageToggle = $("#language-toggle");
261
+ if (languageToggle) {
262
+ languageToggle.textContent = currentLanguage === "zh" ? "English" : "中文";
263
+ }
264
+ var filters = {
265
+ all: "filter.all",
266
+ pending: "filter.pending",
267
+ assigned: "filter.assigned",
268
+ in_progress: "filter.in_progress",
269
+ blocked: "filter.blocked",
270
+ completed: "filter.completed",
271
+ failed: "filter.failed"
272
+ };
273
+ $$("[data-filter]").forEach(function (button) {
274
+ var key = filters[button.getAttribute("data-filter") || "all"];
275
+ if (key) button.textContent = t(key);
276
+ });
277
+ ["low", "medium", "high", "critical"].forEach(function (priority) {
278
+ var option = $('#task-priority option[value="' + priority + '"]');
279
+ if (option) option.textContent = t("priority." + priority);
280
+ });
281
+ var planningHeader = $(".planning-sessions-header");
282
+ if (planningHeader) planningHeader.textContent = t("planning.sessions");
283
+ var workspaceKicker = $(".workspace-sidebar-panel .workspace-panel-kicker");
284
+ if (workspaceKicker) workspaceKicker.textContent = t("tab.workspace");
285
+ var workspaceTitle = $(".workspace-sidebar-panel h3");
286
+ if (workspaceTitle) workspaceTitle.textContent = t("workspace.files");
287
+ var planningPaneTitles = $$(".planning-pane-title");
288
+ if (planningPaneTitles[0]) planningPaneTitles[0].textContent = t("planning.requirement");
289
+ if (planningPaneTitles[1]) planningPaneTitles[1].textContent = t("planning.kickoff");
290
+ var promptKicker = $(".clarification-prompt-kicker");
291
+ if (promptKicker) promptKicker.textContent = t("clarification.kicker");
292
+ var promptTitle = $("#clarification-prompt-title");
293
+ if (promptTitle) promptTitle.textContent = t("clarification.title");
294
+ var promptClose = $("#clarification-prompt-close");
295
+ if (promptClose) promptClose.textContent = t("action.dismiss");
296
+ var detailKicker = $(".task-detail-kicker");
297
+ if (detailKicker) detailKicker.textContent = t("detail.kicker");
298
+ var detailClose = $("#task-detail-close");
299
+ if (detailClose) detailClose.textContent = t("action.close");
300
+ var detailRefresh = $("#task-detail-refresh");
301
+ if (detailRefresh) detailRefresh.textContent = t("action.refresh");
302
+ var liveBadge = $("#task-detail-live-badge");
303
+ if (liveBadge && liveBadge.textContent === "Idle") liveBadge.textContent = t("live.idle");
304
+ }
305
+
306
+ function parseInitialUiState() {
307
+ try {
308
+ const params = new URLSearchParams(window.location.search || "");
309
+ return {
310
+ tab: params.get("tab") || "",
311
+ taskId: params.get("taskId") || "",
312
+ planningRun: params.get("planningRun") || "",
313
+ };
314
+ } catch (_err) {
315
+ return { tab: "", taskId: "", planningRun: "" };
316
+ }
317
+ }
318
+
27
319
  function getSessionStorage() {
28
320
  try {
29
321
  return window.sessionStorage;
@@ -83,6 +375,23 @@
83
375
  return div.innerHTML;
84
376
  }
85
377
 
378
+ async function copyText(text) {
379
+ if (!text) return;
380
+ if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
381
+ await navigator.clipboard.writeText(text);
382
+ return;
383
+ }
384
+ var textarea = document.createElement("textarea");
385
+ textarea.value = text;
386
+ textarea.setAttribute("readonly", "readonly");
387
+ textarea.style.position = "absolute";
388
+ textarea.style.left = "-9999px";
389
+ document.body.appendChild(textarea);
390
+ textarea.select();
391
+ document.execCommand("copy");
392
+ document.body.removeChild(textarea);
393
+ }
394
+
86
395
  function formatTime(ts) {
87
396
  if (!ts) return "";
88
397
  const d = new Date(ts);
@@ -96,6 +405,49 @@
96
405
  return String(value || "").replace(/_/g, " ").replace(/-/g, " ");
97
406
  }
98
407
 
408
+ function normalizeArray(value) {
409
+ return Array.isArray(value) ? value : [];
410
+ }
411
+
412
+ function sortClarifications(items) {
413
+ return normalizeArray(items).slice().sort(function (left, right) {
414
+ return (right.updatedAt || right.createdAt || 0) - (left.updatedAt || left.createdAt || 0);
415
+ });
416
+ }
417
+
418
+ function pendingClarifications() {
419
+ return sortClarifications(teamState.clarifications).filter(function (item) {
420
+ return !item.answer && (item.status || "pending") === "pending";
421
+ });
422
+ }
423
+
424
+ function isTerminalPlanningStatus(status) {
425
+ return ["completed", "failed", "cancelled"].indexOf(String(status || "").toLowerCase()) !== -1;
426
+ }
427
+
428
+ function activePlanningRunCount() {
429
+ return normalizeArray(teamState.controllerRuns).filter(function (run) {
430
+ return run.manifest && run.manifest.kickoffPlan && !isTerminalPlanningStatus(run.status);
431
+ }).length;
432
+ }
433
+
434
+ function activeTaskCount() {
435
+ return normalizeArray(teamState.tasks).filter(function (task) {
436
+ return ["assigned", "in_progress", "review"].indexOf(task.status) !== -1;
437
+ }).length;
438
+ }
439
+
440
+ function blockedTaskCount() {
441
+ return normalizeArray(teamState.tasks).filter(function (task) {
442
+ return task.status === "blocked";
443
+ }).length;
444
+ }
445
+
446
+ function isNearBottom(element) {
447
+ if (!element) return true;
448
+ return (element.scrollHeight - element.scrollTop - element.clientHeight) < 48;
449
+ }
450
+
99
451
  function formatBytes(value) {
100
452
  const bytes = Number(value || 0);
101
453
  if (!bytes) return "0 B";
@@ -399,7 +751,11 @@
399
751
  sections: [
400
752
  renderContractSection("Deliverables", renderContractList(deliverables, function (item) {
401
753
  const prefix = escapeHtml(item.kind || "artifact") + ": " + escapeHtml(item.value || "");
402
- return prefix + (item.summary ? ' <span class="contract-inline-note">— ' + escapeHtml(item.summary) + "</span>" : "");
754
+ var liveLink = "";
755
+ if (item.artifactType === "web-app" && item.liveUrl) {
756
+ liveLink = ' <a class="deliverable-live-link" href="' + escapeHtml(item.liveUrl) + '" target="_blank" rel="noopener">Live Preview</a>';
757
+ }
758
+ return prefix + (item.summary ? ' <span class="contract-inline-note">— ' + escapeHtml(item.summary) + "</span>" : "") + liveLink;
403
759
  })),
404
760
  renderContractSection("Key Points", renderContractList(contract.keyPoints, function (item) {
405
761
  return renderMarkdownInline(item);
@@ -516,6 +872,343 @@
516
872
  });
517
873
  }
518
874
 
875
+ var ROLE_ICONS = {
876
+ architect: "🏗️",
877
+ developer: "💻",
878
+ designer: "🎨",
879
+ "security-engineer": "🔒",
880
+ qa: "🧪",
881
+ devops: "⚙️",
882
+ "tech-lead": "👨‍💻",
883
+ "data-engineer": "📊",
884
+ "ml-engineer": "🤖",
885
+ "infra-engineer": "🖧",
886
+ "dba": "🗄️",
887
+ };
888
+
889
+ function renderKickoffMeetingPanel(kickoffPlan) {
890
+ if (!kickoffPlan || !Array.isArray(kickoffPlan.assessments) || kickoffPlan.assessments.length === 0) {
891
+ return "";
892
+ }
893
+ var assessments = kickoffPlan.assessments;
894
+ var needed = assessments.filter(function (a) { return a.needed; }).length;
895
+ var notNeeded = assessments.length - needed;
896
+
897
+ var header =
898
+ '<div class="kickoff-panel-header">' +
899
+ ' <div class="kickoff-panel-icon">🤝</div>' +
900
+ ' <div class="kickoff-panel-heading">' +
901
+ ' <h4>Team Kickoff Meeting</h4>' +
902
+ ' <div class="kickoff-panel-meta">' +
903
+ assessments.length + " roles assessed · " +
904
+ needed + " confirmed" +
905
+ (notNeeded > 0 ? " · " + notNeeded + " dismissed" : "") +
906
+ " </div>" +
907
+ " </div>" +
908
+ "</div>";
909
+
910
+ var roleCards = assessments.map(function (a) {
911
+ var icon = ROLE_ICONS[a.role] || "👤";
912
+ var statusCls = a.needed ? "kickoff-role-needed" : "kickoff-role-dismissed";
913
+ var statusLabel = a.needed ? "Confirmed" : "Not Needed";
914
+
915
+ var scopeHtml = a.scope
916
+ ? '<div class="kickoff-role-scope">' + renderMarkdownContent(a.scope) + "</div>"
917
+ : "";
918
+
919
+ var tasksHtml = "";
920
+ if (Array.isArray(a.suggestedTasks) && a.suggestedTasks.length > 0) {
921
+ tasksHtml =
922
+ '<div class="kickoff-role-detail">' +
923
+ ' <div class="kickoff-detail-label">📋 Suggested Tasks</div>' +
924
+ ' <ul class="kickoff-detail-list">' +
925
+ a.suggestedTasks.map(function (t) { return "<li>" + escapeHtml(t) + "</li>"; }).join("") +
926
+ " </ul>" +
927
+ "</div>";
928
+ }
929
+
930
+ var risksHtml = "";
931
+ if (Array.isArray(a.risks) && a.risks.length > 0) {
932
+ risksHtml =
933
+ '<div class="kickoff-role-detail">' +
934
+ ' <div class="kickoff-detail-label">⚠️ Risks</div>' +
935
+ ' <ul class="kickoff-detail-list kickoff-risks">' +
936
+ a.risks.map(function (r) { return "<li>" + escapeHtml(r) + "</li>"; }).join("") +
937
+ " </ul>" +
938
+ "</div>";
939
+ }
940
+
941
+ var depsHtml = "";
942
+ if (Array.isArray(a.dependencies) && a.dependencies.length > 0) {
943
+ depsHtml =
944
+ '<div class="kickoff-role-detail">' +
945
+ ' <div class="kickoff-detail-label">🔗 Dependencies</div>' +
946
+ ' <div class="kickoff-deps">' +
947
+ a.dependencies.map(function (d) { return '<span class="kickoff-dep-chip">' + escapeHtml(d) + "</span>"; }).join("") +
948
+ " </div>" +
949
+ "</div>";
950
+ }
951
+
952
+ return (
953
+ '<div class="kickoff-role-card ' + statusCls + '">' +
954
+ ' <div class="kickoff-role-header">' +
955
+ ' <span class="kickoff-role-icon">' + icon + "</span>" +
956
+ ' <span class="kickoff-role-name">' + escapeHtml(a.role) + "</span>" +
957
+ ' <span class="kickoff-role-badge ' + statusCls + '">' + statusLabel + "</span>" +
958
+ " </div>" +
959
+ scopeHtml +
960
+ tasksHtml +
961
+ risksHtml +
962
+ depsHtml +
963
+ "</div>"
964
+ );
965
+ }).join("");
966
+
967
+ var summaryHtml = "";
968
+ if (kickoffPlan.summary) {
969
+ summaryHtml =
970
+ '<div class="kickoff-summary">' +
971
+ ' <div class="kickoff-summary-label">Discussion Summary</div>' +
972
+ ' <div class="kickoff-summary-body markdown-body">' + renderMarkdownContent(kickoffPlan.summary) + "</div>" +
973
+ "</div>";
974
+ }
975
+
976
+ return (
977
+ '<div class="controller-run-section">' +
978
+ ' <div class="kickoff-panel">' +
979
+ header +
980
+ ' <div class="kickoff-role-grid">' + roleCards + "</div>" +
981
+ summaryHtml +
982
+ " </div>" +
983
+ "</div>"
984
+ );
985
+ }
986
+
987
+ // ── Planning Tab ───────────────────────────────────────────────────────
988
+ var selectedPlanningRunId = null;
989
+
990
+ function renderPlanningTab(runs) {
991
+ var sessionList = $("#planning-session-list");
992
+ if (!sessionList) return;
993
+
994
+ var planningRuns = (runs || [])
995
+ .filter(function (r) { return r && r.manifest; })
996
+ .sort(function (a, b) { return (b.updatedAt || 0) - (a.updatedAt || 0); });
997
+
998
+ if (planningRuns.length === 0) {
999
+ sessionList.innerHTML = '<div class="empty-state">' + escapeHtml(t("empty.noPlanningSessions")) + "</div>";
1000
+ showPlanningEmpty();
1001
+ return;
1002
+ }
1003
+
1004
+ // Auto-select first if nothing selected or selection is gone
1005
+ if (!selectedPlanningRunId || !planningRuns.some(function (r) { return r.id === selectedPlanningRunId; })) {
1006
+ selectedPlanningRunId = planningRuns[0].id;
1007
+ }
1008
+
1009
+ sessionList.innerHTML = planningRuns.map(function (run) {
1010
+ var manifest = run.manifest || {};
1011
+ var kp = manifest.kickoffPlan || {};
1012
+ var assessments = kp.assessments || [];
1013
+ var needed = assessments.filter(function (a) { return a.needed; }).length;
1014
+ var roles = (manifest.requiredRoles || []).length;
1015
+ var isActive = run.id === selectedPlanningRunId;
1016
+ var status = String(run.status || "active");
1017
+ var title = manifest.requirementSummary || run.title || "Untitled";
1018
+ if (title.length > 60) title = title.slice(0, 57) + "…";
1019
+
1020
+ return (
1021
+ '<button type="button" class="planning-session-btn' + (isActive ? " active" : "") + '" data-planning-run="' + escapeHtml(run.id) + '">' +
1022
+ ' <div class="planning-session-title-row"><span class="planning-session-status ' + escapeHtml(status) + '"></span><div class="planning-session-title">' + escapeHtml(title) + "</div></div>" +
1023
+ ' <div class="planning-session-meta">' + escapeHtml(humanizeStatus(status)) + " · " + roles + " roles" + (assessments.length ? " · " + needed + " confirmed" : "") + " · " + escapeHtml(formatTime(run.updatedAt) || "") + "</div>" +
1024
+ "</button>"
1025
+ );
1026
+ }).join("");
1027
+
1028
+ // Render selected run
1029
+ renderPlanningDetail(planningRuns.find(function (r) { return r.id === selectedPlanningRunId; }));
1030
+ }
1031
+
1032
+ function showPlanningEmpty() {
1033
+ var empty = $("#planning-empty");
1034
+ var split = $("#planning-split");
1035
+ if (empty) empty.style.display = "";
1036
+ if (split) split.style.display = "none";
1037
+ }
1038
+
1039
+ function renderPlanningDetail(run) {
1040
+ var empty = $("#planning-empty");
1041
+ var split = $("#planning-split");
1042
+ var reqEl = $("#planning-requirement");
1043
+ var kickoffEl = $("#planning-kickoff");
1044
+
1045
+ if (!run || !run.manifest) {
1046
+ showPlanningEmpty();
1047
+ return;
1048
+ }
1049
+
1050
+ if (empty) empty.style.display = "none";
1051
+ if (split) split.style.display = "";
1052
+
1053
+ // Left pane: requirement + manifest summary
1054
+ if (reqEl) {
1055
+ var manifest = run.manifest;
1056
+ var reqLines = [];
1057
+ reqLines.push('<h3>' + escapeHtml(manifest.requirementSummary || run.title || "") + '</h3>');
1058
+ reqLines.push('<div class="planning-req-original">' + renderMarkdownContent(run.request || "") + '</div>');
1059
+
1060
+ // Manifest details
1061
+ if (manifest.requiredRoles && manifest.requiredRoles.length) {
1062
+ reqLines.push('<div class="planning-req-section"><div class="planning-req-label">' + escapeHtml(t("planning.requiredRoles")) + "</div>");
1063
+ reqLines.push('<div class="kickoff-deps">' + manifest.requiredRoles.map(function (r) {
1064
+ var icon = ROLE_ICONS[r] || "👤";
1065
+ return '<span class="kickoff-dep-chip">' + icon + " " + escapeHtml(r) + "</span>";
1066
+ }).join("") + "</div></div>");
1067
+ }
1068
+
1069
+ var created = Array.isArray(manifest.createdTasks) ? manifest.createdTasks : [];
1070
+ var deferred = Array.isArray(manifest.deferredTasks) ? manifest.deferredTasks : [];
1071
+ if (created.length) {
1072
+ reqLines.push('<div class="planning-req-section"><div class="planning-req-label">' + escapeHtml(t("planning.plannedTasks")) + " (" + created.length + ")</div>");
1073
+ reqLines.push('<ul class="planning-task-list">');
1074
+ created.forEach(function (t) {
1075
+ var roleLabel = t.assignedRole ? ' <span class="planning-role-tag">' + escapeHtml(t.assignedRole) + "</span>" : "";
1076
+ reqLines.push("<li>" + escapeHtml(t.title || "Task") + roleLabel + "</li>");
1077
+ });
1078
+ reqLines.push("</ul></div>");
1079
+ }
1080
+ if (deferred.length) {
1081
+ reqLines.push('<div class="planning-req-section"><div class="planning-req-label">' + escapeHtml(t("planning.deferredTasks")) + " (" + deferred.length + ")</div>");
1082
+ reqLines.push('<ul class="planning-task-list planning-deferred">');
1083
+ deferred.forEach(function (t) {
1084
+ var roleLabel = t.assignedRole ? ' <span class="planning-role-tag">' + escapeHtml(t.assignedRole) + "</span>" : "";
1085
+ reqLines.push("<li>" + escapeHtml(t.title || "Task") + roleLabel + "</li>");
1086
+ });
1087
+ reqLines.push("</ul></div>");
1088
+ }
1089
+ if (Array.isArray(manifest.clarificationQuestions) && manifest.clarificationQuestions.length) {
1090
+ reqLines.push('<div class="planning-req-section"><div class="planning-req-label">' + escapeHtml(t("planning.clarificationsNeeded")) + "</div>");
1091
+ reqLines.push('<ul class="planning-task-list planning-deferred">');
1092
+ manifest.clarificationQuestions.forEach(function (question) {
1093
+ reqLines.push("<li>" + escapeHtml(question) + "</li>");
1094
+ });
1095
+ reqLines.push("</ul></div>");
1096
+ }
1097
+ if (manifest.handoffPlan) {
1098
+ reqLines.push('<div class="planning-req-section"><div class="planning-req-label">Handoff Plan</div>');
1099
+ reqLines.push('<div class="planning-req-body markdown-body">' + renderMarkdownContent(manifest.handoffPlan) + "</div></div>");
1100
+ }
1101
+ if (manifest.notes) {
1102
+ reqLines.push('<div class="planning-req-section"><div class="planning-req-label">' + escapeHtml(t("planning.notes")) + "</div>");
1103
+ reqLines.push('<div class="planning-req-body markdown-body">' + renderMarkdownContent(manifest.notes) + "</div></div>");
1104
+ }
1105
+
1106
+ reqEl.innerHTML = reqLines.join("");
1107
+ }
1108
+
1109
+ // Right pane: kickoff meeting or generic controller output
1110
+ if (kickoffEl) {
1111
+ if (run.manifest.kickoffPlan) {
1112
+ kickoffEl.innerHTML = renderKickoffContent(run.manifest.kickoffPlan);
1113
+ } else {
1114
+ kickoffEl.innerHTML =
1115
+ '<div class="planning-req-section"><div class="planning-req-label">' + escapeHtml(t("planning.originalRequest")) + '</div><div class="planning-req-body markdown-body">' + renderMarkdownContent(run.request || "") + "</div></div>" +
1116
+ '<div class="planning-req-section"><div class="planning-req-label">' + escapeHtml(t("planning.controllerOutput")) + '</div><div class="planning-req-body markdown-body">' + renderMarkdownContent(run.reply || t("planning.noControllerOutput")) + "</div></div>";
1117
+ }
1118
+ }
1119
+ }
1120
+
1121
+ function renderKickoffContent(kp) {
1122
+ if (!kp || !Array.isArray(kp.assessments) || kp.assessments.length === 0) {
1123
+ return '<div class="empty-state">' + escapeHtml(t("empty.noKickoffData")) + "</div>";
1124
+ }
1125
+
1126
+ var assessments = kp.assessments;
1127
+ var html = [];
1128
+
1129
+ // Stats bar
1130
+ var needed = assessments.filter(function (a) { return a.needed; }).length;
1131
+ var dismissed = assessments.length - needed;
1132
+ html.push(
1133
+ '<div class="kickoff-stats-bar">' +
1134
+ ' <div class="kickoff-stat"><span class="kickoff-stat-num">' + assessments.length + '</span><span class="kickoff-stat-label">Assessed</span></div>' +
1135
+ ' <div class="kickoff-stat kickoff-stat-ok"><span class="kickoff-stat-num">' + needed + '</span><span class="kickoff-stat-label">Confirmed</span></div>' +
1136
+ (dismissed > 0 ? ' <div class="kickoff-stat kickoff-stat-dim"><span class="kickoff-stat-num">' + dismissed + '</span><span class="kickoff-stat-label">Dismissed</span></div>' : "") +
1137
+ "</div>"
1138
+ );
1139
+
1140
+ // Role cards
1141
+ assessments.forEach(function (a) {
1142
+ var icon = ROLE_ICONS[a.role] || "👤";
1143
+ var statusCls = a.needed ? "kickoff-role-needed" : "kickoff-role-dismissed";
1144
+ var statusLabel = a.needed ? "Confirmed" : "Not Needed";
1145
+
1146
+ var sections = [];
1147
+
1148
+ if (a.scope) {
1149
+ sections.push(
1150
+ '<div class="kickoff-role-scope"><div class="markdown-body">' + renderMarkdownContent(a.scope) + "</div></div>"
1151
+ );
1152
+ }
1153
+
1154
+ if (Array.isArray(a.suggestedTasks) && a.suggestedTasks.length > 0) {
1155
+ sections.push(
1156
+ '<details class="kickoff-collapsible" open>' +
1157
+ ' <summary class="kickoff-detail-label">📋 Suggested Tasks (' + a.suggestedTasks.length + ")</summary>" +
1158
+ ' <ul class="kickoff-detail-list">' +
1159
+ a.suggestedTasks.map(function (t) { return "<li>" + escapeHtml(t) + "</li>"; }).join("") +
1160
+ " </ul>" +
1161
+ "</details>"
1162
+ );
1163
+ }
1164
+
1165
+ if (Array.isArray(a.risks) && a.risks.length > 0) {
1166
+ sections.push(
1167
+ '<details class="kickoff-collapsible">' +
1168
+ ' <summary class="kickoff-detail-label">⚠️ Risks (' + a.risks.length + ")</summary>" +
1169
+ ' <ul class="kickoff-detail-list kickoff-risks">' +
1170
+ a.risks.map(function (r) { return "<li>" + escapeHtml(r) + "</li>"; }).join("") +
1171
+ " </ul>" +
1172
+ "</details>"
1173
+ );
1174
+ }
1175
+
1176
+ if (Array.isArray(a.dependencies) && a.dependencies.length > 0) {
1177
+ sections.push(
1178
+ '<details class="kickoff-collapsible">' +
1179
+ ' <summary class="kickoff-detail-label">🔗 Dependencies (' + a.dependencies.length + ")</summary>" +
1180
+ ' <div class="kickoff-deps">' +
1181
+ a.dependencies.map(function (d) { return '<span class="kickoff-dep-chip">' + escapeHtml(d) + "</span>"; }).join("") +
1182
+ " </div>" +
1183
+ "</details>"
1184
+ );
1185
+ }
1186
+
1187
+ html.push(
1188
+ '<div class="kickoff-role-card ' + statusCls + '">' +
1189
+ ' <div class="kickoff-role-header">' +
1190
+ ' <span class="kickoff-role-icon">' + icon + "</span>" +
1191
+ ' <span class="kickoff-role-name">' + escapeHtml(a.role) + "</span>" +
1192
+ ' <span class="kickoff-role-badge ' + statusCls + '">' + statusLabel + "</span>" +
1193
+ " </div>" +
1194
+ sections.join("") +
1195
+ "</div>"
1196
+ );
1197
+ });
1198
+
1199
+ // Summary
1200
+ if (kp.summary) {
1201
+ html.push(
1202
+ '<div class="kickoff-summary">' +
1203
+ ' <div class="kickoff-summary-label">Discussion Summary</div>' +
1204
+ ' <div class="kickoff-summary-body markdown-body">' + renderMarkdownContent(kp.summary) + "</div>" +
1205
+ "</div>"
1206
+ );
1207
+ }
1208
+
1209
+ return html.join("");
1210
+ }
1211
+
519
1212
  function buildMessageDisplayContent(message) {
520
1213
  const content = normalizeTextValue(message && message.content);
521
1214
  const contract = message && message.contract ? message.contract : null;
@@ -545,6 +1238,14 @@
545
1238
  return null;
546
1239
  }
547
1240
 
1241
+ /** Merge lazy-loaded children into the workspace tree data model. */
1242
+ function mergeWorkspaceSubtree(dirPath, entries) {
1243
+ var dirNode = findWorkspaceNodeByPath(workspaceTree, dirPath);
1244
+ if (dirNode && dirNode.type === "directory") {
1245
+ dirNode.children = entries;
1246
+ }
1247
+ }
1248
+
548
1249
  function findDefaultWorkspacePath(nodes) {
549
1250
  const preferredNames = ["README.md", "SPEC.md", "index.html"];
550
1251
  const queue = [].concat(nodes || []);
@@ -657,10 +1358,13 @@
657
1358
  const data = await apiGet("/workspace/file?path=" + encodeURIComponent(relativePath));
658
1359
  selectedWorkspacePath = relativePath;
659
1360
  selectedWorkspaceFile = data.file || null;
660
- if (!(settings.keepView && selectedWorkspaceView === "preview" && isWorkspacePreviewAvailable(selectedWorkspaceFile))) {
661
- selectedWorkspaceView = "source";
1361
+ if (settings.keepView && selectedWorkspaceView === "preview" && isWorkspacePreviewAvailable(selectedWorkspaceFile)) {
1362
+ // User explicitly chose preview and new file supports it — keep preview
1363
+ } else {
1364
+ // Default: md/html show preview, everything else shows source
1365
+ selectedWorkspaceView = isWorkspacePreviewAvailable(selectedWorkspaceFile) ? "preview" : "source";
662
1366
  }
663
- renderWorkspaceTree(workspaceTree);
1367
+ updateWorkspaceTreeSelection();
664
1368
  renderWorkspaceFile();
665
1369
  } catch (err) {
666
1370
  console.error("Failed to load workspace file:", err);
@@ -675,30 +1379,61 @@
675
1379
  if (!container) return;
676
1380
 
677
1381
  if (!workspaceLoaded) {
678
- container.innerHTML = '<div class="empty-state">Workspace tree loading…</div>';
1382
+ container.innerHTML = '<div class="empty-state">' + escapeHtml(t("empty.workspaceLoading")) + "</div>";
679
1383
  return;
680
1384
  }
681
1385
 
682
1386
  if (!nodes || nodes.length === 0) {
683
- container.innerHTML = '<div class="empty-state">No project files in the workspace yet.</div>';
1387
+ container.innerHTML = '<div class="empty-state">' + escapeHtml(t("empty.noWorkspaceFiles")) + "</div>";
684
1388
  return;
685
1389
  }
686
1390
 
1391
+ // Capture currently expanded directories before re-render
1392
+ var expandedDirs = new Set();
1393
+ var toggles = container.querySelectorAll(".workspace-tree-dir-toggle");
1394
+ for (var i = 0; i < toggles.length; i++) {
1395
+ var toggle = toggles[i];
1396
+ var li = toggle.closest(".workspace-tree-folder");
1397
+ var children = li ? li.querySelector(".workspace-tree-children") : null;
1398
+ if (children && children.style.display !== "none") {
1399
+ expandedDirs.add(toggle.dataset.dirPath);
1400
+ }
1401
+ }
1402
+
687
1403
  container.innerHTML = renderWorkspaceTreeNodes(nodes);
1404
+
1405
+ // Restore expanded directories
1406
+ if (expandedDirs.size > 0) {
1407
+ var newToggles = container.querySelectorAll(".workspace-tree-dir-toggle");
1408
+ for (var j = 0; j < newToggles.length; j++) {
1409
+ var toggleEl = newToggles[j];
1410
+ if (expandedDirs.has(toggleEl.dataset.dirPath)) {
1411
+ var parentLi = toggleEl.closest(".workspace-tree-folder");
1412
+ var childrenDiv = parentLi ? parentLi.querySelector(".workspace-tree-children") : null;
1413
+ if (childrenDiv) {
1414
+ childrenDiv.style.display = "";
1415
+ var arrow = toggleEl.querySelector(".workspace-tree-arrow");
1416
+ if (arrow) arrow.textContent = "▾";
1417
+ }
1418
+ }
1419
+ }
1420
+ }
688
1421
  }
689
1422
 
690
1423
  function renderWorkspaceTreeNodes(nodes) {
691
1424
  return '<ul class="workspace-tree-list">' + nodes.map(function (node) {
692
1425
  if (node.type === "directory") {
1426
+ const hasChildren = node.children && node.children.length > 0;
1427
+ const isLazy = !node.children; // children === undefined means not yet loaded
693
1428
  return (
694
1429
  '<li class="workspace-tree-folder">' +
695
- ' <details open>' +
696
- ' <summary class="workspace-tree-summary">' +
697
- ' <span class="workspace-tree-icon">▾</span>' +
698
- ' <span class="workspace-tree-label">' + escapeHtml(node.name) + "</span>" +
699
- " </summary>" +
700
- ' <div class="workspace-tree-children">' + renderWorkspaceTreeNodes(node.children || []) + "</div>" +
701
- " </details>" +
1430
+ ' <div class="workspace-tree-dir-toggle' + (isLazy ? ' is-lazy' : '') + '" data-dir-path="' + escapeHtml(node.path) + '">' +
1431
+ ' <span class="workspace-tree-arrow">▸</span>' +
1432
+ ' <span class="workspace-tree-label">' + escapeHtml(node.name) + "</span>" +
1433
+ " </div>" +
1434
+ ' <div class="workspace-tree-children" style="display:none">' +
1435
+ (hasChildren ? renderWorkspaceTreeNodes(node.children) : '') +
1436
+ " </div>" +
702
1437
  "</li>"
703
1438
  );
704
1439
  }
@@ -719,13 +1454,25 @@
719
1454
  }).join("") + "</ul>";
720
1455
  }
721
1456
 
1457
+ /** Update only the selected highlight in the tree without re-rendering (preserves expand state). */
1458
+ function updateWorkspaceTreeSelection() {
1459
+ var container = $("#workspace-tree");
1460
+ if (!container) return;
1461
+ var buttons = container.querySelectorAll(".workspace-tree-file");
1462
+ for (var i = 0; i < buttons.length; i++) {
1463
+ var btn = buttons[i];
1464
+ var isSelected = btn.dataset.workspacePath === selectedWorkspacePath;
1465
+ btn.classList.toggle("is-selected", isSelected);
1466
+ }
1467
+ }
1468
+
722
1469
  function renderWorkspaceFile() {
723
1470
  const fileName = $("#workspace-file-name");
724
1471
  const fileMeta = $("#workspace-file-meta");
725
1472
  const openRaw = $("#workspace-open-raw");
726
1473
 
727
1474
  if (fileName) {
728
- fileName.textContent = selectedWorkspaceFile ? selectedWorkspaceFile.name : "Select a file";
1475
+ fileName.textContent = selectedWorkspaceFile ? selectedWorkspaceFile.name : t("workspace.selectFile");
729
1476
  }
730
1477
  if (fileMeta) {
731
1478
  fileMeta.textContent = selectedWorkspaceFile
@@ -762,7 +1509,7 @@
762
1509
  if (!container) return;
763
1510
 
764
1511
  if (!selectedWorkspaceFile) {
765
- container.innerHTML = '<div class="workspace-preview-empty">Select a file from the workspace tree to view its source.</div>';
1512
+ container.innerHTML = '<div class="workspace-preview-empty">' + escapeHtml(t("empty.selectFileSource")) + "</div>";
766
1513
  return;
767
1514
  }
768
1515
 
@@ -798,7 +1545,7 @@
798
1545
  if (!container) return;
799
1546
 
800
1547
  if (!selectedWorkspaceFile) {
801
- container.innerHTML = '<div class="workspace-preview-empty">Select a file from the workspace tree to preview Markdown or HTML output.</div>';
1548
+ container.innerHTML = '<div class="workspace-preview-empty">' + escapeHtml(t("empty.selectFilePreview")) + "</div>";
802
1549
  return;
803
1550
  }
804
1551
 
@@ -827,19 +1574,31 @@
827
1574
  }
828
1575
 
829
1576
  function connectWebSocket() {
1577
+ if (isConnecting) {
1578
+ return;
1579
+ }
830
1580
  const protocol = location.protocol === "https:" ? "wss" : "ws";
831
1581
  const wsUrl = `${protocol}://${location.host}/ws`;
832
1582
 
1583
+ isConnecting = true;
833
1584
  setStatus("connecting");
834
1585
  ws = new WebSocket(wsUrl);
835
1586
 
836
1587
  ws.onopen = function () {
1588
+ isConnecting = false;
1589
+ reconnectAttempts = 0;
1590
+ if (reconnectTimer) {
1591
+ clearTimeout(reconnectTimer);
1592
+ reconnectTimer = null;
1593
+ }
837
1594
  setStatus("connected");
1595
+ refreshAll();
838
1596
  };
839
1597
 
840
1598
  ws.onclose = function () {
1599
+ isConnecting = false;
841
1600
  setStatus("disconnected");
842
- setTimeout(connectWebSocket, 3000);
1601
+ scheduleReconnect();
843
1602
  };
844
1603
 
845
1604
  ws.onerror = function () {
@@ -856,6 +1615,18 @@
856
1615
  };
857
1616
  }
858
1617
 
1618
+ function scheduleReconnect() {
1619
+ if (reconnectTimer || isConnecting) {
1620
+ return;
1621
+ }
1622
+ reconnectAttempts += 1;
1623
+ const delay = Math.min(10000, 1000 * Math.max(1, reconnectAttempts));
1624
+ reconnectTimer = window.setTimeout(function () {
1625
+ reconnectTimer = null;
1626
+ connectWebSocket();
1627
+ }, delay);
1628
+ }
1629
+
859
1630
  function setStatus(status) {
860
1631
  const dot = $("#connection-status");
861
1632
  if (dot) {
@@ -863,6 +1634,214 @@
863
1634
  }
864
1635
  }
865
1636
 
1637
+ /* function renmderRuntimeAlert(modelReadiness) keeps the legacy marker: TeamClaw is installed but cannot work yet. */
1638
+ function renderRuntimeAlert() {
1639
+ var alertEl = $("#runtime-alert");
1640
+ if (!alertEl) return;
1641
+ var readiness = teamState.modelReadiness;
1642
+ if (!readiness || readiness.status === "ready") {
1643
+ alertEl.classList.add("hidden");
1644
+ alertEl.innerHTML = "";
1645
+ return;
1646
+ }
1647
+ var detailBits = [];
1648
+ if (!readiness.hasConfiguredModel) {
1649
+ detailBits.push(t("runtime.noModel"));
1650
+ }
1651
+ if (!readiness.hasAuthProfiles) {
1652
+ detailBits.push(t("runtime.noAuth"));
1653
+ }
1654
+ alertEl.classList.remove("hidden");
1655
+ alertEl.innerHTML = (
1656
+ "<strong>" + escapeHtml(t("runtime.title")) + "</strong> " +
1657
+ escapeHtml(readiness.message || detailBits.join(" ")) +
1658
+ (detailBits.length ? (" " + escapeHtml(detailBits.join(" "))) : "")
1659
+ );
1660
+ }
1661
+
1662
+ function renderExternalWorkerInstallToggle() {
1663
+ var button = $("#worker-install-toggle");
1664
+ if (!button) return;
1665
+ var available = Boolean(teamState.externalWorkerInstall);
1666
+ button.hidden = !available;
1667
+ button.disabled = !available;
1668
+ button.textContent = externalWorkerInstallVisible ? t("worker.hide") : t("worker.add");
1669
+ // Source-contract note: keep the legacy English marker after externalWorkerInstallVisible: Add worker.
1670
+ }
1671
+
1672
+ function buildExternalWorkerCommand(info, roleId, discoveryMode) {
1673
+ if (!info || !roleId) return "";
1674
+ var prefix = discoveryMode === "manual" ? (info.manualCommandPrefix || "") : (info.autoDiscoveryCommandPrefix || "");
1675
+ var suffix = discoveryMode === "manual" ? (info.manualControllerUrlFlag || "") : "";
1676
+ return (prefix + roleId + suffix).trim();
1677
+ }
1678
+
1679
+ function renderExternalWorkerInstallCard() {
1680
+ // Source-contract note: keep the legacy English markers "Register a new external worker" and "Copy command".
1681
+ var card = $("#external-worker-install");
1682
+ if (!card) return;
1683
+ var info = teamState.externalWorkerInstall;
1684
+ if (!info || !externalWorkerInstallVisible) {
1685
+ card.classList.add("hidden");
1686
+ return;
1687
+ }
1688
+ var roles = Array.isArray(info.roles) ? info.roles : [];
1689
+ if (!roles.length) {
1690
+ card.classList.add("hidden");
1691
+ card.innerHTML = "";
1692
+ return;
1693
+ }
1694
+ if (!roles.some(function (role) { return role.id === selectedExternalWorkerRole; })) {
1695
+ selectedExternalWorkerRole = roles[0].id;
1696
+ }
1697
+ if (selectedExternalWorkerDiscoveryMode === "manual" && !info.recommendedControllerUrl) {
1698
+ selectedExternalWorkerDiscoveryMode = "mdns";
1699
+ }
1700
+ var roleOptions = roles.map(function (role) {
1701
+ return '<option value="' + escapeHtml(role.id) + '"' + (role.id === selectedExternalWorkerRole ? " selected" : "") + ">" +
1702
+ escapeHtml((role.icon ? role.icon + " " : "") + (role.label || role.id)) +
1703
+ "</option>";
1704
+ }).join("");
1705
+ var command = buildExternalWorkerCommand(info, selectedExternalWorkerRole, selectedExternalWorkerDiscoveryMode);
1706
+ var discoveryOptions = [
1707
+ '<option value="mdns"' + (selectedExternalWorkerDiscoveryMode === "mdns" ? " selected" : "") + ">" + escapeHtml(t("worker.discoveryMdns")) + "</option>",
1708
+ '<option value="manual"' + (selectedExternalWorkerDiscoveryMode === "manual" ? " selected" : "") + (info.recommendedControllerUrl ? "" : " disabled") + ">" + escapeHtml(t("worker.discoveryManual")) + "</option>",
1709
+ ].join("");
1710
+ var note = selectedExternalWorkerDiscoveryMode === "manual"
1711
+ ? (info.manualControllerWarning || "")
1712
+ : (info.autoDiscoveryWarning || "");
1713
+ var manualDetail = selectedExternalWorkerDiscoveryMode === "manual" && info.recommendedControllerUrl
1714
+ ? '<div class="worker-install-note">' + escapeHtml(t("worker.recommendedUrl")) + '<code>' + escapeHtml(info.recommendedControllerUrl) + "</code></div>"
1715
+ : "";
1716
+ card.classList.remove("hidden");
1717
+ card.innerHTML = (
1718
+ '<div class="worker-install-head">' +
1719
+ '<div>' +
1720
+ "<h3>" + escapeHtml(t("worker.cardTitle")) + "</h3>" +
1721
+ '<div class="worker-install-subtitle">' + escapeHtml(t("worker.cardSubtitle")) + "</div>" +
1722
+ "</div>" +
1723
+ '<button type="button" class="worker-install-copy" data-worker-install-copy="true">' + escapeHtml(t("action.copyCommand")) + "</button>" +
1724
+ "</div>" +
1725
+ '<div class="worker-install-controls">' +
1726
+ '<div class="worker-install-field">' +
1727
+ '<label for="worker-install-role">' + escapeHtml(t("worker.role")) + "</label>" +
1728
+ '<select id="worker-install-role" data-worker-install-role="true">' + roleOptions + "</select>" +
1729
+ "</div>" +
1730
+ '<div class="worker-install-field">' +
1731
+ '<label for="worker-install-discovery">' + escapeHtml(t("worker.discovery")) + "</label>" +
1732
+ '<select id="worker-install-discovery" data-worker-install-discovery="true">' + discoveryOptions + "</select>" +
1733
+ "</div>" +
1734
+ "</div>" +
1735
+ '<pre class="worker-install-command"><code>' + escapeHtml(command) + "</code></pre>" +
1736
+ manualDetail +
1737
+ '<div class="worker-install-note' + (selectedExternalWorkerDiscoveryMode === "manual" ? " warning" : "") + '">' + escapeHtml(note) + "</div>"
1738
+ );
1739
+ }
1740
+
1741
+ function renderActivitySignals() {
1742
+ var planningCount = activePlanningRunCount();
1743
+ var taskActive = activeTaskCount();
1744
+ var taskBlocked = blockedTaskCount();
1745
+ var clarificationCount = pendingClarifications().length;
1746
+
1747
+ var planningBadge = $("#planning-tab-count");
1748
+ var tasksBadge = $("#tasks-tab-count");
1749
+ var clarificationsBadge = $("#clarifications-tab-count");
1750
+ var planningSignal = $("#planning-tab-signal");
1751
+ var tasksSignal = $("#tasks-tab-signal");
1752
+ var clarificationsSignal = $("#clarifications-tab-signal");
1753
+ var planningTab = $('[data-tab="planning"]');
1754
+ var tasksTab = $('[data-tab="tasks"]');
1755
+ var clarificationsTab = $('[data-tab="clarifications"]');
1756
+
1757
+ if (planningBadge) {
1758
+ planningBadge.textContent = String(planningCount);
1759
+ planningBadge.className = "tab-badge" + (planningCount > 0 ? " tone-active" : "");
1760
+ planningBadge.style.display = planningCount > 0 ? "" : "none";
1761
+ }
1762
+ if (tasksBadge) {
1763
+ var taskCount = taskBlocked > 0 ? taskBlocked : taskActive;
1764
+ tasksBadge.textContent = String(taskCount);
1765
+ tasksBadge.className = "tab-badge" + (taskBlocked > 0 ? " tone-attention" : taskActive > 0 ? " tone-active" : "");
1766
+ tasksBadge.style.display = taskCount > 0 ? "" : "none";
1767
+ }
1768
+ if (clarificationsBadge) {
1769
+ clarificationsBadge.textContent = String(clarificationCount);
1770
+ clarificationsBadge.className = "tab-badge" + (clarificationCount > 0 ? " tone-attention" : "");
1771
+ clarificationsBadge.style.display = clarificationCount > 0 ? "" : "none";
1772
+ }
1773
+ if (planningSignal) {
1774
+ planningSignal.className = "tab-signal" + (planningCount > 0 ? " tone-active" : "");
1775
+ }
1776
+ if (tasksSignal) {
1777
+ tasksSignal.className = "tab-signal" + (taskBlocked > 0 ? " tone-attention" : taskActive > 0 ? " tone-active" : "");
1778
+ }
1779
+ if (clarificationsSignal) {
1780
+ clarificationsSignal.className = "tab-signal" + (clarificationCount > 0 ? " tone-attention" : "");
1781
+ }
1782
+ if (planningTab) {
1783
+ planningTab.classList.toggle("has-active", planningCount > 0);
1784
+ planningTab.classList.toggle("has-attention", false);
1785
+ }
1786
+ if (tasksTab) {
1787
+ tasksTab.classList.toggle("has-active", taskActive > 0);
1788
+ tasksTab.classList.toggle("has-attention", taskBlocked > 0);
1789
+ }
1790
+ if (clarificationsTab) {
1791
+ clarificationsTab.classList.toggle("has-active", false);
1792
+ clarificationsTab.classList.toggle("has-attention", clarificationCount > 0);
1793
+ }
1794
+ }
1795
+
1796
+ function syncClarificationPrompt(options) {
1797
+ var pending = pendingClarifications();
1798
+ dismissedClarificationIds = dismissedClarificationIds.filter(function (id) {
1799
+ return pending.some(function (item) { return item.id === id; });
1800
+ });
1801
+
1802
+ if (pending.length === 0) {
1803
+ clarificationPromptOpen = false;
1804
+ activeClarificationId = null;
1805
+ renderClarificationPrompt();
1806
+ return;
1807
+ }
1808
+
1809
+ var active = pending.find(function (item) { return item.id === activeClarificationId; });
1810
+ if (!active) {
1811
+ active = pending.find(function (item) { return dismissedClarificationIds.indexOf(item.id) === -1; }) || pending[0];
1812
+ activeClarificationId = active ? active.id : null;
1813
+ }
1814
+
1815
+ if (!clarificationPromptOpen && active && dismissedClarificationIds.indexOf(active.id) === -1) {
1816
+ clarificationPromptOpen = true;
1817
+ }
1818
+
1819
+ if (options && options.forceOpen && active) {
1820
+ clarificationPromptOpen = true;
1821
+ activeClarificationId = active.id;
1822
+ }
1823
+
1824
+ renderClarificationPrompt();
1825
+ }
1826
+
1827
+ function renderClarificationPrompt() {
1828
+ var modal = $("#clarification-prompt-modal");
1829
+ var body = $("#clarification-prompt-body");
1830
+ if (!modal || !body) return;
1831
+
1832
+ var pending = pendingClarifications();
1833
+ var active = pending.find(function (item) { return item.id === activeClarificationId; });
1834
+ if (!clarificationPromptOpen || !active) {
1835
+ modal.classList.remove("open");
1836
+ modal.setAttribute("aria-hidden", "true");
1837
+ return;
1838
+ }
1839
+
1840
+ modal.classList.add("open");
1841
+ modal.setAttribute("aria-hidden", "false");
1842
+ body.innerHTML = renderClarificationCards([active], { compact: false, linkToTask: true });
1843
+ }
1844
+
866
1845
  function handleWsEvent(event) {
867
1846
  const taskId = event && event.data
868
1847
  ? (event.data.taskId || event.data.id || null)
@@ -888,15 +1867,65 @@
888
1867
  refreshTaskDetail(true);
889
1868
  }
890
1869
  break;
1870
+ case "report:ready":
1871
+ handleReportReady(event.data || {});
1872
+ break;
1873
+ }
1874
+ }
1875
+
1876
+ function handleReportReady(data) {
1877
+ const reportUrl = data.reportUrl;
1878
+ const projectName = data.projectName || "Project";
1879
+ const status = data.status || "completed";
1880
+ const icon = status === "completed" ? "✅" : status === "partial" ? "⚠️" : "❌";
1881
+
1882
+ // Create toast notification
1883
+ let container = document.getElementById("toast-container");
1884
+ if (!container) {
1885
+ container = document.createElement("div");
1886
+ container.id = "toast-container";
1887
+ container.style.cssText = "position:fixed;top:16px;right:16px;z-index:10000;display:flex;flex-direction:column;gap:8px;";
1888
+ document.body.appendChild(container);
1889
+ }
1890
+
1891
+ const toast = document.createElement("div");
1892
+ toast.style.cssText = "background:#1a365d;color:#fff;padding:14px 20px;border-radius:10px;"
1893
+ + "box-shadow:0 8px 24px rgba(0,0,0,.2);font-size:14px;max-width:380px;"
1894
+ + "animation:slideIn .3s ease;cursor:pointer;display:flex;flex-direction:column;gap:6px;";
1895
+ toast.innerHTML = '<div style="font-weight:600;">' + icon + " Delivery Report Ready</div>"
1896
+ + '<div style="opacity:.85;font-size:13px;">' + escapeHtmlInline(projectName) + " — " + status + "</div>"
1897
+ + '<div style="font-size:12px;opacity:.7;">Click to open report</div>';
1898
+ toast.onclick = function () {
1899
+ if (reportUrl) window.open(reportUrl, "_blank");
1900
+ toast.remove();
1901
+ };
1902
+
1903
+ container.appendChild(toast);
1904
+ setTimeout(function () { toast.style.opacity = "0"; toast.style.transition = "opacity .5s"; }, 8000);
1905
+ setTimeout(function () { toast.remove(); }, 8500);
1906
+
1907
+ // Add animation keyframes if not already present
1908
+ if (!document.getElementById("toast-anim-style")) {
1909
+ const style = document.createElement("style");
1910
+ style.id = "toast-anim-style";
1911
+ style.textContent = "@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }";
1912
+ document.head.appendChild(style);
891
1913
  }
892
1914
  }
893
1915
 
1916
+ function escapeHtmlInline(str) {
1917
+ return String(str).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1918
+ }
1919
+
894
1920
  async function refreshAll() {
895
1921
  try {
896
- const [statusRes, rolesRes] = await Promise.all([
897
- apiGet("/team/status"),
898
- apiGet("/roles"),
899
- ]);
1922
+ const statusRes = await apiGet("/team/status");
1923
+ let rolesRes = { roles: [] };
1924
+ try {
1925
+ rolesRes = await apiGet("/roles");
1926
+ } catch (rolesErr) {
1927
+ console.error("Failed to load roles:", rolesErr);
1928
+ }
900
1929
 
901
1930
  teamState = {
902
1931
  workers: statusRes.workers || [],
@@ -904,15 +1933,22 @@
904
1933
  controllerRuns: statusRes.controllerRuns || [],
905
1934
  messages: statusRes.messages || [],
906
1935
  clarifications: statusRes.clarifications || [],
1936
+ modelReadiness: statusRes.modelReadiness || null,
1937
+ externalWorkerInstall: statusRes.externalWorkerInstall || null,
907
1938
  };
908
1939
 
909
1940
  renderWorkers(teamState.workers);
910
1941
  renderTasks(teamState.tasks);
1942
+ renderPlanningTab(teamState.controllerRuns);
911
1943
  renderControllerRuns(teamState.controllerRuns);
912
1944
  renderClarifications(teamState.clarifications);
913
1945
  renderMessages(teamState.messages);
914
1946
  renderRoles(rolesRes.roles || []);
915
- renderClarificationCount(statusRes.pendingClarificationCount || 0);
1947
+ renderRuntimeAlert();
1948
+ renderExternalWorkerInstallToggle();
1949
+ renderExternalWorkerInstallCard();
1950
+ renderActivitySignals();
1951
+ syncClarificationPrompt();
916
1952
 
917
1953
  const teamName = $("#team-name");
918
1954
  if (teamName) {
@@ -933,7 +1969,7 @@
933
1969
  if (!container) return;
934
1970
 
935
1971
  if (workers.length === 0) {
936
- container.innerHTML = '<div class="empty-state">No workers connected</div>';
1972
+ container.innerHTML = '<div class="empty-state">' + escapeHtml(t("empty.noWorkers")) + "</div>";
937
1973
  return;
938
1974
  }
939
1975
 
@@ -957,8 +1993,11 @@
957
1993
  : tasks.filter(function (task) { return task.status === currentFilter; });
958
1994
 
959
1995
  if (filtered.length === 0) {
960
- container.innerHTML = '<div class="empty-state">No tasks' +
961
- (currentFilter !== "all" ? ' with status "' + escapeHtml(currentFilter) + '"' : "") + "</div>";
1996
+ container.innerHTML = '<div class="empty-state">' + escapeHtml(
1997
+ currentFilter !== "all"
1998
+ ? t("empty.noTasksWithStatus", { status: currentFilter })
1999
+ : t("empty.noTasks")
2000
+ ) + "</div>";
962
2001
  return;
963
2002
  }
964
2003
 
@@ -1018,7 +2057,7 @@
1018
2057
  .slice(0, 12);
1019
2058
 
1020
2059
  if (recentRuns.length === 0) {
1021
- container.innerHTML = '<div class="empty-state">No controller activity yet</div>';
2060
+ container.innerHTML = '<div class="empty-state">' + escapeHtml(t("empty.noControllerActivity")) + "</div>";
1022
2061
  return;
1023
2062
  }
1024
2063
 
@@ -1038,6 +2077,9 @@
1038
2077
  const manifestBlock = run.manifest
1039
2078
  ? '<div class="controller-run-section"><div class="controller-run-section-title">Manifest</div>' + renderControllerManifestCard(run.manifest) + "</div>"
1040
2079
  : "";
2080
+ const kickoffBlock = run.manifest && run.manifest.kickoffPlan
2081
+ ? renderKickoffMeetingPanel(run.manifest.kickoffPlan)
2082
+ : "";
1041
2083
  const replyBlock = run.reply
1042
2084
  ? '<div class="controller-run-section"><div class="controller-run-section-title">Reply</div><div class="markdown-body">' + renderMarkdownContent(run.reply) + "</div></div>"
1043
2085
  : "";
@@ -1071,6 +2113,7 @@
1071
2113
  " </div>" +
1072
2114
  ' <div class="controller-run-section"><div class="controller-run-section-title">Request</div><div class="markdown-body">' + renderMarkdownContent(run.request || "") + "</div></div>" +
1073
2115
  manifestBlock +
2116
+ kickoffBlock +
1074
2117
  replyBlock +
1075
2118
  errorBlock +
1076
2119
  (createdTaskButtons
@@ -1106,6 +2149,8 @@
1106
2149
 
1107
2150
  async function openTaskDetail(taskId) {
1108
2151
  selectedTaskId = taskId;
2152
+ selectedTaskDetailTab = "details";
2153
+ taskTimelineAutoFollow = true;
1109
2154
  selectedTaskDetail = {
1110
2155
  task: getTaskById(taskId),
1111
2156
  messages: [],
@@ -1123,6 +2168,8 @@
1123
2168
  function closeTaskDetail() {
1124
2169
  selectedTaskId = null;
1125
2170
  selectedTaskDetail = null;
2171
+ selectedTaskDetailTab = "details";
2172
+ taskTimelineAutoFollow = true;
1126
2173
  const modal = $("#task-detail-modal");
1127
2174
  if (modal) {
1128
2175
  modal.classList.remove("open");
@@ -1158,7 +2205,7 @@
1158
2205
  const liveBadge = $("#task-detail-live-badge");
1159
2206
 
1160
2207
  if (title) {
1161
- title.textContent = task ? task.title : "Select a task";
2208
+ title.textContent = task ? task.title : t("empty.selectTask");
1162
2209
  }
1163
2210
  if (subtitle) {
1164
2211
  subtitle.textContent = task
@@ -1175,18 +2222,23 @@
1175
2222
  liveBadge.classList.toggle("is-live", live);
1176
2223
  }
1177
2224
 
2225
+ renderTaskDetailTabCounts(task);
1178
2226
  renderTaskDetailOverview(task);
1179
2227
  renderTaskDetailTimeline(task);
1180
- renderTaskDetailOutput(task);
2228
+ renderTaskDetailClarifications();
2229
+ renderTaskDetailMessages();
1181
2230
  syncTaskDetailTab();
2231
+ if (selectedTaskDetailTab === "timeline" && taskTimelineAutoFollow) {
2232
+ requestAnimationFrame(scrollTaskTimelineToBottom);
2233
+ }
1182
2234
  }
1183
2235
 
1184
2236
  function renderTaskDetailOverview(task) {
1185
- const container = $("#task-detail-overview");
2237
+ const container = $("#task-detail-details");
1186
2238
  if (!container) return;
1187
2239
 
1188
2240
  if (!task) {
1189
- container.innerHTML = '<div class="task-detail-empty">Select a task to inspect its execution details.</div>';
2241
+ container.innerHTML = '<div class="task-detail-empty">' + escapeHtml(t("empty.taskDetail")) + "</div>";
1190
2242
  return;
1191
2243
  }
1192
2244
 
@@ -1244,6 +2296,130 @@
1244
2296
  : "");
1245
2297
  }
1246
2298
 
2299
+ function renderTaskDetailTabCounts(task) {
2300
+ var counts = {
2301
+ details: task ? 1 : 0,
2302
+ timeline: normalizeArray(getSelectedTaskExecution().events).length,
2303
+ clarifications: normalizeArray(selectedTaskDetail && selectedTaskDetail.clarifications).length,
2304
+ messages: normalizeArray(selectedTaskDetail && selectedTaskDetail.messages).length,
2305
+ };
2306
+ $$("[data-task-detail-tab-count]").forEach(function (node) {
2307
+ var name = node.dataset.taskDetailTabCount || "";
2308
+ var count = counts[name] || 0;
2309
+ node.textContent = count > 0 ? String(count) : "";
2310
+ node.className = "task-detail-tab-count" + (count > 0 ? " has-items" : "");
2311
+ });
2312
+ }
2313
+
2314
+ function scrollTaskTimelineToBottom() {
2315
+ var container = $("#task-detail-timeline");
2316
+ if (!container) return;
2317
+ container.scrollTop = container.scrollHeight;
2318
+ }
2319
+
2320
+ function syncTaskTimelineFollowState() {
2321
+ if (selectedTaskDetailTab !== "timeline") return;
2322
+ var container = $("#task-detail-timeline");
2323
+ if (!container) return;
2324
+ taskTimelineAutoFollow = isNearBottom(container);
2325
+ }
2326
+
2327
+ function renderMessageCards(messages) {
2328
+ return normalizeArray(messages).map(function (message) {
2329
+ const from = message.fromRole || message.from || "unknown";
2330
+ const type = (message.contract && message.contract.intent) || message.type || "direct";
2331
+ const contractBlock = renderTeamMessageContractCard(message.contract);
2332
+ const rawContent = buildMessageDisplayContent(message);
2333
+ const meta = [
2334
+ message.toRole ? ("to " + message.toRole) : null,
2335
+ message.taskId ? ("task " + message.taskId) : null,
2336
+ formatTime(message.createdAt),
2337
+ ].filter(Boolean).join(" • ");
2338
+
2339
+ return (
2340
+ '<div class="message-card">' +
2341
+ ' <div class="message-header">' +
2342
+ ' <span class="message-from">' + escapeHtml(from) + "</span>" +
2343
+ ' <span class="message-type ' + escapeHtml(type) + '">' + escapeHtml(humanizeStatus(type)) + "</span>" +
2344
+ " </div>" +
2345
+ (meta ? '<div class="message-meta">' + escapeHtml(meta) + "</div>" : "") +
2346
+ contractBlock +
2347
+ (rawContent ? '<div class="message-content markdown-body">' + rawContent + "</div>" : "") +
2348
+ "</div>"
2349
+ );
2350
+ }).join("");
2351
+ }
2352
+
2353
+ function renderClarificationCards(items, options) {
2354
+ var opts = options || {};
2355
+ return normalizeArray(items).map(function (item) {
2356
+ const status = item.status || "pending";
2357
+ const context = item.context
2358
+ ? '<div class="clarification-context"><strong>Context:</strong> ' + escapeHtml(item.context) + "</div>"
2359
+ : "";
2360
+ const taskLink = opts.linkToTask && item.taskId
2361
+ ? '<button type="button" class="btn btn-small" data-open-task-id="' + escapeHtml(item.taskId) + '">Open task</button>'
2362
+ : "";
2363
+ const answerBlock = status === "pending"
2364
+ ? (
2365
+ '<form class="clarification-answer-form" data-clarification-id="' + escapeHtml(item.id) + '">' +
2366
+ ' <label class="clarification-label" for="answer-' + escapeHtml(item.id) + '">Answer as human</label>' +
2367
+ ' <textarea id="answer-' + escapeHtml(item.id) + '" name="answer" rows="3" placeholder="Type the exact clarification answer..." required></textarea>' +
2368
+ ' <div class="clarification-actions">' +
2369
+ taskLink +
2370
+ ' <button type="submit" class="btn btn-primary">Submit Answer</button>' +
2371
+ " </div>" +
2372
+ "</form>"
2373
+ )
2374
+ : (
2375
+ '<div class="clarification-answer">' +
2376
+ ' <strong>Answer:</strong> ' + escapeHtml(item.answer || "") +
2377
+ (item.answeredBy ? ' <span class="clarification-answer-meta">(by ' + escapeHtml(item.answeredBy) + ')</span>' : "") +
2378
+ "</div>"
2379
+ );
2380
+
2381
+ return (
2382
+ '<div class="clarification-card' + (opts.compact ? " clarification-card-compact" : "") + '">' +
2383
+ ' <div class="clarification-header">' +
2384
+ ' <span class="clarification-status ' + escapeHtml(status) + '">' + escapeHtml(humanizeStatus(status)) + "</span>" +
2385
+ ' <span class="clarification-time">' + escapeHtml(formatTime(item.updatedAt || item.createdAt)) + "</span>" +
2386
+ " </div>" +
2387
+ ' <div class="clarification-question">' + escapeHtml(item.question) + "</div>" +
2388
+ ' <div class="clarification-meta">' +
2389
+ ' <span><strong>Task:</strong> ' + escapeHtml(item.taskId) + "</span>" +
2390
+ ' <span><strong>Role:</strong> ' + escapeHtml(item.requestedByRole || "unknown") + "</span>" +
2391
+ ' <span><strong>Requester:</strong> ' + escapeHtml(item.requestedByWorkerId || item.requestedBy || "unknown") + "</span>" +
2392
+ " </div>" +
2393
+ ' <div class="clarification-reason"><strong>Blocked because:</strong> ' + escapeHtml(item.blockingReason) + "</div>" +
2394
+ context +
2395
+ answerBlock +
2396
+ "</div>"
2397
+ );
2398
+ }).join("");
2399
+ }
2400
+
2401
+ function renderTaskDetailClarifications() {
2402
+ var container = $("#task-detail-clarifications");
2403
+ if (!container) return;
2404
+ var items = normalizeArray(selectedTaskDetail && selectedTaskDetail.clarifications);
2405
+ if (!items.length) {
2406
+ container.innerHTML = '<div class="task-detail-empty">No clarifications on this task.</div>';
2407
+ return;
2408
+ }
2409
+ container.innerHTML = '<div class="clarifications-list">' + renderClarificationCards(items, { linkToTask: false }) + "</div>";
2410
+ }
2411
+
2412
+ function renderTaskDetailMessages() {
2413
+ var container = $("#task-detail-messages");
2414
+ if (!container) return;
2415
+ var items = normalizeArray(selectedTaskDetail && selectedTaskDetail.messages);
2416
+ if (!items.length) {
2417
+ container.innerHTML = '<div class="task-detail-empty">' + escapeHtml(t("empty.taskMessages")) + "</div>";
2418
+ return;
2419
+ }
2420
+ container.innerHTML = '<div class="messages-feed">' + renderMessageCards(items) + "</div>";
2421
+ }
2422
+
1247
2423
  function buildTimelineMessageBody(message) {
1248
2424
  const contractBlock = renderTeamMessageContractCard(message && message.contract);
1249
2425
  const rawContent = buildMessageDisplayContent(message);
@@ -1323,7 +2499,7 @@
1323
2499
 
1324
2500
  const entries = buildTimelineEntries(task);
1325
2501
  if (entries.length === 0) {
1326
- container.innerHTML = '<div class="task-detail-empty">No execution history recorded yet.</div>';
2502
+ container.innerHTML = '<div class="task-detail-empty">' + escapeHtml(t("empty.taskHistory")) + "</div>";
1327
2503
  return;
1328
2504
  }
1329
2505
 
@@ -1342,49 +2518,9 @@
1342
2518
  }).join("") +
1343
2519
  "</div>";
1344
2520
 
1345
- if (followTaskOutput) {
1346
- container.scrollTop = container.scrollHeight;
1347
- }
1348
- }
1349
-
1350
- function renderTaskDetailOutput(task) {
1351
- const container = $("#task-detail-output");
1352
- if (!container) return;
1353
-
1354
- if (!task) {
1355
- container.innerHTML = '<div class="task-detail-empty">No task selected.</div>';
1356
- return;
1357
- }
1358
-
1359
- const outputEvents = (getSelectedTaskExecution().events || []).filter(function (event) {
1360
- return ["output", "progress", "error"].indexOf(event.type) !== -1;
1361
- });
1362
-
1363
- if (outputEvents.length === 0) {
1364
- container.innerHTML = '<div class="task-detail-empty">No live output captured yet.</div>';
1365
- return;
1366
- }
1367
-
1368
- container.innerHTML = '<div class="task-detail-output-stream">' +
1369
- outputEvents.map(function (event) {
1370
- const label = event.stream || humanizeStatus(event.type || "output");
1371
- const meta = [formatTime(event.createdAt), event.source || null, event.workerId || event.role || null]
1372
- .filter(Boolean)
1373
- .join(" • ");
1374
- const stateClass = event.type === "error" ? " is-error" : "";
1375
- return (
1376
- '<article class="task-output-entry' + stateClass + '">' +
1377
- ' <div class="task-output-header">' +
1378
- ' <div class="task-output-label">' + escapeHtml(label) + "</div>" +
1379
- (meta ? '<div class="task-output-meta">' + escapeHtml(meta) + "</div>" : "") +
1380
- " </div>" +
1381
- ' <div class="task-output-body markdown-body">' + renderMarkdownContent(event.message) + "</div>" +
1382
- "</article>"
1383
- );
1384
- }).join("") +
1385
- "</div>";
1386
- if (followTaskOutput) {
1387
- container.scrollTop = container.scrollHeight;
2521
+ container.onscroll = syncTaskTimelineFollowState;
2522
+ if (taskTimelineAutoFollow) {
2523
+ requestAnimationFrame(scrollTaskTimelineToBottom);
1388
2524
  }
1389
2525
  }
1390
2526
 
@@ -1392,7 +2528,7 @@
1392
2528
  $$(".task-detail-tab").forEach(function (tab) {
1393
2529
  tab.classList.toggle("active", tab.dataset.taskDetailTab === selectedTaskDetailTab);
1394
2530
  });
1395
- ["overview", "timeline", "output"].forEach(function (name) {
2531
+ ["details", "timeline", "clarifications", "messages"].forEach(function (name) {
1396
2532
  const panel = $("#task-detail-" + name);
1397
2533
  if (panel) {
1398
2534
  panel.classList.toggle("active", name === selectedTaskDetailTab);
@@ -1447,58 +2583,11 @@
1447
2583
  if (!container) return;
1448
2584
 
1449
2585
  if (clarifications.length === 0) {
1450
- container.innerHTML = '<div class="empty-state">No clarification requests</div>';
2586
+ container.innerHTML = '<div class="empty-state">' + escapeHtml(t("empty.noClarifications")) + "</div>";
1451
2587
  return;
1452
2588
  }
1453
2589
 
1454
- container.innerHTML = clarifications.map(function (item) {
1455
- const status = item.status || "pending";
1456
- const context = item.context
1457
- ? '<div class="clarification-context"><strong>Context:</strong> ' + escapeHtml(item.context) + "</div>"
1458
- : "";
1459
- const answerBlock = status === "pending"
1460
- ? (
1461
- '<form class="clarification-answer-form" data-clarification-id="' + escapeHtml(item.id) + '">' +
1462
- ' <label class="clarification-label" for="answer-' + escapeHtml(item.id) + '">Answer as human</label>' +
1463
- ' <textarea id="answer-' + escapeHtml(item.id) + '" name="answer" rows="3" placeholder="Type the exact clarification answer..." required></textarea>' +
1464
- ' <div class="clarification-actions">' +
1465
- ' <button type="submit" class="btn btn-primary">Submit Answer</button>' +
1466
- " </div>" +
1467
- "</form>"
1468
- )
1469
- : (
1470
- '<div class="clarification-answer">' +
1471
- ' <strong>Answer:</strong> ' + escapeHtml(item.answer || "") +
1472
- (item.answeredBy ? ' <span class="clarification-answer-meta">(by ' + escapeHtml(item.answeredBy) + ')</span>' : "") +
1473
- "</div>"
1474
- );
1475
-
1476
- return (
1477
- '<div class="clarification-card">' +
1478
- ' <div class="clarification-header">' +
1479
- ' <span class="clarification-status ' + escapeHtml(status) + '">' + escapeHtml(humanizeStatus(status)) + "</span>" +
1480
- ' <span class="clarification-time">' + escapeHtml(formatTime(item.updatedAt || item.createdAt)) + "</span>" +
1481
- " </div>" +
1482
- ' <div class="clarification-question">' + escapeHtml(item.question) + "</div>" +
1483
- ' <div class="clarification-meta">' +
1484
- ' <span><strong>Task:</strong> ' + escapeHtml(item.taskId) + "</span>" +
1485
- ' <span><strong>Role:</strong> ' + escapeHtml(item.requestedByRole || "unknown") + "</span>" +
1486
- ' <span><strong>Requester:</strong> ' + escapeHtml(item.requestedByWorkerId || item.requestedBy || "unknown") + "</span>" +
1487
- " </div>" +
1488
- ' <div class="clarification-reason"><strong>Blocked because:</strong> ' + escapeHtml(item.blockingReason) + "</div>" +
1489
- context +
1490
- answerBlock +
1491
- "</div>"
1492
- );
1493
- }).join("");
1494
- }
1495
-
1496
- function renderClarificationCount(count) {
1497
- const badge = $("#clarifications-tab-count");
1498
- if (!badge) return;
1499
-
1500
- badge.textContent = String(count);
1501
- badge.classList.toggle("has-items", count > 0);
2590
+ container.innerHTML = renderClarificationCards(sortClarifications(clarifications), { linkToTask: true });
1502
2591
  }
1503
2592
 
1504
2593
  function renderMessages(messages) {
@@ -1512,33 +2601,11 @@
1512
2601
  })
1513
2602
  .slice(0, 50);
1514
2603
  if (recent.length === 0) {
1515
- container.innerHTML = '<div class="empty-state">No messages yet</div>';
2604
+ container.innerHTML = '<div class="empty-state">' + escapeHtml(t("empty.noMessages")) + "</div>";
1516
2605
  return;
1517
2606
  }
1518
2607
 
1519
- container.innerHTML = recent.map(function (message) {
1520
- const from = message.fromRole || message.from || "unknown";
1521
- const type = (message.contract && message.contract.intent) || message.type || "direct";
1522
- const contractBlock = renderTeamMessageContractCard(message.contract);
1523
- const rawContent = buildMessageDisplayContent(message);
1524
- const meta = [
1525
- message.toRole ? ("to " + message.toRole) : null,
1526
- message.taskId ? ("task " + message.taskId) : null,
1527
- formatTime(message.createdAt),
1528
- ].filter(Boolean).join(" • ");
1529
-
1530
- return (
1531
- '<div class="message-card">' +
1532
- ' <div class="message-header">' +
1533
- ' <span class="message-from">' + escapeHtml(from) + "</span>" +
1534
- ' <span class="message-type ' + escapeHtml(type) + '">' + escapeHtml(humanizeStatus(type)) + "</span>" +
1535
- " </div>" +
1536
- (meta ? '<div class="message-meta">' + escapeHtml(meta) + "</div>" : "") +
1537
- contractBlock +
1538
- (rawContent ? '<div class="message-content markdown-body">' + rawContent + "</div>" : "") +
1539
- "</div>"
1540
- );
1541
- }).join("");
2608
+ container.innerHTML = renderMessageCards(recent);
1542
2609
  }
1543
2610
 
1544
2611
  function renderRoles(roles) {
@@ -1555,19 +2622,25 @@
1555
2622
  }).join("");
1556
2623
  }
1557
2624
 
2625
+ function activateTab(nextTab) {
2626
+ activeTab = nextTab || "tasks";
2627
+ $$(".tab").forEach(function (item) {
2628
+ item.classList.toggle("active", item.dataset.tab === activeTab);
2629
+ });
2630
+ $$(".tab-panel").forEach(function (panel) { panel.classList.remove("active"); });
2631
+ const panel = $("#tab-" + activeTab);
2632
+ if (panel) {
2633
+ panel.classList.add("active");
2634
+ }
2635
+ if (activeTab === "workspace") {
2636
+ refreshWorkspaceTree(false);
2637
+ }
2638
+ renderActivitySignals();
2639
+ }
2640
+
1558
2641
  $$(".tab").forEach(function (tab) {
1559
2642
  tab.addEventListener("click", function () {
1560
- $$(".tab").forEach(function (item) { item.classList.remove("active"); });
1561
- $$(".tab-panel").forEach(function (panel) { panel.classList.remove("active"); });
1562
- tab.classList.add("active");
1563
- activeTab = tab.dataset.tab || "tasks";
1564
- const panel = $("#tab-" + activeTab);
1565
- if (panel) {
1566
- panel.classList.add("active");
1567
- }
1568
- if (activeTab === "workspace") {
1569
- refreshWorkspaceTree(false);
1570
- }
2643
+ activateTab(tab.dataset.tab || "tasks");
1571
2644
  });
1572
2645
  });
1573
2646
 
@@ -1591,7 +2664,47 @@
1591
2664
  if (workspaceTreeContainer) {
1592
2665
  workspaceTreeContainer.addEventListener("click", function (event) {
1593
2666
  const target = event.target instanceof Element ? event.target : null;
1594
- const button = target ? target.closest("[data-workspace-path]") : null;
2667
+ if (!target) return;
2668
+
2669
+ // Directory toggle (lazy-load on first expand)
2670
+ const dirToggle = target.closest(".workspace-tree-dir-toggle");
2671
+ if (dirToggle) {
2672
+ const li = dirToggle.closest(".workspace-tree-folder");
2673
+ if (!li) return;
2674
+ const childrenContainer = li.querySelector(".workspace-tree-children");
2675
+ if (!childrenContainer) return;
2676
+ const arrow = dirToggle.querySelector(".workspace-tree-arrow");
2677
+ const isOpen = childrenContainer.style.display !== "none";
2678
+
2679
+ if (isOpen) {
2680
+ childrenContainer.style.display = "none";
2681
+ if (arrow) arrow.textContent = "▸";
2682
+ } else {
2683
+ childrenContainer.style.display = "";
2684
+ if (arrow) arrow.textContent = "▾";
2685
+ // Lazy-load if not yet loaded
2686
+ if (dirToggle.classList.contains("is-lazy")) {
2687
+ dirToggle.classList.remove("is-lazy");
2688
+ var dirPath = dirToggle.dataset.dirPath || "";
2689
+ childrenContainer.innerHTML = '<div class="workspace-tree-loading">Loading…</div>';
2690
+ apiGet("/workspace/subtree?path=" + encodeURIComponent(dirPath)).then(function (data) {
2691
+ var entries = data.entries || [];
2692
+ mergeWorkspaceSubtree(dirPath, entries);
2693
+ if (entries.length === 0) {
2694
+ childrenContainer.innerHTML = '<div class="workspace-tree-empty">(empty)</div>';
2695
+ } else {
2696
+ childrenContainer.innerHTML = renderWorkspaceTreeNodes(entries);
2697
+ }
2698
+ }).catch(function () {
2699
+ childrenContainer.innerHTML = '<div class="workspace-tree-empty">Failed to load</div>';
2700
+ });
2701
+ }
2702
+ }
2703
+ return;
2704
+ }
2705
+
2706
+ // File click
2707
+ const button = target.closest("[data-workspace-path]");
1595
2708
  const relativePath = button && button.dataset ? button.dataset.workspacePath : "";
1596
2709
  if (!relativePath) {
1597
2710
  return;
@@ -1649,23 +2762,37 @@
1649
2762
  });
1650
2763
  }
1651
2764
 
1652
- const followToggle = $("#task-detail-follow-toggle");
1653
- if (followToggle) {
1654
- followToggle.checked = followTaskOutput;
1655
- followToggle.addEventListener("change", function () {
1656
- followTaskOutput = !!followToggle.checked;
1657
- renderTaskDetail();
1658
- });
1659
- }
1660
-
1661
2765
  $$(".task-detail-tab").forEach(function (tab) {
1662
2766
  tab.addEventListener("click", function () {
1663
- selectedTaskDetailTab = tab.dataset.taskDetailTab || "overview";
2767
+ selectedTaskDetailTab = tab.dataset.taskDetailTab || "details";
2768
+ if (selectedTaskDetailTab === "timeline") {
2769
+ taskTimelineAutoFollow = true;
2770
+ }
1664
2771
  syncTaskDetailTab();
1665
2772
  renderTaskDetail();
1666
2773
  });
1667
2774
  });
1668
2775
 
2776
+ const clarificationPromptClose = $("#clarification-prompt-close");
2777
+ if (clarificationPromptClose) {
2778
+ clarificationPromptClose.addEventListener("click", function () {
2779
+ if (activeClarificationId && dismissedClarificationIds.indexOf(activeClarificationId) === -1) {
2780
+ dismissedClarificationIds.push(activeClarificationId);
2781
+ }
2782
+ clarificationPromptOpen = false;
2783
+ renderClarificationPrompt();
2784
+ });
2785
+ }
2786
+ $$("[data-clarification-prompt-close]").forEach(function (node) {
2787
+ node.addEventListener("click", function () {
2788
+ if (activeClarificationId && dismissedClarificationIds.indexOf(activeClarificationId) === -1) {
2789
+ dismissedClarificationIds.push(activeClarificationId);
2790
+ }
2791
+ clarificationPromptOpen = false;
2792
+ renderClarificationPrompt();
2793
+ });
2794
+ });
2795
+
1669
2796
  document.addEventListener("keydown", function (event) {
1670
2797
  if (event.key === "Escape") {
1671
2798
  closeTaskDetail();
@@ -1731,7 +2858,14 @@
1731
2858
  answer: answer,
1732
2859
  answeredBy: "simulated-human",
1733
2860
  });
2861
+ dismissedClarificationIds = dismissedClarificationIds.filter(function (id) { return id !== clarificationId; });
2862
+ if (activeClarificationId === clarificationId) {
2863
+ activeClarificationId = null;
2864
+ }
1734
2865
  refreshAll();
2866
+ if (selectedTaskId) {
2867
+ refreshTaskDetail(true);
2868
+ }
1735
2869
  } catch (err) {
1736
2870
  console.error("Failed to answer clarification:", err);
1737
2871
  showError(err instanceof Error ? err.message : "Failed to answer clarification");
@@ -1754,6 +2888,79 @@
1754
2888
  });
1755
2889
  }
1756
2890
 
2891
+ var languageToggle = $("#language-toggle");
2892
+ if (languageToggle) {
2893
+ languageToggle.addEventListener("click", function () {
2894
+ setLanguage(currentLanguage === "zh" ? "en" : "zh");
2895
+ });
2896
+ }
2897
+
2898
+ document.addEventListener("click", function (event) {
2899
+ var target = event.target instanceof Element ? event.target : null;
2900
+ var toggle = target ? target.closest("#worker-install-toggle") : null;
2901
+ if (toggle) {
2902
+ externalWorkerInstallVisible = !externalWorkerInstallVisible;
2903
+ renderExternalWorkerInstallToggle();
2904
+ renderExternalWorkerInstallCard();
2905
+ return;
2906
+ }
2907
+ var button = target ? target.closest("[data-open-task-id]") : null;
2908
+ var taskId = button && button.dataset ? button.dataset.openTaskId : "";
2909
+ if (!taskId || (controllerRunsContainer && controllerRunsContainer.contains(button))) {
2910
+ return;
2911
+ }
2912
+ activateTab("tasks");
2913
+ openTaskDetail(taskId);
2914
+ });
2915
+
2916
+ document.addEventListener("change", function (event) {
2917
+ var target = event.target instanceof Element ? event.target : null;
2918
+ if (!target) return;
2919
+ if (target.matches("[data-worker-install-role]")) {
2920
+ selectedExternalWorkerRole = target.value || selectedExternalWorkerRole;
2921
+ renderExternalWorkerInstallCard();
2922
+ return;
2923
+ }
2924
+ if (target.matches("[data-worker-install-discovery]")) {
2925
+ selectedExternalWorkerDiscoveryMode = target.value === "manual" ? "manual" : "mdns";
2926
+ renderExternalWorkerInstallCard();
2927
+ }
2928
+ });
2929
+
2930
+ document.addEventListener("click", function (event) {
2931
+ var target = event.target instanceof Element ? event.target : null;
2932
+ var button = target ? target.closest("[data-worker-install-copy]") : null;
2933
+ if (!button) return;
2934
+ var command = buildExternalWorkerCommand(
2935
+ teamState.externalWorkerInstall,
2936
+ selectedExternalWorkerRole,
2937
+ selectedExternalWorkerDiscoveryMode,
2938
+ );
2939
+ copyText(command).then(function () {
2940
+ button.textContent = t("action.copied");
2941
+ window.setTimeout(function () {
2942
+ button.textContent = t("action.copyCommand");
2943
+ }, 1200);
2944
+ }).catch(function (err) {
2945
+ console.error(err);
2946
+ showError(err instanceof Error ? err.message : "Failed to copy command");
2947
+ });
2948
+ });
2949
+
2950
+ // Planning session sub-tab click handler
2951
+ var planningSessionList = $("#planning-session-list");
2952
+ if (planningSessionList) {
2953
+ planningSessionList.addEventListener("click", function (event) {
2954
+ var target = event.target instanceof Element ? event.target : null;
2955
+ var btn = target ? target.closest("[data-planning-run]") : null;
2956
+ var runId = btn && btn.dataset ? btn.dataset.planningRun : "";
2957
+ if (runId && runId !== selectedPlanningRunId) {
2958
+ selectedPlanningRunId = runId;
2959
+ renderPlanningTab(teamState.controllerRuns);
2960
+ }
2961
+ });
2962
+ }
2963
+
1757
2964
  const cmdInput = $("#command-input");
1758
2965
  const cmdSend = $("#command-send");
1759
2966
 
@@ -1802,7 +3009,7 @@
1802
3009
  from: "controller",
1803
3010
  fromRole: "controller",
1804
3011
  type: "controller-reply",
1805
- content: data && data.reply ? data.reply : "Controller finished without a textual reply.",
3012
+ content: data && data.reply ? data.reply : t("empty.copiedControllerReply"),
1806
3013
  });
1807
3014
  refreshAll();
1808
3015
  }).catch(function (err) {
@@ -1834,8 +3041,29 @@
1834
3041
  });
1835
3042
  }
1836
3043
 
3044
+ async function applyInitialUiState() {
3045
+ if (initialUiStateApplied) {
3046
+ return;
3047
+ }
3048
+ initialUiStateApplied = true;
3049
+
3050
+ if (initialUiState.planningRun) {
3051
+ selectedPlanningRunId = initialUiState.planningRun;
3052
+ renderPlanningTab(teamState.controllerRuns);
3053
+ }
3054
+
3055
+ if (initialUiState.tab) {
3056
+ activateTab(initialUiState.tab);
3057
+ }
3058
+
3059
+ if (initialUiState.taskId) {
3060
+ await openTaskDetail(initialUiState.taskId);
3061
+ }
3062
+ }
3063
+
3064
+ applyStaticTranslations();
1837
3065
  renderWorkspaceTree(workspaceTree);
1838
3066
  renderWorkspaceFile();
1839
- refreshAll();
3067
+ refreshAll().then(applyInitialUiState).catch(function () {});
1840
3068
  connectWebSocket();
1841
3069
  })();