clouds-coder 2026.3.29__tar.gz → 2026.3.30__tar.gz

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.
@@ -376,10 +376,10 @@ TASK_LEVEL_POLICIES: dict[int, dict] = {
376
376
  },
377
377
  3: {
378
378
  "name": "light_collaboration",
379
- "execution_mode": EXECUTION_MODE_SYNC,
380
- "participants": ["explorer", "developer"],
379
+ "execution_mode": EXECUTION_MODE_SINGLE,
380
+ "participants": ["developer"],
381
381
  "assigned_expert": "developer",
382
- "round_budget": 10,
382
+ "round_budget": 16,
383
383
  "requires_user_confirmation": False,
384
384
  "complexity": "simple",
385
385
  },
@@ -450,7 +450,7 @@ SKILL_BODY_PREVIEW_CHARS = 4_000
450
450
  SKILLS_VIRTUAL_PREFIX = "/skills"
451
451
  SKILLS_EXTERNAL_MOUNT = "__external__"
452
452
  PLAN_MODE_ENABLED_LEVELS = {3, 4, 5}
453
- PLAN_MODE_FORCED_LEVELS = {4, 5}
453
+ PLAN_MODE_FORCED_LEVELS = {3, 4, 5}
454
454
  PLAN_MODE_USER_CHOICES = ("auto", "on", "off")
455
455
  # Task phase definitions for stage-aware delegation
456
456
  TASK_PHASES = ("research", "design", "implement", "test", "review", "deploy")
@@ -1288,6 +1288,475 @@ def model_language_instruction(lang: str) -> str:
1288
1288
  )
1289
1289
 
1290
1290
 
1291
+ BACKEND_I18N = {
1292
+ "en": {
1293
+ "role_explorer": "Explorer",
1294
+ "role_developer": "Developer",
1295
+ "role_reviewer": "Reviewer",
1296
+ "role_manager": "Manager",
1297
+ "role_planner": "Planner",
1298
+ "role_agent": "Agent",
1299
+ "todo_no_changes": "No todo changes.",
1300
+ "todo_no_todos": "No todos.",
1301
+ "todo_working": "Working on: {content}",
1302
+ "todo_completed": "Completed: {content}",
1303
+ "todo_pending": "Pending: {content}",
1304
+ "todo_working_owner": "Working on ({owner}): {content}",
1305
+ "todo_completed_owner": "Completed ({owner}): {content}",
1306
+ "todo_pending_owner": "Pending ({owner}): {content}",
1307
+ "todo_footer": "({done}/{total} completed)",
1308
+ },
1309
+ "zh-CN": {
1310
+ "role_explorer": "探索者",
1311
+ "role_developer": "开发者",
1312
+ "role_reviewer": "审查者",
1313
+ "role_manager": "管理者",
1314
+ "role_planner": "规划者",
1315
+ "role_agent": "Agent",
1316
+ "todo_no_changes": "待办无变化。",
1317
+ "todo_no_todos": "暂无待办。",
1318
+ "todo_working": "进行中:{content}",
1319
+ "todo_completed": "已完成:{content}",
1320
+ "todo_pending": "待处理:{content}",
1321
+ "todo_working_owner": "进行中({owner}):{content}",
1322
+ "todo_completed_owner": "已完成({owner}):{content}",
1323
+ "todo_pending_owner": "待处理({owner}):{content}",
1324
+ "todo_footer": "(已完成 {done}/{total})",
1325
+ },
1326
+ "zh-TW": {
1327
+ "role_explorer": "探索者",
1328
+ "role_developer": "開發者",
1329
+ "role_reviewer": "審查者",
1330
+ "role_manager": "管理者",
1331
+ "role_planner": "規劃者",
1332
+ "role_agent": "Agent",
1333
+ "todo_no_changes": "待辦沒有變化。",
1334
+ "todo_no_todos": "尚無待辦。",
1335
+ "todo_working": "進行中:{content}",
1336
+ "todo_completed": "已完成:{content}",
1337
+ "todo_pending": "待處理:{content}",
1338
+ "todo_working_owner": "進行中({owner}):{content}",
1339
+ "todo_completed_owner": "已完成({owner}):{content}",
1340
+ "todo_pending_owner": "待處理({owner}):{content}",
1341
+ "todo_footer": "(已完成 {done}/{total})",
1342
+ },
1343
+ "ja": {
1344
+ "role_explorer": "探索担当",
1345
+ "role_developer": "開発担当",
1346
+ "role_reviewer": "レビュー担当",
1347
+ "role_manager": "マネージャー",
1348
+ "role_planner": "プランナー",
1349
+ "role_agent": "Agent",
1350
+ "todo_no_changes": "Todo に変更はありません。",
1351
+ "todo_no_todos": "Todo はありません。",
1352
+ "todo_working": "進行中: {content}",
1353
+ "todo_completed": "完了: {content}",
1354
+ "todo_pending": "未着手: {content}",
1355
+ "todo_working_owner": "進行中 ({owner}): {content}",
1356
+ "todo_completed_owner": "完了 ({owner}): {content}",
1357
+ "todo_pending_owner": "未着手 ({owner}): {content}",
1358
+ "todo_footer": "({done}/{total} 完了)",
1359
+ },
1360
+ }
1361
+
1362
+ BACKEND_I18N["en"].update(
1363
+ {
1364
+ "todo_node_suffix": " | node: {topic}",
1365
+ "node_desc_manager_active": "Plan route and coordinate current node handoff ({target} active)",
1366
+ "node_desc_manager": "Plan route and coordinate current node handoff",
1367
+ "node_desc_explorer_active": "Gather constraints and evidence for current node",
1368
+ "node_desc_explorer": "Provide research support and risk notes for current node",
1369
+ "node_desc_developer_active": "Implement concrete outputs and file or tool changes for current node",
1370
+ "node_desc_developer": "Prepare and deliver implementation updates for current node",
1371
+ "node_desc_reviewer_active": "Validate current node and provide pass or fix judgement",
1372
+ "node_desc_reviewer": "Review outputs and keep the quality gate updated for current node",
1373
+ "node_desc_generic": "Handle current node work",
1374
+ "project_answer_objective": "Answer: {objective}",
1375
+ "project_answer_default": "Answer the user's question",
1376
+ "project_analyze_requirements": "Analyze requirements and project structure",
1377
+ "project_implement_objective": "Implement: {objective}",
1378
+ "project_implement_default": "Implement the coding task",
1379
+ "project_compile_check": "Compile / syntax check",
1380
+ "project_min_test": "Minimal functional test",
1381
+ "project_research_objective": "Research: {objective}",
1382
+ "project_research_default": "Run the research task",
1383
+ "project_research_summary": "Organize research findings",
1384
+ "project_execute_objective": "Execute: {objective}",
1385
+ "project_execute_default": "Execute the task",
1386
+ "evidence_structure_analyzed": "structure analyzed",
1387
+ "evidence_files_produced": "{count} file(s) produced",
1388
+ "evidence_compile_passed": "compile check passed",
1389
+ "evidence_test_passed": "tests passed",
1390
+ "evidence_review_passed": "review passed",
1391
+ "step_completed_evidence": "step completed",
1392
+ "plan_step_summary": "📋 Plan step {step}/{total}: {content}",
1393
+ "plan_step_label": "Step {step}/{total}",
1394
+ "plan_step_hint": "[plan-step-advance] Previous step completed. Now at {step_label}: {step_text}\nRead updated plan: read_file {plan_path}\nCall TodoWrite to set subtasks for THIS step ONLY.\nEach subtask MUST include parent_step_id='{parent_step_id}'. Create 3-5 items, one marked in_progress, others pending.\nDo NOT create subtasks for other plan steps.",
1395
+ "stall_execution_blocked_title": "## Execution Blocked\n",
1396
+ "stall_stop_reason": "**Stop reason:** {reason}",
1397
+ "stall_error_details": "**Error details:** {detail}",
1398
+ "stall_recent_error": "**Recent error:**",
1399
+ "stall_repeated_tools": "**Repeated tool calls:** {tools}",
1400
+ "stall_suggested_actions": "**Suggested actions:**",
1401
+ "stall_action_1": "1. Check whether the environment satisfies the task requirements (files exist, dependencies installed)",
1402
+ "stall_action_2": "2. Manually run the failed command and confirm the error output",
1403
+ "stall_action_3": "3. Provide more specific guidance or revise the task description, then retry",
1404
+ "stall_continue_prompt": "Please provide further instructions and I will continue from the new information.",
1405
+ "stall_analysis_title": "### Stall Analysis\n",
1406
+ "stall_goal": "**Goal:** {goal}",
1407
+ "stall_severity": "**Severity score:** {score}",
1408
+ "stall_events": "**Stall event sequence:**",
1409
+ "stall_event_line": "- [{source}] +{points} -> total {cumulative}",
1410
+ "stall_error_context": "**Error context:**",
1411
+ "stall_repeated_tools_label": "**Repeated tools:** {tools}",
1412
+ "stall_last_fault_reason": "**Last fault reason:** {reason}",
1413
+ "stall_open_todos": "**Open todos:**",
1414
+ "plan_file_proposals_title": "# Execution Plan Proposals\n",
1415
+ "plan_file_background": "## Background\n{context}\n",
1416
+ "plan_file_option": "## Option {id}: {title}",
1417
+ "plan_file_recommended": " [RECOMMENDED]",
1418
+ "plan_file_steps": "### Steps",
1419
+ "plan_file_pros": "**Pros:** {text}",
1420
+ "plan_file_cons": "**Cons:** {text}",
1421
+ "plan_file_risk": "**Risk:** {text}",
1422
+ "plan_file_awaiting_choice": "> Awaiting user choice.",
1423
+ "active_plan_title": "# Active Plan: {title}\n",
1424
+ "active_plan_status": "> Status: EXECUTING | Step {current}/{total}",
1425
+ "active_plan_chosen": "> Chosen: Option {choice}",
1426
+ "active_plan_updated": "> Updated: {updated}\n",
1427
+ "active_plan_summary": "## Summary\n{summary}\n",
1428
+ "active_plan_steps": "## Steps\n",
1429
+ "active_plan_step_done": "- [x] Step {idx}: {header}",
1430
+ "active_plan_step_current": "- [>] Step {idx}: {header} <-- CURRENT",
1431
+ "active_plan_step_pending": "- [ ] Step {idx}: {header}",
1432
+ "active_plan_completed_by": "Completed by: {actor}",
1433
+ "active_plan_evidence": "Evidence: {evidence}",
1434
+ "plan_bubble_title": "## 📋 Execution Plans\n",
1435
+ "plan_bubble_background": "**Background:** {context}\n",
1436
+ "plan_bubble_option": "### Option {id}: {title}",
1437
+ "plan_bubble_recommended": " ⭐ Recommended",
1438
+ "plan_bubble_steps": "Steps: {count}",
1439
+ "plan_bubble_risk": "Risk: {risk}",
1440
+ "plan_bubble_full_ref": "Full plan: `{path}`",
1441
+ "plan_bubble_reply": 'Reply with a choice (e.g. "Option A", "A", "choose 1"), or provide revisions.',
1442
+ "plan_read_instruction": "[plan-file] The approved execution plan is at `{path}`.\nUse: read_file {path} to review full steps and live status.\nThe plan file is the authoritative source for step ordering and completion status.\nExecute steps IN ORDER. Do NOT skip ahead. Mark the current step done before advancing.\nIf a step references a skill or workflow, call load_skill before proceeding.",
1443
+ "plan_read_todo_note": "\nTODO PLANNING: At the START of your work, call TodoWrite to list ALL subtasks you plan to complete for {step_label} (status=pending, parent_step_id='{parent_step_id}'). Create 3-5 subtasks for THIS step ONLY — do NOT list subtasks for other plan steps. As you complete each subtask, update it to status=completed. When ALL subtasks are done, call finish_current_task to signal step completion.\n",
1444
+ "plan_proposal_title": "## 📋 Execution Plans\n",
1445
+ "plan_proposal_background": "### Background Analysis\n{context}\n",
1446
+ "plan_proposal_option": "### Option {id}: {title}",
1447
+ "plan_proposal_recommended": " ⭐ Recommended",
1448
+ "plan_proposal_steps": "**Steps:**",
1449
+ "plan_proposal_pros": "**Pros:** {text}",
1450
+ "plan_proposal_cons": "**Cons:** {text}",
1451
+ "plan_proposal_risk": "**Risk:** {text}",
1452
+ "plan_proposal_reply": 'Reply with a choice (e.g. "Option A", "A", "choose 1"), or provide revisions.',
1453
+ "status_project_todos_synced": "project todos synced ({reason})",
1454
+ }
1455
+ )
1456
+ BACKEND_I18N["zh-CN"].update(
1457
+ {
1458
+ "todo_node_suffix": " | 当前节点:{topic}",
1459
+ "node_desc_manager_active": "规划路由并协调当前节点交接({target} 正在处理)",
1460
+ "node_desc_manager": "规划路由并协调当前节点交接",
1461
+ "node_desc_explorer_active": "为当前节点收集约束与证据",
1462
+ "node_desc_explorer": "为当前节点提供调研支持与风险备注",
1463
+ "node_desc_developer_active": "为当前节点实施具体产出以及文件/工具改动",
1464
+ "node_desc_developer": "为当前节点准备并交付实现更新",
1465
+ "node_desc_reviewer_active": "校验当前节点并给出通过/修复判断",
1466
+ "node_desc_reviewer": "审查当前节点产出并维护质量闸门",
1467
+ "node_desc_generic": "处理当前节点工作",
1468
+ "project_answer_objective": "回答:{objective}",
1469
+ "project_answer_default": "回答用户问题",
1470
+ "project_analyze_requirements": "分析需求和项目结构",
1471
+ "project_implement_objective": "实现:{objective}",
1472
+ "project_implement_default": "实现编码任务",
1473
+ "project_compile_check": "编译 / 语法检查",
1474
+ "project_min_test": "最小功能测试",
1475
+ "project_research_objective": "调研:{objective}",
1476
+ "project_research_default": "执行调研任务",
1477
+ "project_research_summary": "整理调研结果",
1478
+ "project_execute_objective": "执行:{objective}",
1479
+ "project_execute_default": "执行任务",
1480
+ "evidence_structure_analyzed": "结构已分析",
1481
+ "evidence_files_produced": "已产出 {count} 个文件",
1482
+ "evidence_compile_passed": "编译通过",
1483
+ "evidence_test_passed": "测试通过",
1484
+ "evidence_review_passed": "审查通过",
1485
+ "step_completed_evidence": "步骤已完成",
1486
+ "plan_step_summary": "📋 计划步骤 {step}/{total}:{content}",
1487
+ "plan_step_label": "步骤 {step}/{total}",
1488
+ "plan_step_hint": "[plan-step-advance] 上一步已完成。当前来到{step_label}:{step_text}\n读取更新后的计划:read_file {plan_path}\n现在调用 TodoWrite,只为当前步骤拆分子任务。\n每个子任务都必须包含 parent_step_id='{parent_step_id}'。创建 3-5 项,其中 1 项为 in_progress,其余为 pending。\n不要为其他计划步骤创建子任务。",
1489
+ "stall_execution_blocked_title": "## 执行遇阻\n",
1490
+ "stall_stop_reason": "**停止原因:** {reason}",
1491
+ "stall_error_details": "**错误详情:** {detail}",
1492
+ "stall_recent_error": "**最近错误:**",
1493
+ "stall_repeated_tools": "**重复工具调用:** {tools}",
1494
+ "stall_suggested_actions": "**建议操作:**",
1495
+ "stall_action_1": "1. 检查环境是否满足任务要求(文件是否存在、依赖是否安装)",
1496
+ "stall_action_2": "2. 手动执行失败的命令,确认错误输出",
1497
+ "stall_action_3": "3. 提供更具体的指导或调整任务描述后重试",
1498
+ "stall_continue_prompt": "请提供进一步指示,我会基于新的信息继续执行。",
1499
+ "stall_analysis_title": "### 卡死分析\n",
1500
+ "stall_goal": "**目标:** {goal}",
1501
+ "stall_severity": "**严重度分数:** {score}",
1502
+ "stall_events": "**卡死事件序列:**",
1503
+ "stall_event_line": "- [{source}] +{points} -> 累计 {cumulative}",
1504
+ "stall_error_context": "**错误上下文:**",
1505
+ "stall_repeated_tools_label": "**重复工具:** {tools}",
1506
+ "stall_last_fault_reason": "**最后故障原因:** {reason}",
1507
+ "stall_open_todos": "**未完成任务:**",
1508
+ "plan_file_proposals_title": "# 执行方案提案\n",
1509
+ "plan_file_background": "## 背景\n{context}\n",
1510
+ "plan_file_option": "## 方案 {id}:{title}",
1511
+ "plan_file_recommended": " [推荐]",
1512
+ "plan_file_steps": "### 步骤",
1513
+ "plan_file_pros": "**优势:** {text}",
1514
+ "plan_file_cons": "**劣势:** {text}",
1515
+ "plan_file_risk": "**风险:** {text}",
1516
+ "plan_file_awaiting_choice": "> 等待用户选择。",
1517
+ "active_plan_title": "# 当前执行方案:{title}\n",
1518
+ "active_plan_status": "> 状态:执行中 | 步骤 {current}/{total}",
1519
+ "active_plan_chosen": "> 已选择:方案 {choice}",
1520
+ "active_plan_updated": "> 更新时间:{updated}\n",
1521
+ "active_plan_summary": "## 摘要\n{summary}\n",
1522
+ "active_plan_steps": "## 步骤\n",
1523
+ "active_plan_step_done": "- [x] 步骤 {idx}:{header}",
1524
+ "active_plan_step_current": "- [>] 步骤 {idx}:{header} <-- 当前",
1525
+ "active_plan_step_pending": "- [ ] 步骤 {idx}:{header}",
1526
+ "active_plan_completed_by": "执行者:{actor}",
1527
+ "active_plan_evidence": "证据:{evidence}",
1528
+ "plan_bubble_title": "## 📋 执行方案\n",
1529
+ "plan_bubble_background": "**背景:** {context}\n",
1530
+ "plan_bubble_option": "### 方案 {id}:{title}",
1531
+ "plan_bubble_recommended": " ⭐推荐",
1532
+ "plan_bubble_steps": "步骤数:{count}",
1533
+ "plan_bubble_risk": "风险:{risk}",
1534
+ "plan_bubble_full_ref": "完整方案详见:`{path}`",
1535
+ "plan_bubble_reply": "请回复选择(如“方案A”“A”“选1”),或输入修改意见。",
1536
+ "plan_read_instruction": "[plan-file] 已批准的执行计划位于 `{path}`。\n使用:read_file {path} 查看完整步骤与实时状态。\n计划文件是步骤顺序与完成状态的唯一权威来源。\n请按顺序执行步骤,不要跳步。完成当前步骤后再推进下一步。\n如果某一步引用了 skill 或 workflow,继续前先调用 load_skill。",
1537
+ "plan_read_todo_note": "\nTODO 更新:一开始就调用 TodoWrite,只为当前步骤({step_label})设置子任务。\n每个子任务都必须包含 parent_step_id='{parent_step_id}'。\n创建 3-5 个只属于当前步骤的子任务,并在完成后及时标记完成。\n不要为其他计划步骤创建子任务。\n",
1538
+ "plan_proposal_title": "## 📋 执行方案\n",
1539
+ "plan_proposal_background": "### 背景分析\n{context}\n",
1540
+ "plan_proposal_option": "### 方案 {id}:{title}",
1541
+ "plan_proposal_recommended": " ⭐推荐",
1542
+ "plan_proposal_steps": "**步骤:**",
1543
+ "plan_proposal_pros": "**优势:** {text}",
1544
+ "plan_proposal_cons": "**劣势:** {text}",
1545
+ "plan_proposal_risk": "**风险:** {text}",
1546
+ "plan_proposal_reply": "请回复选择(如“方案A”“A”“选1”),或输入修改意见。",
1547
+ "status_project_todos_synced": "项目待办已同步({reason})",
1548
+ }
1549
+ )
1550
+ BACKEND_I18N["zh-TW"].update(
1551
+ {
1552
+ "todo_node_suffix": " | 目前節點:{topic}",
1553
+ "node_desc_manager_active": "規劃路由並協調目前節點交接({target} 正在處理)",
1554
+ "node_desc_manager": "規劃路由並協調目前節點交接",
1555
+ "node_desc_explorer_active": "為目前節點蒐集限制與證據",
1556
+ "node_desc_explorer": "為目前節點提供研究支援與風險備註",
1557
+ "node_desc_developer_active": "為目前節點落實具體產出以及檔案/工具變更",
1558
+ "node_desc_developer": "為目前節點準備並交付實作更新",
1559
+ "node_desc_reviewer_active": "驗證目前節點並給出通過/修復判斷",
1560
+ "node_desc_reviewer": "審查目前節點產出並維護品質閘門",
1561
+ "node_desc_generic": "處理目前節點工作",
1562
+ "project_answer_objective": "回答:{objective}",
1563
+ "project_answer_default": "回答使用者問題",
1564
+ "project_analyze_requirements": "分析需求與專案結構",
1565
+ "project_implement_objective": "實作:{objective}",
1566
+ "project_implement_default": "實作程式任務",
1567
+ "project_compile_check": "編譯 / 語法檢查",
1568
+ "project_min_test": "最小功能測試",
1569
+ "project_research_objective": "調研:{objective}",
1570
+ "project_research_default": "執行調研任務",
1571
+ "project_research_summary": "整理調研結果",
1572
+ "project_execute_objective": "執行:{objective}",
1573
+ "project_execute_default": "執行任務",
1574
+ "evidence_structure_analyzed": "結構已分析",
1575
+ "evidence_files_produced": "已產出 {count} 個檔案",
1576
+ "evidence_compile_passed": "編譯通過",
1577
+ "evidence_test_passed": "測試通過",
1578
+ "evidence_review_passed": "審查通過",
1579
+ "step_completed_evidence": "步驟已完成",
1580
+ "plan_step_summary": "📋 計畫步驟 {step}/{total}:{content}",
1581
+ "plan_step_label": "步驟 {step}/{total}",
1582
+ "plan_step_hint": "[plan-step-advance] 上一步已完成。現在來到{step_label}:{step_text}\n讀取更新後的計畫:read_file {plan_path}\n現在呼叫 TodoWrite,只為目前步驟拆分子任務。\n每個子任務都必須包含 parent_step_id='{parent_step_id}'。建立 3-5 項,其中 1 項為 in_progress,其餘為 pending。\n不要為其他計畫步驟建立子任務。",
1583
+ "stall_execution_blocked_title": "## 執行受阻\n",
1584
+ "stall_stop_reason": "**停止原因:** {reason}",
1585
+ "stall_error_details": "**錯誤詳情:** {detail}",
1586
+ "stall_recent_error": "**最近錯誤:**",
1587
+ "stall_repeated_tools": "**重複工具呼叫:** {tools}",
1588
+ "stall_suggested_actions": "**建議操作:**",
1589
+ "stall_action_1": "1. 檢查環境是否符合任務要求(檔案是否存在、依賴是否安裝)",
1590
+ "stall_action_2": "2. 手動執行失敗命令,確認錯誤輸出",
1591
+ "stall_action_3": "3. 提供更具體的指示或調整任務描述後再重試",
1592
+ "stall_continue_prompt": "請提供進一步指示,我會依照新的資訊繼續執行。",
1593
+ "stall_analysis_title": "### 卡住分析\n",
1594
+ "stall_goal": "**目標:** {goal}",
1595
+ "stall_severity": "**嚴重度分數:** {score}",
1596
+ "stall_events": "**卡住事件序列:**",
1597
+ "stall_event_line": "- [{source}] +{points} -> 累計 {cumulative}",
1598
+ "stall_error_context": "**錯誤上下文:**",
1599
+ "stall_repeated_tools_label": "**重複工具:** {tools}",
1600
+ "stall_last_fault_reason": "**最後故障原因:** {reason}",
1601
+ "stall_open_todos": "**未完成任務:**",
1602
+ "plan_file_proposals_title": "# 執行方案提案\n",
1603
+ "plan_file_background": "## 背景\n{context}\n",
1604
+ "plan_file_option": "## 方案 {id}:{title}",
1605
+ "plan_file_recommended": " [推薦]",
1606
+ "plan_file_steps": "### 步驟",
1607
+ "plan_file_pros": "**優勢:** {text}",
1608
+ "plan_file_cons": "**劣勢:** {text}",
1609
+ "plan_file_risk": "**風險:** {text}",
1610
+ "plan_file_awaiting_choice": "> 等待使用者選擇。",
1611
+ "active_plan_title": "# 目前執行方案:{title}\n",
1612
+ "active_plan_status": "> 狀態:執行中 | 步驟 {current}/{total}",
1613
+ "active_plan_chosen": "> 已選擇:方案 {choice}",
1614
+ "active_plan_updated": "> 更新時間:{updated}\n",
1615
+ "active_plan_summary": "## 摘要\n{summary}\n",
1616
+ "active_plan_steps": "## 步驟\n",
1617
+ "active_plan_step_done": "- [x] 步驟 {idx}:{header}",
1618
+ "active_plan_step_current": "- [>] 步驟 {idx}:{header} <-- 目前",
1619
+ "active_plan_step_pending": "- [ ] 步驟 {idx}:{header}",
1620
+ "active_plan_completed_by": "執行者:{actor}",
1621
+ "active_plan_evidence": "證據:{evidence}",
1622
+ "plan_bubble_title": "## 📋 執行方案\n",
1623
+ "plan_bubble_background": "**背景:** {context}\n",
1624
+ "plan_bubble_option": "### 方案 {id}:{title}",
1625
+ "plan_bubble_recommended": " ⭐推薦",
1626
+ "plan_bubble_steps": "步驟數:{count}",
1627
+ "plan_bubble_risk": "風險:{risk}",
1628
+ "plan_bubble_full_ref": "完整方案詳見:`{path}`",
1629
+ "plan_bubble_reply": "請回覆選擇(如「方案A」「A」「選1」),或輸入修改意見。",
1630
+ "plan_read_instruction": "[plan-file] 已核准的執行計畫位於 `{path}`。\n使用:read_file {path} 查看完整步驟與即時狀態。\n計畫檔是步驟順序與完成狀態的唯一權威來源。\n請依序執行步驟,不要跳步。完成目前步驟後再推進下一步。\n如果某一步引用了 skill 或 workflow,繼續前先呼叫 load_skill。",
1631
+ "plan_read_todo_note": "\nTODO 更新:一開始就呼叫 TodoWrite,只為目前步驟({step_label})設定子任務。\n每個子任務都必須包含 parent_step_id='{parent_step_id}'。\n建立 3-5 個只屬於目前步驟的子任務,並在完成後即時標記完成。\n不要為其他計畫步驟建立子任務。\n",
1632
+ "plan_proposal_title": "## 📋 執行方案\n",
1633
+ "plan_proposal_background": "### 背景分析\n{context}\n",
1634
+ "plan_proposal_option": "### 方案 {id}:{title}",
1635
+ "plan_proposal_recommended": " ⭐推薦",
1636
+ "plan_proposal_steps": "**步驟:**",
1637
+ "plan_proposal_pros": "**優勢:** {text}",
1638
+ "plan_proposal_cons": "**劣勢:** {text}",
1639
+ "plan_proposal_risk": "**風險:** {text}",
1640
+ "plan_proposal_reply": "請回覆選擇(如「方案A」「A」「選1」),或輸入修改意見。",
1641
+ "status_project_todos_synced": "專案待辦已同步({reason})",
1642
+ }
1643
+ )
1644
+ BACKEND_I18N["ja"].update(
1645
+ {
1646
+ "todo_node_suffix": " | 現在のノード: {topic}",
1647
+ "node_desc_manager_active": "現在のノードの引き継ぎを計画し調整する ({target} が担当中)",
1648
+ "node_desc_manager": "現在のノードの引き継ぎを計画し調整する",
1649
+ "node_desc_explorer_active": "現在のノードに必要な制約と証拠を集める",
1650
+ "node_desc_explorer": "現在のノードに調査支援とリスクメモを提供する",
1651
+ "node_desc_developer_active": "現在のノードで具体的な成果物とファイル/ツール変更を実装する",
1652
+ "node_desc_developer": "現在のノード向けの実装更新を準備して反映する",
1653
+ "node_desc_reviewer_active": "現在のノードを検証し、通過か修正かを判断する",
1654
+ "node_desc_reviewer": "出力をレビューし、現在のノードの品質ゲートを維持する",
1655
+ "node_desc_generic": "現在のノード作業を処理する",
1656
+ "project_answer_objective": "回答: {objective}",
1657
+ "project_answer_default": "ユーザーの質問に回答する",
1658
+ "project_analyze_requirements": "要件とプロジェクト構造を分析する",
1659
+ "project_implement_objective": "実装: {objective}",
1660
+ "project_implement_default": "コーディング作業を実装する",
1661
+ "project_compile_check": "コンパイル / 構文チェック",
1662
+ "project_min_test": "最小機能テスト",
1663
+ "project_research_objective": "調査: {objective}",
1664
+ "project_research_default": "調査タスクを実行する",
1665
+ "project_research_summary": "調査結果を整理する",
1666
+ "project_execute_objective": "実行: {objective}",
1667
+ "project_execute_default": "タスクを実行する",
1668
+ "evidence_structure_analyzed": "構造分析済み",
1669
+ "evidence_files_produced": "{count} 個のファイルを生成済み",
1670
+ "evidence_compile_passed": "コンパイル確認済み",
1671
+ "evidence_test_passed": "テスト通過",
1672
+ "evidence_review_passed": "レビュー通過",
1673
+ "step_completed_evidence": "ステップ完了",
1674
+ "plan_step_summary": "📋 計画ステップ {step}/{total}: {content}",
1675
+ "plan_step_label": "ステップ {step}/{total}",
1676
+ "plan_step_hint": "[plan-step-advance] 前のステップが完了しました。現在は{step_label}: {step_text}\n更新済みプランを読む: read_file {plan_path}\n今すぐ TodoWrite を呼び出し、現在のステップだけのサブタスクを設定してください。\n各サブタスクには parent_step_id='{parent_step_id}' を必ず含めてください。3-5 件作成し、1 件を in_progress、残りを pending にしてください。\n他の計画ステップのサブタスクは作成しないでください。",
1677
+ "stall_execution_blocked_title": "## 実行が停止しました\n",
1678
+ "stall_stop_reason": "**停止理由:** {reason}",
1679
+ "stall_error_details": "**エラー詳細:** {detail}",
1680
+ "stall_recent_error": "**直近のエラー:**",
1681
+ "stall_repeated_tools": "**重複したツール呼び出し:** {tools}",
1682
+ "stall_suggested_actions": "**推奨アクション:**",
1683
+ "stall_action_1": "1. 環境がタスク要件を満たしているか確認する(ファイルの存在、依存関係の導入など)",
1684
+ "stall_action_2": "2. 失敗したコマンドを手動で実行し、エラー出力を確認する",
1685
+ "stall_action_3": "3. より具体的な指示を与えるか、タスク記述を調整してから再試行する",
1686
+ "stall_continue_prompt": "追加の指示をいただければ、新しい情報に基づいて続行します。",
1687
+ "stall_analysis_title": "### 停滞分析\n",
1688
+ "stall_goal": "**目標:** {goal}",
1689
+ "stall_severity": "**重大度スコア:** {score}",
1690
+ "stall_events": "**停滞イベント列:**",
1691
+ "stall_event_line": "- [{source}] +{points} -> 累計 {cumulative}",
1692
+ "stall_error_context": "**エラー文脈:**",
1693
+ "stall_repeated_tools_label": "**重複ツール:** {tools}",
1694
+ "stall_last_fault_reason": "**直近の故障理由:** {reason}",
1695
+ "stall_open_todos": "**未完了 Todo:**",
1696
+ "plan_file_proposals_title": "# 実行プラン候補\n",
1697
+ "plan_file_background": "## 背景\n{context}\n",
1698
+ "plan_file_option": "## 案 {id}: {title}",
1699
+ "plan_file_recommended": " [推奨]",
1700
+ "plan_file_steps": "### 手順",
1701
+ "plan_file_pros": "**利点:** {text}",
1702
+ "plan_file_cons": "**欠点:** {text}",
1703
+ "plan_file_risk": "**リスク:** {text}",
1704
+ "plan_file_awaiting_choice": "> ユーザー選択待ち。",
1705
+ "active_plan_title": "# 現在の実行プラン: {title}\n",
1706
+ "active_plan_status": "> 状態: 実行中 | ステップ {current}/{total}",
1707
+ "active_plan_chosen": "> 選択済み: 案 {choice}",
1708
+ "active_plan_updated": "> 更新時刻: {updated}\n",
1709
+ "active_plan_summary": "## 要約\n{summary}\n",
1710
+ "active_plan_steps": "## 手順\n",
1711
+ "active_plan_step_done": "- [x] ステップ {idx}: {header}",
1712
+ "active_plan_step_current": "- [>] ステップ {idx}: {header} <-- 現在",
1713
+ "active_plan_step_pending": "- [ ] ステップ {idx}: {header}",
1714
+ "active_plan_completed_by": "完了者: {actor}",
1715
+ "active_plan_evidence": "証拠: {evidence}",
1716
+ "plan_bubble_title": "## 📋 実行プラン\n",
1717
+ "plan_bubble_background": "**背景:** {context}\n",
1718
+ "plan_bubble_option": "### 案 {id}: {title}",
1719
+ "plan_bubble_recommended": " ⭐推奨",
1720
+ "plan_bubble_steps": "手順数: {count}",
1721
+ "plan_bubble_risk": "リスク: {risk}",
1722
+ "plan_bubble_full_ref": "完全なプラン: `{path}`",
1723
+ "plan_bubble_reply": "選択肢(例: 「案A」「A」「1を選ぶ」)を返信するか、修正要望を入力してください。",
1724
+ "plan_read_instruction": "[plan-file] 承認済みの実行計画は `{path}` にあります。\nread_file {path} を使って完全な手順と進行状況を確認してください。\n計画ファイルは手順順序と完了状態の唯一の正式ソースです。\n必ず順番に実行し、飛ばさないでください。現在のステップを完了してから次へ進んでください。\nステップが skill や workflow を参照している場合は、続行前に load_skill を呼び出してください。",
1725
+ "plan_read_todo_note": "\nTODO 更新: 最初に TodoWrite を呼び出し、現在のステップ({step_label})だけのサブタスクを設定してください。\n各サブタスクには parent_step_id='{parent_step_id}' を必ず含めてください。\n現在のステップだけを分解した 3-5 件のサブタスクを作り、完了ごとに完了状態へ更新してください。\n他の計画ステップのサブタスクは作成しないでください。\n",
1726
+ "plan_proposal_title": "## 📋 実行プラン\n",
1727
+ "plan_proposal_background": "### 背景分析\n{context}\n",
1728
+ "plan_proposal_option": "### 案 {id}: {title}",
1729
+ "plan_proposal_recommended": " ⭐推奨",
1730
+ "plan_proposal_steps": "**手順:**",
1731
+ "plan_proposal_pros": "**利点:** {text}",
1732
+ "plan_proposal_cons": "**欠点:** {text}",
1733
+ "plan_proposal_risk": "**リスク:** {text}",
1734
+ "plan_proposal_reply": "選択肢(例: 「案A」「A」「1を選ぶ」)を返信するか、修正要望を入力してください。",
1735
+ "status_project_todos_synced": "プロジェクト Todo を同期しました({reason})",
1736
+ }
1737
+ )
1738
+
1739
+
1740
+ def backend_i18n_text(language: str, key: str, **kwargs) -> str:
1741
+ code = normalize_ui_language(language)
1742
+ pack = BACKEND_I18N.get(code, BACKEND_I18N["en"])
1743
+ fallback = BACKEND_I18N["en"]
1744
+ template = str(pack.get(key, fallback.get(key, key)))
1745
+ if kwargs:
1746
+ try:
1747
+ return template.format(**{k: ("" if v is None else v) for k, v in kwargs.items()})
1748
+ except Exception:
1749
+ return template
1750
+ return template
1751
+
1752
+
1753
+ def backend_role_label(role: str, language: str) -> str:
1754
+ role_key = str(role or "").strip().lower()
1755
+ if role_key in {"explorer", "developer", "reviewer", "manager", "planner"}:
1756
+ return backend_i18n_text(language, f"role_{role_key}")
1757
+ return backend_i18n_text(language, "role_agent")
1758
+
1759
+
1291
1760
  def _detect_os_shell_instruction() -> str:
1292
1761
  """Return a shell environment note for the agent system prompt based on the host OS."""
1293
1762
  import platform as _platform
@@ -3112,7 +3581,7 @@ def parse_llm_config_profiles(config: dict, default_ollama_url: str, default_oll
3112
3581
  media_endpoints=build_profile_media_endpoints("siliconflow"),
3113
3582
  )
3114
3583
 
3115
- # ── vLLM (local) ────────────────────────────────────────���─────
3584
+ # ── vLLM (local) ──────────────────────────────────────────────
3116
3585
  vllm_url = str(config.get("vllm_url", "")).strip()
3117
3586
  vllm_model = str(config.get("vllm_model", "")).strip()
3118
3587
  vllm_key = str(config.get("vllm_key", "")).strip()
@@ -4251,10 +4720,61 @@ class EventHub:
4251
4720
  pass
4252
4721
 
4253
4722
  class TodoManager:
4254
- def __init__(self):
4723
+ def __init__(self, language: str = DEFAULT_UI_LANGUAGE):
4724
+ self.language = normalize_ui_language(language)
4255
4725
  self.items: list[dict] = []
4256
4726
  self.lock = threading.Lock()
4257
4727
 
4728
+ def _text(self, key: str, **kwargs) -> str:
4729
+ return backend_i18n_text(self.language, key, **kwargs)
4730
+
4731
+ def _owner_label(self, owner: str) -> str:
4732
+ return backend_role_label(owner, self.language) if str(owner or "").strip() else ""
4733
+
4734
+ def no_changes_text(self) -> str:
4735
+ return self._text("todo_no_changes")
4736
+
4737
+ def set_language(self, language: str, *, relabel: bool = True) -> str:
4738
+ with self.lock:
4739
+ self.language = normalize_ui_language(language)
4740
+ if relabel and self.items:
4741
+ refreshed = []
4742
+ for item in self.items:
4743
+ row = dict(item)
4744
+ row["activeForm"] = self._default_active_form(
4745
+ row.get("status", "pending"),
4746
+ row.get("content", ""),
4747
+ owner=row.get("owner", ""),
4748
+ )
4749
+ refreshed.append(row)
4750
+ self.items = refreshed
4751
+ return self.language
4752
+
4753
+ def _default_active_form(
4754
+ self,
4755
+ status: str,
4756
+ content: str,
4757
+ *,
4758
+ owner: str = "",
4759
+ note: str = "",
4760
+ ) -> str:
4761
+ state = str(status or "pending").strip().lower()
4762
+ base = normalize_work_text(str(content or "").strip(), state) or str(content or "").strip()
4763
+ suffix = f" ({trim(str(note or '').strip(), 180)})" if note else ""
4764
+ final_content = f"{base}{suffix}".strip()
4765
+ owner_label = self._owner_label(owner)
4766
+ if owner_label:
4767
+ if state == "in_progress":
4768
+ return self._text("todo_working_owner", owner=owner_label, content=final_content)
4769
+ if state == "completed":
4770
+ return self._text("todo_completed_owner", owner=owner_label, content=final_content)
4771
+ return self._text("todo_pending_owner", owner=owner_label, content=final_content)
4772
+ if state == "in_progress":
4773
+ return self._text("todo_working", content=final_content)
4774
+ if state == "completed":
4775
+ return self._text("todo_completed", content=final_content)
4776
+ return self._text("todo_pending", content=final_content)
4777
+
4258
4778
  def update(self, items: list[dict]) -> str:
4259
4779
  if not isinstance(items, list):
4260
4780
  raise ValueError("items must be array")
@@ -4313,12 +4833,7 @@ class TodoManager:
4313
4833
  else:
4314
4834
  worker_in_progress_seen = True
4315
4835
  if not active_form:
4316
- if status == "in_progress":
4317
- active_form = f"Working on: {content}"
4318
- elif status == "completed":
4319
- active_form = f"Completed: {content}"
4320
- else:
4321
- active_form = f"Pending: {content}"
4836
+ active_form = self._default_active_form(status, content, owner=owner)
4322
4837
  row = {"content": content, "status": status, "activeForm": active_form}
4323
4838
  if owner:
4324
4839
  row["owner"] = owner
@@ -4335,11 +4850,15 @@ class TodoManager:
4335
4850
  for row in validated:
4336
4851
  if row["status"] == "pending":
4337
4852
  row["status"] = "in_progress"
4338
- row["activeForm"] = row.get("activeForm") or f"Working on: {row['content']}"
4853
+ row["activeForm"] = row.get("activeForm") or self._default_active_form(
4854
+ "in_progress",
4855
+ row["content"],
4856
+ owner=row.get("owner", ""),
4857
+ )
4339
4858
  break
4340
4859
  with self.lock:
4341
4860
  if self.items == validated:
4342
- return "No todo changes."
4861
+ return self.no_changes_text()
4343
4862
  self.items = validated
4344
4863
  return self.render()
4345
4864
 
@@ -4351,7 +4870,7 @@ class TodoManager:
4351
4870
  with self.lock:
4352
4871
  items = list(self.items)
4353
4872
  if not items:
4354
- return "No todos."
4873
+ return self._text("todo_no_todos")
4355
4874
  lines = []
4356
4875
  for item in items:
4357
4876
  marker = {"completed": "[x]", "in_progress": "[>]", "pending": "[ ]"}.get(
@@ -4362,7 +4881,7 @@ class TodoManager:
4362
4881
  )
4363
4882
  lines.append(f"{marker} {item['content']}{suffix}")
4364
4883
  done = sum(1 for x in items if x["status"] == "completed")
4365
- lines.append(f"\n({done}/{len(items)} completed)")
4884
+ lines.append(f"\n{self._text('todo_footer', done=done, total=len(items))}")
4366
4885
  return "\n".join(lines)
4367
4886
 
4368
4887
  def snapshot(self) -> list[dict]:
@@ -4386,8 +4905,12 @@ class TodoManager:
4386
4905
  base = normalize_work_text(str(rows[idx].get("content", "")).strip()) or str(
4387
4906
  rows[idx].get("content", "")
4388
4907
  ).strip()
4389
- suffix = f" ({note})" if note else ""
4390
- rows[idx]["activeForm"] = f"Completed: {base}{suffix}".strip()
4908
+ rows[idx]["activeForm"] = self._default_active_form(
4909
+ "completed",
4910
+ base,
4911
+ owner=rows[idx].get("owner", ""),
4912
+ note=note,
4913
+ )
4391
4914
  changed += 1
4392
4915
  else:
4393
4916
  for idx, row in enumerate(rows):
@@ -4396,8 +4919,12 @@ class TodoManager:
4396
4919
  base = normalize_work_text(str(rows[idx].get("content", "")).strip()) or str(
4397
4920
  rows[idx].get("content", "")
4398
4921
  ).strip()
4399
- suffix = f" ({note})" if note else ""
4400
- rows[idx]["activeForm"] = f"Completed: {base}{suffix}".strip()
4922
+ rows[idx]["activeForm"] = self._default_active_form(
4923
+ "completed",
4924
+ base,
4925
+ owner=rows[idx].get("owner", ""),
4926
+ note=note,
4927
+ )
4401
4928
  changed = 1
4402
4929
  break
4403
4930
  if changed > 0:
@@ -4420,8 +4947,12 @@ class TodoManager:
4420
4947
  base = normalize_work_text(str(rows[idx].get("content", "")).strip()) or str(
4421
4948
  rows[idx].get("content", "")
4422
4949
  ).strip()
4423
- suffix = f" ({note})" if note else ""
4424
- rows[idx]["activeForm"] = f"Completed: {base}{suffix}".strip()
4950
+ rows[idx]["activeForm"] = self._default_active_form(
4951
+ "completed",
4952
+ base,
4953
+ owner=rows[idx].get("owner", ""),
4954
+ note=note,
4955
+ )
4425
4956
  changed += 1
4426
4957
  if changed > 0:
4427
4958
  self.items = rows
@@ -11265,7 +11796,7 @@ class SessionState:
11265
11796
  self.active_profile_id = ""
11266
11797
  self.multimodal_capability_cache: dict[str, dict] = {}
11267
11798
  self.failed_selections: list[str] = []
11268
- self.todo = TodoManager()
11799
+ self.todo = TodoManager(self.ui_language)
11269
11800
  self.single_advance_prompt_enhance = False
11270
11801
  self.skills = SkillStore(skills_root)
11271
11802
  self.skill_load_cache: dict[str, dict] = {}
@@ -11311,6 +11842,7 @@ class SessionState:
11311
11842
  self.runtime_task_complexity = ""
11312
11843
  self.runtime_complexity_floor = ""
11313
11844
  self.runtime_task_level_floor = 0
11845
+ self.runtime_task_level_ceiling = 0 # 0 = no ceiling; set from plan risk on approval
11314
11846
  self.runtime_scale_preference = "balanced"
11315
11847
  self.runtime_direct_objective = ""
11316
11848
  self.runtime_reclassify_goal = ""
@@ -12402,6 +12934,9 @@ class SessionState:
12402
12934
  self.runtime_task_level_floor = int(
12403
12935
  raw.get("runtime_task_level_floor", self.runtime_task_level_floor) or 0
12404
12936
  )
12937
+ self.runtime_task_level_ceiling = int(
12938
+ raw.get("runtime_task_level_ceiling", self.runtime_task_level_ceiling) or 0
12939
+ )
12405
12940
  self.runtime_goal_reset_pending = bool(
12406
12941
  raw.get("runtime_goal_reset_pending", self.runtime_goal_reset_pending)
12407
12942
  )
@@ -12494,6 +13029,7 @@ class SessionState:
12494
13029
  row["model"] = fmodel.strip()
12495
13030
  row["selection"] = f"{self.active_profile_id}::{row.get('model','')}"
12496
13031
  self.model_profiles[self.active_profile_id] = row
13032
+ self._set_ui_language(self.ui_language, relabel_todos=True)
12497
13033
  self._apply_active_profile()
12498
13034
  self._prune_skill_load_cache()
12499
13035
  with self.lock:
@@ -12566,6 +13102,7 @@ class SessionState:
12566
13102
  "runtime_reclassify_required": bool(self.runtime_reclassify_required),
12567
13103
  "runtime_complexity_floor": str(self.runtime_complexity_floor or ""),
12568
13104
  "runtime_task_level_floor": int(self.runtime_task_level_floor or 0),
13105
+ "runtime_task_level_ceiling": int(self.runtime_task_level_ceiling or 0),
12569
13106
  "runtime_goal_reset_pending": bool(self.runtime_goal_reset_pending),
12570
13107
  "runtime_plan_mode_needed": bool(self.runtime_plan_mode_needed),
12571
13108
  "runtime_plan_approved": bool(self.runtime_plan_approved),
@@ -14598,17 +15135,14 @@ class SessionState:
14598
15135
  {
14599
15136
  "content": "Split task into small subtasks",
14600
15137
  "status": "in_progress",
14601
- "activeForm": "Working on: Split task into small subtasks",
14602
15138
  },
14603
15139
  {
14604
15140
  "content": "Execute one subtask and persist intermediate result",
14605
15141
  "status": "pending",
14606
- "activeForm": "Pending: Execute one subtask and persist intermediate result",
14607
15142
  },
14608
15143
  {
14609
15144
  "content": "Validate and continue remaining subtasks",
14610
15145
  "status": "pending",
14611
- "activeForm": "Pending: Validate and continue remaining subtasks",
14612
15146
  },
14613
15147
  ]
14614
15148
  )
@@ -16768,30 +17302,46 @@ class SessionState:
16768
17302
  return False
16769
17303
  done_markers = [
16770
17304
  "任务完成",
17305
+ "任務完成",
16771
17306
  "已完成",
16772
17307
  "全部完成",
17308
+ "全部完成了",
16773
17309
  "处理完成",
17310
+ "處理完成",
16774
17311
  "修复完成",
17312
+ "修復完成",
16775
17313
  "解释如上",
17314
+ "說明如上",
16776
17315
  "上述代码已为您编写完毕",
16777
17316
  "done",
16778
17317
  "completed",
16779
17318
  "finished",
16780
17319
  "all set",
17320
+ "完了しました",
17321
+ "対応完了",
17322
+ "修正完了",
17323
+ "以上です",
17324
+ "作成しました",
16781
17325
  # 明确表示拒绝/无法完成也应视为终结
16782
17326
  "抱歉",
16783
17327
  "sorry",
16784
17328
  "无法",
16785
17329
  "cannot",
16786
17330
  "unable",
17331
+ "無法",
17332
+ "すみません",
17333
+ "できません",
16787
17334
  ]
16788
17335
  if any(x in t for x in done_markers):
16789
17336
  return False
16790
17337
  continue_markers = [
16791
17338
  "让我",
17339
+ "讓我",
16792
17340
  "我将",
16793
17341
  "继续",
17342
+ "繼續",
16794
17343
  "接下来",
17344
+ "接下來",
16795
17345
  "重新分析",
16796
17346
  "修复代码",
16797
17347
  "i will",
@@ -16799,6 +17349,12 @@ class SessionState:
16799
17349
  "let me",
16800
17350
  "next",
16801
17351
  "continue",
17352
+ "続けます",
17353
+ "これから",
17354
+ "次に",
17355
+ "再分析",
17356
+ "修正します",
17357
+ "進めます",
16802
17358
  ]
16803
17359
  return any(x in t for x in continue_markers)
16804
17360
 
@@ -16819,9 +17375,19 @@ class SessionState:
16819
17375
  "要不要",
16820
17376
  "是否",
16821
17377
  "可选项",
17378
+ "請選擇",
17379
+ "請告訴我",
17380
+ "你希望",
17381
+ "要不要",
17382
+ "是否要",
17383
+ "どの案",
17384
+ "どれを選ぶ",
17385
+ "選んでください",
17386
+ "希望しますか",
17387
+ "必要ですか",
16822
17388
  ]
16823
17389
  has_question = ("?" in t) or ("?" in text)
16824
- has_option_list = any(token in t for token in ["1.", "2.", "3.", "option", "选项"])
17390
+ has_option_list = any(token in t for token in ["1.", "2.", "3.", "option", "选项", "選項", "方案", "案"])
16825
17391
  return has_question and (has_option_list or any(x in t for x in ask_markers))
16826
17392
 
16827
17393
  def _looks_like_conclusive_reply(self, text: str) -> bool:
@@ -16832,25 +17398,39 @@ class SessionState:
16832
17398
  negative = [
16833
17399
  "未完成",
16834
17400
  "还没完成",
17401
+ "還沒完成",
16835
17402
  "继续",
17403
+ "繼續",
16836
17404
  "next step",
16837
17405
  "todo",
16838
17406
  "pending",
16839
17407
  "in_progress",
16840
17408
  "需要继续",
16841
17409
  "待处理",
17410
+ "待處理",
17411
+ "未完了",
17412
+ "まだ完了していない",
17413
+ "続行",
17414
+ "次のステップ",
17415
+ "保留",
17416
+ "進行中",
16842
17417
  ]
16843
17418
  if any(x in t for x in negative):
16844
17419
  return False
16845
17420
  done_markers = [
16846
17421
  "任务完成",
17422
+ "任務完成",
16847
17423
  "处理完成",
17424
+ "處理完成",
16848
17425
  "修复完成",
17426
+ "修復完成",
16849
17427
  "以上就是",
16850
17428
  "解释如上",
17429
+ "說明如上",
16851
17430
  "上述代码已为您编写完毕",
16852
17431
  "已为您创建",
16853
17432
  "已为你创建",
17433
+ "已為您建立",
16854
17434
  "all set",
16855
17435
  "done",
16856
17436
  "completed",
@@ -16865,6 +17445,12 @@ class SessionState:
16865
17445
  "that's all",
16866
17446
  "that is all",
16867
17447
  "as requested",
17448
+ "完了しました",
17449
+ "対応完了",
17450
+ "修正完了",
17451
+ "以上です",
17452
+ "作成しました",
17453
+ "準備できました",
16868
17454
  # 明确表示无法完成的标记
16869
17455
  "抱歉,我无法",
16870
17456
  "无法直接获取",
@@ -16876,6 +17462,11 @@ class SessionState:
16876
17462
  "建议你通过",
16877
17463
  "i cannot",
16878
17464
  "i'm unable",
17465
+ "抱歉,我無法",
17466
+ "無法直接取得",
17467
+ "無法完成",
17468
+ "できません",
17469
+ "不可能です",
16879
17470
  ]
16880
17471
  return any(x in t for x in done_markers)
16881
17472
 
@@ -16920,6 +17511,19 @@ class SessionState:
16920
17511
  "总结",
16921
17512
  "说明如下",
16922
17513
  "结果如下",
17514
+ "已完成",
17515
+ "修復了",
17516
+ "修改了",
17517
+ "實作了",
17518
+ "總結",
17519
+ "說明如下",
17520
+ "結果如下",
17521
+ "実装しました",
17522
+ "修正しました",
17523
+ "更新しました",
17524
+ "作成しました",
17525
+ "まとめ",
17526
+ "結果は以下",
16923
17527
  ]
16924
17528
  if any(x in low for x in informative_markers):
16925
17529
  return True
@@ -16943,7 +17547,9 @@ class SessionState:
16943
17547
  return False
16944
17548
  markers = [
16945
17549
  "是什么",
17550
+ "是什麼",
16946
17551
  "为什么",
17552
+ "為什麼",
16947
17553
  "怎么",
16948
17554
  "如何",
16949
17555
  "explain",
@@ -16951,6 +17557,13 @@ class SessionState:
16951
17557
  "why",
16952
17558
  "how to",
16953
17559
  "difference",
17560
+ "差異",
17561
+ "とは",
17562
+ "なぜ",
17563
+ "どうやって",
17564
+ "どうすれば",
17565
+ "違い",
17566
+ "何ですか",
16954
17567
  ]
16955
17568
  return any(x in t for x in markers)
16956
17569
 
@@ -17009,6 +17622,13 @@ class SessionState:
17009
17622
  "ready to use",
17010
17623
  "as requested",
17011
17624
  "that is all",
17625
+ "希望這能幫到你",
17626
+ "說明如上",
17627
+ "程式碼已修改完成",
17628
+ "請查看上述程式碼",
17629
+ "完了しました",
17630
+ "以上です",
17631
+ "ご確認ください",
17012
17632
  ]
17013
17633
  if any(x in tail for x in endpoint_markers):
17014
17634
  score += 2
@@ -17031,11 +17651,58 @@ class SessionState:
17031
17651
  low = str(text or "").strip().lower()
17032
17652
  if not low:
17033
17653
  return ""
17034
- if any(x in low for x in ["task_completed", "task completed", "已完成", "done", "completed", "finished"]):
17654
+ if any(
17655
+ x in low
17656
+ for x in [
17657
+ "task_completed",
17658
+ "task completed",
17659
+ "已完成",
17660
+ "完成",
17661
+ "已完成了",
17662
+ "done",
17663
+ "completed",
17664
+ "finished",
17665
+ "任務完成",
17666
+ "完了",
17667
+ "完了しました",
17668
+ ]
17669
+ ):
17035
17670
  return "TASK_COMPLETED"
17036
- if any(x in low for x in ["valid_planning", "valid planning", "plan", "planning", "next step", "analysis"]):
17671
+ if any(
17672
+ x in low
17673
+ for x in [
17674
+ "valid_planning",
17675
+ "valid planning",
17676
+ "plan",
17677
+ "planning",
17678
+ "next step",
17679
+ "analysis",
17680
+ "计划",
17681
+ "計畫",
17682
+ "规划",
17683
+ "規劃",
17684
+ "下一步",
17685
+ "分析",
17686
+ "計画",
17687
+ "次のステップ",
17688
+ ]
17689
+ ):
17037
17690
  return "VALID_PLANNING"
17038
- if any(x in low for x in ["empty_rambling", "empty rambling", "rambling", "idle", "stalled", "hallucination", "空想"]):
17691
+ if any(
17692
+ x in low
17693
+ for x in [
17694
+ "empty_rambling",
17695
+ "empty rambling",
17696
+ "rambling",
17697
+ "idle",
17698
+ "stalled",
17699
+ "hallucination",
17700
+ "空想",
17701
+ "停滞",
17702
+ "停滯",
17703
+ "だらだら",
17704
+ ]
17705
+ ):
17039
17706
  return "EMPTY_RAMBLING"
17040
17707
  return ""
17041
17708
 
@@ -17234,6 +17901,163 @@ class SessionState:
17234
17901
  )
17235
17902
  return result
17236
17903
 
17904
+ def _generate_run_completion_summary(self):
17905
+ """Generate a brief summary bubble when a run completes, so user isn't left without feedback."""
17906
+ # Guard: plan proposal is waiting for user selection — run ended but task not started yet
17907
+ if self.runtime_plan_mode_needed and not self.runtime_plan_approved:
17908
+ return
17909
+ try:
17910
+ bb = self._ensure_blackboard()
17911
+ board_md = self._blackboard_read_state_markdown(max_items=10)
17912
+ # Collect completed plan steps
17913
+ plan_steps = [t for t in bb.get("project_todos", []) if t.get("category") == "plan_step"]
17914
+ completed = [t for t in plan_steps if t.get("status") == "completed"]
17915
+ pending = [t for t in plan_steps if t.get("status") != "completed"]
17916
+ # Collect recent file operations from step_files
17917
+ step_files = bb.get("step_files", {})
17918
+ file_list = []
17919
+ for _sf_key, entries in (step_files.items() if isinstance(step_files, dict) else []):
17920
+ if isinstance(entries, list):
17921
+ for e in entries[-5:]:
17922
+ fp = str(e.get("path", "") if isinstance(e, dict) else e or "").strip()
17923
+ if fp:
17924
+ file_list.append(fp)
17925
+ file_list = list(dict.fromkeys(file_list))[-10:] # Dedupe, last 10
17926
+ # Build context for LLM summary
17927
+ goal = trim(str(bb.get("original_goal", "") or self.runtime_reclassify_goal or ""), 600)
17928
+ lang_hint = model_language_instruction(self.ui_language)
17929
+ parts = [f"GOAL: {goal}"] if goal else []
17930
+ if completed:
17931
+ parts.append(f"COMPLETED STEPS ({len(completed)}/{len(plan_steps)}):")
17932
+ for t in completed:
17933
+ idx = int(t.get("plan_step_index", 0) or 0) + 1
17934
+ parts.append(f" ✅ {idx}. {trim(str(t.get('content', '')), 100)}")
17935
+ if pending:
17936
+ parts.append(f"REMAINING ({len(pending)}):")
17937
+ for t in pending[:3]:
17938
+ idx = int(t.get("plan_step_index", 0) or 0) + 1
17939
+ parts.append(f" ⬜ {idx}. {trim(str(t.get('content', '')), 100)}")
17940
+ if file_list:
17941
+ parts.append(f"FILES CREATED/MODIFIED: {', '.join(file_list[:8])}")
17942
+ if board_md:
17943
+ parts.append(f"BLACKBOARD STATE:\n{trim(board_md, 800)}")
17944
+ context = "\n".join(parts)
17945
+ # --- Supplement context from messages history when plan steps are unavailable ---
17946
+ # (pure sync mode: no plan steps, so file_list/ops may be empty above)
17947
+ files_from_msgs: list[str] = []
17948
+ ops_from_msgs: list[str] = []
17949
+ agent_conclusion = ""
17950
+ for _m in self.messages[-60:]:
17951
+ if not isinstance(_m, dict):
17952
+ continue
17953
+ _role = _m.get("role", "")
17954
+ # file_patch type → extract file location
17955
+ if _m.get("type") == "file_patch":
17956
+ _loc = str(_m.get("location", "") or _m.get("path", "") or "").strip()
17957
+ if _loc and _loc not in files_from_msgs:
17958
+ files_from_msgs.append(_loc)
17959
+ # command output → capture brief preview
17960
+ if _m.get("type") == "command" and _role == "system":
17961
+ _cmd = trim(str(_m.get("content", "") or ""), 120)
17962
+ if _cmd:
17963
+ ops_from_msgs.append(_cmd)
17964
+ # last non-manager/non-planner assistant reply → agent self-description
17965
+ if _role == "assistant" and str(_m.get("agent_role", "") or "") not in ("manager", "planner"):
17966
+ _txt = str(_m.get("content", "") or "").strip()
17967
+ if _txt:
17968
+ agent_conclusion = trim(_txt, 400)
17969
+ # Merge: use message-derived file list only when step_files gave nothing
17970
+ if not file_list and files_from_msgs:
17971
+ file_list = files_from_msgs[:8]
17972
+ # Append agent conclusion and bash ops to context if not already present
17973
+ if agent_conclusion and "AGENT LAST OUTPUT" not in context:
17974
+ context += f"\nAGENT LAST OUTPUT:\n{agent_conclusion}"
17975
+ if ops_from_msgs and "BASH OPS" not in context:
17976
+ context += f"\nBASH OPS (last {min(3, len(ops_from_msgs))}):\n" + "\n".join(ops_from_msgs[-3:])
17977
+ # ---
17978
+ prompt = (
17979
+ f"{lang_hint}\n"
17980
+ "Generate a BRIEF completion summary for the user (3-8 sentences).\n"
17981
+ "Include: what was accomplished, key files created, and any remaining work.\n"
17982
+ "Use markdown formatting. Be concise and informative.\n"
17983
+ f"Do NOT include code blocks unless essential.\n\n"
17984
+ f"CONTEXT:\n{context}"
17985
+ )
17986
+ rsp = self.ollama.chat(
17987
+ [{"role": "user", "content": prompt}],
17988
+ max_tokens=800,
17989
+ temperature=0.3,
17990
+ )
17991
+ summary_text = str(rsp.get("content", "") or "").strip()
17992
+ if not summary_text or len(summary_text) < 10:
17993
+ summary_text = self._generate_static_completion_summary(
17994
+ completed, pending, file_list, goal, agent_text=agent_conclusion
17995
+ )
17996
+ self.messages.append({
17997
+ "role": "assistant",
17998
+ "content": summary_text,
17999
+ "ts": now_ts(),
18000
+ "agent_role": "explorer",
18001
+ })
18002
+ self._emit("message", {
18003
+ "role": "assistant",
18004
+ "text": trim(summary_text, int(ASSISTANT_MESSAGE_EVENT_MAX_CHARS)),
18005
+ "summary": "run completion summary",
18006
+ "agent_role": "explorer",
18007
+ })
18008
+ except Exception:
18009
+ try:
18010
+ bb = self._ensure_blackboard()
18011
+ plan_steps = [t for t in bb.get("project_todos", []) if t.get("category") == "plan_step"]
18012
+ completed = [t for t in plan_steps if t.get("status") == "completed"]
18013
+ pending = [t for t in plan_steps if t.get("status") != "completed"]
18014
+ goal = trim(str(bb.get("original_goal", "") or ""), 200)
18015
+ summary_text = self._generate_static_completion_summary(completed, pending, [], goal)
18016
+ self.messages.append({
18017
+ "role": "assistant",
18018
+ "content": summary_text,
18019
+ "ts": now_ts(),
18020
+ "agent_role": "explorer",
18021
+ })
18022
+ self._emit("message", {
18023
+ "role": "assistant",
18024
+ "text": trim(summary_text, int(ASSISTANT_MESSAGE_EVENT_MAX_CHARS)),
18025
+ "summary": "run completion summary (static)",
18026
+ "agent_role": "explorer",
18027
+ })
18028
+ except Exception:
18029
+ pass
18030
+
18031
+ def _generate_static_completion_summary(
18032
+ self,
18033
+ completed: list[dict],
18034
+ pending: list[dict],
18035
+ file_list: list[str],
18036
+ goal: str,
18037
+ agent_text: str = "",
18038
+ ) -> str:
18039
+ """Static fallback summary when LLM call fails."""
18040
+ lines = ["## ✅ 任务执行完成\n"]
18041
+ if goal:
18042
+ lines.append(f"**目标:** {trim(goal, 200)}\n")
18043
+ total = len(completed) + len(pending)
18044
+ if completed:
18045
+ lines.append(f"**进度:** {len(completed)}/{total} 步骤已完成")
18046
+ for t in completed[-5:]:
18047
+ idx = int(t.get("plan_step_index", 0) or 0) + 1
18048
+ lines.append(f"- ✅ {idx}. {trim(str(t.get('content', '')), 80)}")
18049
+ if pending:
18050
+ lines.append(f"\n**待完成:** {len(pending)} 步骤")
18051
+ for t in pending[:3]:
18052
+ idx = int(t.get("plan_step_index", 0) or 0) + 1
18053
+ lines.append(f"- ⬜ {idx}. {trim(str(t.get('content', '')), 80)}")
18054
+ # When no plan steps exist (pure sync mode), show agent conclusion text instead
18055
+ if not completed and agent_text:
18056
+ lines.append(f"\n**执行摘要:** {trim(agent_text, 300)}")
18057
+ if file_list:
18058
+ lines.append(f"\n**涉及文件:** {', '.join(file_list[:6])}")
18059
+ return "\n".join(lines)
18060
+
17237
18061
  def _should_soft_pause_no_action(self, assistant_text: str, tool_calls: list | None = None) -> bool:
17238
18062
  if tool_calls:
17239
18063
  return False
@@ -17253,7 +18077,7 @@ class SessionState:
17253
18077
  def _todo_active_brief(self) -> str:
17254
18078
  rows = self.todo.snapshot()
17255
18079
  if not rows:
17256
- return "No todos."
18080
+ return self._ui_text("todo_no_todos")
17257
18081
  active = []
17258
18082
  open_count = 0
17259
18083
  done_count = 0
@@ -20540,13 +21364,23 @@ class SessionState:
20540
21364
  board["project_todos"] = []
20541
21365
  else:
20542
21366
  clean_todos = []
21367
+ import re as _re_norm
21368
+ _mid_re_norm = _re_norm.compile(r"(?<=\S)\s+(\d+\.\d+\s)")
20543
21369
  for pt in bb_src_todos[:40]:
20544
21370
  if not isinstance(pt, dict):
20545
21371
  continue
21372
+ raw_content = trim(str(pt.get("content", "") or ""), 1500)
21373
+ raw_full = trim(str(pt.get("full_content", "") or ""), 1500)
21374
+ # Migration: if full_content is empty but content has sub-steps, auto-split
21375
+ if not raw_full and raw_content and pt.get("category") == "plan_step":
21376
+ normalized = _mid_re_norm.sub(r"\n\1", raw_content)
21377
+ if "\n" in normalized:
21378
+ raw_full = normalized
21379
+ raw_content = normalized.split("\n")[0].strip()
20546
21380
  clean_todos.append({
20547
21381
  "id": trim(str(pt.get("id", "") or ""), 20),
20548
- "content": trim(str(pt.get("content", "") or ""), 400),
20549
- "full_content": trim(str(pt.get("full_content", "") or ""), 1500),
21382
+ "content": trim(raw_content, 400),
21383
+ "full_content": trim(raw_full, 1500),
20550
21384
  "status": str(pt.get("status", "pending") or "pending") if str(pt.get("status", "pending") or "pending") in ("pending", "in_progress", "completed") else "pending",
20551
21385
  "category": trim(str(pt.get("category", "") or ""), 40),
20552
21386
  "plan_step_index": int(pt.get("plan_step_index", -1)) if pt.get("plan_step_index") is not None else -1,
@@ -21118,16 +21952,7 @@ class SessionState:
21118
21952
  self._blackboard_touch()
21119
21953
 
21120
21954
  def _todo_owner_display_name(self, owner: str) -> str:
21121
- role = str(owner or "").strip().lower()
21122
- if role == "manager":
21123
- return "Manager"
21124
- if role == "explorer":
21125
- return "Explorer"
21126
- if role == "developer":
21127
- return "Developer"
21128
- if role == "reviewer":
21129
- return "Reviewer"
21130
- return "Agent"
21955
+ return self._agent_display_name(owner)
21131
21956
 
21132
21957
  def _todo_stage_focus_owner(self, board: dict | None = None) -> str:
21133
21958
  bb = board if isinstance(board, dict) else self._ensure_blackboard()
@@ -21305,21 +22130,24 @@ class SessionState:
21305
22130
  def _owner_desc(owner: str) -> str:
21306
22131
  if owner == "manager":
21307
22132
  if delegate_target:
21308
- return f"Plan route and coordinate current node handoff ({self._agent_display_name(delegate_target)} active)"
21309
- return "Plan route and coordinate current node handoff"
22133
+ return self._ui_text(
22134
+ "node_desc_manager_active",
22135
+ target=self._agent_display_name(delegate_target),
22136
+ )
22137
+ return self._ui_text("node_desc_manager")
21310
22138
  if owner == "explorer":
21311
22139
  if delegate_target == "explorer":
21312
- return "Gather constraints/evidence for current node"
21313
- return "Provide research support and risk notes for current node"
22140
+ return self._ui_text("node_desc_explorer_active")
22141
+ return self._ui_text("node_desc_explorer")
21314
22142
  if owner == "developer":
21315
22143
  if delegate_target == "developer":
21316
- return "Implement concrete outputs and file/tool changes for current node"
21317
- return "Prepare and deliver implementation updates for current node"
22144
+ return self._ui_text("node_desc_developer_active")
22145
+ return self._ui_text("node_desc_developer")
21318
22146
  if owner == "reviewer":
21319
22147
  if delegate_target == "reviewer":
21320
- return "Validate current node and provide pass/fix judgement"
21321
- return "Review outputs and keep quality gate updated for current node"
21322
- return "Handle current node work"
22148
+ return self._ui_text("node_desc_reviewer_active")
22149
+ return self._ui_text("node_desc_reviewer")
22150
+ return self._ui_text("node_desc_generic")
21323
22151
 
21324
22152
  finish_ok, _ = self._can_auto_finish_from_approval(bb)
21325
22153
  rows: list[dict] = []
@@ -21369,13 +22197,17 @@ class SessionState:
21369
22197
  status_value = str(row.get("status", "pending") or "pending")
21370
22198
  topic_suffix = ""
21371
22199
  if node_topic and status_value == "in_progress":
21372
- topic_suffix = f" | node: {trim(node_topic, 140)}"
22200
+ topic_suffix = self._ui_text("todo_node_suffix", topic=trim(node_topic, 140))
21373
22201
  if status_value == "in_progress":
21374
- row["activeForm"] = f"Working on ({label}): {text}{topic_suffix}"
22202
+ row["activeForm"] = self._ui_text(
22203
+ "todo_working_owner",
22204
+ owner=label,
22205
+ content=f"{text}{topic_suffix}",
22206
+ )
21375
22207
  elif status_value == "completed":
21376
- row["activeForm"] = f"Completed ({label}): {text}"
22208
+ row["activeForm"] = self._ui_text("todo_completed_owner", owner=label, content=text)
21377
22209
  else:
21378
- row["activeForm"] = f"Pending ({label}): {text}"
22210
+ row["activeForm"] = self._ui_text("todo_pending_owner", owner=label, content=text)
21379
22211
  return rows
21380
22212
 
21381
22213
  # ── Project-based todo generation & status tracking ──────────────
@@ -21387,23 +22219,47 @@ class SessionState:
21387
22219
  objective = trim(str(profile.get("direct_objective", "") or ""), 200)
21388
22220
 
21389
22221
  if task_type == "simple_qa":
21390
- return [{"content": f"回答: {objective}" if objective else "回答用户问题", "category": "implement"}]
22222
+ return [
22223
+ {
22224
+ "content": self._ui_text("project_answer_objective", objective=objective)
22225
+ if objective
22226
+ else self._ui_text("project_answer_default"),
22227
+ "category": "implement",
22228
+ }
22229
+ ]
21391
22230
 
21392
22231
  if task_type in ("simple_code", "engineering"):
21393
22232
  return [
21394
- {"content": "分析需求和项目结构", "category": "setup"},
21395
- {"content": f"实现: {objective}" if objective else "实现编码任务", "category": "implement"},
21396
- {"content": "编译/语法检查", "category": "compile_test"},
21397
- {"content": "最小功能测试", "category": "min_test"},
22233
+ {"content": self._ui_text("project_analyze_requirements"), "category": "setup"},
22234
+ {
22235
+ "content": self._ui_text("project_implement_objective", objective=objective)
22236
+ if objective
22237
+ else self._ui_text("project_implement_default"),
22238
+ "category": "implement",
22239
+ },
22240
+ {"content": self._ui_text("project_compile_check"), "category": "compile_test"},
22241
+ {"content": self._ui_text("project_min_test"), "category": "min_test"},
21398
22242
  ]
21399
22243
 
21400
22244
  if task_type == "research":
21401
22245
  return [
21402
- {"content": f"调研: {objective}" if objective else "执行调研任务", "category": "implement"},
21403
- {"content": "整理调研结果", "category": "review"},
22246
+ {
22247
+ "content": self._ui_text("project_research_objective", objective=objective)
22248
+ if objective
22249
+ else self._ui_text("project_research_default"),
22250
+ "category": "implement",
22251
+ },
22252
+ {"content": self._ui_text("project_research_summary"), "category": "review"},
21404
22253
  ]
21405
22254
 
21406
- return [{"content": f"执行: {objective}" if objective else "执行任务", "category": "implement"}]
22255
+ return [
22256
+ {
22257
+ "content": self._ui_text("project_execute_objective", objective=objective)
22258
+ if objective
22259
+ else self._ui_text("project_execute_default"),
22260
+ "category": "implement",
22261
+ }
22262
+ ]
21407
22263
 
21408
22264
  def _init_project_todos(self, board: dict | None = None):
21409
22265
  bb = board if isinstance(board, dict) else self._ensure_blackboard()
@@ -21501,16 +22357,36 @@ class SessionState:
21501
22357
  continue
21502
22358
  cat = todo.get("category", "")
21503
22359
  if cat == "setup" and (research_count > 0 or code_count > 0):
21504
- todo.update(status="completed", completed_at=float(now_ts()), evidence="结构已分析")
22360
+ todo.update(
22361
+ status="completed",
22362
+ completed_at=float(now_ts()),
22363
+ evidence=self._ui_text("evidence_structure_analyzed"),
22364
+ )
21505
22365
  elif cat == "implement" and code_count > 0:
21506
- todo.update(status="completed", completed_at=float(now_ts()),
21507
- completed_by="developer", evidence=f"{code_count} 文件已产出")
22366
+ todo.update(
22367
+ status="completed",
22368
+ completed_at=float(now_ts()),
22369
+ completed_by="developer",
22370
+ evidence=self._ui_text("evidence_files_produced", count=code_count),
22371
+ )
21508
22372
  elif cat == "compile_test" and self._has_compile_pass_evidence(bb):
21509
- todo.update(status="completed", completed_at=float(now_ts()), evidence="编译通过")
22373
+ todo.update(
22374
+ status="completed",
22375
+ completed_at=float(now_ts()),
22376
+ evidence=self._ui_text("evidence_compile_passed"),
22377
+ )
21510
22378
  elif cat == "min_test" and self._has_test_pass_evidence(bb):
21511
- todo.update(status="completed", completed_at=float(now_ts()), evidence="测试通过")
22379
+ todo.update(
22380
+ status="completed",
22381
+ completed_at=float(now_ts()),
22382
+ evidence=self._ui_text("evidence_test_passed"),
22383
+ )
21512
22384
  elif cat == "review" and feedback_pass:
21513
- todo.update(status="completed", completed_at=float(now_ts()), evidence="审查通过")
22385
+ todo.update(
22386
+ status="completed",
22387
+ completed_at=float(now_ts()),
22388
+ evidence=self._ui_text("evidence_review_passed"),
22389
+ )
21514
22390
  elif cat == "plan_step":
21515
22391
  # Plan steps 不自动完成,由 _advance_plan_step 显式推进
21516
22392
  # 但如果当前步骤之前的所有步骤都完成了,标记当前步骤为 in_progress
@@ -21601,7 +22477,7 @@ class SessionState:
21601
22477
  current["status"] = "completed"
21602
22478
  current["completed_at"] = float(now_ts())
21603
22479
  current["completed_by"] = actor
21604
- current["evidence"] = trim(str(evidence or "").strip(), 200) or "step completed"
22480
+ current["evidence"] = trim(str(evidence or "").strip(), 200) or self._ui_text("step_completed_evidence")
21605
22481
  # 推进 cursor,激活下一步
21606
22482
  cursor = int(bb.get("plan_step_cursor", 0) or 0)
21607
22483
  bb["plan_step_cursor"] = cursor + 1
@@ -21615,7 +22491,12 @@ class SessionState:
21615
22491
  step_idx = int(next_step.get("plan_step_index", 0) or 0) + 1
21616
22492
  total = int(bb.get("plan_step_total", len(todos)) or len(todos))
21617
22493
  self._emit("status", {
21618
- "summary": f"📋 Plan step {step_idx}/{total}: {trim(str(next_step.get('content', '') or ''), 120)}"
22494
+ "summary": self._ui_text(
22495
+ "plan_step_summary",
22496
+ step=step_idx,
22497
+ total=total,
22498
+ content=trim(str(next_step.get("content", "") or ""), 120),
22499
+ )
21619
22500
  })
21620
22501
  self.blackboard = bb
21621
22502
  self._blackboard_touch()
@@ -21648,14 +22529,13 @@ class SessionState:
21648
22529
  _ns_total = int(bb.get("plan_step_total", 0) or 0)
21649
22530
  _ns_text = trim(str(next_step.get("content", "") or ""), 200)
21650
22531
  _ns_id = str(next_step.get("id", "") or "")
21651
- _ns_label = f"Step {_ns_idx}" + (f"/{_ns_total}" if _ns_total else "")
21652
- _hint = (
21653
- f"[plan-step-advance] Previous step completed. Now at {_ns_label}: {_ns_text}\n"
21654
- f"Read updated plan: read_file {PLAN_FILE_RELATIVE_PATH}\n"
21655
- f"Call TodoWrite to set subtasks for THIS step ONLY.\n"
21656
- f"Each subtask MUST include parent_step_id='{_ns_id}'. "
21657
- f"Create 3-5 items, one marked in_progress, others pending.\n"
21658
- f"Do NOT create subtasks for other plan steps."
22532
+ _ns_label = self._ui_text("plan_step_label", step=_ns_idx, total=_ns_total)
22533
+ _hint = self._ui_text(
22534
+ "plan_step_hint",
22535
+ step_label=_ns_label,
22536
+ step_text=_ns_text,
22537
+ plan_path=PLAN_FILE_RELATIVE_PATH,
22538
+ parent_step_id=_ns_id,
21659
22539
  )
21660
22540
  self.messages.append({"role": "system", "content": _hint, "ts": now_ts()})
21661
22541
  # Also inject into active agent context for multi-agent mode
@@ -21678,6 +22558,19 @@ class SessionState:
21678
22558
  None,
21679
22559
  )
21680
22560
  if not current:
22561
+ # No plan steps — detect finish_current_task for pure-sync mode coordination.
22562
+ # Sets sync_worker_round_done flag so the sync loop can signal the manager.
22563
+ # Only reachable in pure sync mode (no plan steps); plan+sync always has `current`
22564
+ # set, so this branch never executes for plan modes.
22565
+ results = worker_step.get("tool_results", []) or []
22566
+ called_finish = any(
22567
+ isinstance(r, dict) and r.get("ok")
22568
+ and str(r.get("name", "")) in ("finish_current_task", "finish_task")
22569
+ for r in results
22570
+ )
22571
+ if called_finish:
22572
+ bb["sync_worker_round_done"] = True
22573
+ self._save_blackboard(bb)
21681
22574
  return
21682
22575
  # 1. Manager explicitly requested advancement
21683
22576
  manager_requested = bool(route.get("advance_plan_step_requested", False))
@@ -21685,30 +22578,41 @@ class SessionState:
21685
22578
  worker_produced_output = self._worker_step_has_evidence(worker_step)
21686
22579
  # 3. All subtasks for this step are completed
21687
22580
  subtasks_all_done = self._step_subtasks_all_completed(current)
21688
- # 4. File-evidence fallback: when worker doesn't call TodoWrite, use phase heuristics
22581
+ # 4. Phase-based file+bash evidence (implement requires BOTH write + bash)
21689
22582
  step_content = str(current.get("full_content", "") or current.get("content", "") or "").lower()
21690
22583
  phase = self._plan_step_phase_hint(step_content)
21691
22584
  results = worker_step.get("tool_results", []) or []
21692
- wrote_files_count = sum(
21693
- 1 for r in results
21694
- if isinstance(r, dict) and r.get("ok", False)
22585
+ wrote_files = any(
22586
+ isinstance(r, dict) and r.get("ok", False)
21695
22587
  and str(r.get("name", "")) in ("write_file", "edit_file")
22588
+ for r in results
21696
22589
  )
21697
22590
  ran_bash_ok = any(
21698
22591
  isinstance(r, dict) and r.get("ok", False) and str(r.get("name", "")) == "bash"
21699
22592
  for r in results
21700
22593
  )
21701
- file_evidence_strong = (
21702
- phase in ("implement", "design") and wrote_files_count >= 2
21703
- ) or (
21704
- phase in ("test", "review") and ran_bash_ok
22594
+ phase_evidence = False
22595
+ if phase in ("research", "design") and wrote_files:
22596
+ phase_evidence = True
22597
+ elif phase == "implement" and wrote_files and ran_bash_ok:
22598
+ phase_evidence = True
22599
+ elif phase in ("test", "review") and ran_bash_ok:
22600
+ phase_evidence = True
22601
+ # 5. finish_current_task is an explicit completion signal — override evidence check
22602
+ called_finish = any(
22603
+ isinstance(r, dict) and r.get("ok")
22604
+ and str(r.get("name", "")) in ("finish_current_task", "finish_task", "mark_done")
22605
+ for r in results
21705
22606
  )
21706
22607
  # Advance when:
22608
+ # - Worker called finish (strongest signal), OR
21707
22609
  # - Manager requested AND worker produced output, OR
21708
22610
  # - All subtasks completed AND worker produced output, OR
21709
- # - Strong file evidence (fallback when worker forgets TodoWrite)
21710
- has_strong_evidence = worker_produced_output and (
21711
- manager_requested or subtasks_all_done or file_evidence_strong
22611
+ # - Phase heuristics confirm (write+bash for implement)
22612
+ has_strong_evidence = called_finish or (
22613
+ worker_produced_output and (
22614
+ manager_requested or subtasks_all_done or phase_evidence
22615
+ )
21712
22616
  )
21713
22617
  if has_strong_evidence:
21714
22618
  evidence = self._collect_step_evidence(current, worker_step)
@@ -21743,7 +22647,14 @@ class SessionState:
21743
22647
  and str(r.get("parent_step_id", "") or "") == step_id
21744
22648
  ]
21745
22649
  if not worker_items:
21746
- return False
22650
+ # Fallback: no parent_step_id linkage — check ALL worker items
22651
+ all_worker = [
22652
+ r for r in snap
22653
+ if str(r.get("owner", "") or "").lower() in worker_owners
22654
+ ]
22655
+ if all_worker:
22656
+ return all(str(r.get("status", "")).lower() == "completed" for r in all_worker)
22657
+ return True # No worker items at all → nothing blocks advancement
21747
22658
  # Extract major step number from plan step content (e.g., "1. Project init" → "1")
21748
22659
  import re
21749
22660
  step_content = str(plan_step.get("full_content", "") or plan_step.get("content", "") or "")
@@ -21816,12 +22727,12 @@ class SessionState:
21816
22727
  subtasks_done = self._step_subtasks_all_completed(current)
21817
22728
  if subtasks_done and (wrote_files or ran_bash_ok):
21818
22729
  should_advance = True
21819
- # Priority 2: Phase-based heuristics (relaxedwrote_files OR bash, not both)
22730
+ # Priority 2: Phase-based heuristics (strictimplement requires BOTH write + bash)
21820
22731
  if not should_advance:
21821
22732
  if phase in ("research", "design") and wrote_files:
21822
22733
  should_advance = True
21823
- elif phase == "implement" and wrote_files:
21824
- # Relaxed: implement step done when files are written (don't require bash)
22734
+ elif phase == "implement" and wrote_files and ran_bash_ok:
22735
+ # Strict: implement step needs both file writes AND successful bash
21825
22736
  should_advance = True
21826
22737
  elif phase in ("test", "review") and ran_bash_ok and not any(
21827
22738
  not r.get("ok", False) for r in tool_results if str(r.get("name", "")) == "bash"
@@ -21855,20 +22766,30 @@ class SessionState:
21855
22766
  _total = int(_bb_after.get("plan_step_total", 0) or 0)
21856
22767
  _step_text = trim(str(_new_step.get("content", "") or ""), 200)
21857
22768
  _step_id = str(_new_step.get("id", "") or "")
21858
- _step_label = f"Step {_step_idx}" + (f"/{_total}" if _total else "")
21859
- _hint = (
21860
- f"[plan-step-advance] Previous step completed. Now at {_step_label}: {_step_text}\n"
21861
- f"Read updated plan: read_file {PLAN_FILE_RELATIVE_PATH}\n"
21862
- f"Call TodoWrite to set subtasks for THIS step ONLY.\n"
21863
- f"Each subtask MUST include parent_step_id='{_step_id}'. "
21864
- f"Create 3-5 items, one marked in_progress, others pending.\n"
21865
- f"Do NOT create subtasks for other plan steps."
22769
+ _step_label = self._ui_text("plan_step_label", step=_step_idx, total=_total)
22770
+ _hint = self._ui_text(
22771
+ "plan_step_hint",
22772
+ step_label=_step_label,
22773
+ step_text=_step_text,
22774
+ plan_path=PLAN_FILE_RELATIVE_PATH,
22775
+ parent_step_id=_step_id,
21866
22776
  )
21867
22777
  self.messages.append({"role": "system", "content": _hint, "ts": now_ts()})
21868
22778
  except Exception:
21869
22779
  pass
21870
22780
  else:
21871
22781
  self._sync_todos_from_blackboard(reason="single-agent-round")
22782
+ # Nudge: if agent wrote files but didn't call TodoWrite, remind it
22783
+ called_todo = any(str(r.get("name", "")) == "TodoWrite" for r in tool_results)
22784
+ if (wrote_files or ran_bash_ok) and not called_todo and current:
22785
+ _sid = str(current.get("id", "") or "")
22786
+ if _sid:
22787
+ _nudge = (
22788
+ f"[todo-sync] You made progress on the current step but did not update TodoWrite.\n"
22789
+ f"Call TodoWrite to mark completed subtasks and create new ones.\n"
22790
+ f"Each subtask must include parent_step_id='{_sid}'."
22791
+ )
22792
+ self.messages.append({"role": "system", "content": _nudge, "ts": now_ts()})
21872
22793
 
21873
22794
  def _todo_project_rows_from_blackboard(self, board: dict | None = None) -> list[dict]:
21874
22795
  bb = board if isinstance(board, dict) else self._ensure_blackboard()
@@ -21881,9 +22802,9 @@ class SessionState:
21881
22802
  c = todo.get("content", "")
21882
22803
  ev = todo.get("evidence", "")
21883
22804
  af = {
21884
- "in_progress": f"Working on: {c}",
21885
- "completed": f"Done: {c}" + (f" ({ev})" if ev else ""),
21886
- }.get(s, f"Pending: {c}")
22805
+ "in_progress": self._ui_text("todo_working", content=c),
22806
+ "completed": self._ui_text("todo_completed", content=f"{c}" + (f" ({ev})" if ev else "")),
22807
+ }.get(s, self._ui_text("todo_pending", content=c))
21887
22808
  rows.append({"key": f"bb:proj:{todo.get('id', '')}", "content": c, "status": s, "activeForm": af})
21888
22809
  return rows
21889
22810
 
@@ -21981,8 +22902,11 @@ class SessionState:
21981
22902
  todo_out = self.todo.update(merged)
21982
22903
  except Exception:
21983
22904
  return
21984
- if todo_out != "No todo changes." and reason:
21985
- self._emit("status", {"summary": f"project todos synced ({trim(reason, 120)})"})
22905
+ if todo_out != self.todo.no_changes_text() and reason:
22906
+ self._emit(
22907
+ "status",
22908
+ {"summary": self._ui_text("status_project_todos_synced", reason=trim(reason, 120))},
22909
+ )
21986
22910
 
21987
22911
  def _blackboard_set_status(self, status: str, note: str = ""):
21988
22912
  board = self._ensure_blackboard()
@@ -22774,6 +23698,14 @@ class SessionState:
22774
23698
  _level_floor = int(getattr(self, 'runtime_task_level_floor', 0) or 0)
22775
23699
  if _level_floor > 0 and int(level) < _level_floor:
22776
23700
  level = _level_floor
23701
+ # Ceiling protection: plan-approved risk lock prevents escalation above ceiling
23702
+ _level_ceiling = int(getattr(self, 'runtime_task_level_ceiling', 0) or 0)
23703
+ if _level_ceiling > 0 and int(level) > _level_ceiling:
23704
+ level = _level_ceiling
23705
+ _ceiling_policy = TASK_LEVEL_POLICIES.get(level, {})
23706
+ mode = str(_ceiling_policy.get("execution_mode", mode) or mode)
23707
+ if _ceiling_policy.get("complexity"):
23708
+ complexity = _ceiling_policy["complexity"]
22777
23709
  _complexity_floor = str(getattr(self, 'runtime_complexity_floor', '') or '').strip()
22778
23710
  if _complexity_floor == "complex" and complexity == "simple":
22779
23711
  complexity = "complex"
@@ -24754,6 +25686,14 @@ class SessionState:
24754
25686
  effective_level = int(task_level)
24755
25687
  if int(self.runtime_task_level_floor or 0) > 0:
24756
25688
  effective_level = max(effective_level, int(self.runtime_task_level_floor))
25689
+ # Ceiling protection: plan-approved risk lock prevents manager from escalating above ceiling
25690
+ _level_ceiling = int(self.runtime_task_level_ceiling or 0)
25691
+ if _level_ceiling > 0 and effective_level > _level_ceiling:
25692
+ effective_level = _level_ceiling
25693
+ # Re-derive execution_mode and participants from the capped level's policy
25694
+ _capped_policy = TASK_LEVEL_POLICIES.get(effective_level, {})
25695
+ execution_mode = str(_capped_policy.get("execution_mode", execution_mode) or execution_mode)
25696
+ participants = list(_capped_policy.get("participants", participants) or participants)
24757
25697
  profile["task_level"] = effective_level
24758
25698
  profile["execution_mode"] = execution_mode
24759
25699
  profile["participants"] = list(participants)
@@ -25002,14 +25942,10 @@ class SessionState:
25002
25942
  _active_step_id = str(_pt.get("id", "") or "")
25003
25943
  break
25004
25944
  todo_update_note = (
25005
- f"TODO UPDATE: Call TodoWrite at the START to set subtasks for THIS step ONLY.\n"
25006
- f"Each subtask MUST include parent_step_id='{_active_step_id}'.\n"
25007
- f"CRITICAL SCOPE RULE:\n"
25008
- f"- Create 3-5 subtasks that break down ONLY the current step's work.\n"
25009
- f"- Do NOT create subtasks for other plan steps (do NOT list step 2, 3, 4 etc.).\n"
25010
- f"- Do NOT duplicate the plan step titles as subtasks.\n"
25011
- f"- Each subtask should be a concrete action within THIS step.\n"
25012
- f"Mark each subtask completed as you finish it. When ALL are done, the step auto-advances.\n"
25945
+ f"TODO PLANNING: At the START, call TodoWrite to list ALL subtasks (status=pending, parent_step_id='{_active_step_id}').\n"
25946
+ f"SCOPE RULE: Create 3-5 subtasks for THIS step ONLY — do NOT list other plan steps or duplicate plan step titles.\n"
25947
+ f"As you complete each subtask, update it to status=completed.\n"
25948
+ f"When ALL subtasks are done: call finish_current_task to signal step completion.\n"
25013
25949
  )
25014
25950
  # Build step_files context note for cross-agent file visibility
25015
25951
  step_files_note = ""
@@ -25651,8 +26587,19 @@ class SessionState:
25651
26587
  if len(self.agent_messages) > int(am_limit * 1.5):
25652
26588
  self.agent_messages = self.agent_messages[-am_limit:]
25653
26589
 
26590
+ def _set_ui_language(self, language: str, *, relabel_todos: bool = True) -> str:
26591
+ lang = normalize_ui_language(language)
26592
+ self.ui_language = lang
26593
+ todo = getattr(self, "todo", None)
26594
+ if isinstance(todo, TodoManager):
26595
+ todo.set_language(lang, relabel=relabel_todos)
26596
+ return lang
26597
+
26598
+ def _ui_text(self, key: str, **kwargs) -> str:
26599
+ return backend_i18n_text(getattr(self, "ui_language", DEFAULT_UI_LANGUAGE), key, **kwargs)
26600
+
25654
26601
  def _agent_display_name(self, role: str) -> str:
25655
- return AGENT_ROLE_LABELS.get(self._sanitize_agent_role(role), str(role or "").strip().title() or "Agent")
26602
+ return backend_role_label(self._sanitize_agent_role(role), getattr(self, "ui_language", DEFAULT_UI_LANGUAGE))
25656
26603
 
25657
26604
  def _emit_agent_message(self, role: str, text: str, summary: str = ""):
25658
26605
  role_key = self._sanitize_agent_role(role)
@@ -25966,7 +26913,6 @@ class SessionState:
25966
26913
  {
25967
26914
  "content": content,
25968
26915
  "status": "pending",
25969
- "activeForm": f"Pending: {content}",
25970
26916
  }
25971
26917
  )
25972
26918
  if not clean_items:
@@ -25975,7 +26921,6 @@ class SessionState:
25975
26921
  if in_progress_index < 0 or in_progress_index >= len(clean_items):
25976
26922
  in_progress_index = 0
25977
26923
  clean_items[in_progress_index]["status"] = "in_progress"
25978
- clean_items[in_progress_index]["activeForm"] = f"Working on: {clean_items[in_progress_index]['content']}"
25979
26924
  return self.todo.update(clean_items)
25980
26925
 
25981
26926
  def _analyze_todo_result(self, tool_name: str, output: str) -> tuple[str, str]:
@@ -25985,7 +26930,7 @@ class SessionState:
25985
26930
  return ("failed", "empty output")
25986
26931
  if txt.startswith("Error:"):
25987
26932
  return ("failed", txt[6:].strip() or "unknown error")
25988
- if "no todo changes" in low:
26933
+ if txt == self.todo.no_changes_text() or "no todo changes" in low:
25989
26934
  if self.todo.snapshot():
25990
26935
  return ("ok", "todo already up to date")
25991
26936
  return ("repeat", "same todo payload repeated")
@@ -26362,22 +27307,18 @@ class SessionState:
26362
27307
  {
26363
27308
  "content": f"Triage failure root cause ({trim(reason, 120)})",
26364
27309
  "status": "in_progress",
26365
- "activeForm": f"Working on: Triage failure root cause ({trim(reason, 80)})",
26366
27310
  },
26367
27311
  {
26368
27312
  "content": "Recover critical context with context_recall if compacted/truncated",
26369
27313
  "status": "pending",
26370
- "activeForm": "Pending: Recover critical context with context_recall if compacted/truncated",
26371
27314
  },
26372
27315
  {
26373
27316
  "content": f"Split goal into 3-7 subtasks and execute one tool step at a time ({trim(goal, 90)})",
26374
27317
  "status": "pending",
26375
- "activeForm": "Pending: Split goal into 3-7 subtasks and execute one tool step at a time",
26376
27318
  },
26377
27319
  {
26378
27320
  "content": "If still blocked, output explicit blocker and required next input",
26379
27321
  "status": "pending",
26380
- "activeForm": "Pending: If still blocked, output explicit blocker and required next input",
26381
27322
  },
26382
27323
  ]
26383
27324
  try:
@@ -26814,7 +27755,7 @@ class SessionState:
26814
27755
  isinstance(it, dict) and str(it.get("status", it.get("state", ""))).lower() in {"completed", "done", "finished", "finish"}
26815
27756
  for it in new_items
26816
27757
  ):
26817
- self._refresh_loaded_skills_for_execution_focus(trigger="step-completed") # noqa: removed
27758
+ self._refresh_loaded_skills_for_execution_focus(trigger="step-completed") # noqa: E501
26818
27759
  pass # Skills are loaded on-demand by the model
26819
27760
  except Exception:
26820
27761
  pass
@@ -26878,6 +27819,21 @@ class SessionState:
26878
27819
  )
26879
27820
  },
26880
27821
  )
27822
+ # finish_current_task is a strong signal — advance plan step if active
27823
+ try:
27824
+ _bb_fin = self._ensure_blackboard()
27825
+ _cur_ps = next(
27826
+ (t for t in _bb_fin.get("project_todos", [])
27827
+ if t.get("category") == "plan_step" and t.get("status") == "in_progress"),
27828
+ None,
27829
+ )
27830
+ if _cur_ps:
27831
+ self._advance_plan_step(
27832
+ evidence=f"finish_current_task called: {trim(summary, 100)}",
27833
+ actor=str(role_key or "developer"),
27834
+ )
27835
+ except Exception:
27836
+ pass
26881
27837
  return (
26882
27838
  f"{name} acknowledged{': ' + summary if summary else ''}; "
26883
27839
  f"todo_completed={updated}"
@@ -27985,6 +28941,8 @@ class SessionState:
27985
28941
 
27986
28942
  def _multi_agent_sync_blackboard_worker(self, *, pinned_selection: str):
27987
28943
  idle_counts = {role: 0 for role in AGENT_ROLES}
28944
+ _prev_delegation_hash = ""
28945
+ _repeat_delegation_count = 0
27988
28946
  media_last_user_ts = -1.0
27989
28947
  media_inputs_pool: list[dict] | None = None
27990
28948
  media_seen_ts_by_role: dict[str, float] = {
@@ -27996,6 +28954,26 @@ class SessionState:
27996
28954
  board = self._ensure_blackboard()
27997
28955
  profile = self._ensure_blackboard_task_profile(board)
27998
28956
  budget_val = self._blackboard_round_budget(board)
28957
+ # Fix 7: Pure sync no-plan — if complex task and no plan steps exist, prompt manager
28958
+ # to create them before delegating. Guard: only fires when no plan_step items exist,
28959
+ # so plan+sync mode (which already has plan steps) is completely unaffected.
28960
+ _sync_has_plan = any(
28961
+ isinstance(t, dict) and t.get("category") == "plan_step"
28962
+ for t in board.get("project_todos", [])
28963
+ )
28964
+ _sync_complexity = str(profile.get("complexity", "simple") or "simple")
28965
+ if not _sync_has_plan and _sync_complexity in ("moderate", "complex", "expert"):
28966
+ self.messages.append({
28967
+ "role": "system",
28968
+ "content": (
28969
+ "[SYNC-INIT] No plan steps found for this task. Before delegating to workers, "
28970
+ "use write_to_blackboard to add 3-5 plan_step items to project_todos. "
28971
+ 'Each item: {"category":"plan_step","content":"N. Step title",'
28972
+ '"status":"pending","owner":"manager"}. '
28973
+ "This enables proper todo tracking and completion detection."
28974
+ ),
28975
+ "ts": now_ts(),
28976
+ })
27999
28977
  self._blackboard_set_status("INITIALIZING", "sync collaborative loop started")
28000
28978
  self._emit(
28001
28979
  "status",
@@ -28091,6 +29069,29 @@ class SessionState:
28091
29069
  self._mark_all_done_silently(note)
28092
29070
  self._emit("status", {"summary": "manager decided finish; run paused"})
28093
29071
  break
29072
+ # Detect manager stuck: same instruction repeated N times → force advance + break
29073
+ import hashlib as _hl_mgr
29074
+ _cur_hash = _hl_mgr.sha1((target + "|" + instruction).encode("utf-8")).hexdigest()[:12]
29075
+ if _cur_hash == _prev_delegation_hash:
29076
+ _repeat_delegation_count += 1
29077
+ else:
29078
+ _repeat_delegation_count = 0
29079
+ _prev_delegation_hash = _cur_hash
29080
+ if _repeat_delegation_count >= 3:
29081
+ self._emit("status", {"summary": f"manager stuck: repeated identical delegation x{_repeat_delegation_count + 1}; forcing advance"})
29082
+ _bb_stuck = self._ensure_blackboard()
29083
+ _stuck_step = next(
29084
+ (t for t in _bb_stuck.get("project_todos", [])
29085
+ if t.get("category") == "plan_step" and t.get("status") == "in_progress"),
29086
+ None,
29087
+ )
29088
+ if _stuck_step:
29089
+ self._advance_plan_step(evidence="manager stuck: repeated delegation", actor="manager")
29090
+ else:
29091
+ self._blackboard_mark_approved("manager stuck loop break", "manager")
29092
+ self._mark_all_done_silently("manager stuck: repeated delegation break")
29093
+ break
29094
+ _repeat_delegation_count = 0
28094
29095
  role = self._sanitize_agent_role(target) or "developer"
28095
29096
  self._inject_manager_instruction(
28096
29097
  role,
@@ -28119,6 +29120,42 @@ class SessionState:
28119
29120
  self._blackboard_update_from_worker_step(role, step)
28120
29121
  # Post-execution plan step advancement (replaces pre-execution advancement)
28121
29122
  self._post_execution_plan_step_check(route, step if isinstance(step, dict) else {})
29123
+ # Fix 6b: Pure sync no-plan — read worker-done signal and notify manager
29124
+ _bb_sync = self._ensure_blackboard()
29125
+ if _bb_sync.pop("sync_worker_round_done", False):
29126
+ self._save_blackboard(_bb_sync)
29127
+ self._append_agent_context_message(
29128
+ "manager",
29129
+ {
29130
+ "role": "system",
29131
+ "content": (
29132
+ "[worker-done] Worker completed the current task and called finish_current_task. "
29133
+ "Assess progress: assign the next task or conclude the session."
29134
+ ),
29135
+ "ts": now_ts(),
29136
+ "agent_role": "manager",
29137
+ },
29138
+ mirror_to_global=False,
29139
+ )
29140
+ # Nudge: if worker wrote files but didn't call TodoWrite, inject reminder
29141
+ _step_dict = step if isinstance(step, dict) else {}
29142
+ _step_results = _step_dict.get("tool_results", []) or []
29143
+ _wrote = any(isinstance(r, dict) and r.get("ok") and str(r.get("name", "")) in ("write_file", "edit_file") for r in _step_results)
29144
+ _did_todo = any(isinstance(r, dict) and str(r.get("name", "")) == "TodoWrite" for r in _step_results)
29145
+ if _wrote and not _did_todo:
29146
+ _bb_nudge = self._ensure_blackboard()
29147
+ _cur_step = next((t for t in _bb_nudge.get("project_todos", []) if t.get("category") == "plan_step" and t.get("status") == "in_progress"), None)
29148
+ if _cur_step:
29149
+ _nid = str(_cur_step.get("id", "") or "")
29150
+ if _nid:
29151
+ _nudge_msg = (
29152
+ f"[todo-sync] You made progress but did not call TodoWrite.\n"
29153
+ f"Update your subtasks: mark completed ones, add new ones if needed.\n"
29154
+ f"Each subtask must include parent_step_id='{_nid}'."
29155
+ )
29156
+ self._append_agent_context_message(role, {
29157
+ "role": "system", "content": _nudge_msg, "ts": now_ts(), "agent_role": role,
29158
+ }, mirror_to_global=False)
28122
29159
  # ── Agent turn 结束后的终止检测:结论性回复 + 无待办 + 无错误 → 自动 finish ──
28123
29160
  agent_text = self._latest_agent_assistant_text(role)
28124
29161
  if (
@@ -28882,21 +29919,21 @@ class SessionState:
28882
29919
 
28883
29920
  def _emit_stall_conclusion(self, trigger_source: str, last_fault_reason: str = "", stall_context: dict | None = None):
28884
29921
  ctx = stall_context or self._collect_stall_context(last_fault_reason=last_fault_reason)
28885
- lines = ["## 执行遇阻\n"]
28886
- lines.append(f"**停止原因:** {trim(str(trigger_source), 200)}")
29922
+ lines = [self._ui_text("stall_execution_blocked_title")]
29923
+ lines.append(self._ui_text("stall_stop_reason", reason=trim(str(trigger_source), 200)))
28887
29924
  if last_fault_reason:
28888
- lines.append(f"**错误详情:** {trim(str(last_fault_reason), 400)}")
29925
+ lines.append(self._ui_text("stall_error_details", detail=trim(str(last_fault_reason), 400)))
28889
29926
  error_ctx = str(ctx.get("error_context", "") or "").strip()
28890
29927
  if error_ctx:
28891
- lines.append(f"\n**最近错误:**\n```\n{trim(error_ctx, 600)}\n```")
29928
+ lines.append(f"\n{self._ui_text('stall_recent_error')}\n```\n{trim(error_ctx, 600)}\n```")
28892
29929
  repeated = ctx.get("repeated_tools", [])
28893
29930
  if repeated:
28894
- lines.append(f"\n**重复工具调用:** {', '.join(repeated)}")
28895
- lines.append("\n**建议操作:**")
28896
- lines.append("1. 检查环境是否满足任务要求(文件是否存在、依赖是否安装)")
28897
- lines.append("2. 手动执行失败的命令,确认错误信息")
28898
- lines.append("3. 提供更具体的指导或修改任务描述后重试")
28899
- lines.append("\n请提供进一步指示,我将根据新信息继续执行。")
29931
+ lines.append(f"\n{self._ui_text('stall_repeated_tools', tools=', '.join(repeated))}")
29932
+ lines.append(f"\n{self._ui_text('stall_suggested_actions')}")
29933
+ lines.append(self._ui_text("stall_action_1"))
29934
+ lines.append(self._ui_text("stall_action_2"))
29935
+ lines.append(self._ui_text("stall_action_3"))
29936
+ lines.append(f"\n{self._ui_text('stall_continue_prompt')}")
28900
29937
  conclusion_md = "\n".join(lines)
28901
29938
  self.messages.append({
28902
29939
  "role": "assistant",
@@ -28912,29 +29949,36 @@ class SessionState:
28912
29949
  })
28913
29950
 
28914
29951
  def _format_stall_findings(self, stall_context: dict) -> str:
28915
- lines = ["### 卡死分析\n"]
29952
+ lines = [self._ui_text("stall_analysis_title")]
28916
29953
  goal = str(stall_context.get("goal", "") or "").strip()
28917
29954
  if goal:
28918
- lines.append(f"**目标:** {trim(goal, 400)}")
28919
- lines.append(f"**严重度分数:** {stall_context.get('severity_score', 0)}")
29955
+ lines.append(self._ui_text("stall_goal", goal=trim(goal, 400)))
29956
+ lines.append(self._ui_text("stall_severity", score=stall_context.get("severity_score", 0)))
28920
29957
  events = stall_context.get("stall_events", [])
28921
29958
  if events:
28922
- lines.append("\n**卡死事件序列:**")
29959
+ lines.append(f"\n{self._ui_text('stall_events')}")
28923
29960
  for ev in events[-6:]:
28924
29961
  if isinstance(ev, dict):
28925
- lines.append(f"- [{ev.get('source', '?')}] +{ev.get('points', 0)} → 累计 {ev.get('cumulative', '?')}")
29962
+ lines.append(
29963
+ self._ui_text(
29964
+ "stall_event_line",
29965
+ source=ev.get("source", "?"),
29966
+ points=ev.get("points", 0),
29967
+ cumulative=ev.get("cumulative", "?"),
29968
+ )
29969
+ )
28926
29970
  error_ctx = str(stall_context.get("error_context", "") or "").strip()
28927
29971
  if error_ctx:
28928
- lines.append(f"\n**错误上下文:**\n```\n{trim(error_ctx, 500)}\n```")
29972
+ lines.append(f"\n{self._ui_text('stall_error_context')}\n```\n{trim(error_ctx, 500)}\n```")
28929
29973
  repeated = stall_context.get("repeated_tools", [])
28930
29974
  if repeated:
28931
- lines.append(f"\n**重复工具:** {', '.join(repeated)}")
29975
+ lines.append(f"\n{self._ui_text('stall_repeated_tools_label', tools=', '.join(repeated))}")
28932
29976
  fault_reason = str(stall_context.get("last_fault_reason", "") or "").strip()
28933
29977
  if fault_reason:
28934
- lines.append(f"**最后故障原因:** {trim(fault_reason, 200)}")
29978
+ lines.append(self._ui_text("stall_last_fault_reason", reason=trim(fault_reason, 200)))
28935
29979
  open_todos = stall_context.get("open_todos", [])
28936
29980
  if open_todos:
28937
- lines.append("\n**未完成任务:**")
29981
+ lines.append(f"\n{self._ui_text('stall_open_todos')}")
28938
29982
  for t in open_todos[:4]:
28939
29983
  lines.append(f"- {trim(str(t), 100)}")
28940
29984
  return "\n".join(lines)
@@ -29108,8 +30152,9 @@ class SessionState:
29108
30152
  if not content:
29109
30153
  continue
29110
30154
  if content in {
29111
- "继续", "continue", "go on", "接着", "a", "b", "c",
29112
- "方案a", "方案b", "方案c", "keep going", "proceed",
30155
+ "继续", "繼續", "continue", "go on", "接着", "接著", "続行", "続けて",
30156
+ "a", "b", "c", "方案a", "方案b", "方案c", "案a", "案b", "案c",
30157
+ "keep going", "proceed", "確認", "确认",
29113
30158
  }:
29114
30159
  continue
29115
30160
  if len(content) > 10:
@@ -29176,10 +30221,10 @@ class SessionState:
29176
30221
 
29177
30222
  def _format_plan_file_preselection(self, proposal: dict) -> str:
29178
30223
  """Full MD content with ALL options for model review (no char limit)."""
29179
- lines = ["# Execution Plan Proposals\n"]
30224
+ lines = [self._ui_text("plan_file_proposals_title")]
29180
30225
  context = str(proposal.get("context", "") or "").strip()
29181
30226
  if context:
29182
- lines.append(f"## Background\n{context}\n")
30227
+ lines.append(self._ui_text("plan_file_background", context=context))
29183
30228
  recommended = str(proposal.get("recommended", "") or "").strip()
29184
30229
  options = proposal.get("options", [])
29185
30230
  if not isinstance(options, list):
@@ -29189,9 +30234,9 @@ class SessionState:
29189
30234
  continue
29190
30235
  opt_id = str(opt.get("id", "") or "").strip()
29191
30236
  title = str(opt.get("title", "") or "").strip()
29192
- header = f"## Option {opt_id}: {title}"
30237
+ header = self._ui_text("plan_file_option", id=opt_id, title=title)
29193
30238
  if opt_id == recommended:
29194
- header += " [RECOMMENDED]"
30239
+ header += self._ui_text("plan_file_recommended")
29195
30240
  lines.append("---\n")
29196
30241
  lines.append(header)
29197
30242
  summary = str(opt.get("summary", "") or "").strip()
@@ -29199,7 +30244,7 @@ class SessionState:
29199
30244
  lines.append(summary)
29200
30245
  steps = opt.get("steps", [])
29201
30246
  if isinstance(steps, list) and steps:
29202
- lines.append("\n### Steps")
30247
+ lines.append(f"\n{self._ui_text('plan_file_steps')}")
29203
30248
  import re as _re_plan
29204
30249
  _mid_re = _re_plan.compile(r"(?<=\S)\s+(\d+\.\d+\s)")
29205
30250
  for i, s in enumerate(steps):
@@ -29218,16 +30263,16 @@ class SessionState:
29218
30263
  lines.append(f"{i + 1}. {step_str}")
29219
30264
  pros = str(opt.get("pros", "") or "").strip()
29220
30265
  if pros:
29221
- lines.append(f"\n**Pros:** {pros}")
30266
+ lines.append(f"\n{self._ui_text('plan_file_pros', text=pros)}")
29222
30267
  cons = str(opt.get("cons", "") or "").strip()
29223
30268
  if cons:
29224
- lines.append(f"**Cons:** {cons}")
30269
+ lines.append(self._ui_text("plan_file_cons", text=cons))
29225
30270
  risk = str(opt.get("risk", "") or "").strip()
29226
30271
  if risk:
29227
- lines.append(f"**Risk:** {risk}")
30272
+ lines.append(self._ui_text("plan_file_risk", text=risk))
29228
30273
  lines.append("")
29229
30274
  lines.append("---")
29230
- lines.append("> Awaiting user choice.")
30275
+ lines.append(self._ui_text("plan_file_awaiting_choice"))
29231
30276
  return "\n".join(lines)
29232
30277
 
29233
30278
  def _format_plan_file_execution(self, choice_id: str) -> str:
@@ -29247,14 +30292,14 @@ class SessionState:
29247
30292
  completed = sum(1 for t in plan_todos if t.get("status") == "completed")
29248
30293
  current_idx = completed + 1
29249
30294
 
29250
- lines = [f"# Active Plan: {title}\n"]
29251
- lines.append(f"> Status: EXECUTING | Step {current_idx}/{total}")
29252
- lines.append(f"> Chosen: Option {choice_id}")
30295
+ lines = [self._ui_text("active_plan_title", title=title)]
30296
+ lines.append(self._ui_text("active_plan_status", current=current_idx, total=total))
30297
+ lines.append(self._ui_text("active_plan_chosen", choice=choice_id))
29253
30298
  from datetime import datetime as _dt_cls
29254
- lines.append(f"> Updated: {_dt_cls.now().isoformat(timespec='seconds')}\n")
30299
+ lines.append(self._ui_text("active_plan_updated", updated=_dt_cls.now().isoformat(timespec="seconds")))
29255
30300
  if summary:
29256
- lines.append(f"## Summary\n{summary}\n")
29257
- lines.append("## Steps\n")
30301
+ lines.append(self._ui_text("active_plan_summary", summary=summary))
30302
+ lines.append(self._ui_text("active_plan_steps"))
29258
30303
  import re as _re_exec
29259
30304
  _mid_re_exec = _re_exec.compile(r"(?<=\S)\s+(\d+\.\d+\s)")
29260
30305
  for t in plan_todos:
@@ -29268,22 +30313,22 @@ class SessionState:
29268
30313
  if status == "completed":
29269
30314
  actor = str(t.get("completed_by", "") or "")
29270
30315
  evidence = str(t.get("evidence", "") or "")
29271
- lines.append(f"- [x] Step {idx}: {header}")
30316
+ lines.append(self._ui_text("active_plan_step_done", idx=idx, header=header))
29272
30317
  for sub in sub_lines:
29273
30318
  lines.append(f" - {sub.strip()}")
29274
30319
  meta_parts = []
29275
30320
  if actor:
29276
- meta_parts.append(f"Completed by: {actor}")
30321
+ meta_parts.append(self._ui_text("active_plan_completed_by", actor=actor))
29277
30322
  if evidence:
29278
- meta_parts.append(f"Evidence: {evidence}")
30323
+ meta_parts.append(self._ui_text("active_plan_evidence", evidence=evidence))
29279
30324
  if meta_parts:
29280
30325
  lines.append(f" > {' | '.join(meta_parts)}")
29281
30326
  elif status == "in_progress":
29282
- lines.append(f"- [>] Step {idx}: {header} <-- CURRENT")
30327
+ lines.append(self._ui_text("active_plan_step_current", idx=idx, header=header))
29283
30328
  for sub in sub_lines:
29284
30329
  lines.append(f" - {sub.strip()}")
29285
30330
  else:
29286
- lines.append(f"- [ ] Step {idx}: {header}")
30331
+ lines.append(self._ui_text("active_plan_step_pending", idx=idx, header=header))
29287
30332
  for sub in sub_lines:
29288
30333
  lines.append(f" - {sub.strip()}")
29289
30334
  return "\n".join(lines) + "\n"
@@ -29302,10 +30347,10 @@ class SessionState:
29302
30347
 
29303
30348
  def _format_plan_bubble_preselection(self, proposal: dict) -> str:
29304
30349
  """Condensed bubble for UI (under PLAN_BUBBLE_MAX_CHARS). No full step listing."""
29305
- lines = ["## 📋 执行方案\n"]
30350
+ lines = [self._ui_text("plan_bubble_title")]
29306
30351
  context = str(proposal.get("context", "") or "").strip()
29307
30352
  if context:
29308
- lines.append(f"**背景:** {trim(context, 300)}\n")
30353
+ lines.append(self._ui_text("plan_bubble_background", context=trim(context, 300)))
29309
30354
  recommended = str(proposal.get("recommended", "") or "").strip()
29310
30355
  options = proposal.get("options", [])
29311
30356
  if not isinstance(options, list):
@@ -29316,9 +30361,9 @@ class SessionState:
29316
30361
  opt_id = str(opt.get("id", "") or "").strip()
29317
30362
  title = str(opt.get("title", "") or "").strip()
29318
30363
  is_rec = opt_id == recommended
29319
- header = f"### 方案 {opt_id}: {title}"
30364
+ header = self._ui_text("plan_bubble_option", id=opt_id, title=title)
29320
30365
  if is_rec:
29321
- header += " ⭐推荐"
30366
+ header += self._ui_text("plan_bubble_recommended")
29322
30367
  lines.append(header)
29323
30368
  summary = str(opt.get("summary", "") or "").strip()
29324
30369
  if summary:
@@ -29326,14 +30371,14 @@ class SessionState:
29326
30371
  steps = opt.get("steps", [])
29327
30372
  step_count = len(steps) if isinstance(steps, list) else 0
29328
30373
  risk = str(opt.get("risk", "") or "").strip()
29329
- meta = f"步骤数: {step_count}"
30374
+ meta = self._ui_text("plan_bubble_steps", count=step_count)
29330
30375
  if risk:
29331
- meta += f" | 风险: {risk}"
30376
+ meta += f" | {self._ui_text('plan_bubble_risk', risk=risk)}"
29332
30377
  lines.append(meta)
29333
30378
  lines.append("")
29334
30379
  lines.append("---")
29335
- lines.append(f"完整方案详见: `{PLAN_FILE_RELATIVE_PATH}`")
29336
- lines.append('请回复选择(如"方案A"、"A"、"选1"),或输入修改意见。')
30380
+ lines.append(self._ui_text("plan_bubble_full_ref", path=PLAN_FILE_RELATIVE_PATH))
30381
+ lines.append(self._ui_text("plan_bubble_reply"))
29337
30382
  return trim("\n".join(lines), PLAN_BUBBLE_MAX_CHARS)
29338
30383
 
29339
30384
  def _plan_file_read_instruction(self) -> str:
@@ -29349,19 +30394,14 @@ class SessionState:
29349
30394
  break
29350
30395
  todo_note = ""
29351
30396
  if active_step_id:
29352
- todo_note = (
29353
- f"\nTODO UPDATE: Call TodoWrite at the START to set subtasks for the current step (Step {active_step_idx}) ONLY.\n"
29354
- f"Each subtask MUST include parent_step_id='{active_step_id}'.\n"
29355
- f"Create 3-5 subtasks that break down ONLY the current step's work.\n"
29356
- f"Do NOT create subtasks for other plan steps. Mark each subtask completed as you finish it.\n"
30397
+ todo_note = self._ui_text(
30398
+ "plan_read_todo_note",
30399
+ step_label=self._ui_text("plan_step_label", step=active_step_idx, total=int(bb.get("plan_step_total", 0) or 0)),
30400
+ parent_step_id=active_step_id,
29357
30401
  )
29358
30402
  return (
29359
- f"[plan-file] The approved execution plan is at `{PLAN_FILE_RELATIVE_PATH}`.\n"
29360
- f"Use: read_file {PLAN_FILE_RELATIVE_PATH} to review full steps and live status.\n"
29361
- "The plan file is the authoritative source for step ordering and completion status.\n"
29362
- "Execute steps IN ORDER. Do NOT skip ahead. Mark current step done before advancing.\n"
29363
- "If a step references a skill or workflow, call load_skill to load it before proceeding."
29364
- f"{todo_note}"
30403
+ self._ui_text("plan_read_instruction", path=PLAN_FILE_RELATIVE_PATH)
30404
+ + todo_note
29365
30405
  )
29366
30406
 
29367
30407
  @staticmethod
@@ -29456,10 +30496,10 @@ class SessionState:
29456
30496
  # ── (legacy) _format_plan_proposal_markdown ──────────────────────
29457
30497
 
29458
30498
  def _format_plan_proposal_markdown(self, proposal: dict) -> str:
29459
- lines = ["## 📋 执行方案\n"]
30499
+ lines = [self._ui_text("plan_proposal_title")]
29460
30500
  context = str(proposal.get("context", "") or "").strip()
29461
30501
  if context:
29462
- lines.append(f"### 背景分析\n{context}\n")
30502
+ lines.append(self._ui_text("plan_proposal_background", context=context))
29463
30503
  recommended = str(proposal.get("recommended", "") or "").strip()
29464
30504
  options = proposal.get("options", [])
29465
30505
  if not isinstance(options, list):
@@ -29470,30 +30510,30 @@ class SessionState:
29470
30510
  opt_id = str(opt.get("id", "") or "").strip()
29471
30511
  title = str(opt.get("title", "") or "").strip()
29472
30512
  is_recommended = opt_id == recommended
29473
- header = f"### 方案 {opt_id}: {title}"
30513
+ header = self._ui_text("plan_proposal_option", id=opt_id, title=title)
29474
30514
  if is_recommended:
29475
- header += " ⭐推荐"
30515
+ header += self._ui_text("plan_proposal_recommended")
29476
30516
  lines.append(header)
29477
30517
  summary = str(opt.get("summary", "") or "").strip()
29478
30518
  if summary:
29479
30519
  lines.append(summary)
29480
30520
  steps = opt.get("steps", [])
29481
30521
  if isinstance(steps, list) and steps:
29482
- lines.append("\n**步骤:**")
30522
+ lines.append(f"\n{self._ui_text('plan_proposal_steps')}")
29483
30523
  for i, s in enumerate(steps):
29484
30524
  lines.append(f"{i+1}. {s}")
29485
30525
  pros = str(opt.get("pros", "") or "").strip()
29486
30526
  if pros:
29487
- lines.append(f"\n**优势:** {pros}")
30527
+ lines.append(f"\n{self._ui_text('plan_proposal_pros', text=pros)}")
29488
30528
  cons = str(opt.get("cons", "") or "").strip()
29489
30529
  if cons:
29490
- lines.append(f"**劣势:** {cons}")
30530
+ lines.append(self._ui_text("plan_proposal_cons", text=cons))
29491
30531
  risk = str(opt.get("risk", "") or "").strip()
29492
30532
  if risk:
29493
- lines.append(f"**风险:** {risk}")
30533
+ lines.append(self._ui_text("plan_proposal_risk", text=risk))
29494
30534
  lines.append("")
29495
30535
  lines.append("---")
29496
- lines.append('请回复选择(如"方案A"、"A"、"选1"),或输入修改意见。')
30536
+ lines.append(self._ui_text("plan_proposal_reply"))
29497
30537
  return "\n".join(lines)
29498
30538
 
29499
30539
  def _parse_plan_choice(self, text: str, proposal: dict) -> str:
@@ -29510,14 +30550,14 @@ class SessionState:
29510
30550
  return low.upper()
29511
30551
  # "方案A", "方案 A", "option A"
29512
30552
  import re
29513
- m = re.search(r'(?:方案|option|选项)\s*([a-zA-Z0-9])', low, re.IGNORECASE)
30553
+ m = re.search(r'(?:方案|選項|选项|option|案|プラン)\s*([a-zA-Z0-9])', low, re.IGNORECASE)
29514
30554
  if m:
29515
30555
  candidate = m.group(1).upper()
29516
30556
  if candidate in option_ids:
29517
30557
  return candidate
29518
30558
  # "选1", "第1个", "第一个"
29519
30559
  num_map = {"一": "1", "二": "2", "三": "3", "1": "1", "2": "2", "3": "3"}
29520
- m2 = re.search(r'(?:选|第)\s*([一二三1-3])', low)
30560
+ m2 = re.search(r'(?:选|選|第|choose|pick)\s*([一二三1-3])', low, re.IGNORECASE)
29521
30561
  if m2:
29522
30562
  idx_str = num_map.get(m2.group(1), "")
29523
30563
  if idx_str:
@@ -29526,7 +30566,10 @@ class SessionState:
29526
30566
  return option_ids[idx]
29527
30567
  # "继续"/"确认"/"推荐" → pick recommended
29528
30568
  recommended = str(proposal.get("recommended", "") or "").strip()
29529
- confirm_tokens = ("继续", "确认", "推荐", "推荐方案", "go", "proceed", "continue", "yes", "ok")
30569
+ confirm_tokens = (
30570
+ "继续", "繼續", "确认", "確認", "推荐", "推薦", "推荐方案", "推薦方案",
30571
+ "go", "proceed", "continue", "yes", "ok", "続行", "確認する", "おすすめ", "推奨",
30572
+ )
29530
30573
  if any(tok in low for tok in confirm_tokens) and recommended:
29531
30574
  return recommended
29532
30575
  # --- Slow path: LLM semantic matching ---
@@ -29602,7 +30645,33 @@ class SessionState:
29602
30645
  self._blackboard_history("manager", f"plan approved: option {choice_id} — {chosen.get('title', '')}")
29603
30646
  # Lock complexity/level floor to prevent manager downgrade during plan execution
29604
30647
  self.runtime_complexity_floor = str(self.runtime_task_complexity or "complex")
29605
- self.runtime_task_level_floor = int(self.runtime_task_level or 4)
30648
+ # --- Risk-based complexity lock: set floor AND ceiling from plan option's risk field ---
30649
+ # Read risk NOW — blackboard compaction later drops the risk key from options
30650
+ _plan_risk = str(chosen.get("risk", "") or "").strip().lower()
30651
+ if _plan_risk not in ("low", "medium", "high"):
30652
+ # Fallback: scan option summary/description for risk label
30653
+ import re as _re_risk
30654
+ _rt = str(chosen.get("summary", "") or chosen.get("description", "") or "")
30655
+ _rm = _re_risk.search(r'风险[::]\s*(low|medium|high)|risk[::]\s*(low|medium|high)', _rt, _re_risk.I)
30656
+ _plan_risk = ((_rm.group(1) or _rm.group(2)) if _rm else "medium").lower()
30657
+ _current_level = int(self.runtime_task_level or 3)
30658
+ _user_override = int(getattr(self, "user_task_level_override", 0) or 0)
30659
+ if _user_override > 0:
30660
+ # User explicitly set level → absolute lock, no up and no down
30661
+ self.runtime_task_level_floor = _user_override
30662
+ self.runtime_task_level_ceiling = _user_override
30663
+ elif _plan_risk == "medium":
30664
+ # Medium risk → exact lock at current level
30665
+ self.runtime_task_level_floor = _current_level
30666
+ self.runtime_task_level_ceiling = _current_level
30667
+ elif _plan_risk == "high":
30668
+ # High risk → allow +1 upgrade, no downgrade
30669
+ self.runtime_task_level_floor = _current_level
30670
+ self.runtime_task_level_ceiling = min(5, _current_level + 1)
30671
+ else: # low
30672
+ # Low risk → allow -1 downgrade, no upgrade
30673
+ self.runtime_task_level_floor = max(1, _current_level - 1)
30674
+ self.runtime_task_level_ceiling = _current_level
29606
30675
  # Auto-create todos from plan steps → write into bb["project_todos"]
29607
30676
  steps = self._group_plan_steps(chosen.get("steps", []))
29608
30677
  if steps and isinstance(steps, list):
@@ -29640,7 +30709,6 @@ class SessionState:
29640
30709
  "key": f"bb:proj:{t['id']}",
29641
30710
  "content": t["content"],
29642
30711
  "status": t["status"],
29643
- "activeForm": f"Working on: {t['content']}" if t["status"] == "in_progress" else f"Pending: {t['content']}",
29644
30712
  }
29645
30713
  for t in plan_todos[:40]
29646
30714
  ])
@@ -30985,6 +32053,11 @@ class SessionState:
30985
32053
  )
30986
32054
  },
30987
32055
  )
32056
+ # Generate completion summary bubble before finishing
32057
+ try:
32058
+ self._generate_run_completion_summary()
32059
+ except Exception:
32060
+ pass
30988
32061
  self._emit("status", {"summary": "run finished"})
30989
32062
  cb = self.run_finished_callback
30990
32063
  if cb:
@@ -31748,7 +32821,7 @@ class SessionManager:
31748
32821
  if clear_cap_cache:
31749
32822
  sess.multimodal_capability_cache = {}
31750
32823
  sess.ollama.clear_probe_cache()
31751
- sess.ui_language = normalize_ui_language(self.user_language)
32824
+ sess._set_ui_language(self.user_language, relabel_todos=True)
31752
32825
  sess.auto_model_switch = bool(self.auto_model_switch)
31753
32826
  sess.arbiter_enabled = bool(self.arbiter_enabled)
31754
32827
  sess.arbiter_model = str(self.arbiter_model or "").strip()
@@ -32223,7 +33296,7 @@ class SessionManager:
32223
33296
  with self.lock:
32224
33297
  self.user_language = lang
32225
33298
  for sess in self.sessions.values():
32226
- sess.ui_language = lang
33299
+ sess._set_ui_language(lang, relabel_todos=True)
32227
33300
  sess.updated_at = now_ts()
32228
33301
  sess._persist()
32229
33302
  self._persist_user_prefs()
@@ -32235,7 +33308,7 @@ class SessionManager:
32235
33308
  sess = self.sessions.get(session_id)
32236
33309
  if not sess:
32237
33310
  raise KeyError(session_id)
32238
- sess.ui_language = lang
33311
+ sess._set_ui_language(lang, relabel_todos=True)
32239
33312
  sess.updated_at = now_ts()
32240
33313
  sess._persist()
32241
33314
  if set_user_default:
@@ -32958,7 +34031,7 @@ const I18N={
32958
34031
  btn_send:'送出',btn_interrupt:'中斷',btn_compact:'壓縮',btn_refresh:'重新整理',btn_export_session:'匯出會話',
32959
34032
  btn_clear_stale_todos:'清除陳舊待辦',
32960
34033
  prompt_placeholder:'描述你的任務,或將檔案拖入此處...',
32961
- upload_drop:'拖曳上傳程式碼 / Markdown / PDF / Excel / Word / PPT / CSV��或點擊此處選擇檔案',
34034
+ upload_drop:'拖曳上傳程式碼 / Markdown / PDF / Excel / Word / PPT / CSV,或點擊此處選擇檔案',
32962
34035
  upload_file_hint:'支援拖入檔案:程式碼 / Markdown / PDF / Excel / Word / PPT / CSV',
32963
34036
  upload_pick_file:'選擇檔案',
32964
34037
  upload_drop_release:'釋放以上傳檔案',
@@ -32977,7 +34050,7 @@ const I18N={
32977
34050
  copy_code:'複製程式碼',copy_done:'已複製',
32978
34051
  btn_tools:'工具 ▾',btn_compact_action:'壓縮',btn_refresh_action:'重新整理',
32979
34052
  btn_level:'等級',level_auto:'自動',level_1_simple:'L1 簡單',level_2_multi:'L2 多輪',
32980
- level_3_collab:'L3 協���',level_4_complex:'L4 複雜',level_5_system:'L5 系統',
34053
+ level_3_collab:'L3 協作',level_4_complex:'L4 複雜',level_5_system:'L5 系統',
32981
34054
 
32982
34055
 
32983
34056
  llm_fill_config:'填寫 LLM 設定',llm_provider:'供應商',llm_confirm:'確認',llm_import_config:'匯入設定',
@@ -33023,6 +34096,88 @@ const I18N={
33023
34096
  todo_plan_steps:'計画ステップ',todo_subtasks:'サブタスク'
33024
34097
  }
33025
34098
  };
34099
+ Object.assign(I18N['en'],{
34100
+ sec_todos:'Todos',sec_tasks:'Tasks',sec_activity:'Activity',sec_commands:'Commands',sec_diffs:'File Diffs',sec_catalog:'Catalog',
34101
+ role_explorer:'Explorer',role_developer:'Developer',role_reviewer:'Reviewer',role_manager:'Manager',role_planner:'Planner',role_agent:'Agent',
34102
+ callout_warning:'Warning',callout_notice:'Notice',callout_instruction:'Instruction',callout_tip:'Tip',callout_reminder:'Reminder',
34103
+ event_manager_delegate_title:'Manager Delegate',event_objective:'Objective',event_instruction:'Instruction',event_intent:'intent',
34104
+ event_tool_calls_title:'Tool Calls',event_tool_calls_note:'Model scheduled these tools for the current turn.',event_tool_calls_empty:'No structured tool metadata was attached to this turn.',
34105
+ event_skill_loaded_title:'Skill Loaded',event_skill_loaded_note:'Skill context was auto-loaded into the current run.',event_skill_loaded_empty:'No public description was attached to this skill notification.',event_skill_label:'skill',
34106
+ event_loaded:'loaded',event_preview_truncated:'preview truncated',
34107
+ event_file_patch_title:'File Patch',event_session:'session',
34108
+ event_upload_title:'Upload',event_upload_path:'path',event_upload_filename:'filename',event_preview_unavailable:'Preview unavailable for this upload.',event_upload_parsing:'Parsing uploaded file in background. The bubble will refresh when parsing completes.',event_upload_failed:'Upload parsing failed',
34109
+ event_command_title:'Command',event_command_label:'command',event_cwd:'cwd',event_changed:'changed',event_command_empty:'No command output captured.',event_ui_truncated:'UI truncated',event_model_truncated:'Model truncated',event_temp_read_file:'Temp read_file',event_buffered:'Buffered',
34110
+ event_truncation_recovery:'Truncation Recovery',event_truncation_state:'Structured truncation recovery state',event_truncation_note:'Model output hit a truncation boundary and entered recovery mode.',
34111
+ event_live_model_call_title:'Agent Turn Model Call',event_live_model_call_note:'The active agent is in a model call. This timer updates live while generation is in progress.',
34112
+ event_auto_continue:'Auto Continue',event_arbiter_continue:'Arbiter Continue',event_continuation_briefing:'Continuation Briefing',event_reminder:'Reminder',event_todo_rescue:'Todo Rescue',event_tool_retry:'Tool Retry',event_segmented_retry:'Segmented Retry',event_forced_converge:'Forced Converge',event_no_tool_recovery:'No-Tool Recovery',event_context_recall:'Context Recall',event_failure_recovery:'Failure Recovery',event_truncate_rescue:'Truncation Rescue',event_thinking_recovery:'Thinking Recovery',event_fault_prefill:'Fault Prefill',event_edit_recovery:'Edit Recovery',
34113
+ state_on:'on',state_off:'off',
34114
+ rt_session:'session',rt_model:'model',rt_thinking:'thinking',rt_thinking_stream:'thinking_stream',rt_mode:'mode',rt_active_agent:'active_agent',rt_blackboard:'bb',rt_task:'task',rt_complexity:'complexity',rt_judgement:'judgement',rt_budget:'budget',rt_remaining:'remaining',rt_blackboard_cycles:'bb_cycles',rt_round_limit:'round_limit',rt_round:'round',rt_phase:'phase',rt_queued_inputs:'queued_inputs',rt_run_timeout:'run_timeout',rt_ctx_used:'ctx_used',rt_ctx_limit:'ctx_limit',rt_ctx_mode:'ctx_mode',rt_manual_lock:'manual-lock',rt_adaptive:'adaptive',rt_ctx_left:'ctx_left',rt_truncation:'truncation',rt_trunc_retry:'trunc_retry',rt_trunc_tokens:'trunc_tokens~',rt_archive:'archive',rt_last_compact:'last_compact',rt_ollama:'ollama',rt_files:'files',rt_ui_mode:'ui_mode',
34115
+ fe_nodes:'nodes={n}',fe_loading:'loading...',fe_tree_truncated:'tree truncated at {n} nodes',fe_items:'{n} item(s)',
34116
+ cmd_ui_preview_truncated:'UI preview truncated',cmd_model_context_truncated:'Model context truncated',cmd_temp_read_file_ready:'Temp read_file ready',cmd_buffered_copy:'Buffered copy',cmd_prev:'Prev',cmd_next:'Next',cmd_preview:'preview',cmd_of:'of',cmd_read_file_path:'read_file path',cmd_buffer_ref:'buffer_ref',cmd_chars:'chars',cmd_lines:'lines',cmd_strategy:'strategy',cmd_full_output:'full_output',cmd_exit:'exit',cmd_default_name:'command'
34117
+ });
34118
+ Object.assign(I18N['zh-CN'],{
34119
+ sec_todos:'待办',sec_tasks:'任务',sec_activity:'活动',sec_commands:'命令',sec_diffs:'文件差异',sec_catalog:'目录',
34120
+ no_todos:'暂无待办',no_tasks:'暂无任务',no_catalog:'暂无目录',
34121
+ role_explorer:'探索者',role_developer:'开发者',role_reviewer:'审查者',role_manager:'管理者',role_planner:'规划者',role_agent:'Agent',
34122
+ callout_warning:'警告',callout_notice:'提示',callout_instruction:'指令',callout_tip:'建议',callout_reminder:'提醒',
34123
+ event_manager_delegate_title:'管理者委派',event_objective:'目标',event_instruction:'指令',event_intent:'意图',
34124
+ event_tool_calls_title:'工具调用',event_tool_calls_note:'模型已为当前轮安排以下工具调用。',event_tool_calls_empty:'当前轮没有附带结构化工具元数据。',
34125
+ event_skill_loaded_title:'Skill 已加载',event_skill_loaded_note:'Skill 上下文已自动加载到当前运行。',event_skill_loaded_empty:'该 skill 通知没有附带公开描述。',event_skill_label:'skill',
34126
+ event_loaded:'已加载',event_preview_truncated:'预览被截断',
34127
+ event_file_patch_title:'文件补丁',event_session:'会话',
34128
+ event_upload_title:'上传',event_upload_path:'路径',event_upload_filename:'文件名',event_preview_unavailable:'该上传暂不支持预览。',event_upload_parsing:'正在后台解析上传文件。解析完成后气泡会自动刷新。',event_upload_failed:'上传解析失败',
34129
+ event_command_title:'命令',event_command_label:'命令',event_cwd:'工作目录',event_changed:'变更',event_command_empty:'未捕获到命令输出。',event_ui_truncated:'UI 截断',event_model_truncated:'模型截断',event_temp_read_file:'临时 read_file',event_buffered:'已缓冲',
34130
+ event_truncation_recovery:'截断恢复',event_truncation_state:'结构化截断恢复状态',event_truncation_note:'模型输出触发了截断边界,已进入恢复流程。',
34131
+ event_live_model_call_title:'Agent 轮次模型调用',event_live_model_call_note:'当前活跃 agent 正在进行模型调用。计时器会在生成期间实时更新。',
34132
+ event_auto_continue:'自动继续',event_arbiter_continue:'裁决继续',event_continuation_briefing:'续跑简报',event_reminder:'提醒',event_todo_rescue:'待办救援',event_tool_retry:'工具重试',event_segmented_retry:'分段重试',event_forced_converge:'强制收敛',event_no_tool_recovery:'无工具恢复',event_context_recall:'上下文召回',event_failure_recovery:'故障恢复',event_truncate_rescue:'截断救援',event_thinking_recovery:'思考恢复',event_fault_prefill:'故障预填',event_edit_recovery:'编辑恢复',
34133
+ state_on:'开',state_off:'关',
34134
+ rt_session:'会话',rt_model:'模型',rt_thinking:'思考',rt_thinking_stream:'思考流',rt_mode:'模式',rt_active_agent:'活跃代理',rt_blackboard:'黑板',rt_task:'任务',rt_complexity:'复杂度',rt_judgement:'裁决',rt_budget:'预算',rt_remaining:'剩余',rt_blackboard_cycles:'黑板轮次',rt_round_limit:'轮次上限',rt_round:'轮次',rt_phase:'阶段',rt_queued_inputs:'排队输入',rt_run_timeout:'运行超时',rt_ctx_used:'上下文已用',rt_ctx_limit:'上下文上限',rt_ctx_mode:'上下文模式',rt_manual_lock:'手动锁定',rt_adaptive:'自适应',rt_ctx_left:'上下文剩余',rt_truncation:'截断数',rt_trunc_retry:'截断重试',rt_trunc_tokens:'截断Token~',rt_archive:'归档',rt_last_compact:'最近压缩',rt_ollama:'Ollama',rt_files:'文件根目录',rt_ui_mode:'界面模式',
34135
+ fe_nodes:'节点={n}',fe_loading:'加载中...',fe_tree_truncated:'目录树在 {n} 个节点处被截断',fe_items:'{n} 项',
34136
+ cmd_ui_preview_truncated:'UI 预览截断',cmd_model_context_truncated:'模型上下文截断',cmd_temp_read_file_ready:'临时 read_file 已就绪',cmd_buffered_copy:'缓冲副本',cmd_prev:'上一页',cmd_next:'下一页',cmd_preview:'预览',cmd_of:'共',cmd_read_file_path:'read_file 路径',cmd_buffer_ref:'缓冲引用',cmd_chars:'字符',cmd_lines:'行',cmd_strategy:'策略',cmd_full_output:'完整输出',cmd_exit:'退出码',cmd_default_name:'命令'
34137
+ });
34138
+ Object.assign(I18N['zh-TW'],{
34139
+ upload_drop:'拖曳上傳程式碼 / Markdown / PDF / Excel / Word / PPT / CSV,或點擊此處選擇檔案',
34140
+ sec_todos:'待辦',sec_tasks:'任務',sec_activity:'活動',sec_commands:'命令',sec_diffs:'檔案差異',sec_catalog:'目錄',
34141
+ no_todos:'尚無待辦',no_tasks:'尚無任務',no_catalog:'尚無目錄',
34142
+ level_3_collab:'L3 協作',
34143
+ role_explorer:'探索者',role_developer:'開發者',role_reviewer:'審查者',role_manager:'管理者',role_planner:'規劃者',role_agent:'Agent',
34144
+ callout_warning:'警告',callout_notice:'提示',callout_instruction:'指令',callout_tip:'建議',callout_reminder:'提醒',
34145
+ event_manager_delegate_title:'管理者委派',event_objective:'目標',event_instruction:'指令',event_intent:'意圖',
34146
+ event_tool_calls_title:'工具呼叫',event_tool_calls_note:'模型已為目前輪安排以下工具呼叫。',event_tool_calls_empty:'目前輪沒有附帶結構化工具中繼資料。',
34147
+ event_skill_loaded_title:'Skill 已載入',event_skill_loaded_note:'Skill 上下文已自動載入到目前執行。',event_skill_loaded_empty:'此 skill 通知沒有附帶公開描述。',event_skill_label:'skill',
34148
+ event_loaded:'已載入',event_preview_truncated:'預覽已截斷',
34149
+ event_file_patch_title:'檔案補丁',event_session:'會話',
34150
+ event_upload_title:'上傳',event_upload_path:'路徑',event_upload_filename:'檔名',event_preview_unavailable:'此上傳暫時無法預覽。',event_upload_parsing:'正在背景解析上傳檔案。解析完成後氣泡會自動更新。',event_upload_failed:'上傳解析失敗',
34151
+ event_command_title:'命令',event_command_label:'命令',event_cwd:'工作目錄',event_changed:'變更',event_command_empty:'未擷取到命令輸出。',event_ui_truncated:'UI 截斷',event_model_truncated:'模型截斷',event_temp_read_file:'暫存 read_file',event_buffered:'已緩衝',
34152
+ event_truncation_recovery:'截斷恢復',event_truncation_state:'結構化截斷恢復狀態',event_truncation_note:'模型輸出觸發截斷邊界,已進入恢復流程。',
34153
+ event_live_model_call_title:'Agent 輪次模型呼叫',event_live_model_call_note:'目前活躍 agent 正在進行模型呼叫。計時器會在生成期間即時更新。',
34154
+ event_auto_continue:'自動繼續',event_arbiter_continue:'裁決繼續',event_continuation_briefing:'續跑簡報',event_reminder:'提醒',event_todo_rescue:'待辦救援',event_tool_retry:'工具重試',event_segmented_retry:'分段重試',event_forced_converge:'強制收斂',event_no_tool_recovery:'無工具恢復',event_context_recall:'上下文召回',event_failure_recovery:'故障恢復',event_truncate_rescue:'截斷救援',event_thinking_recovery:'思考恢復',event_fault_prefill:'故障預填',event_edit_recovery:'編輯恢復',
34155
+ state_on:'開',state_off:'關',
34156
+ rt_session:'會話',rt_model:'模型',rt_thinking:'思考',rt_thinking_stream:'思考流',rt_mode:'模式',rt_active_agent:'活躍代理',rt_blackboard:'黑板',rt_task:'任務',rt_complexity:'複雜度',rt_judgement:'裁決',rt_budget:'預算',rt_remaining:'剩餘',rt_blackboard_cycles:'黑板輪次',rt_round_limit:'輪次上限',rt_round:'輪次',rt_phase:'階段',rt_queued_inputs:'排隊輸入',rt_run_timeout:'執行逾時',rt_ctx_used:'上下文已用',rt_ctx_limit:'上下文上限',rt_ctx_mode:'上下文模式',rt_manual_lock:'手動鎖定',rt_adaptive:'自適應',rt_ctx_left:'上下文剩餘',rt_truncation:'截斷數',rt_trunc_retry:'截斷重試',rt_trunc_tokens:'截斷Token~',rt_archive:'封存',rt_last_compact:'最近壓縮',rt_ollama:'Ollama',rt_files:'檔案根目錄',rt_ui_mode:'介面模式',
34157
+ fe_nodes:'節點={n}',fe_loading:'載入中...',fe_tree_truncated:'目錄樹在 {n} 個節點處被截斷',fe_items:'{n} 項',
34158
+ cmd_ui_preview_truncated:'UI 預覽截斷',cmd_model_context_truncated:'模型上下文截斷',cmd_temp_read_file_ready:'暫存 read_file 已就緒',cmd_buffered_copy:'緩衝副本',cmd_prev:'上一頁',cmd_next:'下一頁',cmd_preview:'預覽',cmd_of:'共',cmd_read_file_path:'read_file 路徑',cmd_buffer_ref:'緩衝引用',cmd_chars:'字元',cmd_lines:'行',cmd_strategy:'策略',cmd_full_output:'完整輸出',cmd_exit:'退出碼',cmd_default_name:'命令'
34159
+ });
34160
+ Object.assign(I18N['ja'],{
34161
+ sec_todos:'Todo',sec_tasks:'タスク',sec_activity:'アクティビティ',sec_commands:'コマンド',sec_diffs:'ファイル差分',sec_catalog:'カタログ',
34162
+ thinking:'思考',thinking_stream:'思考(ストリーム)',copy_code:'コードをコピー',copy_done:'コピーしました',
34163
+ no_todos:'Todo はありません',no_tasks:'タスクはありません',no_catalog:'カタログなし',
34164
+ role_explorer:'探索担当',role_developer:'開発担当',role_reviewer:'レビュー担当',role_manager:'マネージャー',role_planner:'プランナー',role_agent:'Agent',
34165
+ callout_warning:'警告',callout_notice:'通知',callout_instruction:'指示',callout_tip:'ヒント',callout_reminder:'リマインダー',
34166
+ event_manager_delegate_title:'マネージャー委任',event_objective:'目的',event_instruction:'指示',event_intent:'意図',
34167
+ event_tool_calls_title:'ツール呼び出し',event_tool_calls_note:'モデルはこのターンで次のツール呼び出しを予定しました。',event_tool_calls_empty:'このターンには構造化されたツールメタデータがありません。',
34168
+ event_skill_loaded_title:'Skill 読み込み完了',event_skill_loaded_note:'Skill コンテキストが現在の実行に自動読み込みされました。',event_skill_loaded_empty:'この skill 通知には公開説明が付いていません。',event_skill_label:'skill',
34169
+ event_loaded:'読み込み済み',event_preview_truncated:'プレビュー切り詰め',
34170
+ event_file_patch_title:'ファイルパッチ',event_session:'セッション',
34171
+ event_upload_title:'アップロード',event_upload_path:'パス',event_upload_filename:'ファイル名',event_preview_unavailable:'このアップロードではプレビューを利用できません。',event_upload_parsing:'アップロードファイルをバックグラウンドで解析中です。完了するとバブルが更新されます。',event_upload_failed:'アップロード解析失敗',
34172
+ event_command_title:'コマンド',event_command_label:'コマンド',event_cwd:'作業ディレクトリ',event_changed:'変更',event_command_empty:'コマンド出力は取得されませんでした。',event_ui_truncated:'UI 切り詰め',event_model_truncated:'モデル切り詰め',event_temp_read_file:'一時 read_file',event_buffered:'バッファ済み',
34173
+ event_truncation_recovery:'切り詰め復旧',event_truncation_state:'構造化切り詰め復旧状態',event_truncation_note:'モデル出力が切り詰め境界に達したため、復旧フローに入りました。',
34174
+ event_live_model_call_title:'Agent ターンモデル呼び出し',event_live_model_call_note:'現在のアクティブ agent はモデル呼び出し中です。生成中はこのタイマーがリアルタイム更新されます。',
34175
+ event_auto_continue:'自動継続',event_arbiter_continue:'判定継続',event_continuation_briefing:'継続ブリーフ',event_reminder:'リマインダー',event_todo_rescue:'Todo 救援',event_tool_retry:'ツール再試行',event_segmented_retry:'分割再試行',event_forced_converge:'強制収束',event_no_tool_recovery:'ツールなし復旧',event_context_recall:'コンテキスト再呼び出し',event_failure_recovery:'障害復旧',event_truncate_rescue:'切り詰め救援',event_thinking_recovery:'思考復旧',event_fault_prefill:'障害プリフィル',event_edit_recovery:'編集復旧',
34176
+ state_on:'オン',state_off:'オフ',
34177
+ rt_session:'セッション',rt_model:'モデル',rt_thinking:'思考',rt_thinking_stream:'思考ストリーム',rt_mode:'モード',rt_active_agent:'アクティブAgent',rt_blackboard:'黒板',rt_task:'タスク',rt_complexity:'複雑度',rt_judgement:'判定',rt_budget:'予算',rt_remaining:'残り',rt_blackboard_cycles:'黒板サイクル',rt_round_limit:'ラウンド上限',rt_round:'ラウンド',rt_phase:'フェーズ',rt_queued_inputs:'待機入力',rt_run_timeout:'実行タイムアウト',rt_ctx_used:'コンテキスト使用量',rt_ctx_limit:'コンテキスト上限',rt_ctx_mode:'コンテキストモード',rt_manual_lock:'手動固定',rt_adaptive:'適応',rt_ctx_left:'残りコンテキスト',rt_truncation:'切り詰め数',rt_trunc_retry:'切り詰め再試行',rt_trunc_tokens:'切り詰めToken~',rt_archive:'アーカイブ',rt_last_compact:'直近 compact',rt_ollama:'Ollama',rt_files:'ファイルルート',rt_ui_mode:'UIモード',
34178
+ fe_nodes:'ノード={n}',fe_loading:'読み込み中...',fe_tree_truncated:'ツリーは {n} ノードで切り詰められました',fe_items:'{n} 件',
34179
+ cmd_ui_preview_truncated:'UI プレビュー切り詰め',cmd_model_context_truncated:'モデルコンテキスト切り詰め',cmd_temp_read_file_ready:'一時 read_file 準備完了',cmd_buffered_copy:'バッファコピー',cmd_prev:'前へ',cmd_next:'次へ',cmd_preview:'プレビュー',cmd_of:'全',cmd_read_file_path:'read_file パス',cmd_buffer_ref:'buffer_ref',cmd_chars:'文字',cmd_lines:'行',cmd_strategy:'戦略',cmd_full_output:'完全出力',cmd_exit:'終了コード',cmd_default_name:'コマンド'
34180
+ });
33026
34181
  function currentLang(){const fromSnap=String(S.snap?.ui_language||'').trim();if(fromSnap&&I18N[fromSnap])return fromSnap;const fromCfg=String(S.config?.language||'').trim();if(fromCfg&&I18N[fromCfg])return fromCfg;return 'zh-CN'}
33027
34182
  function normalizeUiStyle(raw){const key=String(raw||'').trim().toLowerCase().replace(/-/g,'_');if(['trad','traditional','classic','legacy','old'].includes(key))return'trad';return'neo'}
33028
34183
  function applyUiStyle(){const style=normalizeUiStyle(S.config?.ui_style||'neo');if(document.body)document.body.setAttribute('data-ui-style',style);document.documentElement.setAttribute('data-ui-style',style)}
@@ -33031,7 +34186,7 @@ function setText(id,key){const el=E(id);if(el)el.textContent=t(key)}
33031
34186
  function setPlaceholder(id,key){const el=E(id);if(el)el.placeholder=t(key)}
33032
34187
  function applyMainI18n(){document.documentElement.lang=currentLang();const h1=document.querySelector('header h1');if(h1)h1.textContent=t('app_title');const hp=document.querySelectorAll('header p');if(hp&&hp[0])hp[0].textContent=t('app_subtitle');if(hp&&hp[1])hp[1].textContent=t('powered_by');setText('applyModelBtn','apply_model');setText('llmConfigBtn','upload_llm_config');setText('llmModalTitle','llm_fill_config');setText('llmProviderLabel','llm_provider');setText('llmConfigConfirm','llm_confirm');setText('llmConfigImport','llm_import_config');setText('newSessionBtn','btn_new_session');setText('renameSessionBtn','btn_rename');setText('deleteSessionBtn','btn_delete');setText('sendBtn','btn_send');setText('interruptBtn','btn_interrupt');setText('toolsMenuBtn','btn_tools');setText('compactAction','btn_compact_action');setText('refreshAction','btn_refresh_action');setText('previewReloadBtn','btn_refresh');setText('previewCopyBtn','copy_code');setText('downloadSessionBtn','btn_export_session');setText('clearStaleTodosBtn','btn_clear_stale_todos');setText('refreshFilesBtn','btn_refresh');setPlaceholder('prompt','prompt_placeholder');const up=E('uploadDrop');if(up)up.textContent=t('upload_drop');const pfht=E('promptFileHintText');if(pfht)pfht.textContent=t('upload_file_hint');const pfpk=E('promptFilePick');if(pfpk)pfpk.textContent=t('upload_pick_file');const pdol=E('promptDropOverlay');if(pdol)pdol.textContent=t('upload_drop_release');const panels=document.querySelectorAll('.panel-title');if(panels&&panels[0])panels[0].textContent=t('panel_sessions');if(panels&&panels[1])panels[1].textContent=t('panel_conversation');if(panels&&panels[2])panels[2].textContent=t('panel_runtime');const hs=document.querySelectorAll('#runtimeScroll h3');const keys=['sec_todos','sec_tasks','sec_activity','sec_commands','sec_diffs','sec_files','sec_catalog'];for(let i=0;i<hs.length&&i<keys.length;i++){hs[i].textContent=t(keys[i])}const _lvl2=S.snap?.user_task_level||0;updateLevelBtn(_lvl2);renderPreviewTabs()}
33033
34188
  function renderLanguageControls(){const sel=E('langSelect');if(!sel)return;const langs=Array.isArray(S.config?.supported_languages)?S.config.supported_languages:[];if(!langs.length){sel.innerHTML='';return}const cur=String(S.config?.language||currentLang());sel.innerHTML='';for(const row of langs){const code=String(row?.code||'').trim();if(!code)continue;const op=document.createElement('option');op.value=code;op.textContent=String(row?.label||code);sel.appendChild(op)}if(cur)sel.value=cur}
33034
- async function setLanguage(lang){const code=String(lang||'').trim();if(!code)return;await api('/api/config/language',{method:'POST',body:JSON.stringify({language:code})});S.config=S.config||{};S.config.language=code;if(S.snap)S.snap.ui_language=code;applyMainI18n();renderLanguageControls();renderStats();renderSessions();renderBoards();renderSkillsEntryLink()}
34189
+ async function setLanguage(lang){const code=String(lang||'').trim();if(!code)return;await api('/api/config/language',{method:'POST',body:JSON.stringify({language:code})});S.config=S.config||{};S.config.language=code;if(S.snap)S.snap.ui_language=code;if(S.mdWorker){try{S.mdWorker.terminate()}catch(_){}S.mdWorker=null}applyMainI18n();renderLanguageControls();renderStats();renderSessions();renderBoards();renderUploadList();renderChat('language');renderSkillsEntryLink()}
33035
34190
  async function api(path,opt={}){const o=(opt&&typeof opt==='object')?{...opt}:{};const timeoutMs=Math.max(1000,Math.min(180000,Number(o.timeoutMs||45000)||45000));delete o.timeoutMs;const ctl=(typeof AbortController==='function')?new AbortController():null;let timer=0;try{if(ctl){timer=setTimeout(()=>{try{ctl.abort()}catch(_){ }},timeoutMs)}const hdr={...(o.headers||{}), 'Content-Type':'application/json'};const r=await fetch(path,{...o,headers:hdr,signal:(ctl?ctl.signal:o.signal)});const t=await r.text();if(!r.ok){let msg=t;try{msg=JSON.parse(t).error||t}catch(_){}throw new Error(msg||'request failed')}return t?JSON.parse(t):{}}catch(err){if(err&&err.name==='AbortError'){throw new Error('request timeout')}throw err}finally{if(timer)clearTimeout(timer)}}
33036
34191
  function esc(s){return String(s??'').replace(/[&<>"]/g,c=>({ '&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;' }[c]))}
33037
34192
  function showError(msg){const el=E('errorBox');if(!msg){el.classList.add('hidden');el.textContent='';return}el.textContent=msg;el.classList.remove('hidden')}
@@ -33086,7 +34241,7 @@ function setPanelHtml(id,html){
33086
34241
  }
33087
34242
  function formatContextLeft(snap){const left=Number(snap?.context_left_tokens);const pct=Number(snap?.context_left_percent);if(!Number.isFinite(left)||!Number.isFinite(pct))return '-';return `${left} (${pct.toFixed(1)}%)`}
33088
34243
  function scheduleCompactRefreshBurst(count=COMPACT_AUTO_REFRESH_COUNT){if(!S.activeId)return;const n=Math.max(1,Math.min(10,Number(count)||COMPACT_AUTO_REFRESH_COUNT));const delay=Math.max(90,Math.min(1400,90+((n-1)*COMPACT_AUTO_REFRESH_INTERVAL_MS)));scheduleSnapshot({forceFull:false,delayMs:delay,allowWhenFrozen:true})}
33089
- function renderCtxLive(snap){const box=E('ctxLive');const textEl=E('ctxLiveText');const fill=E('ctxLiveFill');if(!box||!textEl||!fill)return;const left=Number(snap?.context_left_tokens);const pct=Number(snap?.context_left_percent);if(!Number.isFinite(left)||!Number.isFinite(pct)){textEl.textContent='ctx_left=-';fill.style.width='0%';box.classList.remove('warn','danger');return}const safePct=Math.max(0,Math.min(100,pct));textEl.textContent=`ctx_left=${left} (${safePct.toFixed(1)}%)`;fill.style.width=`${safePct}%`;box.classList.toggle('warn',safePct<=35&&safePct>15);box.classList.toggle('danger',safePct<=15)}
34244
+ function renderCtxLive(snap){const box=E('ctxLive');const textEl=E('ctxLiveText');const fill=E('ctxLiveFill');if(!box||!textEl||!fill)return;const left=Number(snap?.context_left_tokens);const pct=Number(snap?.context_left_percent);if(!Number.isFinite(left)||!Number.isFinite(pct)){textEl.textContent=`${t('rt_ctx_left')}=-`;fill.style.width='0%';box.classList.remove('warn','danger');return}const safePct=Math.max(0,Math.min(100,pct));textEl.textContent=`${t('rt_ctx_left')}=${left} (${safePct.toFixed(1)}%)`;fill.style.width=`${safePct}%`;box.classList.toggle('warn',safePct<=35&&safePct>15);box.classList.toggle('danger',safePct<=15)}
33090
34245
  function showCompactToast(text){let el=document.querySelector('.compact-toast');if(!el){el=document.createElement('div');el.className='compact-toast';document.body.appendChild(el)}el.textContent=text;el.classList.add('show');if(el._t)clearTimeout(el._t);el._t=setTimeout(()=>el.classList.remove('show'),2800)}
33091
34246
  function parseCompactReason(data){const direct=String(data?.reason||'').trim();if(direct)return direct;const s=String(data?.summary||'');const m=s.match(/context compacted \\(([^)]*)\\)/);return m?String(m[1]||'').trim():''}
33092
34247
  function isRenderRuntimeEventType(evtType){return RENDER_EVT_TYPES.has(String(evtType||''))}
@@ -33639,11 +34794,11 @@ function renderInlineMarkdown(raw){
33639
34794
  }
33640
34795
  function _mdCalloutLabel(tag){
33641
34796
  const low=String(tag||'').toLowerCase();
33642
- if(low==='warning')return 'Warning';
33643
- if(low==='notice')return 'Notice';
33644
- if(low==='instruction')return 'Instruction';
33645
- if(low==='tip')return 'Tip';
33646
- return 'Reminder';
34797
+ if(low==='warning')return t('callout_warning');
34798
+ if(low==='notice')return t('callout_notice');
34799
+ if(low==='instruction')return t('callout_instruction');
34800
+ if(low==='tip')return t('callout_tip');
34801
+ return t('callout_reminder');
33647
34802
  }
33648
34803
  function _mdExtractCallouts(src,inlineRenderer){
33649
34804
  const blocks=[];
@@ -33758,6 +34913,13 @@ function _mdWorkerEnsure(){
33758
34913
  if(S.mdWorker)return S.mdWorker;
33759
34914
  if(typeof Worker!=='function'||typeof Blob!=='function'||typeof URL==='undefined'||typeof URL.createObjectURL!=='function')return null;
33760
34915
  const workerSrc=String.raw`
34916
+ const CALLOUT_LABELS=${JSON.stringify({
34917
+ warning:t('callout_warning'),
34918
+ notice:t('callout_notice'),
34919
+ instruction:t('callout_instruction'),
34920
+ tip:t('callout_tip'),
34921
+ reminder:t('callout_reminder'),
34922
+ })};
33761
34923
  const esc=s=>String(s??'').replace(/[&<>"]/g,c=>(c==='&'?'&amp;':(c==='<'?'&lt;':(c==='>'?'&gt;':'&quot;'))));
33762
34924
  function inline(raw){
33763
34925
  let s=esc(String(raw||''));
@@ -33783,11 +34945,8 @@ function isTableSeparator(line){
33783
34945
  }
33784
34946
  function calloutLabel(tag){
33785
34947
  const low=String(tag||'').toLowerCase();
33786
- if(low==='warning')return 'Warning';
33787
- if(low==='notice')return 'Notice';
33788
- if(low==='instruction')return 'Instruction';
33789
- if(low==='tip')return 'Tip';
33790
- return 'Reminder';
34948
+ if(Object.prototype.hasOwnProperty.call(CALLOUT_LABELS,low))return CALLOUT_LABELS[low];
34949
+ return CALLOUT_LABELS.reminder||'Reminder';
33791
34950
  }
33792
34951
  function extractCallouts(src){
33793
34952
  const blocks=[];
@@ -34665,7 +35824,7 @@ function _chatVirtReleaseNode(node){
34665
35824
  }
34666
35825
  function _chatVirtReleaseRendered(root){if(!root)return;for(const node of root.querySelectorAll('.msg[data-vk]')){_chatVirtReleaseNode(node)}}
34667
35826
  function _chatVirtAgentRoleKey(raw){const role=String(raw||'').trim().toLowerCase();return(role==='explorer'||role==='developer'||role==='reviewer'||role==='manager'||role==='planner')?role:''}
34668
- function _chatVirtAgentRoleLabel(role){if(role==='explorer')return'Explorer';if(role==='developer')return'Developer';if(role==='reviewer')return'Reviewer';if(role==='manager')return'Manager';if(role==='planner')return'Planner';return''}
35827
+ function _chatVirtAgentRoleLabel(role){if(role==='explorer')return t('role_explorer');if(role==='developer')return t('role_developer');if(role==='reviewer')return t('role_reviewer');if(role==='manager')return t('role_manager');if(role==='planner')return t('role_planner');return t('role_agent')}
34669
35828
  function _stripLeadingAgentTitle(raw,agentRole){
34670
35829
  let txt=String(raw||'').replace(/^\\uFEFF/,'').trimStart();
34671
35830
  const role=_chatVirtAgentRoleKey(agentRole);
@@ -34706,21 +35865,21 @@ function _stripObjectiveInstructionForWorker(raw){
34706
35865
  return txt;
34707
35866
  }
34708
35867
  const RUNTIME_HINT_RENDER_META={
34709
- 'auto-continue':{label:'Auto Continue',tone:'instruction'},
34710
- 'arbiter-continue':{label:'Arbiter Continue',tone:'instruction'},
34711
- 'continuation-briefing':{label:'Continuation Briefing',tone:'instruction'},
34712
- 'reminder':{label:'Reminder',tone:'reminder'},
34713
- 'todo-rescue':{label:'Todo Rescue',tone:'warning'},
34714
- 'tool-retry':{label:'Tool Retry',tone:'warning'},
34715
- 'segmented-retry':{label:'Segmented Retry',tone:'warning'},
34716
- 'forced-converge':{label:'Forced Converge',tone:'warning'},
34717
- 'no-tool-recovery':{label:'No-Tool Recovery',tone:'warning'},
34718
- 'auto-context-recall':{label:'Context Recall',tone:'notice'},
34719
- 'failure-recovery':{label:'Failure Recovery',tone:'warning'},
34720
- 'truncate-rescue':{label:'Truncation Rescue',tone:'warning'},
34721
- 'thinking-empty-recovery':{label:'Thinking Recovery',tone:'warning'},
34722
- 'fault-prefill':{label:'Fault Prefill',tone:'warning'},
34723
- 'edit-recovery':{label:'Edit Recovery',tone:'warning'},
35868
+ 'auto-continue':{labelKey:'event_auto_continue',tone:'instruction'},
35869
+ 'arbiter-continue':{labelKey:'event_arbiter_continue',tone:'instruction'},
35870
+ 'continuation-briefing':{labelKey:'event_continuation_briefing',tone:'instruction'},
35871
+ 'reminder':{labelKey:'event_reminder',tone:'reminder'},
35872
+ 'todo-rescue':{labelKey:'event_todo_rescue',tone:'warning'},
35873
+ 'tool-retry':{labelKey:'event_tool_retry',tone:'warning'},
35874
+ 'segmented-retry':{labelKey:'event_segmented_retry',tone:'warning'},
35875
+ 'forced-converge':{labelKey:'event_forced_converge',tone:'warning'},
35876
+ 'no-tool-recovery':{labelKey:'event_no_tool_recovery',tone:'warning'},
35877
+ 'auto-context-recall':{labelKey:'event_context_recall',tone:'notice'},
35878
+ 'failure-recovery':{labelKey:'event_failure_recovery',tone:'warning'},
35879
+ 'truncate-rescue':{labelKey:'event_truncate_rescue',tone:'warning'},
35880
+ 'thinking-empty-recovery':{labelKey:'event_thinking_recovery',tone:'warning'},
35881
+ 'fault-prefill':{labelKey:'event_fault_prefill',tone:'warning'},
35882
+ 'edit-recovery':{labelKey:'event_edit_recovery',tone:'warning'},
34724
35883
  };
34725
35884
  function _chatVirtParseRuntimeHint(raw){
34726
35885
  const txt=String(raw||'').trim();
@@ -34729,7 +35888,8 @@ function _chatVirtParseRuntimeHint(raw){
34729
35888
  if(!m)return null;
34730
35889
  const name=String(m[1]||'').trim().toLowerCase();
34731
35890
  if(!Object.prototype.hasOwnProperty.call(RUNTIME_HINT_RENDER_META,name))return null;
34732
- return {name,body:String(m[2]||'').trim(),meta:RUNTIME_HINT_RENDER_META[name]||{label:'Runtime Hint',tone:'notice'}};
35891
+ const meta=RUNTIME_HINT_RENDER_META[name]||{labelKey:'event_reminder',tone:'notice'};
35892
+ return {name,body:String(m[2]||'').trim(),meta:{label:t(String(meta.labelKey||'event_reminder')),tone:String(meta.tone||'notice')}};
34733
35893
  }
34734
35894
  function _chatVirtBuildMessageNode(m){
34735
35895
  let kind='assistant_text';
@@ -34763,7 +35923,7 @@ function _chatVirtBuildMessageNode(m){
34763
35923
  if(m.type==='manager_delegate'){
34764
35924
  const info=(m&&typeof m.data==='object')?m.data:{};
34765
35925
  const targetRole=_chatVirtAgentRoleKey(info.target);
34766
- const targetLabel=String(info.target_label||_chatVirtAgentRoleLabel(targetRole)||info.target||'Agent');
35926
+ const targetLabel=String(info.target_label||_chatVirtAgentRoleLabel(targetRole)||info.target||t('role_agent'));
34767
35927
  const level=Math.floor(Number(info.task_level||0));
34768
35928
  const mode=String(info.execution_mode||'').trim();
34769
35929
  const taskType=String(info.task_type||'').trim();
@@ -34784,24 +35944,24 @@ function _chatVirtBuildMessageNode(m){
34784
35944
  pills.push(`budget=${budgetNum<=0?'unlimited':budgetNum}`);
34785
35945
  if(Number.isFinite(remainNum))pills.push(`remaining=${remainNum<0?'unlimited':remainNum}`);
34786
35946
  const pillsHtml=pills.map(x=>`<span class=\"manager-delegate-pill\">${esc(String(x))}</span>`).join('');
34787
- const routeHtml=`<div class=\"manager-delegate-route\"><span class=\"agent-bus-pill manager\">Manager</span><span class=\"agent-bus-arrow\">→</span><span class=\"agent-bus-pill${targetRole?(' '+targetRole):''}\">${esc(targetLabel)}</span></div>`;
34788
- const objectiveHtml=(objective&&instruction&&objective.toLowerCase()===instruction.toLowerCase())?'':(objective?`<div class=\"manager-delegate-line\"><span>Objective</span><div>${esc(objective)}</div></div>`:'');
34789
- const instructionHtml=instruction?`<div class=\"manager-delegate-line\"><span>Instruction</span><div>${esc(instruction)}</div></div>`:'';
34790
- d.innerHTML=`${roleBadge}<div class=\"manager-delegate-card\"><div class=\"manager-delegate-head\">Manager Delegate</div>${routeHtml}<div class=\"manager-delegate-pills\">${pillsHtml}</div>${objectiveHtml}${instructionHtml}</div>`;
35947
+ const routeHtml=`<div class=\"manager-delegate-route\"><span class=\"agent-bus-pill manager\">${esc(t('role_manager'))}</span><span class=\"agent-bus-arrow\">→</span><span class=\"agent-bus-pill${targetRole?(' '+targetRole):''}\">${esc(targetLabel)}</span></div>`;
35948
+ const objectiveHtml=(objective&&instruction&&objective.toLowerCase()===instruction.toLowerCase())?'':(objective?`<div class=\"manager-delegate-line\"><span>${esc(t('event_objective'))}</span><div>${esc(objective)}</div></div>`:'');
35949
+ const instructionHtml=instruction?`<div class=\"manager-delegate-line\"><span>${esc(t('event_instruction'))}</span><div>${esc(instruction)}</div></div>`:'';
35950
+ d.innerHTML=`${roleBadge}<div class=\"manager-delegate-card\"><div class=\"manager-delegate-head\">${esc(t('event_manager_delegate_title'))}</div>${routeHtml}<div class=\"manager-delegate-pills\">${pillsHtml}</div>${objectiveHtml}${instructionHtml}</div>`;
34791
35951
  return d;
34792
35952
  }
34793
35953
  if(m.type==='agent_bus'){
34794
35954
  const info=(m&&typeof m.data==='object')?m.data:{};
34795
35955
  const fromRole=_chatVirtAgentRoleKey(info.from)||agentRole;
34796
35956
  const toRole=_chatVirtAgentRoleKey(info.to);
34797
- const fromLabel=fromRole?_chatVirtAgentRoleLabel(fromRole):String(info.from||'Agent');
34798
- const toLabel=toRole?_chatVirtAgentRoleLabel(toRole):String(info.to||'Agent');
35957
+ const fromLabel=fromRole?_chatVirtAgentRoleLabel(fromRole):String(info.from||t('role_agent'));
35958
+ const toLabel=toRole?_chatVirtAgentRoleLabel(toRole):String(info.to||t('role_agent'));
34799
35959
  const intent=String(info.intent||'message').trim()||'message';
34800
35960
  const payloadRaw=String(info.payload||'').trim()||String(m.text||'').trim();
34801
35961
  const payload=_stripObjectiveInstructionForWorker(payloadRaw)||payloadRaw;
34802
35962
  const fromCls=fromRole?` ${fromRole}`:'';
34803
35963
  const toCls=toRole?` ${toRole}`:'';
34804
- d.innerHTML=`${roleBadge}<div class=\"agent-bus-card\"><div class=\"agent-bus-route\"><span class=\"agent-bus-pill${fromCls}\">${esc(fromLabel)}</span><span class=\"agent-bus-arrow\">→</span><span class=\"agent-bus-pill${toCls}\">${esc(toLabel)}</span></div><div class=\"agent-bus-intent\">intent: ${esc(intent)}</div><div class=\"agent-bus-payload\">${esc(payload)}</div></div>`;
35964
+ d.innerHTML=`${roleBadge}<div class=\"agent-bus-card\"><div class=\"agent-bus-route\"><span class=\"agent-bus-pill${fromCls}\">${esc(fromLabel)}</span><span class=\"agent-bus-arrow\">→</span><span class=\"agent-bus-pill${toCls}\">${esc(toLabel)}</span></div><div class=\"agent-bus-intent\">${esc(t('event_intent'))}: ${esc(intent)}</div><div class=\"agent-bus-payload\">${esc(payload)}</div></div>`;
34805
35965
  return d;
34806
35966
  }
34807
35967
  if(m.type==='tool_calls'){
@@ -34811,27 +35971,27 @@ function _chatVirtBuildMessageNode(m){
34811
35971
  const txt=String(m.text||'').trim().replace(/^\\[tool calls\\]\\s*/i,'');
34812
35972
  tools=txt?txt.split(',').map(x=>String(x||'').trim()).filter(Boolean):[];
34813
35973
  }
34814
- const pills=[_chatVirtEventPillHtml(`${tools.length||0} tool${tools.length===1?'':'s'}`,'neutral')];
35974
+ const pills=[_chatVirtEventPillHtml(String(tools.length||0),'neutral')];
34815
35975
  const bodyHtml=tools.length
34816
- ? `<div class=\"msg-event-body\"><div class=\"msg-event-note\">Model scheduled these tools for the current turn.</div><div class=\"msg-event-tool-grid\">${tools.slice(0,24).map(name=>_chatVirtEventPillHtml(String(name||'?'),'info')).join('')}</div></div>`
34817
- : `<div class=\"msg-event-body\"><div class=\"msg-event-note\">No structured tool metadata was attached to this turn.</div></div>`;
34818
- d.innerHTML=`${roleBadge}${_chatVirtEventCardHtml('Tool Calls',tools.length?`Auto-triggered chain for this turn`:'Tool dispatch metadata',pills,[],bodyHtml,'msg-event-card-tools')}`;
35976
+ ? `<div class=\"msg-event-body\"><div class=\"msg-event-note\">${esc(t('event_tool_calls_note'))}</div><div class=\"msg-event-tool-grid\">${tools.slice(0,24).map(name=>_chatVirtEventPillHtml(String(name||'?'),'info')).join('')}</div></div>`
35977
+ : `<div class=\"msg-event-body\"><div class=\"msg-event-note\">${esc(t('event_tool_calls_empty'))}</div></div>`;
35978
+ d.innerHTML=`${roleBadge}${_chatVirtEventCardHtml(t('event_tool_calls_title'),tools.length?`${tools.length}`:'',pills,[],bodyHtml,'msg-event-card-tools')}`;
34819
35979
  return d;
34820
35980
  }
34821
35981
  if(kind==='skill_loaded'){
34822
35982
  const parsed=_chatVirtParseSkillLoaded(String(m.text||''))||{name:'skill',desc:String(m.text||''),truncated:false};
34823
35983
  const pills=[
34824
- _chatVirtEventPillHtml('loaded','ok'),
34825
- parsed.truncated?_chatVirtEventPillHtml('preview truncated','warn'):'',
35984
+ _chatVirtEventPillHtml(t('event_loaded'),'ok'),
35985
+ parsed.truncated?_chatVirtEventPillHtml(t('event_preview_truncated'),'warn'):'',
34826
35986
  ];
34827
35987
  const grid=[
34828
- _chatVirtEventCellHtml('skill',String(parsed.name||''),{mono:true}),
35988
+ _chatVirtEventCellHtml(t('event_skill_label'),String(parsed.name||''),{mono:true}),
34829
35989
  ];
34830
35990
  const descHtml=String(parsed.desc||'').trim()
34831
35991
  ? `<div class="msg-md">${renderMarkdownCached(String(parsed.desc||''),`${String(m._vk||'')}:skill`)}</div>`
34832
- : `<div class="msg-event-note">No public description was attached to this skill notification.</div>`;
34833
- const bodyHtml=`<div class="msg-event-body"><div class="msg-event-note">Skill context was auto-loaded into the current run.</div>${descHtml}</div>`;
34834
- d.innerHTML=`${roleBadge}${_chatVirtEventCardHtml('Skill Loaded',String(parsed.name||'').trim()||'skill context',pills,grid,bodyHtml,'msg-event-card-skill')}`;
35992
+ : `<div class="msg-event-note">${esc(t('event_skill_loaded_empty'))}</div>`;
35993
+ const bodyHtml=`<div class="msg-event-body"><div class="msg-event-note">${esc(t('event_skill_loaded_note'))}</div>${descHtml}</div>`;
35994
+ d.innerHTML=`${roleBadge}${_chatVirtEventCardHtml(t('event_skill_loaded_title'),String(parsed.name||'').trim()||'skill context',pills,grid,bodyHtml,'msg-event-card-skill')}`;
34835
35995
  d.setAttribute('data-math-request',`${String(m._vk||'')}:skill`);
34836
35996
  return d;
34837
35997
  }
@@ -34843,10 +36003,10 @@ function _chatVirtBuildMessageNode(m){
34843
36003
  const pills=[_chatVirtEventPillHtml(`+${p.added??0}`,'ok'),_chatVirtEventPillHtml(`-${p.deleted??0}`,'warn')];
34844
36004
  const grid=[
34845
36005
  _chatVirtEventCellHtml(t('rel_path'),String(loc||''),{mono:true}),
34846
- _chatVirtEventCellHtml('session',String(root||''),{mono:true}),
36006
+ _chatVirtEventCellHtml(t('event_session'),String(root||''),{mono:true}),
34847
36007
  ];
34848
36008
  const bodyHtml=`<div class=\"msg-event-body\">${preview}<div class=\"msg-diff-shell\">${diffHtml(p.diff_numbered||p.diff||'')}</div></div>`;
34849
- d.innerHTML=`${roleBadge}${_chatVirtEventCardHtml('File Patch',String(loc||'').trim()||'workspace update',pills,grid,bodyHtml,'msg-event-card-diff')}`;
36009
+ d.innerHTML=`${roleBadge}${_chatVirtEventCardHtml(t('event_file_patch_title'),String(loc||'').trim()||'workspace update',pills,grid,bodyHtml,'msg-event-card-diff')}`;
34850
36010
  return d;
34851
36011
  }
34852
36012
  if(m.type==='upload'&&m.data){
@@ -34861,19 +36021,19 @@ function _chatVirtBuildMessageNode(m){
34861
36021
  parseStatus?_chatVirtEventPillHtml(`parse ${parseStatus}`,parseStatus==='done'?'ok':(parseStatus==='failed'?'error':'warn')):'',
34862
36022
  ];
34863
36023
  const grid=[
34864
- _chatVirtEventCellHtml('path',String(upath||''),{mono:true}),
34865
- _chatVirtEventCellHtml('filename',String(u.filename||''),{mono:true}),
36024
+ _chatVirtEventCellHtml(t('event_upload_path'),String(upath||''),{mono:true}),
36025
+ _chatVirtEventCellHtml(t('event_upload_filename'),String(u.filename||''),{mono:true}),
34866
36026
  ];
34867
- let previewHtml=`<div class=\"msg-event-note\">Preview unavailable for this upload.</div>`;
36027
+ let previewHtml=`<div class=\"msg-event-note\">${esc(t('event_preview_unavailable'))}</div>`;
34868
36028
  if(parseStatus==='pending'){
34869
- previewHtml=`<div class=\"msg-event-note\">Parsing uploaded file in background. The bubble will refresh when parsing completes.</div>`;
36029
+ previewHtml=`<div class=\"msg-event-note\">${esc(t('event_upload_parsing'))}</div>`;
34870
36030
  }else if(parseStatus==='failed'){
34871
- previewHtml=`<div class=\"msg-event-note\">Upload parsing failed${parseError?`: ${esc(parseError)}`:''}</div>`;
36031
+ previewHtml=`<div class=\"msg-event-note\">${esc(t('event_upload_failed'))}${parseError?`: ${esc(parseError)}`:''}</div>`;
34872
36032
  }else if(String(u.preview||'').trim()){
34873
36033
  previewHtml=`<pre class=\"msg-code-shell\">${esc(u.preview||'')}</pre>`;
34874
36034
  }
34875
36035
  const bodyHtml=`<div class=\"msg-event-body\">${preview}${previewHtml}</div>`;
34876
- d.innerHTML=`${roleBadge}${_chatVirtEventCardHtml('Upload',String(u.filename||'').trim()||'session upload',pills,grid,bodyHtml,'msg-event-card-upload')}`;
36036
+ d.innerHTML=`${roleBadge}${_chatVirtEventCardHtml(t('event_upload_title'),String(u.filename||'').trim()||'session upload',pills,grid,bodyHtml,'msg-event-card-upload')}`;
34877
36037
  return d;
34878
36038
  }
34879
36039
  if(m.type==='command'&&m.data){
@@ -34888,19 +36048,19 @@ function _chatVirtBuildMessageNode(m){
34888
36048
  _chatVirtEventPillHtml(`exit ${exitTxt}`,exitTone),
34889
36049
  durationTxt?_chatVirtEventPillHtml(durationTxt,'neutral','mono'):'',
34890
36050
  pageCount>1?_chatVirtEventPillHtml(`page ${pageIndex||1}/${pageCount}`,'info','mono'):'',
34891
- x.ui_truncated?_chatVirtEventPillHtml('UI truncated','warn'):'',
34892
- x.model_truncated?_chatVirtEventPillHtml('Model truncated','warn'):'',
34893
- x.temp_output_path?_chatVirtEventPillHtml('Temp read_file','info'):'',
34894
- x.buffer_ref?_chatVirtEventPillHtml('Buffered','neutral'):'',
36051
+ x.ui_truncated?_chatVirtEventPillHtml(t('event_ui_truncated'),'warn'):'',
36052
+ x.model_truncated?_chatVirtEventPillHtml(t('event_model_truncated'),'warn'):'',
36053
+ x.temp_output_path?_chatVirtEventPillHtml(t('event_temp_read_file'),'info'):'',
36054
+ x.buffer_ref?_chatVirtEventPillHtml(t('event_buffered'),'neutral'):'',
34895
36055
  ];
34896
36056
  const grid=[
34897
- _chatVirtEventCellHtml('command',`$ ${String(x.command||'')}`,{mono:true}),
34898
- _chatVirtEventCellHtml('cwd',String(x.cwd||''),{mono:true}),
34899
- changedFiles?_chatVirtEventCellHtml('changed',changedFiles,{mono:true}):'',
36057
+ _chatVirtEventCellHtml(t('event_command_label'),`$ ${String(x.command||'')}`,{mono:true}),
36058
+ _chatVirtEventCellHtml(t('event_cwd'),String(x.cwd||''),{mono:true}),
36059
+ changedFiles?_chatVirtEventCellHtml(t('event_changed'),changedFiles,{mono:true}):'',
34900
36060
  ];
34901
36061
  const outputTxt=String(x.output||'');
34902
- const bodyHtml=`<div class=\"msg-event-body\">${outputTxt?`<pre class=\"msg-code-shell\">${esc(outputTxt)}</pre>`:'<div class=\"msg-event-note\">No command output captured.</div>'}</div>`;
34903
- d.innerHTML=`${roleBadge}${_chatVirtEventCardHtml('Command',String(x.name||'command'),pills,grid,bodyHtml,'msg-event-card-command')}`;
36062
+ const bodyHtml=`<div class=\"msg-event-body\">${outputTxt?`<pre class=\"msg-code-shell\">${esc(outputTxt)}</pre>`:`<div class=\"msg-event-note\">${esc(t('event_command_empty'))}</div>`}</div>`;
36063
+ d.innerHTML=`${roleBadge}${_chatVirtEventCardHtml(t('event_command_title'),String(x.name||'command'),pills,grid,bodyHtml,'msg-event-card-command')}`;
34904
36064
  return d;
34905
36065
  }
34906
36066
  if(m.type==='live_thinking'){
@@ -34918,7 +36078,7 @@ function _chatVirtBuildMessageNode(m){
34918
36078
  const toolTxt=String(m.tool||'').trim();
34919
36079
  const extra=[];if(kindTxt)extra.push('kind='+kindTxt);if(toolTxt)extra.push('tool='+toolTxt);
34920
36080
  const extraTxt=extra.length?(' · '+extra.map(x=>esc(x)).join(' · ')):'';
34921
- const label=lang.startsWith('zh')?'截断恢复':(lang.startsWith('ja')?'切り詰め復旧':'Truncation Recovery');
36081
+ const label=t('event_truncation_recovery');
34922
36082
  const stateTxt=lang.startsWith('zh')?(active?'进行中':'已完成'):(lang.startsWith('ja')?(active?'進行中':'完了'):(active?'active':'done'));
34923
36083
  const key=`${m._vk}:live-trunc`;
34924
36084
  const pills=[
@@ -34928,8 +36088,8 @@ function _chatVirtBuildMessageNode(m){
34928
36088
  kindTxt?_chatVirtEventPillHtml(`kind ${kindTxt}`,'neutral'):'',
34929
36089
  toolTxt?_chatVirtEventPillHtml(`tool ${toolTxt}`,'info'):'',
34930
36090
  ];
34931
- const noteHtml=`<div class=\"msg-event-body\"><div class=\"msg-event-note\">Model output hit a truncation boundary and entered recovery mode.${extraTxt?` ${extraTxt}`:''}</div><div class=\"msg-md\">${renderMarkdownCached(String(m.text||''),key)}</div></div>`;
34932
- d.innerHTML=`${roleBadge}${_chatVirtEventCardHtml(label,'Structured truncation recovery state',pills,[],noteHtml,'msg-event-card-truncation')}`;
36091
+ const noteHtml=`<div class=\"msg-event-body\"><div class=\"msg-event-note\">${esc(t('event_truncation_note'))}${extraTxt?` ${extraTxt}`:''}</div><div class=\"msg-md\">${renderMarkdownCached(String(m.text||''),key)}</div></div>`;
36092
+ d.innerHTML=`${roleBadge}${_chatVirtEventCardHtml(label,t('event_truncation_state'),pills,[],noteHtml,'msg-event-card-truncation')}`;
34933
36093
  d.setAttribute('data-math-request',key);
34934
36094
  return d;
34935
36095
  }
@@ -34947,8 +36107,8 @@ function _chatVirtBuildMessageNode(m){
34947
36107
  _chatVirtEventPillHtml(t('running'),'live'),
34948
36108
  _chatVirtEventPillHtml(_chatVirtLiveRunText(label,elapsedNow),'neutral','mono'),
34949
36109
  ];
34950
- const bodyHtml=`<div class=\"msg-event-body\"><div class=\"msg-event-note\">The active agent is in a model call. This timer updates live while generation is in progress.</div></div>`;
34951
- d.innerHTML=`${roleBadge}${_chatVirtEventCardHtml('Agent Turn Model Call',label,pills,[],bodyHtml,'msg-event-card-live')}`;
36110
+ const bodyHtml=`<div class=\"msg-event-body\"><div class=\"msg-event-note\">${esc(t('event_live_model_call_note'))}</div></div>`;
36111
+ d.innerHTML=`${roleBadge}${_chatVirtEventCardHtml(t('event_live_model_call_title'),label,pills,[],bodyHtml,'msg-event-card-live')}`;
34952
36112
  const elapsedEl=d.querySelector('.msg-event-pill.mono');
34953
36113
  if(elapsedEl)elapsedEl.setAttribute('data-run-elapsed-text','1');
34954
36114
  return d;
@@ -35386,15 +36546,47 @@ function _feSize(bytes){const n=Number(bytes||0);if(!Number.isFinite(n)||n<0)ret
35386
36546
  function _feTs(ts){const n=Number(ts||0);if(!Number.isFinite(n)||n<=0)return'';try{return new Date(n*1000).toLocaleString()}catch(_){return''}}
35387
36547
  function _feKindLabel(kind){const k=String(kind||'').trim().toLowerCase();if(k==='html')return'HTML';if(k==='markdown')return'MD';if(k==='code')return'CODE';return''}
35388
36548
  function _feIcon(kind,type='file'){if(type==='dir')return'📁';const k=String(kind||'').trim().toLowerCase();if(k==='html')return'🌐';if(k==='markdown')return'📝';if(k==='code')return'⌘';return'📄'}
35389
- function _feRenderNodes(nodes,depth,st){const rows=Array.isArray(nodes)?nodes:[];if(!rows.length)return'';let out='';for(const node of rows){const type=String(node?.type||'');const name=String(node?.name||'').trim();const path=String(node?.path||'').trim();if(!name)continue;if(type==='dir'){const hasOwn=Object.prototype.hasOwnProperty.call(st.expanded,path);const open=hasOwn?!!st.expanded[path]:(depth<1);const kids=Array.isArray(node?.children)?node.children:[];out+=`<div class=\"fe-row dir\" style=\"--depth:${depth}\"><button class=\"fe-toggle\" data-fe-toggle=\"${esc(path)}\" data-fe-open=\"${open?'1':'0'}\">${open?'▾':'▸'}</button><span class=\"fe-icon\">${_feIcon('', 'dir')}</span><span class=\"fe-name\">${esc(name)}</span><span class=\"fe-meta\">${esc(kids.length)} item(s)</span></div>`;if(open&&kids.length){out+=_feRenderNodes(kids,depth+1,st)}continue}const kind=String(node?.preview_kind||'').trim();const canPreview=kind==='html'||kind==='markdown'||kind==='code';const active=(String(st.selected||'')===path);const sizeText=_feSize(node?.size);const timeText=_feTs(node?.mtime);const kindLabel=_feKindLabel(kind);const kindHtml=kindLabel?`<span class=\"fe-kind\">${esc(kindLabel)}</span>`:'';const clickAttr=canPreview?` data-fe-open-path=\"${esc(path)}\"`:'';out+=`<div class=\"fe-row file${active?' active':''}\" style=\"--depth:${depth}\"${clickAttr}><span class=\"fe-icon\">${_feIcon(kind,'file')}</span><span class=\"fe-name\">${esc(name)}</span>${kindHtml}<span class=\"fe-meta\">${esc(sizeText)}${timeText?` · ${esc(timeText)}`:''}</span></div>`}return out}
35390
- function renderFileExplorer(){const host=E('fileExplorer');if(!host)return;const sid=String(S.activeId||'').trim();if(!sid){host.innerHTML=`<div class=\"fe-empty mono\">${esc(t('no_files'))}</div>`;return}const st=ensureFileExplorerState(sid);if(!st){host.innerHTML=`<div class=\"fe-empty mono\">${esc(t('no_files'))}</div>`;return}const tree=(st&&typeof st.tree==='object')?st.tree:null;const children=Array.isArray(tree?.children)?tree.children:[];const rootText=String(st.root||S.snap?.session_files_root||'').trim();const summary=`nodes=${Number(st.nodeCount||0)}${st.inflight?' · loading...':''}`;const treeHtml=children.length?`<div class=\"file-explorer-tree\">${_feRenderNodes(children,0,st)}</div>`:`<div class=\"fe-empty mono\">${esc(t('no_files'))}</div>`;const truncHtml=st.truncated?`<div class=\"fe-trunc mono\">tree truncated at ${esc(Number(st.maxNodes||0))} nodes</div>`:'';host.innerHTML=`<div class=\"file-explorer-wrap\"><div class=\"file-explorer-head\"><span class=\"mono\">${esc(rootText||'/workspace')}</span><span>${esc(summary)}</span></div>${treeHtml}${truncHtml}</div>`;for(const btn of host.querySelectorAll('[data-fe-toggle]')){btn.onclick=(ev)=>{ev.preventDefault();ev.stopPropagation();const p=String(btn.getAttribute('data-fe-toggle')||'');const open=String(btn.getAttribute('data-fe-open')||'')==='1';st.expanded[p]=!open;renderFileExplorer()}}for(const row of host.querySelectorAll('[data-fe-open-path]')){row.onclick=(ev)=>{if(ev.target&&ev.target.closest&&ev.target.closest('[data-fe-toggle]'))return;const rel=String(row.getAttribute('data-fe-open-path')||'').trim();if(!rel)return;st.selected=rel;renderFileExplorer();openPreviewTab(rel)}}}
36549
+ function _feRenderNodes(nodes,depth,st){const rows=Array.isArray(nodes)?nodes:[];if(!rows.length)return'';let out='';for(const node of rows){const type=String(node?.type||'');const name=String(node?.name||'').trim();const path=String(node?.path||'').trim();if(!name)continue;if(type==='dir'){const hasOwn=Object.prototype.hasOwnProperty.call(st.expanded,path);const open=hasOwn?!!st.expanded[path]:(depth<1);const kids=Array.isArray(node?.children)?node.children:[];out+=`<div class=\"fe-row dir\" style=\"--depth:${depth}\"><button class=\"fe-toggle\" data-fe-toggle=\"${esc(path)}\" data-fe-open=\"${open?'1':'0'}\">${open?'▾':'▸'}</button><span class=\"fe-icon\">${_feIcon('', 'dir')}</span><span class=\"fe-name\">${esc(name)}</span><span class=\"fe-meta\">${esc(t('fe_items',{n:kids.length}))}</span></div>`;if(open&&kids.length){out+=_feRenderNodes(kids,depth+1,st)}continue}const kind=String(node?.preview_kind||'').trim();const canPreview=kind==='html'||kind==='markdown'||kind==='code';const active=(String(st.selected||'')===path);const sizeText=_feSize(node?.size);const timeText=_feTs(node?.mtime);const kindLabel=_feKindLabel(kind);const kindHtml=kindLabel?`<span class=\"fe-kind\">${esc(kindLabel)}</span>`:'';const clickAttr=canPreview?` data-fe-open-path=\"${esc(path)}\"`:'';out+=`<div class=\"fe-row file${active?' active':''}\" style=\"--depth:${depth}\"${clickAttr}><span class=\"fe-icon\">${_feIcon(kind,'file')}</span><span class=\"fe-name\">${esc(name)}</span>${kindHtml}<span class=\"fe-meta\">${esc(sizeText)}${timeText?` · ${esc(timeText)}`:''}</span></div>`}return out}
36550
+ function renderFileExplorer(){const host=E('fileExplorer');if(!host)return;const sid=String(S.activeId||'').trim();if(!sid){host.innerHTML=`<div class=\"fe-empty mono\">${esc(t('no_files'))}</div>`;return}const st=ensureFileExplorerState(sid);if(!st){host.innerHTML=`<div class=\"fe-empty mono\">${esc(t('no_files'))}</div>`;return}const tree=(st&&typeof st.tree==='object')?st.tree:null;const children=Array.isArray(tree?.children)?tree.children:[];const rootText=String(st.root||S.snap?.session_files_root||'').trim();const summary=[t('fe_nodes',{n:Number(st.nodeCount||0)}),st.inflight?t('fe_loading'):''].filter(Boolean).join(' · ');const treeHtml=children.length?`<div class=\"file-explorer-tree\">${_feRenderNodes(children,0,st)}</div>`:`<div class=\"fe-empty mono\">${esc(t('no_files'))}</div>`;const truncHtml=st.truncated?`<div class=\"fe-trunc mono\">${esc(t('fe_tree_truncated',{n:Number(st.maxNodes||0)}))}</div>`:'';host.innerHTML=`<div class=\"file-explorer-wrap\"><div class=\"file-explorer-head\"><span class=\"mono\">${esc(rootText||'/workspace')}</span><span>${esc(summary)}</span></div>${treeHtml}${truncHtml}</div>`;for(const btn of host.querySelectorAll('[data-fe-toggle]')){btn.onclick=(ev)=>{ev.preventDefault();ev.stopPropagation();const p=String(btn.getAttribute('data-fe-toggle')||'');const open=String(btn.getAttribute('data-fe-open')||'')==='1';st.expanded[p]=!open;renderFileExplorer()}}for(const row of host.querySelectorAll('[data-fe-open-path]')){row.onclick=(ev)=>{if(ev.target&&ev.target.closest&&ev.target.closest('[data-fe-toggle]'))return;const rel=String(row.getAttribute('data-fe-open-path')||'').trim();if(!rel)return;st.selected=rel;renderFileExplorer();openPreviewTab(rel)}}}
35391
36551
  function renderUploadList(){const host=E('uploadList');if(!host)return;const enabled=!!S.config?.show_upload_list;host.classList.toggle('hidden',!enabled);if(!enabled){host.innerHTML='';return}const uploads=(S.snap?.uploads||[]).slice(-8).reverse();host.innerHTML=uploads.map(u=>{const status=String(u.parse_status||'').trim();const statusTxt=status?` · parse=${status}`:'';const err=String(u.parse_error||'').trim();return `<div class="upload-entry"><div class="upload-entry-top"><span class="upload-entry-name">${esc(u.filename||'')}</span><span class="upload-entry-meta">${esc(u.kind||'file')} · ${esc(_feSize(u.size||0))}${esc(statusTxt)}</span></div><div class="upload-entry-path">${esc(u.workspace_path||'')}</div>${err?`<div class="upload-entry-path">${esc(err)}</div>`:''}</div>`}).join('')||`<div class="upload-empty">${esc(t('no_uploads'))}</div>`}
35392
36552
  async function refreshFileExplorer(force=false){const sid=String(S.activeId||'').trim();if(!sid)return;const st=ensureFileExplorerState(sid);if(!st)return;const now=Date.now();if(st.inflight)return;if(!force&&st.tree&&(now-Number(st.fetchedAt||0)<1400))return;st.inflight=true;const btn=E('refreshFilesBtn');if(btn)btn.disabled=true;renderFileExplorer();try{const payload=await api(_fePath(sid));if(String(S.activeId||'')!==sid)return;st.tree=(payload&&typeof payload==='object'&&payload.tree&&typeof payload.tree==='object')?payload.tree:null;st.root=String(payload?.root||S.snap?.session_files_root||'');st.nodeCount=Number(payload?.node_count||0);st.truncated=!!payload?.truncated;st.maxNodes=Number(payload?.max_nodes||0);st.fetchedAt=Date.now();renderFileExplorer()}catch(err){if(String(S.activeId||'')===sid){const host=E('fileExplorer');if(host)host.innerHTML=`<div class=\"fe-empty mono\">${esc(err?.message||String(err))}</div>`}}finally{st.inflight=false;if(btn)btn.disabled=false}}
35393
36553
  function _cmdStateKey(op){const d=(op&&typeof op==='object'&&op.data&&typeof op.data==='object')?op.data:{};return String(op?.id||op?.seq||`${String(d.name||'cmd')}:${String(d.command||'')}:${Number(op?.ts||0)}`)}
35394
36554
  function _cmdPageCount(op){const d=(op&&typeof op==='object'&&op.data&&typeof op.data==='object')?op.data:{};const pages=Array.isArray(d.ui_output_pages)?d.ui_output_pages:[];return Math.max(1,pages.length||Number(d.ui_output_page_count||0)||1)}
35395
36555
  function _cmdCurrentPage(op){if(!S.commandPageState||typeof S.commandPageState!=='object')S.commandPageState={};const key=_cmdStateKey(op);const total=_cmdPageCount(op);let page=Number(S.commandPageState[key]||1);if(!Number.isFinite(page)||page<1)page=1;if(page>total)page=total;S.commandPageState[key]=page;return page}
35396
36556
  function _cmdPageText(op,page){const d=(op&&typeof op==='object'&&op.data&&typeof op.data==='object')?op.data:{};const pages=Array.isArray(d.ui_output_pages)?d.ui_output_pages:[];if(!pages.length)return String(d.output||'');const idx=Math.max(0,Math.min(pages.length-1,Number(page||1)-1));return String(pages[idx]||'')}
35397
- function renderBoards(){const uiState=S.staticMode?(S.frozen?'static':'live'):'live';E('status').textContent=`session=${S.snap?.id||'-'} | model=${S.snap?.model||'-'} | thinking=${S.snap?.thinking?'on':'off'} | thinking_stream=${S.snap?.thinking_stream?'on':'off'} | mode=${S.snap?.execution_mode||S.config?.execution_mode||'sync'} | active_agent=${S.snap?.agent_active_role||'-'} | bb=${S.snap?.blackboard?.status||'-'} | task=${S.snap?.blackboard?.task_profile?.task_type||'-'} | complexity=${S.snap?.blackboard?.task_profile?.complexity||'-'} | judgement=${S.snap?.blackboard?.manager_judgement?.progress||'-'} | budget=${S.snap?.blackboard?.task_profile?.round_budget??'-'} | remaining=${S.snap?.blackboard?.manager_judgement?.remaining_rounds??'-'} | bb_cycles=${S.snap?.blackboard?.manager_cycles??'-'} | round_limit=${S.snap?.max_agent_rounds||'-'} | round=${S.snap?.agent_round_index??'-'} | phase=${S.snap?.agent_phase||'idle'} | queued_inputs=${S.snap?.queued_user_inputs_count??0} | run_timeout=${S.snap?.max_run_seconds??'-'}s | ctx_used=${S.snap?.context_tokens_estimate??'-'} | ctx_limit=${S.snap?.context_token_upper_bound||'-'} | ctx_mode=${S.snap?.context_token_limit_locked?'manual-lock':'adaptive'} | ctx_left=${formatContextLeft(S.snap)} | truncation=${S.snap?.truncation_count||0} | trunc_retry=${S.snap?.live_truncation_attempts||0} | trunc_tokens~=${S.snap?.live_truncation_tokens||0} | archive=${S.snap?.compact_segments_count||0} | last_compact=${S.snap?.last_compact_reason||'-'} | ollama=${S.snap?.ollama_base_url||'-'} | files=${S.snap?.session_files_root||'-'} | ui_mode=${uiState} | ${S.snap?.running?'running':'idle'}`;
36557
+ function renderBoards(){const uiState=S.staticMode?(S.frozen?'static':'live'):'live';const boolWord=v=>t(v?'state_on':'state_off');const activeRole=String(S.snap?.agent_active_role||'').trim();const activeRoleLabel=activeRole?_chatVirtAgentRoleLabel(activeRole):'-';E('status').textContent=[
36558
+ `${t('rt_session')}=${S.snap?.id||'-'}`,
36559
+ `${t('rt_model')}=${S.snap?.model||'-'}`,
36560
+ `${t('rt_thinking')}=${boolWord(S.snap?.thinking)}`,
36561
+ `${t('rt_thinking_stream')}=${boolWord(S.snap?.thinking_stream)}`,
36562
+ `${t('rt_mode')}=${S.snap?.execution_mode||S.config?.execution_mode||'sync'}`,
36563
+ `${t('rt_active_agent')}=${activeRoleLabel}`,
36564
+ `${t('rt_blackboard')}=${S.snap?.blackboard?.status||'-'}`,
36565
+ `${t('rt_task')}=${S.snap?.blackboard?.task_profile?.task_type||'-'}`,
36566
+ `${t('rt_complexity')}=${S.snap?.blackboard?.task_profile?.complexity||'-'}`,
36567
+ `${t('rt_judgement')}=${S.snap?.blackboard?.manager_judgement?.progress||'-'}`,
36568
+ `${t('rt_budget')}=${S.snap?.blackboard?.task_profile?.round_budget??'-'}`,
36569
+ `${t('rt_remaining')}=${S.snap?.blackboard?.manager_judgement?.remaining_rounds??'-'}`,
36570
+ `${t('rt_blackboard_cycles')}=${S.snap?.blackboard?.manager_cycles??'-'}`,
36571
+ `${t('rt_round_limit')}=${S.snap?.max_agent_rounds||'-'}`,
36572
+ `${t('rt_round')}=${S.snap?.agent_round_index??'-'}`,
36573
+ `${t('rt_phase')}=${S.snap?.agent_phase||t('idle')}`,
36574
+ `${t('rt_queued_inputs')}=${S.snap?.queued_user_inputs_count??0}`,
36575
+ `${t('rt_run_timeout')}=${S.snap?.max_run_seconds??'-'}s`,
36576
+ `${t('rt_ctx_used')}=${S.snap?.context_tokens_estimate??'-'}`,
36577
+ `${t('rt_ctx_limit')}=${S.snap?.context_token_upper_bound||'-'}`,
36578
+ `${t('rt_ctx_mode')}=${t(S.snap?.context_token_limit_locked?'rt_manual_lock':'rt_adaptive')}`,
36579
+ `${t('rt_ctx_left')}=${formatContextLeft(S.snap)}`,
36580
+ `${t('rt_truncation')}=${S.snap?.truncation_count||0}`,
36581
+ `${t('rt_trunc_retry')}=${S.snap?.live_truncation_attempts||0}`,
36582
+ `${t('rt_trunc_tokens')}=${S.snap?.live_truncation_tokens||0}`,
36583
+ `${t('rt_archive')}=${S.snap?.compact_segments_count||0}`,
36584
+ `${t('rt_last_compact')}=${S.snap?.last_compact_reason||'-'}`,
36585
+ `${t('rt_ollama')}=${S.snap?.ollama_base_url||'-'}`,
36586
+ `${t('rt_files')}=${S.snap?.session_files_root||'-'}`,
36587
+ `${t('rt_ui_mode')}=${uiState}`,
36588
+ S.snap?.running?t('running'):t('idle')
36589
+ ].join(' | ');
35398
36590
  renderCtxLive(S.snap);
35399
36591
  const _pmBtn=E('planModeBtn');if(_pmBtn){const _pm=S.snap?.plan_mode_preference||'auto';_pmBtn.textContent='Plan: '+_pm.charAt(0).toUpperCase()+_pm.slice(1)}
35400
36592
  const _lvl=S.snap?.user_task_level||0;updateLevelBtn(_lvl)
@@ -35404,7 +36596,7 @@ setPanelHtml('tasks',renderTaskBoard(S.snap?.tasks||[]));
35404
36596
  setPanelHtml('activity',(S.snap?.activity||[]).slice(-80).sort((a,b)=>Number(a.ts||0)-Number(b.ts||0)).map(a=>`<div class=\"mono\">${new Date(a.ts*1000).toLocaleTimeString()} · ${esc(a.summary)}</div>`).join('')||`<div class=\"mono\">${esc(t('no_activity'))}</div>`);
35405
36597
  const ops=S.snap?.operations||[];
35406
36598
  const cmds=ops.filter(x=>x.type==='command').slice(-30).reverse();
35407
- setPanelHtml('commands',cmds.map(e=>{const d=(e&&typeof e==='object'&&e.data&&typeof e.data==='object')?e.data:{};const page=_cmdCurrentPage(e);const total=_cmdPageCount(e);const totalAll=Math.max(total,Number(d.ui_output_page_total||0)||total);const flags=[d.ui_truncated?`<span class=\"cmd-flag warn\">UI preview truncated</span>`:'',d.model_truncated?`<span class=\"cmd-flag info\">Model context truncated</span>`:'',d.temp_output_path?`<span class=\"cmd-flag info\">Temp read_file ready</span>`:'',d.buffer_ref?`<span class=\"cmd-flag\">Buffered copy</span>`:''].filter(Boolean).join('');const pager=total>1?`<div class=\"cmd-pager\"><button data-cmd-key=\"${esc(_cmdStateKey(e))}\" data-cmd-page=\"-1\" data-cmd-total=\"${esc(total)}\" ${page<=1?'disabled':''}>Prev</button><span class=\"cmd-sub\">preview ${esc(page)}/${esc(total)}${totalAll>total?` of ${esc(totalAll)}`:''}</span><button data-cmd-key=\"${esc(_cmdStateKey(e))}\" data-cmd-page=\"1\" data-cmd-total=\"${esc(total)}\" ${page>=total?'disabled':''}>Next</button></div>`:'';const extra=[d.temp_output_path?`<div class=\"cmd-sub\">read_file path: ${esc(d.temp_output_path)}</div>`:'',d.buffer_ref?`<div class=\"cmd-sub\">buffer_ref: ${esc(d.buffer_ref)} · chars=${esc(d.buffer_chars||0)}</div>`:'',Number(d.output_full_chars||0)>0?`<div class=\"cmd-sub\">full_output: ${esc(d.output_full_chars)} chars · ${esc(d.output_full_lines||0)} lines · strategy=${esc(d.long_output_strategy||'inline')}</div>`:''].filter(Boolean).join('');const output=String(_cmdPageText(e,page)||'').trim();return `<div class=\"cmd-item\"><div class=\"cmd-main\">${esc(d.name||'command')} · exit=${esc(d.exit_code??'-')}</div><div class=\"cmd-sub\">${esc(d.command||'')}<br>${esc(d.cwd||'')}</div>${flags?`<div class=\"cmd-flags\">${flags}</div>`:''}${extra}${output?`<div class=\"cmd-output\">${esc(output)}</div>`:''}${pager}</div>`}).join('')||`<div class=\"mono\">${esc(t('no_commands'))}</div>`);
36599
+ setPanelHtml('commands',cmds.map(e=>{const d=(e&&typeof e==='object'&&e.data&&typeof e.data==='object')?e.data:{};const page=_cmdCurrentPage(e);const total=_cmdPageCount(e);const totalAll=Math.max(total,Number(d.ui_output_page_total||0)||total);const flags=[d.ui_truncated?`<span class=\"cmd-flag warn\">${esc(t('cmd_ui_preview_truncated'))}</span>`:'',d.model_truncated?`<span class=\"cmd-flag info\">${esc(t('cmd_model_context_truncated'))}</span>`:'',d.temp_output_path?`<span class=\"cmd-flag info\">${esc(t('cmd_temp_read_file_ready'))}</span>`:'',d.buffer_ref?`<span class=\"cmd-flag\">${esc(t('cmd_buffered_copy'))}</span>`:''].filter(Boolean).join('');const pager=total>1?`<div class=\"cmd-pager\"><button data-cmd-key=\"${esc(_cmdStateKey(e))}\" data-cmd-page=\"-1\" data-cmd-total=\"${esc(total)}\" ${page<=1?'disabled':''}>${esc(t('cmd_prev'))}</button><span class=\"cmd-sub\">${esc(t('cmd_preview'))} ${esc(page)}/${esc(total)}${totalAll>total?` · ${esc(t('cmd_of'))} ${esc(totalAll)}`:''}</span><button data-cmd-key=\"${esc(_cmdStateKey(e))}\" data-cmd-page=\"1\" data-cmd-total=\"${esc(total)}\" ${page>=total?'disabled':''}>${esc(t('cmd_next'))}</button></div>`:'';const extra=[d.temp_output_path?`<div class=\"cmd-sub\">${esc(t('cmd_read_file_path'))}: ${esc(d.temp_output_path)}</div>`:'',d.buffer_ref?`<div class=\"cmd-sub\">${esc(t('cmd_buffer_ref'))}: ${esc(d.buffer_ref)} · ${esc(t('cmd_chars'))}=${esc(d.buffer_chars||0)}</div>`:'',Number(d.output_full_chars||0)>0?`<div class=\"cmd-sub\">${esc(t('cmd_full_output'))}: ${esc(d.output_full_chars)} ${esc(t('cmd_chars'))} · ${esc(d.output_full_lines||0)} ${esc(t('cmd_lines'))} · ${esc(t('cmd_strategy'))}=${esc(d.long_output_strategy||'inline')}</div>`:''].filter(Boolean).join('');const output=String(_cmdPageText(e,page)||'').trim();return `<div class=\"cmd-item\"><div class=\"cmd-main\">${esc(d.name||t('cmd_default_name'))} · ${esc(t('cmd_exit'))}=${esc(d.exit_code??'-')}</div><div class=\"cmd-sub\">${esc(d.command||'')}<br>${esc(d.cwd||'')}</div>${flags?`<div class=\"cmd-flags\">${flags}</div>`:''}${extra}${output?`<div class=\"cmd-output\">${esc(output)}</div>`:''}${pager}</div>`}).join('')||`<div class=\"mono\">${esc(t('no_commands'))}</div>`);
35408
36600
  const cmdHost=E('commands');if(cmdHost){for(const btn of cmdHost.querySelectorAll('[data-cmd-page]')){btn.onclick=(ev)=>{ev.preventDefault();const key=String(btn.getAttribute('data-cmd-key')||'').trim();const step=Number(btn.getAttribute('data-cmd-page')||0);const total=Math.max(1,Number(btn.getAttribute('data-cmd-total')||1));if(!key||!step)return;if(!S.commandPageState||typeof S.commandPageState!=='object')S.commandPageState={};const cur=Number(S.commandPageState[key]||1);S.commandPageState[key]=Math.max(1,Math.min(total,cur+step));renderBoards()}}}
35409
36601
  const diffs=ops.filter(x=>x.type==='file_patch').slice(-20).reverse();
35410
36602
  setPanelHtml('diffs',diffs.map(e=>`<div class=\"diff-item\"><div class=\"diff-head\">${esc(e.data.path)} (+${esc(e.data.added)} / -${esc(e.data.deleted)})</div><div class=\"cmd-sub\">${esc(e.data.session_rel_path||e.data.path||'')}<br>${esc(e.data.session_root||'')}</div><div class=\"diff-body\">${diffHtml(e.data.diff_numbered||e.data.diff)}</div></div>`).join('')||`<div class=\"mono\">${esc(t('no_diffs'))}</div>`);
@@ -35735,88 +36927,88 @@ SKILLS_INDEX_HTML = """<!doctype html>
35735
36927
  <div class="actions">
35736
36928
  <select id="skillsLangSelect"></select>
35737
36929
  <select id="modelSelect"></select>
35738
- <button id="applyModelBtn" class="subtle">Apply Model</button>
35739
- <button id="refreshBtn" class="subtle">Refresh</button>
35740
- <a id="agentLink" href="#">Open Agent UI</a>
36930
+ <button id="applyModelBtn" class="subtle">应用模型</button>
36931
+ <button id="refreshBtn" class="subtle">刷新</button>
36932
+ <a id="agentLink" href="#">打开 Agent UI</a>
35741
36933
  </div>
35742
36934
  </header>
35743
36935
  <div class="status-cards" id="topStats"></div>
35744
36936
  <main class="skills-main">
35745
36937
  <aside class="panel skills-panel-left">
35746
- <div class="panel-title">Rules & Knowledge</div>
36938
+ <div class="panel-title">规则与知识</div>
35747
36939
  <div class="row">
35748
- <button id="analyzeBtn">Analyze agents/docs</button>
35749
- <button id="scanBtn" class="subtle">Scan Skills</button>
36940
+ <button id="analyzeBtn">分析 agents/docs</button>
36941
+ <button id="scanBtn" class="subtle">扫描 Skills</button>
35750
36942
  </div>
35751
36943
  <div id="rulesSummary" class="mono block-scroll compact-block"></div>
35752
- <h3>Rules</h3>
36944
+ <h3>规则</h3>
35753
36945
  <div id="rulesList" class="block-scroll grow-block"></div>
35754
- <h3>Sources</h3>
36946
+ <h3>来源</h3>
35755
36947
  <div id="sourceList" class="block-scroll grow-block"></div>
35756
36948
  </aside>
35757
36949
  <section class="panel skills-panel-center">
35758
- <div class="panel-title">Flow Builder</div>
36950
+ <div class="panel-title">流程构建器</div>
35759
36951
  <div class="row compact flow-tabs">
35760
- <button id="flowTabNodeBtn" class="subtle active">Node</button>
35761
- <button id="flowTabLinkBtn" class="subtle">Manual Link</button>
36952
+ <button id="flowTabNodeBtn" class="subtle active">节点</button>
36953
+ <button id="flowTabLinkBtn" class="subtle">手动连线</button>
35762
36954
  </div>
35763
36955
  <div id="flowPanelNode" class="flow-panel active">
35764
36956
  <div class="row compact">
35765
- <input id="nodeTitle" placeholder="Node title">
36957
+ <input id="nodeTitle" placeholder="节点标题">
35766
36958
  <select id="nodeType">
35767
- <option value="goal">goal</option>
35768
- <option value="input">input</option>
35769
- <option value="process">process</option>
35770
- <option value="check">check</option>
35771
- <option value="output">output</option>
36959
+ <option value="goal">目标</option>
36960
+ <option value="input">输入</option>
36961
+ <option value="process">流程</option>
36962
+ <option value="check">检查</option>
36963
+ <option value="output">输出</option>
35772
36964
  </select>
35773
- <button id="addNodeBtn">Add Node</button>
36965
+ <button id="addNodeBtn">添加节点</button>
35774
36966
  </div>
35775
- <textarea id="nodeContent" class="node-content" placeholder="Node content..."></textarea>
36967
+ <textarea id="nodeContent" class="node-content" placeholder="节点内容..."></textarea>
35776
36968
  </div>
35777
36969
  <div id="flowPanelLink" class="flow-panel">
35778
36970
  <div class="row compact">
35779
36971
  <select id="edgeFrom"></select>
35780
36972
  <select id="edgeFromSide">
35781
- <option value="">from:auto</option>
35782
- <option value="top">from:top</option>
35783
- <option value="right">from:right</option>
35784
- <option value="bottom">from:bottom</option>
35785
- <option value="left">from:left</option>
36973
+ <option value="">起点:自动</option>
36974
+ <option value="top">起点:上</option>
36975
+ <option value="right">起点:右</option>
36976
+ <option value="bottom">起点:下</option>
36977
+ <option value="left">起点:左</option>
35786
36978
  </select>
35787
36979
  <select id="edgeTo"></select>
35788
36980
  <select id="edgeToSide">
35789
- <option value="">to:auto</option>
35790
- <option value="top">to:top</option>
35791
- <option value="right">to:right</option>
35792
- <option value="bottom">to:bottom</option>
35793
- <option value="left">to:left</option>
36981
+ <option value="">终点:自动</option>
36982
+ <option value="top">终点:上</option>
36983
+ <option value="right">终点:右</option>
36984
+ <option value="bottom">终点:下</option>
36985
+ <option value="left">终点:左</option>
35794
36986
  </select>
35795
- <input id="edgeLabel" placeholder="edge label">
35796
- <button id="addEdgeBtn" class="subtle">Connect</button>
35797
- <button id="removeNodeBtn" class="subtle danger">Delete Node</button>
36987
+ <input id="edgeLabel" placeholder="连线标签">
36988
+ <button id="addEdgeBtn" class="subtle">连接</button>
36989
+ <button id="removeNodeBtn" class="subtle danger">删除节点</button>
35798
36990
  </div>
35799
36991
  <div class="row compact edge-meta-row">
35800
36992
  <label class="inline-check">
35801
36993
  <input id="edgeBidirectional" type="checkbox">
35802
- Bidirectional
36994
+ 双向
35803
36995
  </label>
35804
- <input id="edgeReturnN" type="number" min="1" step="1" value="1" placeholder="return n">
35805
- <span class="mono edge-tip">Drag ports + hold Shift => bidirectional (n)</span>
36996
+ <input id="edgeReturnN" type="number" min="1" step="1" value="1" placeholder="返回 n">
36997
+ <span class="mono edge-tip">拖拽端口并按住 Shift => 双向链路 (n)</span>
35806
36998
  </div>
35807
36999
  </div>
35808
37000
  <div class="flow-stage">
35809
37001
  <div id="flowZoomPill" class="flow-zoom-pill">
35810
- <button id="flowZoomOutBtn" class="subtle" title="Zoom out">-</button>
37002
+ <button id="flowZoomOutBtn" class="subtle" title="缩小">-</button>
35811
37003
  <span id="flowZoomText" class="mono">100%</span>
35812
- <button id="flowZoomInBtn" class="subtle" title="Zoom in">+</button>
37004
+ <button id="flowZoomInBtn" class="subtle" title="放大">+</button>
35813
37005
  </div>
35814
37006
  <div id="flowWrap" class="flow-wrap">
35815
37007
  <svg id="flowSvg"></svg>
35816
37008
  <div id="flowCanvas"></div>
35817
37009
  </div>
35818
37010
  <div id="flowHelpOverlay" class="flow-wrap-help">
35819
- <div class="t">Canvas Tips</div>
37011
+ <div class="t">画布提示</div>
35820
37012
  <div>1. 拖拽节点移动位置</div>
35821
37013
  <div>2. 从节点四边圆点拖拽连线</div>
35822
37014
  <div>3. 按住 Shift 拖拽 => 双向链路</div>
@@ -35825,41 +37017,41 @@ SKILLS_INDEX_HTML = """<!doctype html>
35825
37017
  </div>
35826
37018
  </div>
35827
37019
  <div class="row">
35828
- <button id="resetFlowBtn" class="subtle">Reset Flow</button>
35829
- <button id="exportFlowBtn" class="subtle">Export Flow JSON</button>
35830
- <button id="importFlowBtn" class="subtle">Import Flow JSON</button>
37020
+ <button id="resetFlowBtn" class="subtle">重置流程</button>
37021
+ <button id="exportFlowBtn" class="subtle">导出 Flow JSON</button>
37022
+ <button id="importFlowBtn" class="subtle">导入 Flow JSON</button>
35831
37023
  </div>
35832
37024
  <textarea id="flowJson" class="mono flow-json" placeholder="Flow JSON..."></textarea>
35833
37025
  </section>
35834
37026
  <aside class="panel skills-panel-right">
35835
- <div class="panel-title">Skill Draft & Publish</div>
35836
- <input id="skillName" placeholder="skill name (e.g. web-api-review)">
35837
- <input id="skillPath" placeholder="skill path (e.g. generated/web-api-review)">
35838
- <input id="skillDesc" placeholder="short description">
35839
- <textarea id="requirements" class="req-box" placeholder="extra requirements..."></textarea>
37027
+ <div class="panel-title">技能草稿与发布</div>
37028
+ <input id="skillName" placeholder="技能名称(例如 web-api-review">
37029
+ <input id="skillPath" placeholder="技能路径(例如 generated/web-api-review">
37030
+ <input id="skillDesc" placeholder="简短描述">
37031
+ <textarea id="requirements" class="req-box" placeholder="额外要求..."></textarea>
35840
37032
  <div class="row">
35841
- <button id="generateBtn">Generate + Inject</button>
35842
- <button id="saveBtn" class="subtle">Save Current Markdown</button>
37033
+ <button id="generateBtn">生成并注入</button>
37034
+ <button id="saveBtn" class="subtle">保存当前 Markdown</button>
35843
37035
  </div>
35844
- <textarea id="skillMarkdown" class="mono skill-md" placeholder="Generated SKILL.md content..."></textarea>
37036
+ <textarea id="skillMarkdown" class="mono skill-md" placeholder="生成的 SKILL.md 内容..."></textarea>
35845
37037
  <div class="skills-catalog">
35846
37038
  <div class="row compact">
35847
- <h3>Skills Explorer</h3>
37039
+ <h3>技能浏览器</h3>
35848
37040
  <span id="skillsStats" class="mono"></span>
35849
37041
  </div>
35850
37042
  <div class="row compact explorer-actions">
35851
- <button id="previewToFlowBtn" class="subtle">Load To Flow Builder</button>
37043
+ <button id="previewToFlowBtn" class="subtle">载入到流程构建器</button>
35852
37044
  </div>
35853
37045
  <div id="skillsUploadDrop" class="upload-drop skills-upload-drop">拖拽上传 skills(SKILL.md / .zip),或使用下方上传按钮</div>
35854
37046
  <div class="row compact upload-actions">
35855
- <button id="skillsUploadFileBtn" class="subtle">Upload Files</button>
35856
- <button id="skillsUploadFolderBtn" class="subtle">Upload Folder</button>
37047
+ <button id="skillsUploadFileBtn" class="subtle">上传文件</button>
37048
+ <button id="skillsUploadFolderBtn" class="subtle">上传文件夹</button>
35857
37049
  </div>
35858
37050
  <input id="skillsUploadInput" type="file" multiple accept=".zip,.md,.markdown,.txt">
35859
37051
  <input id="skillsUploadDirInput" type="file" multiple webkitdirectory directory>
35860
37052
  <div id="skillsUploadList" class="mono upload-list"></div>
35861
37053
  <div id="skillsTree" class="block-scroll tree-scroll"></div>
35862
- <h3>Selected Skill</h3>
37054
+ <h3>已选 Skill</h3>
35863
37055
  <div id="skillPreview" class="mono block-scroll preview-scroll"></div>
35864
37056
  </div>
35865
37057
  <div id="errorBox" class="error-box hidden"></div>
@@ -35974,22 +37166,70 @@ const I18N={'en':{title:'Fona Skills Studio',subtitle:'Visual Skills authoring p
35974
37166
  'zh-CN':{title:'Fona Skills Studio',subtitle:'基于现有 WebUI 风格的图形化 Skills 制作平台',flow_line:'Flowchart → LLM 解析 → SKILL.md 注入 ./skills',apply_model:'应用模型',refresh:'刷新',open_agent:'打开 Agent UI',rules_knowledge:'Rules & Knowledge',analyze:'分析 agents/docs',scan:'扫描 Skills',rules:'规则',sources:'来源',flow_builder:'流程构建器',tab_node:'节点',tab_manual_link:'手动连线',add_node:'添加节点',connect:'连接',delete_node:'删除节点',bidirectional:'双向',drag_tip:'拖拽端口并按 Shift => 双向链路 (n)',canvas_tips:'画布提示',tip1:'1. 拖拽节点移动位置',tip2:'2. 从节点四边端口拖拽连线',tip3:'3. 按住 Shift 拖拽 => 双向链路',tip4:'4. 双击连线附近 => 删除连线',tip5:'5. 使用 +/- 按钮缩放',reset_flow:'重置流程',export_flow:'导出 Flow JSON',import_flow:'导入 Flow JSON',draft_publish:'技能草稿与发布',generate_inject:'生成并注入',save_markdown:'保存当前 Markdown',skills_explorer:'技能浏览器',load_to_flow:'载入到流程构建器',upload_drop:'拖拽上传 skills(SKILL.md / .zip),或使用下方上传按钮',upload_files:'上传文件',upload_folder:'上传文件夹',selected_skill:'已选 Skill',stat_rules:'规则',stat_skills:'技能',stat_model:'模型',stat_nodes:'流程节点',no_rules:'暂无规则',no_sources:'暂无来源',no_skill_selected:'未选择 Skill',no_uploads:'暂无上传',select_skill_first:'请先选择一个 skill',no_model_selected:'未选择模型',invalid_flow_json:'无效的 flow json',summary:'摘要',generated_at:'生成时间',skills_unit:'个 skills',upload_parse_failed:'上传解析失败',folder:'目录',empty:'(空)'},
35975
37167
  'zh-TW':{title:'Fona Skills Studio',subtitle:'基於現有 WebUI 風格的圖形化 Skills 製作平台',flow_line:'Flowchart → LLM 解析 → SKILL.md 注入 ./skills',apply_model:'套用模型',refresh:'重新整理',open_agent:'開啟 Agent UI',rules_knowledge:'Rules & Knowledge',analyze:'分析 agents/docs',scan:'掃描 Skills',rules:'規則',sources:'來源',flow_builder:'流程建構器',tab_node:'節點',tab_manual_link:'手動連線',add_node:'新增節點',connect:'連線',delete_node:'刪除節點',bidirectional:'雙向',drag_tip:'拖曳端口並按 Shift => 雙向鏈路 (n)',canvas_tips:'畫布提示',tip1:'1. 拖曳節點移動位置',tip2:'2. 從節點四邊端口拖曳連線',tip3:'3. 按住 Shift 拖曳 => 雙向鏈路',tip4:'4. 雙擊連線附近 => 刪除連線',tip5:'5. 使用 +/- 按鈕縮放',reset_flow:'重設流程',export_flow:'匯出 Flow JSON',import_flow:'匯入 Flow JSON',draft_publish:'技能草稿與發布',generate_inject:'生成並注入',save_markdown:'儲存目前 Markdown',skills_explorer:'技能瀏覽器',load_to_flow:'載入至流程建構器',upload_drop:'拖曳上傳 skills(SKILL.md / .zip),或使用下方上傳按鈕',upload_files:'上傳檔案',upload_folder:'上傳資料夾',selected_skill:'已選 Skill',stat_rules:'規則',stat_skills:'技能',stat_model:'模型',stat_nodes:'流程節點',no_rules:'尚無規則',no_sources:'尚無來源',no_skill_selected:'未選擇 Skill',no_uploads:'尚無上傳',select_skill_first:'請先選擇一個 skill',no_model_selected:'尚未選擇模型',invalid_flow_json:'無效的 flow json',summary:'摘要',generated_at:'產生時間',skills_unit:'個 skills',upload_parse_failed:'上傳解析失敗',folder:'資料夾',empty:'(空)'},
35976
37168
  'ja':{title:'Fona Skills Studio',subtitle:'既存 WebUI スタイルのビジュアル Skills 制作プラットフォーム',flow_line:'Flowchart → LLM 解析 → SKILL.md を ./skills へ注入',apply_model:'モデル適用',refresh:'更新',open_agent:'Agent UI を開く',rules_knowledge:'Rules & Knowledge',analyze:'agents/docs を解析',scan:'Skills をスキャン',rules:'ルール',sources:'ソース',flow_builder:'フロービルダー',tab_node:'ノード',tab_manual_link:'手動リンク',add_node:'ノード追加',connect:'接続',delete_node:'ノード削除',bidirectional:'双方向',drag_tip:'ポートをドラッグ + Shift で双方向リンク (n)',canvas_tips:'Canvas Tips',tip1:'1. ノードをドラッグして移動',tip2:'2. ノード端子からドラッグして接続',tip3:'3. Shift を押しながらドラッグで双方向',tip4:'4. エッジ付近をダブルクリックで削除',tip5:'5. +/- ボタンでズーム',reset_flow:'Flow をリセット',export_flow:'Flow JSON をエクスポート',import_flow:'Flow JSON をインポート',draft_publish:'Skill 下書きと公開',generate_inject:'生成して注入',save_markdown:'現在の Markdown を保存',skills_explorer:'Skills Explorer',load_to_flow:'Flow Builder に読み込む',upload_drop:'skills(SKILL.md / .zip)をドラッグ&ドロップ、または下のアップロードを使用',upload_files:'ファイルをアップロード',upload_folder:'フォルダをアップロード',selected_skill:'選択中 Skill',stat_rules:'ルール',stat_skills:'Skills',stat_model:'モデル',stat_nodes:'Flow ノード',no_rules:'ルールなし',no_sources:'ソースなし',no_skill_selected:'Skill が選択されていません',no_uploads:'アップロードなし',select_skill_first:'先に skill を選択してください',no_model_selected:'モデルが未選択です',invalid_flow_json:'無効な flow json',summary:'summary',generated_at:'generated_at',skills_unit:'skills',upload_parse_failed:'upload parse failed',folder:'folder',empty:'(empty)'}};
37169
+ Object.assign(I18N['en'],{
37170
+ summary:'Summary',generated_at:'Generated At',skills_unit:'skills',upload_parse_failed:'Upload parse failed',select_skill_first:'Select a skill first',no_model_selected:'No model selected',invalid_flow_json:'Invalid flow JSON',folder:'folder',empty:'(empty)',file_too_large:'File too large',
37171
+ placeholder_node_title:'Node title',placeholder_node_content:'Node content...',placeholder_edge_label:'edge label',placeholder_return_n:'return n',placeholder_flow_json:'Flow JSON...',placeholder_skill_name:'skill name (e.g. web-api-review)',placeholder_skill_path:'skill path (e.g. generated/web-api-review)',placeholder_skill_desc:'short description',placeholder_requirements:'extra requirements...',placeholder_skill_markdown:'Generated SKILL.md content...',
37172
+ node_type_goal:'Goal',node_type_input:'Input',node_type_process:'Process',node_type_check:'Check',node_type_output:'Output',
37173
+ edge_from_auto:'from:auto',edge_from_top:'from:top',edge_from_right:'from:right',edge_from_bottom:'from:bottom',edge_from_left:'from:left',edge_to_auto:'to:auto',edge_to_top:'to:top',edge_to_right:'to:right',edge_to_bottom:'to:bottom',edge_to_left:'to:left',
37174
+ zoom_out:'Zoom out',zoom_in:'Zoom in',source_bytes:'bytes',
37175
+ meta_name:'name',meta_path:'path',meta_provider:'provider',meta_protocol:'protocol',meta_description:'description',
37176
+ no_preview:'(no preview)',upload_imported:'imported',upload_skipped:'skipped',upload_errors:'errors',imported_skill:'Imported Skill',
37177
+ flow_goal_title:'Goal',flow_inputs_title:'Inputs',flow_process_title:'Process',flow_checks_title:'Checks',flow_output_title:'Output',
37178
+ flow_goal_desc:'Define target skill behavior.',flow_inputs_desc:'List user intent, constraints, and context.',flow_process_desc:'Translate into deterministic workflow.',flow_checks_desc:'Validate quality and failure handling.',flow_output_desc:'Emit SKILL.md and inject to ./skills.',
37179
+ parse_goal_fallback:'Define target behavior.',parse_input_fallback:'Collect user intent, constraints, and required files.',parse_process_fallback:'Execute deterministic workflow with clear tool usage and outputs.',parse_checks_fallback:'Validate outputs, handle failure paths, and enforce quality gates.',parse_output_fallback:'Produce final answer and artifacts with traceable evidence.'
37180
+ });
37181
+ Object.assign(I18N['zh-CN'],{
37182
+ rules_knowledge:'规则与知识',canvas_tips:'画布提示',skills_explorer:'技能浏览器',summary:'摘要',generated_at:'生成时间',skills_unit:'项技能',upload_parse_failed:'上传解析失败',select_skill_first:'请先选择一个 Skill',no_model_selected:'未选择模型',invalid_flow_json:'无效的 Flow JSON',folder:'目录',empty:'(空)',file_too_large:'文件过大',
37183
+ placeholder_node_title:'节点标题',placeholder_node_content:'节点内容...',placeholder_edge_label:'连线标签',placeholder_return_n:'返回 n',placeholder_flow_json:'Flow JSON...',placeholder_skill_name:'技能名称(例如 web-api-review)',placeholder_skill_path:'技能路径(例如 generated/web-api-review)',placeholder_skill_desc:'简短描述',placeholder_requirements:'额外要求...',placeholder_skill_markdown:'生成的 SKILL.md 内容...',
37184
+ node_type_goal:'目标',node_type_input:'输入',node_type_process:'流程',node_type_check:'检查',node_type_output:'输出',
37185
+ edge_from_auto:'起点:自动',edge_from_top:'起点:上',edge_from_right:'起点:右',edge_from_bottom:'起点:下',edge_from_left:'起点:左',edge_to_auto:'终点:自动',edge_to_top:'终点:上',edge_to_right:'终点:右',edge_to_bottom:'终点:下',edge_to_left:'终点:左',
37186
+ zoom_out:'缩小',zoom_in:'放大',source_bytes:'字节',
37187
+ meta_name:'名称',meta_path:'路径',meta_provider:'提供方',meta_protocol:'协议',meta_description:'描述',
37188
+ no_preview:'(无预览)',upload_imported:'导入',upload_skipped:'跳过',upload_errors:'错误',imported_skill:'导入的 Skill',
37189
+ flow_goal_title:'目标',flow_inputs_title:'输入',flow_process_title:'流程',flow_checks_title:'检查',flow_output_title:'输出',
37190
+ flow_goal_desc:'定义目标 Skill 的行为。',flow_inputs_desc:'列出用户意图、约束和上下文。',flow_process_desc:'转换为确定性工作流。',flow_checks_desc:'校验质量与失败处理。',flow_output_desc:'生成 SKILL.md 并注入到 ./skills。',
37191
+ parse_goal_fallback:'定义目标行为。',parse_input_fallback:'收集用户意图、约束和所需文件。',parse_process_fallback:'执行具备清晰工具调用与输出的确定性工作流。',parse_checks_fallback:'校验输出、处理失败路径并落实质量门禁。',parse_output_fallback:'产出最终答复和可追溯工件。'
37192
+ });
37193
+ Object.assign(I18N['zh-TW'],{
37194
+ rules_knowledge:'規則與知識',canvas_tips:'畫布提示',skills_explorer:'技能瀏覽器',summary:'摘要',generated_at:'產生時間',skills_unit:'項技能',upload_parse_failed:'上傳解析失敗',select_skill_first:'請先選擇一個 Skill',no_model_selected:'尚未選擇模型',invalid_flow_json:'無效的 Flow JSON',folder:'資料夾',empty:'(空)',file_too_large:'檔案過大',
37195
+ placeholder_node_title:'節點標題',placeholder_node_content:'節點內容...',placeholder_edge_label:'連線標籤',placeholder_return_n:'返回 n',placeholder_flow_json:'Flow JSON...',placeholder_skill_name:'技能名稱(例如 web-api-review)',placeholder_skill_path:'技能路徑(例如 generated/web-api-review)',placeholder_skill_desc:'簡短描述',placeholder_requirements:'額外要求...',placeholder_skill_markdown:'生成的 SKILL.md 內容...',
37196
+ node_type_goal:'目標',node_type_input:'輸入',node_type_process:'流程',node_type_check:'檢查',node_type_output:'輸出',
37197
+ edge_from_auto:'起點:自動',edge_from_top:'起點:上',edge_from_right:'起點:右',edge_from_bottom:'起點:下',edge_from_left:'起點:左',edge_to_auto:'終點:自動',edge_to_top:'終點:上',edge_to_right:'終點:右',edge_to_bottom:'終點:下',edge_to_left:'終點:左',
37198
+ zoom_out:'縮小',zoom_in:'放大',source_bytes:'位元組',
37199
+ meta_name:'名稱',meta_path:'路徑',meta_provider:'提供方',meta_protocol:'協定',meta_description:'描述',
37200
+ no_preview:'(無預覽)',upload_imported:'匯入',upload_skipped:'略過',upload_errors:'錯誤',imported_skill:'匯入的 Skill',
37201
+ flow_goal_title:'目標',flow_inputs_title:'輸入',flow_process_title:'流程',flow_checks_title:'檢查',flow_output_title:'輸出',
37202
+ flow_goal_desc:'定義目標 Skill 的行為。',flow_inputs_desc:'列出使用者意圖、約束與上下文。',flow_process_desc:'轉換為確定性工作流程。',flow_checks_desc:'檢查品質與失敗處理。',flow_output_desc:'產出 SKILL.md 並注入到 ./skills。',
37203
+ parse_goal_fallback:'定義目標行為。',parse_input_fallback:'蒐集使用者意圖、約束與所需檔案。',parse_process_fallback:'執行具有清楚工具呼叫與輸出的確定性流程。',parse_checks_fallback:'檢查輸出、處理失敗路徑並落實品質門檻。',parse_output_fallback:'產出最終回覆與可追溯工件。'
37204
+ });
37205
+ Object.assign(I18N['ja'],{
37206
+ rules_knowledge:'ルールと知識',canvas_tips:'キャンバスのヒント',skills_explorer:'スキルエクスプローラー',summary:'概要',generated_at:'生成時刻',skills_unit:'件のスキル',upload_parse_failed:'アップロード解析失敗',select_skill_first:'先に Skill を選択してください',no_model_selected:'モデルが未選択です',invalid_flow_json:'無効な Flow JSON',folder:'フォルダ',empty:'(空)',file_too_large:'ファイルが大きすぎます',
37207
+ placeholder_node_title:'ノードタイトル',placeholder_node_content:'ノード内容...',placeholder_edge_label:'エッジラベル',placeholder_return_n:'戻り n',placeholder_flow_json:'Flow JSON...',placeholder_skill_name:'スキル名(例: web-api-review)',placeholder_skill_path:'スキルパス(例: generated/web-api-review)',placeholder_skill_desc:'短い説明',placeholder_requirements:'追加要件...',placeholder_skill_markdown:'生成された SKILL.md 内容...',
37208
+ node_type_goal:'目標',node_type_input:'入力',node_type_process:'処理',node_type_check:'検証',node_type_output:'出力',
37209
+ edge_from_auto:'始点:自動',edge_from_top:'始点:上',edge_from_right:'始点:右',edge_from_bottom:'始点:下',edge_from_left:'始点:左',edge_to_auto:'終点:自動',edge_to_top:'終点:上',edge_to_right:'終点:右',edge_to_bottom:'終点:下',edge_to_left:'終点:左',
37210
+ zoom_out:'縮小',zoom_in:'拡大',source_bytes:'バイト',
37211
+ meta_name:'名前',meta_path:'パス',meta_provider:'プロバイダー',meta_protocol:'プロトコル',meta_description:'説明',
37212
+ no_preview:'(プレビューなし)',upload_imported:'取込',upload_skipped:'スキップ',upload_errors:'エラー',imported_skill:'インポート済み Skill',
37213
+ flow_goal_title:'目標',flow_inputs_title:'入力',flow_process_title:'処理',flow_checks_title:'検証',flow_output_title:'出力',
37214
+ flow_goal_desc:'対象 Skill の振る舞いを定義します。',flow_inputs_desc:'ユーザー意図、制約、文脈を整理します。',flow_process_desc:'確定的なワークフローへ変換します。',flow_checks_desc:'品質と失敗時処理を検証します。',flow_output_desc:'SKILL.md を生成し ./skills へ注入します。',
37215
+ parse_goal_fallback:'目標の振る舞いを定義します。',parse_input_fallback:'ユーザー意図、制約、必要ファイルを収集します。',parse_process_fallback:'明確なツール利用と出力を伴う確定的ワークフローを実行します。',parse_checks_fallback:'出力を検証し、失敗経路を処理し、品質ゲートを適用します。',parse_output_fallback:'最終回答と追跡可能な成果物を生成します。'
37216
+ });
35977
37217
  function currentLang(){const c=String(S.config?.language||'').trim();if(c&&I18N[c])return c;return 'zh-CN'}
35978
- function t(key){const lang=currentLang();const pack=I18N[lang]||I18N['en'];return String((pack&&pack[key])??(I18N['en']&&I18N['en'][key])??key)}
37218
+ function t(key,vars){const lang=currentLang();const pack=I18N[lang]||I18N['en'];let txt=String((pack&&pack[key])??(I18N['en']&&I18N['en'][key])??key);if(vars&&typeof vars==='object'){for(const [k,v] of Object.entries(vars)){txt=txt.replaceAll('{'+k+'}',String(v??''))}}return txt}
35979
37219
  function setText(id,key){const el=E(id);if(el)el.textContent=t(key)}
35980
37220
  function setPlaceholder(id,key){const el=E(id);if(el)el.placeholder=t(key)}
35981
37221
  function renderLanguageControls(){const sel=E('skillsLangSelect');if(!sel)return;const langs=Array.isArray(S.config?.supported_languages)?S.config.supported_languages:[];sel.innerHTML='';for(const row of langs){const code=String(row?.code||'').trim();if(!code)continue;const op=document.createElement('option');op.value=code;op.textContent=String(row?.label||code);sel.appendChild(op)}if(S.config?.language)sel.value=S.config.language}
35982
37222
  async function setLanguage(lang){const code=String(lang||'').trim();if(!code)return;await api('/api/skillslab/language',{method:'POST',body:JSON.stringify({language:code})});S.config=S.config||{};S.config.language=code;applySkillsI18n();renderLanguageControls();setStats();renderRules();renderSkills()}
35983
- function applySkillsI18n(){document.documentElement.lang=currentLang();const h1=document.querySelector('header h1');if(h1)h1.textContent=t('title');const hp=document.querySelectorAll('header p');if(hp&&hp[0])hp[0].textContent=t('subtitle');if(hp&&hp[1])hp[1].textContent=t('flow_line');setText('applyModelBtn','apply_model');setText('refreshBtn','refresh');setText('agentLink','open_agent');const panels=document.querySelectorAll('.panel-title');if(panels&&panels[0])panels[0].textContent=t('rules_knowledge');if(panels&&panels[1])panels[1].textContent=t('flow_builder');if(panels&&panels[2])panels[2].textContent=t('draft_publish');setText('analyzeBtn','analyze');setText('scanBtn','scan');setText('flowTabNodeBtn','tab_node');setText('flowTabLinkBtn','tab_manual_link');setText('addNodeBtn','add_node');setText('addEdgeBtn','connect');setText('removeNodeBtn','delete_node');setText('resetFlowBtn','reset_flow');setText('exportFlowBtn','export_flow');setText('importFlowBtn','import_flow');setText('generateBtn','generate_inject');setText('saveBtn','save_markdown');setText('previewToFlowBtn','load_to_flow');setText('skillsUploadFileBtn','upload_files');setText('skillsUploadFolderBtn','upload_folder');const ud=E('skillsUploadDrop');if(ud)ud.textContent=t('upload_drop');const edgeTip=document.querySelector('.edge-tip');if(edgeTip)edgeTip.textContent=t('drag_tip');setPlaceholder('nodeTitle','tab_node');setPlaceholder('nodeContent','flow_builder');setPlaceholder('edgeLabel','connect');setPlaceholder('flowJson','export_flow');setPlaceholder('skillName','skills_explorer');setPlaceholder('skillPath','skills_explorer');setPlaceholder('skillDesc','draft_publish');setPlaceholder('requirements','rules_knowledge');setPlaceholder('skillMarkdown','draft_publish');const hs=document.querySelectorAll('.skills-panel-left h3, .skills-panel-right h3');if(hs&&hs[0])hs[0].textContent=t('rules');if(hs&&hs[1])hs[1].textContent=t('sources');if(hs&&hs[2])hs[2].textContent=t('skills_explorer');if(hs&&hs[3])hs[3].textContent=t('selected_skill');const tip=document.querySelector('#flowHelpOverlay .t');if(tip)tip.textContent=t('canvas_tips');const tipRows=document.querySelectorAll('#flowHelpOverlay div');if(tipRows&&tipRows[1])tipRows[1].textContent=t('tip1');if(tipRows&&tipRows[2])tipRows[2].textContent=t('tip2');if(tipRows&&tipRows[3])tipRows[3].textContent=t('tip3');if(tipRows&&tipRows[4])tipRows[4].textContent=t('tip4');if(tipRows&&tipRows[5])tipRows[5].textContent=t('tip5');const inlineCheck=document.querySelector('.inline-check');if(inlineCheck){const txt=inlineCheck.childNodes[inlineCheck.childNodes.length-1];if(txt&&txt.nodeType===Node.TEXT_NODE)txt.textContent=' '+t('bidirectional')}}
37223
+ function applySkillsI18n(){document.documentElement.lang=currentLang();const h1=document.querySelector('header h1');if(h1)h1.textContent=t('title');const hp=document.querySelectorAll('header p');if(hp&&hp[0])hp[0].textContent=t('subtitle');if(hp&&hp[1])hp[1].textContent=t('flow_line');setText('applyModelBtn','apply_model');setText('refreshBtn','refresh');setText('agentLink','open_agent');const panels=document.querySelectorAll('.panel-title');if(panels&&panels[0])panels[0].textContent=t('rules_knowledge');if(panels&&panels[1])panels[1].textContent=t('flow_builder');if(panels&&panels[2])panels[2].textContent=t('draft_publish');setText('analyzeBtn','analyze');setText('scanBtn','scan');setText('flowTabNodeBtn','tab_node');setText('flowTabLinkBtn','tab_manual_link');setText('addNodeBtn','add_node');setText('addEdgeBtn','connect');setText('removeNodeBtn','delete_node');setText('resetFlowBtn','reset_flow');setText('exportFlowBtn','export_flow');setText('importFlowBtn','import_flow');setText('generateBtn','generate_inject');setText('saveBtn','save_markdown');setText('previewToFlowBtn','load_to_flow');setText('skillsUploadFileBtn','upload_files');setText('skillsUploadFolderBtn','upload_folder');const ud=E('skillsUploadDrop');if(ud)ud.textContent=t('upload_drop');const edgeTip=document.querySelector('.edge-tip');if(edgeTip)edgeTip.textContent=t('drag_tip');setPlaceholder('nodeTitle','placeholder_node_title');setPlaceholder('nodeContent','placeholder_node_content');setPlaceholder('edgeLabel','placeholder_edge_label');setPlaceholder('edgeReturnN','placeholder_return_n');setPlaceholder('flowJson','placeholder_flow_json');setPlaceholder('skillName','placeholder_skill_name');setPlaceholder('skillPath','placeholder_skill_path');setPlaceholder('skillDesc','placeholder_skill_desc');setPlaceholder('requirements','placeholder_requirements');setPlaceholder('skillMarkdown','placeholder_skill_markdown');const hs=document.querySelectorAll('.skills-panel-left h3, .skills-panel-right h3');if(hs&&hs[0])hs[0].textContent=t('rules');if(hs&&hs[1])hs[1].textContent=t('sources');if(hs&&hs[2])hs[2].textContent=t('skills_explorer');if(hs&&hs[3])hs[3].textContent=t('selected_skill');const tip=document.querySelector('#flowHelpOverlay .t');if(tip)tip.textContent=t('canvas_tips');const tipRows=document.querySelectorAll('#flowHelpOverlay div');if(tipRows&&tipRows[1])tipRows[1].textContent=t('tip1');if(tipRows&&tipRows[2])tipRows[2].textContent=t('tip2');if(tipRows&&tipRows[3])tipRows[3].textContent=t('tip3');if(tipRows&&tipRows[4])tipRows[4].textContent=t('tip4');if(tipRows&&tipRows[5])tipRows[5].textContent=t('tip5');const inlineCheck=document.querySelector('.inline-check');if(inlineCheck){const txt=inlineCheck.childNodes[inlineCheck.childNodes.length-1];if(txt&&txt.nodeType===Node.TEXT_NODE)txt.textContent=' '+t('bidirectional')}const nodeType=E('nodeType');if(nodeType){for(const op of Array.from(nodeType.options||[])){const key=String(op.value||'').trim();if(key)op.textContent=t('node_type_'+key)}}const fromSel=E('edgeFromSide');if(fromSel){const map=['edge_from_auto','edge_from_top','edge_from_right','edge_from_bottom','edge_from_left'];Array.from(fromSel.options||[]).forEach((op,idx)=>{if(map[idx])op.textContent=t(map[idx])})}const toSel=E('edgeToSide');if(toSel){const map=['edge_to_auto','edge_to_top','edge_to_right','edge_to_bottom','edge_to_left'];Array.from(toSel.options||[]).forEach((op,idx)=>{if(map[idx])op.textContent=t(map[idx])})}const zoomOut=E('flowZoomOutBtn');if(zoomOut)zoomOut.title=t('zoom_out');const zoomIn=E('flowZoomInBtn');if(zoomIn)zoomIn.title=t('zoom_in')}
35984
37224
  async function api(path,opt={}){const o=(opt&&typeof opt==='object')?{...opt}:{};const timeoutMs=Math.max(1000,Math.min(180000,Number(o.timeoutMs||45000)||45000));delete o.timeoutMs;const ctl=(typeof AbortController==='function')?new AbortController():null;let timer=0;try{if(ctl){timer=setTimeout(()=>{try{ctl.abort()}catch(_){ }},timeoutMs)}const hdr={...(o.headers||{}), 'Content-Type':'application/json'};const r=await fetch(path,{...o,headers:hdr,signal:(ctl?ctl.signal:o.signal)});const t=await r.text();if(!r.ok){let msg=t;try{msg=JSON.parse(t).error||t}catch(_){}throw new Error(msg||'request failed')}return t?JSON.parse(t):{}}catch(err){if(err&&err.name==='AbortError'){throw new Error('request timeout')}throw err}finally{if(timer)clearTimeout(timer)}}
35985
37225
  function esc(s){return String(s??'').replace(/[&<>"]/g,c=>({ '&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;' }[c]))}
35986
37226
  function showError(msg){const el=E('errorBox');if(!msg){el.classList.add('hidden');el.textContent='';return}el.textContent=msg;el.classList.remove('hidden')}
35987
37227
  function ab2b64(buf){let bin='';const bytes=new Uint8Array(buf);const chunk=0x8000;for(let i=0;i<bytes.length;i+=chunk){bin+=String.fromCharCode(...bytes.subarray(i,i+chunk))}return btoa(bin)}
35988
- function defaultFlow(){return{nodes:[{id:'goal',type:'goal',title:'Goal',content:'Define target skill behavior.',x:30,y:30},{id:'inputs',type:'input',title:'Inputs',content:'List user intent, constraints, and context.',x:280,y:30},{id:'process',type:'process',title:'Process',content:'Translate into deterministic workflow.',x:530,y:30},{id:'checks',type:'check',title:'Checks',content:'Validate quality and failure handling.',x:280,y:210},{id:'output',type:'output',title:'Output',content:'Emit SKILL.md and inject to ./skills.',x:530,y:210}],edges:[{from:'goal',to:'inputs',label:''},{from:'inputs',to:'process',label:''},{from:'process',to:'checks',label:''},{from:'checks',to:'output',label:''}]}}
37228
+ function defaultFlow(){return{nodes:[{id:'goal',type:'goal',title:t('flow_goal_title'),content:t('flow_goal_desc'),x:30,y:30},{id:'inputs',type:'input',title:t('flow_inputs_title'),content:t('flow_inputs_desc'),x:280,y:30},{id:'process',type:'process',title:t('flow_process_title'),content:t('flow_process_desc'),x:530,y:30},{id:'checks',type:'check',title:t('flow_checks_title'),content:t('flow_checks_desc'),x:280,y:210},{id:'output',type:'output',title:t('flow_output_title'),content:t('flow_output_desc'),x:530,y:210}],edges:[{from:'goal',to:'inputs',label:''},{from:'inputs',to:'process',label:''},{from:'process',to:'checks',label:''},{from:'checks',to:'output',label:''}]}}
35989
37229
  function normalizeSkillScan(payload){if(Array.isArray(payload)){const skills=payload.map(x=>({name:x.name||x.qualified_name||'skill',description:x.description||'',path:'',skill_file:x.qualified_name||x.name||'',provider:x.provider_id||'',protocol:x.protocol||'',preview:''}));return{skills_count:skills.length,skills,tree:{type:'dir',name:'skills',path:'',children:skills.map(x=>({type:'skill',...x}))},warnings:[]}}const obj=(payload&&typeof payload==='object')?payload:{};const skills=Array.isArray(obj.skills)?obj.skills:[];const tree=(obj.tree&&typeof obj.tree==='object')?obj.tree:{type:'dir',name:'skills',path:'',children:[]};const warnings=Array.isArray(obj.warnings)?obj.warnings:[];return{skills_count:Number(obj.skills_count||skills.length)||skills.length,skills,tree,warnings}}
35990
37230
  function setStats(){const model=S.config?.model_catalog?.selected||'-';const skills=S.skillScan?.skills_count||0;const rules=(S.rules?.rules||[]).length;E('topStats').innerHTML=[[t('stat_rules'),rules],[t('stat_skills'),skills],[t('stat_model'),model],[t('stat_nodes'),(S.flow.nodes||[]).length]].map(([k,v])=>`<div class=\"stat\"><div class=\"k\">${esc(k)}</div><div class=\"v\">${esc(v)}</div></div>`).join('');const st=E('skillsStats');if(st)st.textContent=`${skills} ${t('skills_unit')}`}
35991
37231
  function renderModelControls(){const sel=E('modelSelect');if(!sel)return;sel.innerHTML='';const cat=S.config?.model_catalog||{};const opts=cat.options||[];if(opts.length){for(const it of opts){const op=document.createElement('option');op.value=it.selection;op.textContent=it.label||it.selection;sel.appendChild(op)}}else{for(const m of (cat.models||[])){const op=document.createElement('option');op.value=m;op.textContent=m;sel.appendChild(op)}}if(cat.selected)sel.value=cat.selected}
35992
- function renderRules(){const r=S.rules||{};E('rulesSummary').innerHTML=`<div>${esc(t('summary'))}: ${esc(r.summary||'-')}</div><div>${esc(t('generated_at'))}: ${esc(r.generated_at||'-')}</div>`;E('rulesList').innerHTML=(r.rules||[]).map(x=>`<div>• ${esc(x)}</div>`).join('')||`<div class=\"mono\">${esc(t('no_rules'))}</div>`;E('sourceList').innerHTML=(r.sources||[]).map(s=>`<div class=\"mono\">${esc(s.path)} (${esc(s.bytes)} bytes)</div>`).join('')||`<div class=\"mono\">${esc(t('no_sources'))}</div>`}
37232
+ function renderRules(){const r=S.rules||{};E('rulesSummary').innerHTML=`<div>${esc(t('summary'))}: ${esc(r.summary||'-')}</div><div>${esc(t('generated_at'))}: ${esc(r.generated_at||'-')}</div>`;E('rulesList').innerHTML=(r.rules||[]).map(x=>`<div>• ${esc(x)}</div>`).join('')||`<div class=\"mono\">${esc(t('no_rules'))}</div>`;E('sourceList').innerHTML=(r.sources||[]).map(s=>`<div class=\"mono\">${esc(s.path)} (${esc(s.bytes)} ${esc(t('source_bytes'))})</div>`).join('')||`<div class=\"mono\">${esc(t('no_sources'))}</div>`}
35993
37233
  function buildSkillMap(){S.skillMap={};for(const row of (S.skillScan?.skills||[])){const key=String(row.skill_file||row.path||row.name||'').trim();if(!key)continue;S.skillMap[key]=row}}
35994
37234
  function skillItemHtml(skill){const file=String(skill.skill_file||skill.path||'');const active=(file&&file===S.activeSkillFile)?' active':'';return `<div class=\"skill-item${active}\" data-skill-file=\"${esc(file)}\"><div class=\"name\">${esc(skill.name||'skill')}</div><div class=\"desc\">${esc(skill.description||'')}</div><div class=\"path\">${esc(file||skill.path||'')}</div></div>`}
35995
37235
  function treeNodeHtml(node,depth=0){if(!node||typeof node!=='object')return'';if(String(node.type)==='skill')return skillItemHtml(node);const children=Array.isArray(node.children)?node.children:[];const openAttr=depth<=1?' open':'';const title=esc(node.name||t('folder'));const childHtml=children.length?children.map(x=>treeNodeHtml(x,depth+1)).join(''):`<div class=\"mono\">${esc(t('empty'))}</div>`;return `<details${openAttr}><summary><span class=\"tree-folder\">${title}</span></summary><div class=\"tree-children\">${childHtml}</div></details>`}
@@ -35999,12 +37239,12 @@ function safeNodeId(base,taken){const b=flowSlug(base,'n');if(!taken.has(b)){tak
35999
37239
  function parseFrontMatterMd(md){const src=String(md||'');const m=src.match(/^---\\s*\\n([\\s\\S]*?)\\n---\\s*(?:\\n|$)/);if(!m)return{meta:{},body:src};const meta={};for(const line of String(m[1]||'').split(/\\r?\\n/)){const idx=line.indexOf(':');if(idx<=0)continue;const k=line.slice(0,idx).trim();const v=line.slice(idx+1).trim();if(k)meta[k]=v}return{meta,body:src.slice(m[0].length)}}
36000
37240
  function collectSections(body){const out=[];const lines=String(body||'').split(/\\r?\\n/);let cur={title:'Overview',content:''};for(const line of lines){const hm=String(line||'').match(/^#{1,3}\\s+(.+?)\\s*$/);if(hm){if(String(cur.content||'').trim())out.push(cur);cur={title:String(hm[1]||'').trim()||'Section',content:''};continue}cur.content+=(cur.content?'\\n':'')+line}if(String(cur.content||'').trim())out.push(cur);return out}
36001
37241
  function pickSectionText(sections,keywords,fallback=''){const keys=(keywords||[]).map(k=>String(k).toLowerCase());for(const sec of sections){const t=String(sec?.title||'').toLowerCase();if(keys.some(k=>t.includes(k)))return String(sec?.content||'').trim()}return String(fallback||'').trim()}
36002
- function parseSkillToFlow(md,fallbackName,fallbackDesc){const parsed=parseFrontMatterMd(md);const meta=parsed.meta||{};const body=String(parsed.body||md||'').trim();const sections=collectSections(body);const nonEmptyLines=body.split(/\\r?\\n/).map(x=>x.trim()).filter(Boolean);const overview=nonEmptyLines.slice(0,8).join('\\n');const title=String(meta.name||fallbackName||'Imported Skill').trim()||'Imported Skill';const desc=String(meta.description||fallbackDesc||'').trim();const goalText=trimText(desc||pickSectionText(sections,['goal','purpose','overview','目标','概述'],overview||'Define target behavior.'),680);const inputText=trimText(pickSectionText(sections,['input','parameter','context','prerequisite','输入','上下文','前置'],'Collect user intent, constraints, and required files.'),720);const processText=trimText(pickSectionText(sections,['workflow','process','steps','instruction','procedure','流程','步骤','执行'],body||'Execute deterministic workflow with clear tool usage and outputs.'),900);const checkText=trimText(pickSectionText(sections,['check','validation','verify','quality','guardrail','failure','test','检查','校验','验证'],'Validate outputs, handle failure paths, and enforce quality gates.'),720);const outputText=trimText(pickSectionText(sections,['output','deliverable','response','result','输出','产出'],'Produce final answer and artifacts with traceable evidence.'),680);const taken=new Set();const nodes=[{id:safeNodeId('goal',taken),type:'goal',title:'Goal',content:goalText,x:30,y:30},{id:safeNodeId('inputs',taken),type:'input',title:'Inputs',content:inputText,x:280,y:30},{id:safeNodeId('process',taken),type:'process',title:'Process',content:processText,x:530,y:30},{id:safeNodeId('checks',taken),type:'check',title:'Checks',content:checkText,x:280,y:220},{id:safeNodeId('output',taken),type:'output',title:'Output',content:outputText,x:530,y:220}];const edges=[{from:nodes[0].id,to:nodes[1].id,label:''},{from:nodes[1].id,to:nodes[2].id,label:''},{from:nodes[2].id,to:nodes[3].id,label:''},{from:nodes[3].id,to:nodes[4].id,label:''}];return{title,description:desc,nodes,edges}}
37242
+ function parseSkillToFlow(md,fallbackName,fallbackDesc){const parsed=parseFrontMatterMd(md);const meta=parsed.meta||{};const body=String(parsed.body||md||'').trim();const sections=collectSections(body);const nonEmptyLines=body.split(/\\r?\\n/).map(x=>x.trim()).filter(Boolean);const overview=nonEmptyLines.slice(0,8).join('\\n');const title=String(meta.name||fallbackName||t('imported_skill')).trim()||t('imported_skill');const desc=String(meta.description||fallbackDesc||'').trim();const goalText=trimText(desc||pickSectionText(sections,['goal','purpose','overview','objective','目标','目標','概述','概要','ゴール','目的'],overview||t('parse_goal_fallback')),680);const inputText=trimText(pickSectionText(sections,['input','parameter','context','prerequisite','输入','輸入','上下文','前置','前提','入力','文脈'],t('parse_input_fallback')),720);const processText=trimText(pickSectionText(sections,['workflow','process','steps','instruction','procedure','流程','步骤','步驟','执行','執行','ワークフロー','手順'],body||t('parse_process_fallback')),900);const checkText=trimText(pickSectionText(sections,['check','validation','verify','quality','guardrail','failure','test','检查','檢查','校验','驗證','検証','品質'],t('parse_checks_fallback')),720);const outputText=trimText(pickSectionText(sections,['output','deliverable','response','result','输出','輸出','产出','產出','成果','出力'],t('parse_output_fallback')),680);const taken=new Set();const nodes=[{id:safeNodeId('goal',taken),type:'goal',title:t('flow_goal_title'),content:goalText,x:30,y:30},{id:safeNodeId('inputs',taken),type:'input',title:t('flow_inputs_title'),content:inputText,x:280,y:30},{id:safeNodeId('process',taken),type:'process',title:t('flow_process_title'),content:processText,x:530,y:30},{id:safeNodeId('checks',taken),type:'check',title:t('flow_checks_title'),content:checkText,x:280,y:220},{id:safeNodeId('output',taken),type:'output',title:t('flow_output_title'),content:outputText,x:530,y:220}];const edges=[{from:nodes[0].id,to:nodes[1].id,label:''},{from:nodes[1].id,to:nodes[2].id,label:''},{from:nodes[2].id,to:nodes[3].id,label:''},{from:nodes[3].id,to:nodes[4].id,label:''}];return{title,description:desc,nodes,edges}}
36003
37243
  function upsertSkillRow(row){if(!row||!row.skill_file)return;const key=String(row.skill_file);if(Array.isArray(S.skillScan?.skills)){const idx=S.skillScan.skills.findIndex(x=>String(x.skill_file||x.path||x.name||'')===key);if(idx>=0){S.skillScan.skills[idx]={...S.skillScan.skills[idx],...row}}}if(S.skillMap)S.skillMap[key]={...(S.skillMap[key]||{}),...row}}
36004
- async function ensureSkillContent(skillFile){const key=String(skillFile||S.activeSkillFile||'').trim();if(!key)throw new Error('no skill selected');const row=S.skillMap[key]||{};if(String(row.content||'').trim())return row;const out=await api('/api/skillslab/skill?skill_file='+encodeURIComponent(key));const merged={...row,...out};upsertSkillRow(merged);return merged}
37244
+ async function ensureSkillContent(skillFile){const key=String(skillFile||S.activeSkillFile||'').trim();if(!key)throw new Error(t('no_skill_selected'));const row=S.skillMap[key]||{};if(String(row.content||'').trim())return row;const out=await api('/api/skillslab/skill?skill_file='+encodeURIComponent(key));const merged={...row,...out};upsertSkillRow(merged);return merged}
36005
37245
  async function selectSkill(skillFile){S.activeSkillFile=String(skillFile||'').trim();renderSkills();try{await ensureSkillContent(S.activeSkillFile);renderSkillPreview();showError('')}catch(err){renderSkillPreview();showError(err.message||String(err))}}
36006
- function renderSkillPreview(){const el=E('skillPreview');if(!el)return;const key=S.activeSkillFile||Object.keys(S.skillMap||{})[0]||'';if(!key){el.innerHTML=`<div class=\"mono\">${esc(t('no_skill_selected'))}</div>`;return}S.activeSkillFile=key;const row=S.skillMap[key]||{};const header=`name: ${row.name||'-'}\\npath: ${row.skill_file||row.path||'-'}\\nprovider: ${row.provider||'-'}\\nprotocol: ${row.protocol||'-'}\\ndescription: ${row.description||'-'}`;const fullBody=String(row.content||row.preview||'').trim();el.innerHTML=`<div>${esc(header)}</div><hr><pre>${esc(trimText(fullBody||'(no preview)',3600))}</pre>`}
36007
- function renderUploadReports(){const el=E('skillsUploadList');if(!el)return;const rows=(S.uploadReports||[]).slice(-20).reverse();el.innerHTML=rows.map(r=>`${esc(r.filename)} · imported=${esc(r.imported_count||0)} skipped=${esc(r.skipped_count||0)} errors=${esc(r.error_count||0)}`).join('<br>')||`<div>${esc(t('no_uploads'))}</div>`}
37246
+ function renderSkillPreview(){const el=E('skillPreview');if(!el)return;const key=S.activeSkillFile||Object.keys(S.skillMap||{})[0]||'';if(!key){el.innerHTML=`<div class=\"mono\">${esc(t('no_skill_selected'))}</div>`;return}S.activeSkillFile=key;const row=S.skillMap[key]||{};const header=[`${t('meta_name')}: ${row.name||'-'}`,`${t('meta_path')}: ${row.skill_file||row.path||'-'}`,`${t('meta_provider')}: ${row.provider||'-'}`,`${t('meta_protocol')}: ${row.protocol||'-'}`,`${t('meta_description')}: ${row.description||'-'}`].join('\\n');const fullBody=String(row.content||row.preview||'').trim();el.innerHTML=`<div>${esc(header)}</div><hr><pre>${esc(trimText(fullBody||t('no_preview'),3600))}</pre>`}
37247
+ function renderUploadReports(){const el=E('skillsUploadList');if(!el)return;const rows=(S.uploadReports||[]).slice(-20).reverse();el.innerHTML=rows.map(r=>`${esc(r.filename)} · ${esc(t('upload_imported'))}=${esc(r.imported_count||0)} ${esc(t('upload_skipped'))}=${esc(r.skipped_count||0)} ${esc(t('upload_errors'))}=${esc(r.error_count||0)}`).join('<br>')||`<div>${esc(t('no_uploads'))}</div>`}
36008
37248
  function renderSkills(){buildSkillMap();const treeEl=E('skillsTree');if(treeEl){const root=S.skillScan?.tree||{type:'dir',name:'skills',path:'',children:[]};treeEl.innerHTML=`<div class=\"skill-tree\">${treeNodeHtml(root,0)}</div>`;for(const el of treeEl.querySelectorAll('.skill-item')){el.onclick=()=>selectSkill(el.getAttribute('data-skill-file')||'').catch(err=>showError(err.message||String(err)));el.ondblclick=()=>loadSelectedSkillToFlow().catch(err=>showError(err.message||String(err)))}}renderSkillPreview();renderUploadReports();setStats()}
36009
37249
  async function loadSelectedSkillToFlow(){const key=String(S.activeSkillFile||'').trim();if(!key)throw new Error(t('select_skill_first'));const row=await ensureSkillContent(key);const parsed=parseSkillToFlow(row.content||row.preview||'',row.name,row.description);S.flow={nodes:parsed.nodes,edges:parsed.edges};S.selectedNodeId=S.flow.nodes[0]?.id||null;renderFlow();renderNodeEditor();exportFlow();if(E('skillMarkdown'))E('skillMarkdown').value=String(row.content||'');if(E('skillName')&&!E('skillName').value.trim())E('skillName').value=flowSlug(row.name||parsed.title||'skill','skill');if(E('skillDesc')&&!E('skillDesc').value.trim())E('skillDesc').value=String(row.description||parsed.description||'');showError('')}
36010
37250
  const FLOW_SIDES=['top','right','bottom','left'];
@@ -36050,7 +37290,7 @@ async function scanSkills(){try{const out=await api('/api/skillslab/skills');S.s
36050
37290
  async function generateSkill(){try{const payload={skill_name:E('skillName').value.trim(),skill_path:E('skillPath').value.trim(),description:E('skillDesc').value.trim(),requirements:E('requirements').value.trim(),nodes:S.flow.nodes,edges:S.flow.edges,auto_inject:true,overwrite:true};const out=await api('/api/skillslab/generate',{method:'POST',body:JSON.stringify(payload)});if(out.skill_name)E('skillName').value=out.skill_name;if(out.skill_path)E('skillPath').value=out.skill_path;if(out.description)E('skillDesc').value=out.description;E('skillMarkdown').value=out.skill_markdown||'';await scanSkills();showError('')}catch(err){showError(err.message||String(err))}}
36051
37291
  async function saveSkill(){try{const payload={path:E('skillPath').value.trim()||E('skillName').value.trim(),content:E('skillMarkdown').value,overwrite:true};await api('/api/skillslab/save',{method:'POST',body:JSON.stringify(payload)});await scanSkills();showError('')}catch(err){showError(err.message||String(err))}}
36052
37292
  function isUploadSkillCandidate(name){const n=String(name||'').toLowerCase();if(n.endsWith('.zip')||n.endsWith('.md')||n.endsWith('.markdown')||n.endsWith('.txt'))return true;return n.endsWith('/skill.md')||n.endsWith('skill.md')}
36053
- async function uploadSkillFiles(fileList){if(!fileList||!fileList.length)return;for(const file of Array.from(fileList)){const relName=String(file.webkitRelativePath||file.name||'').replace(/\\\\/g,'/');if(!isUploadSkillCandidate(relName)){S.uploadReports.push({filename:relName||file.name||'unknown',imported_count:0,skipped_count:1,error_count:0});continue}try{if(file.size>30*1024*1024){throw new Error(`File too large: ${file.name} (>30MB)`)}const arr=await file.arrayBuffer();const payload={filename:relName||file.name,mime:file.type||'',content_b64:ab2b64(arr),overwrite:false};const out=await api('/api/skillslab/upload',{method:'POST',body:JSON.stringify(payload)});S.uploadReports.push({filename:relName||file.name,imported_count:Number(out.imported_count||0),skipped_count:Number(out.skipped_count||0),error_count:Number(out.error_count||0)});if(out.scan)S.skillScan=normalizeSkillScan(out.scan);if(Array.isArray(out.errors)&&out.errors.length){showError(String(out.errors[0].error||t('upload_parse_failed')))}else{showError('')}}catch(err){S.uploadReports.push({filename:relName||file.name,imported_count:0,skipped_count:0,error_count:1});showError(err.message||String(err))}}renderSkills()}
37293
+ async function uploadSkillFiles(fileList){if(!fileList||!fileList.length)return;for(const file of Array.from(fileList)){const relName=String(file.webkitRelativePath||file.name||'').replace(/\\\\/g,'/');if(!isUploadSkillCandidate(relName)){S.uploadReports.push({filename:relName||file.name||'unknown',imported_count:0,skipped_count:1,error_count:0});continue}try{if(file.size>30*1024*1024){throw new Error(`${t('file_too_large')}: ${file.name} (>30MB)`)}const arr=await file.arrayBuffer();const payload={filename:relName||file.name,mime:file.type||'',content_b64:ab2b64(arr),overwrite:false};const out=await api('/api/skillslab/upload',{method:'POST',body:JSON.stringify(payload)});S.uploadReports.push({filename:relName||file.name,imported_count:Number(out.imported_count||0),skipped_count:Number(out.skipped_count||0),error_count:Number(out.error_count||0)});if(out.scan)S.skillScan=normalizeSkillScan(out.scan);if(Array.isArray(out.errors)&&out.errors.length){showError(String(out.errors[0].error||t('upload_parse_failed')))}else{showError('')}}catch(err){S.uploadReports.push({filename:relName||file.name,imported_count:0,skipped_count:0,error_count:1});showError(err.message||String(err))}}renderSkills()}
36054
37294
  function bindSkillUpload(){const drop=E('skillsUploadDrop');const input=E('skillsUploadInput');const dirInput=E('skillsUploadDirInput');const fileBtn=E('skillsUploadFileBtn');const folderBtn=E('skillsUploadFolderBtn');if(!drop||!input||!dirInput)return;const consume=async(files,resetter)=>{try{await uploadSkillFiles(files)}catch(err){showError(err.message||String(err))}if(typeof resetter==='function')resetter()};drop.onclick=()=>input.click();if(fileBtn)fileBtn.onclick=()=>input.click();if(folderBtn)folderBtn.onclick=()=>dirInput.click();input.onchange=()=>consume(input.files,()=>{input.value=''});dirInput.onchange=()=>consume(dirInput.files,()=>{dirInput.value=''});for(const evt of ['dragenter','dragover']){drop.addEventListener(evt,e=>{e.preventDefault();drop.classList.add('dragover')})}for(const evt of ['dragleave','dragend']){drop.addEventListener(evt,e=>{e.preventDefault();drop.classList.remove('dragover')})}drop.addEventListener('drop',e=>{e.preventDefault();drop.classList.remove('dragover');consume(e.dataTransfer?.files||[])})}
36055
37295
  function bindFlowZoom(){const outBtn=E('flowZoomOutBtn');const inBtn=E('flowZoomInBtn');if(outBtn)outBtn.onclick=()=>zoomFlowBy(-0.1);if(inBtn)inBtn.onclick=()=>zoomFlowBy(0.1);updateFlowZoomUI()}
36056
37296
  function bindFlowPan(){const wrap=E('flowWrap');if(!wrap)return;wrap.addEventListener('mousedown',ev=>{if(ev.button!==0)return;const target=ev.target;if(!target||!target.closest||!target.closest('#flowWrap'))return;if(target.closest('.flow-node,.flow-port,input,textarea,select,button,a,label,.flow-zoom-pill'))return;S.drag=null;S.linkDrag=null;S.pan={sx:ev.clientX,sy:ev.clientY,left:wrap.scrollLeft,top:wrap.scrollTop};wrap.classList.add('flow-panning');ev.preventDefault()})}