codebee 0.1.100 → 0.1.102

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/app/core/store.py CHANGED
@@ -145,8 +145,15 @@ def create_task(payload):
145
145
  goal = _text(payload.get("goal"), "goal")
146
146
  workdir = _text(payload.get("workdir"), "workdir")
147
147
  if not goal:
148
- raise ValueError("目标描述不能为空")
149
- title = title or goal.splitlines()[0][:30] # 标题可省略,自动取目标首行
148
+ # 扫榜选材的菜单承诺「方向可留空,默认分析总榜热门题材」——
149
+ # paihang.rank_scan_prompt 与 pipeline 两侧均已支持空 goal,这里放行;
150
+ # 其余类型目标必填不变。
151
+ if payload.get("type") == "rank_scan":
152
+ title = title or flow.get("name") or "扫榜选材"
153
+ else:
154
+ raise ValueError("目标描述不能为空")
155
+ else:
156
+ title = title or goal.splitlines()[0][:30] # 标题可省略,自动取目标首行
150
157
  if not workdir:
151
158
  # 未指定目录 → 用「默认保存路径」(设置里可改;内置回落 <数据目录>/workspace)。
152
159
  # 默认路径允许自动创建;用户显式给的目录仍必须已存在。
@@ -1723,12 +1730,27 @@ def retry_task(task_id):
1723
1730
  # 结构——这正是「重写未达标章」按钮(前端 done+未达标态放行
1724
1731
  # retry)区别于断点续跑的语义。
1725
1732
  redo = set()
1733
+ redo_issues = []
1726
1734
  if (prev.get("verdict") or {}).get("publishable") is False:
1727
1735
  redo = {int(c["chapter"]) for c in v_scores
1728
1736
  if not c.get("passed") and c.get("chapter") is not None}
1737
+ # 未达标原因账(verdict.major_issues,章节级+全局级)随继承
1738
+ # 下发:流水线把本章点名项与全书级项渲染进重写章起草提示词,
1739
+ # 作者第一遍就知道往哪改,不再盲写等修订拉回。被 major 点名
1740
+ # 的过线章一并回炉——总判定卡在 major 上时只重写未过线章
1741
+ # 消不掉账,「重写未达标章」会空转(2026-10-10 实案)。
1742
+ for x in (prev.get("verdict") or {}).get("major_issues") or []:
1743
+ if not isinstance(x, dict):
1744
+ continue
1745
+ redo_issues.append(x)
1746
+ try:
1747
+ redo.add(int(x.get("chapter")))
1748
+ except (TypeError, ValueError):
1749
+ pass
1729
1750
  if redo:
1730
1751
  done = [n for n in done if n not in redo]
1731
- v_scores = [c for c in v_scores if c.get("passed")]
1752
+ v_scores = [c for c in v_scores if c.get("passed")
1753
+ and c.get("chapter") not in redo]
1732
1754
  if done or redo:
1733
1755
  if redo:
1734
1756
  scores = v_scores # 未达标重写:只带已过线章的分数
@@ -1743,6 +1765,8 @@ def retry_task(task_id):
1743
1765
  # 落账的 chapter_scores——否则多轮失败恢复会把全部
1744
1766
  # 已过线章节重新评审(实测一晚白烧数百万 token)
1745
1767
  "chapter_scores": scores,
1768
+ # 未达标重写的原因账(达标续跑时为空表)
1769
+ "redo_issues": redo_issues,
1746
1770
  }
1747
1771
  break
1748
1772
  else:
@@ -2111,7 +2135,7 @@ def clear_stream_state(run_id=None, n=None):
2111
2135
 
2112
2136
  def finish_step(run_id, n, status, summary="", exit_code=None,
2113
2137
  cost_usd=0.0, tokens=0.0, duration_s=None, model=None, output=None,
2114
- followups=None, thinking=None, partial=False):
2138
+ followups=None, thinking=None, partial=False, acp_events=None):
2115
2139
  with LOCK:
2116
2140
  run = _RUNS.get(run_id)
2117
2141
  if not run:
@@ -2146,6 +2170,9 @@ def finish_step(run_id, n, status, summary="", exit_code=None,
2146
2170
  s["partial"] = True
2147
2171
  else:
2148
2172
  s.pop("partial", None)
2173
+ if acp_events:
2174
+ s["acp_events"] = [dict(item) for item in acp_events[:200]
2175
+ if isinstance(item, dict)]
2149
2176
  # 收尾即清运行中态:stream/activity/live 只是过程快照,留着会让
2150
2177
  # 前端把「已结束」的步骤仍当实时流渲染(也白占 run.json 体积)
2151
2178
  s.pop("stream", None)
package/app/main.py CHANGED
@@ -2848,6 +2848,7 @@ class Handler(BaseHTTPRequestHandler):
2848
2848
  "thinking": s.get("thinking") or "",
2849
2849
  "stream": (s.get("stream") or "") if running else "",
2850
2850
  "activity": (s.get("activity") or []) if running else [],
2851
+ "acp_events": s.get("acp_events") or [],
2851
2852
  "live": s.get("live") or 0,
2852
2853
  })
2853
2854
 
package/app/ui/app.js CHANGED
@@ -3123,7 +3123,20 @@ async function createTask() {
3123
3123
  workdir: $("f-workdir").value.trim(),
3124
3124
  thinking: $("f-thinking").value,
3125
3125
  };
3126
- if (!payload.goal) {
3126
+ const sandboxBackend = $("f-sandbox-backend").value;
3127
+ if (sandboxBackend === "docker") {
3128
+ const dockerImage = $("f-sandbox-image").value.trim();
3129
+ if (!dockerImage) {
3130
+ msg.className = "msg err";
3131
+ msg.textContent = t("选择 Docker 后请填写镜像名");
3132
+ $("f-sandbox-image").focus();
3133
+ resetSubmit();
3134
+ return;
3135
+ }
3136
+ payload.sandbox = { backend: "docker", docker_image: dockerImage };
3137
+ }
3138
+ // 扫榜选材允许目标留空(菜单承诺:默认分析总榜热门题材),其余类型必填
3139
+ if (!payload.goal && payload.type !== "rank_scan") {
3127
3140
  msg.className = "msg err";
3128
3141
  msg.textContent = t("请先填写目标");
3129
3142
  $("f-goal").focus();
@@ -3252,6 +3265,12 @@ async function createTask() {
3252
3265
  }
3253
3266
  }
3254
3267
 
3268
+ function syncSandboxComposer() {
3269
+ const docker = $("f-sandbox-backend") && $("f-sandbox-backend").value === "docker";
3270
+ const row = $("row-sandbox-image");
3271
+ if (row) row.classList.toggle("hidden", !docker);
3272
+ }
3273
+
3255
3274
  /* ---------------------------------------------------------- 附件(截图/文件) */
3256
3275
  /* 待提交附件:选中/粘贴即上传到服务端 pending 区,创建任务时随 payload 落盘
3257
3276
  * 到工作目录 _attachments/ 并注入上下文。图片另走 codex 原生 -i 直读;
@@ -3957,6 +3976,10 @@ async function newFromTaskAsync(id) {
3957
3976
  // 不随预填带走:新任务要么重新上传,要么由 _attachments 路径引用
3958
3977
  $("f-context").value = (tk.context || "").split("\n## 附件材料")[0].trimEnd();
3959
3978
  $("f-workdir").value = tk.workdir || "";
3979
+ const taskSandbox = tk.sandbox || {};
3980
+ $("f-sandbox-backend").value = taskSandbox.backend === "docker" ? "docker" : "native";
3981
+ $("f-sandbox-image").value = taskSandbox.docker_image || "";
3982
+ syncSandboxComposer();
3960
3983
  S.atts = []; renderAttachChips(); // 附件清单不继承:同目录引用已随 context 保留
3961
3984
  queueGitProbe(); // 工作目录变了,重新探测代码版本
3962
3985
  $("f-mode").value = ["fast", "expert", "manual"].includes(tk.mode) ? tk.mode : "auto";
@@ -4815,7 +4838,13 @@ window.loadRunCheckpoints = async function (requestedRunId) {
4815
4838
  const run = (S.lastRun && S.lastRun.id === runId) ? S.lastRun : latest;
4816
4839
  box.dataset.hasRestorable = files.some((file) => file.restorable) ? "true" : "false";
4817
4840
  refreshCheckpointRestoreAvailability(run);
4818
- body.innerHTML = (data.files && data.files.warning ? '<p class="hint">' + esc(data.files.warning) + "</p>" : "") +
4841
+ const checkpoint = data.checkpoint || {};
4842
+ const steps = Array.isArray(checkpoint.steps) ? checkpoint.steps : [];
4843
+ const stepSummary = steps.length ? '<div class="rd-checkpoint-steps"><strong>' + esc(t("步骤回放预览")) + '</strong>' +
4844
+ steps.map((step) => '<div class="rd-checkpoint-step"><span>#' + esc(String(step.step ?? "")) + ' · ' + esc(step.role || t("未命名步骤")) + '</span><span class="chip">' + esc(step.status || step.event || "unknown") + (step.attempt ? ' · ' + esc(t("第") + step.attempt + t("次")) : "") + '</span></div>').join("") +
4845
+ (checkpoint.truncated ? '<p class="hint">' + esc(t("仅显示最近 200 个步骤,共 ") + (checkpoint.total_steps || steps.length)) + '</p>' : '') +
4846
+ (checkpoint.replayable ? '<p class="hint">' + esc(t("所有步骤均已完成,可人工复核后继续")) + '</p>' : '<p class="hint">' + esc(t("无法安全自动回放:") + (checkpoint.blocked_reason || t("需要人工核对"))) + '</p>') + '</div>' : '';
4847
+ body.innerHTML = (data.files && data.files.warning ? '<p class="hint">' + esc(data.files.warning) + "</p>" : "") + stepSummary +
4819
4848
  (files.length ? files.map((file) => '<article class="rd-checkpoint-row"><strong>' + esc(file.path) + '</strong><span class="chip">' + esc(file.status) + (file.restorable ? " · " + esc(t("可恢复")) : "") + '</span><pre>' + esc(file.diff || "") + "</pre></article>").join("") : '<span class="hint">' + esc(t("此运行没有文件快照")) + "</span>");
4820
4849
  box.dataset.runId = runId;
4821
4850
  box.classList.remove("needs-load");
@@ -6185,6 +6214,11 @@ function pbBlock(task, platform) {
6185
6214
  S.pubAuto.running.platform === platform) ? S.pubAuto.running : null;
6186
6215
  if (run && run.status === "running") {
6187
6216
  btns += '<span class="pb-book">' + esc((run.as_draft ? t("存草稿中 ") : t("自动发布中 ")) + run.done + "/" + run.total) + "</span>";
6217
+ }
6218
+ if (run && Array.isArray(run.notes) && run.notes.length) {
6219
+ // 对账剔除/跳过/重试的过程记录(auto 层 notes),最近三条
6220
+ btns += '<div class="pb-hint">' +
6221
+ esc(run.notes.slice(-3).join(";")) + "</div>";
6188
6222
  } else if (au.pending > 0) {
6189
6223
  // 门禁不再禁用按钮:点了走 pbApiWithQualityOverride 的复审/强制对话,
6190
6224
  // 死按钮只会让用户无路可走(2026-10-08 实案:guard false 全灰点不了)
@@ -6209,9 +6243,6 @@ function pbBlock(task, platform) {
6209
6243
  const key = task.id + ":" + platform;
6210
6244
  const sig = its.map((x) => x.n).join(",");
6211
6245
  S.pbSel = S.pbSel || {};
6212
- if (!S.pbSel[key] || S.pbSel[key].sig !== sig)
6213
- S.pbSel[key] = { sig, sel: new Set(its.map((x) => x.n)) };
6214
- const sel = S.pbSel[key].sel;
6215
6246
  // 连续段按卷名分组;无卷计划的整块平铺不显示卷头
6216
6247
  const groups = [];
6217
6248
  for (const x of its) {
@@ -6219,10 +6250,22 @@ function pbBlock(task, platform) {
6219
6250
  if (g && g.v === x.v) g.ns.push(x.n);
6220
6251
  else groups.push({ v: x.v, ns: [x.n] });
6221
6252
  }
6253
+ if (!S.pbSel[key] || S.pbSel[key].sig !== sig)
6254
+ S.pbSel[key] = { sig, sel: new Set(its.map((x) => x.n)), groups };
6255
+ const sel = S.pbSel[key].sel;
6222
6256
  let chips = "";
6223
- for (const g of groups) {
6224
- if (groups.length > 1 || g.v)
6225
- chips += '<div class="pb-vol">' + esc(g.v || t("未分卷")) + "</div>";
6257
+ for (let gi = 0; gi < groups.length; gi++) {
6258
+ const g = groups[gi];
6259
+ if (groups.length > 1 || g.v) {
6260
+ // 卷头=整卷开关:全选打勾、部分选中显示 已选/总(2026-10-10 按卷选章)
6261
+ const cnt = g.ns.filter((n) => sel.has(n)).length;
6262
+ const allOn = cnt === g.ns.length;
6263
+ chips += '<div class="pb-vol"><label class="pb-vol-sel' + (allOn ? " on" : "") +
6264
+ '" title="' + esc(t("勾选卷头=整卷进所选,再点取消")) + '">' +
6265
+ '<input type="checkbox" ' + (allOn ? "checked" : "") +
6266
+ ' onchange="pbSelVolume(\'' + esc(task.id) + "','" + platform + "'," + gi + ',this.checked)">' +
6267
+ esc(g.v || t("未分卷")) + t("(") + (allOn ? "" : cnt + "/") + g.ns.length + t(")") + "</label></div>";
6268
+ }
6226
6269
  for (const n of g.ns)
6227
6270
  chips += '<label class="pb-chip' + (sel.has(n) ? " on" : "") + '">' +
6228
6271
  '<input type="checkbox" ' + (sel.has(n) ? "checked" : "") +
@@ -6580,6 +6623,17 @@ window.pbSelAll = function (taskId, platform, all) {
6580
6623
  S._pbSig = ""; pbKick();
6581
6624
  };
6582
6625
 
6626
+ window.pbSelVolume = function (taskId, platform, gi, on) {
6627
+ const key = taskId + ":" + platform;
6628
+ const ent = (S.pbSel || {})[key];
6629
+ const g = ent && ent.groups && ent.groups[gi];
6630
+ if (!g || !g.ns.length) return;
6631
+ if (on) for (const n of g.ns) ent.sel.add(n);
6632
+ else for (const n of g.ns) ent.sel.delete(n);
6633
+ pbSelCountSync(key);
6634
+ S._pbSig = ""; pbKick();
6635
+ };
6636
+
6583
6637
  window.pbPublishSelected = async function (taskId, platform, autoSubmit) {
6584
6638
  const key = taskId + ":" + platform;
6585
6639
  const ent = (S.pbSel || {})[key];
@@ -8578,15 +8632,15 @@ function hiveThinkLine(lines) {
8578
8632
  * localStorage 持久化/WebGL 不可用降级,以及把 renderHive 的泳道数据翻译给场景。 —— */
8579
8633
  let hiveScene = null; // Hive3D 场景实例(懒创建)
8580
8634
  let hiveSceneDead = false; // WebGL 不可用/上下文丢失——本次页面周期内不再尝试
8581
- let hiveMode = "3d"; // 当前视图模式(orch.hiveView)
8635
+ let hiveMode = "live3d"; // live3d is the real scene; reference and 2d remain opt-in views
8582
8636
  let hiveRunId = ""; // 最近一次 renderHive 的 run.id(尾巴/思考缓存键前缀)
8583
8637
  const hiveLiveMeta = {}; // rel -> { started_at }(3D 芯片秒表用)
8584
8638
  let hiveDialogPoll = null;
8585
8639
  let hiveDialogKey = "";
8586
8640
 
8587
- function setHiveSceneButtons(is3d) {
8641
+ function setHiveSceneButtons(mode) {
8588
8642
  document.querySelectorAll("[data-hive-view]").forEach((button) => {
8589
- const active = button.dataset.hiveView === (is3d ? "3d" : "2d");
8643
+ const active = button.dataset.hiveView === mode;
8590
8644
  button.classList.toggle("active", active);
8591
8645
  button.setAttribute("aria-pressed", active ? "true" : "false");
8592
8646
  });
@@ -8657,28 +8711,80 @@ function closeHiveMonitorLog() {
8657
8711
  function setHiveSceneMode(mode, opts) {
8658
8712
  const viewport = $("rd-hive-viewport");
8659
8713
  if (!viewport) return;
8660
- let is3d = mode !== "2d";
8661
- if (is3d) {
8714
+ let selected = ["live3d", "2d"].includes(mode) ? mode : "reference";
8715
+ if (selected !== "2d") {
8662
8716
  const sc = ensureHiveScene();
8663
8717
  if (!sc) {
8664
- is3d = false;
8718
+ selected = "2d";
8665
8719
  if (!(opts && opts.silent)) toast(t("当前环境不支持 WebGL,已切换 2D 列表"), true);
8666
8720
  }
8667
8721
  }
8668
- hiveMode = is3d ? "3d" : "2d";
8669
- if (hiveScene) hiveScene.setActive(is3d);
8670
- viewport.classList.toggle("hive-mode-3d", is3d);
8671
- viewport.classList.toggle("hive-mode-2d", !is3d);
8722
+ hiveMode = selected;
8723
+ if (hiveScene) {
8724
+ hiveScene.setDisplayMode(selected);
8725
+ hiveScene.setActive(selected !== "2d");
8726
+ }
8727
+ viewport.classList.toggle("hive-mode-3d", selected === "reference");
8728
+ viewport.classList.toggle("hive-mode-live3d", selected === "live3d");
8729
+ viewport.classList.toggle("hive-mode-2d", selected === "2d");
8672
8730
  const tools = document.querySelector(".hive-scene-tools");
8673
- if (tools) tools.classList.toggle("hive-2d", !is3d);
8674
- setHiveSceneButtons(is3d);
8675
- try { localStorage.setItem("orch.hiveView", hiveMode); } catch (e) { /* 隐私模式等 */ }
8731
+ if (tools) tools.classList.toggle("hive-2d", selected === "2d");
8732
+ setHiveSceneButtons(selected);
8733
+ const hint = document.querySelector(".hive-scene-hint");
8734
+ if (hint) {
8735
+ const hintKey = selected === "live3d"
8736
+ ? "拖动旋转 · Shift+拖动平移 · 滚轮缩放 · 双击复位 · 点击屏幕看日志"
8737
+ : selected === "2d" ? "点击格子看日志"
8738
+ : "拖动平移 · 滚轮缩放 · 双击复位 · 点击屏幕看日志";
8739
+ hint.dataset.i18n = hintKey;
8740
+ hint.textContent = t(hintKey);
8741
+ }
8742
+ try { localStorage.setItem("orch.hiveView.v2", hiveMode); } catch (e) { /* 隐私模式等 */ }
8743
+ }
8744
+
8745
+ function syncHiveFullscreenButton() {
8746
+ const stage = $("hive-scene-stage");
8747
+ const button = $("hive-scene-fullscreen");
8748
+ const active = !!stage && document.fullscreenElement === stage;
8749
+ if (stage) stage.classList.toggle("is-fullscreen", active);
8750
+ if (!button) return;
8751
+ const textKey = active ? "退出全屏" : "全屏";
8752
+ const labelKey = active ? "退出全屏" : "进入全屏";
8753
+ button.dataset.i18n = textKey;
8754
+ button.dataset.i18nTitle = labelKey;
8755
+ button.dataset.i18nAria = labelKey;
8756
+ button.textContent = t(textKey);
8757
+ button.title = t(labelKey);
8758
+ button.setAttribute("aria-label", t(labelKey));
8759
+ button.setAttribute("aria-pressed", active ? "true" : "false");
8760
+ }
8761
+
8762
+ async function toggleHiveFullscreen() {
8763
+ const stage = $("hive-scene-stage");
8764
+ if (!stage) return;
8765
+ try {
8766
+ if (document.fullscreenElement === stage) {
8767
+ await document.exitFullscreen();
8768
+ return;
8769
+ }
8770
+ if (typeof stage.requestFullscreen !== "function" || document.fullscreenEnabled === false) {
8771
+ toast(t("当前浏览器不支持全屏"), true);
8772
+ return;
8773
+ }
8774
+ await stage.requestFullscreen();
8775
+ } catch (error) {
8776
+ toast(t("全屏失败:") + (error && error.message ? error.message : ""), true);
8777
+ }
8676
8778
  }
8677
8779
 
8678
8780
  function setupHiveSceneControls() {
8679
8781
  const viewport = $("rd-hive-viewport");
8680
8782
  if (!viewport || viewport.dataset.controlsReady) return;
8681
8783
  viewport.dataset.controlsReady = "true";
8784
+ const fullscreenButton = $("hive-scene-fullscreen");
8785
+ if (fullscreenButton) fullscreenButton.addEventListener("click", toggleHiveFullscreen);
8786
+ document.addEventListener("fullscreenchange", syncHiveFullscreenButton);
8787
+ syncHiveFullscreenButton();
8682
8788
  const hiveDialog = $("hive-log-dialog");
8683
8789
  const hiveClose = $("hive-log-close");
8684
8790
  if (hiveClose) hiveClose.addEventListener("click", closeHiveMonitorLog);
@@ -8691,14 +8797,23 @@ function setupHiveSceneControls() {
8691
8797
  });
8692
8798
  document.querySelectorAll("[data-hive-scene-action]").forEach((button) => {
8693
8799
  button.addEventListener("click", () => {
8694
- if (hiveMode !== "3d" || !hiveScene) return;
8800
+ if (hiveMode === "2d" || !hiveScene) return;
8695
8801
  if (button.dataset.hiveSceneAction === "reset") hiveScene.resetView();
8696
8802
  else hiveScene.zoomAt(button.dataset.hiveSceneAction === "zoom-in" ? 1 / 1.18 : 1.18);
8697
8803
  });
8698
8804
  });
8699
- let saved = "3d";
8700
- try { saved = localStorage.getItem("orch.hiveView") || "3d"; } catch (e) { /* ignore */ }
8701
- setHiveSceneMode(saved === "2d" ? "2d" : "3d", { silent: true });
8805
+ let saved = "live3d";
8806
+ try {
8807
+ saved = localStorage.getItem("orch.hiveView.v2") || "";
8808
+ if (!saved) {
8809
+ // Migrate an explicitly selected 2D/live3D preference; the old default "reference"
8810
+ // was auto-persisted for every new visitor, so it is not a reliable user preference.
8811
+ const legacy = localStorage.getItem("orch.hiveView");
8812
+ saved = legacy === "2d" || legacy === "live3d" ? legacy : "live3d";
8813
+ localStorage.setItem("orch.hiveView.v2", saved);
8814
+ }
8815
+ } catch (e) { /* ignore storage restrictions and keep the live 3D default */ }
8816
+ setHiveSceneMode(["reference", "live3d", "2d"].includes(saved) ? saved : "live3d", { silent: true });
8702
8817
  }
8703
8818
 
8704
8819
  /* renderHive → 场景数据翻译:泳道=蜂巢塔的一层,格子按状态映射高度/颜色。
@@ -8758,7 +8873,8 @@ function hiveSceneSync(run, lanes, byStage) {
8758
8873
  const sc = ensureHiveScene();
8759
8874
  if (!sc) return;
8760
8875
  sc.sync(model); // 2D 模式也同步:切回 3D 时数据即 ready(画布未激活零 GPU 开销)
8761
- sc.setActive(hiveMode === "3d");
8876
+ sc.setDisplayMode(hiveMode);
8877
+ sc.setActive(hiveMode !== "2d");
8762
8878
  }
8763
8879
 
8764
8880
  window.renderHive = function (run) {
@@ -9322,6 +9438,20 @@ function chatThinkHTML(it) {
9322
9438
  '<div class="ct-body">' + esc(think) + "</div></details>";
9323
9439
  }
9324
9440
 
9441
+ function chatAcpEventsHTML(it) {
9442
+ const events = Array.isArray(it.acp_events) ? it.acp_events.slice(-8) : [];
9443
+ if (!events.length) return "";
9444
+ return '<details class="chat-think chat-acp-events"><summary>' + esc(t("ACP 活动")) +
9445
+ '<span class="ct-len">' + events.length + ' ' + esc(t("条")) + '</span></summary>' +
9446
+ '<div class="ct-acts">' + events.map((event) => {
9447
+ const type = String(event.sessionUpdate || "event");
9448
+ const label = event.title || event.status || event.modeId || "";
9449
+ const entries = Array.isArray(event.entries) ? event.entries.map((entry) =>
9450
+ '<div class="rd-acp-plan-entry">' + esc(entry.status || "") + ' · ' + esc(entry.content || "") + '</div>').join("") : "";
9451
+ return '<div><code>' + esc(type) + '</code>' + (label ? ' · ' + esc(label) : "") + entries + '</div>';
9452
+ }).join("") + '</div></details>';
9453
+ }
9454
+
9325
9455
  /* 运行中直连对话的快轮询:SSE/轮询最密 2s、闲时 8s,思考是逐句吐的——
9326
9456
  * 1.2s 直拉时间线把增量打印出来。run 终态、切详情、分区隐藏即停。 */
9327
9457
  function stopChatLive() {
@@ -9424,6 +9554,7 @@ function drawChatFlow(run, data, active) {
9424
9554
  (it.status || "") + ":" + String(it.text || "").length + ":" +
9425
9555
  String(it.thinking || "").length + ":" + String(it.stream || "").length + ":" +
9426
9556
  (it.activity || []).length + ":" + (it.followups || []).length +
9557
+ ":" + (it.acp_events || []).length +
9427
9558
  ":" + (it.consumed ? 1 : 0), "") + "|" + ((data && data.result) ? "r" : "");
9428
9559
  if (liveSig === chatLiveSig && flow.childElementCount) return;
9429
9560
  chatLiveSig = liveSig;
@@ -9497,6 +9628,7 @@ function drawChatFlow(run, data, active) {
9497
9628
  '<span class="chat-avatar" aria-hidden="true"><svg class="ico"><use href="#i-bee"></use></svg></span>' +
9498
9629
  '<div class="chat-bubble agent">' +
9499
9630
  chatThinkHTML(it) +
9631
+ chatAcpEventsHTML(it) +
9500
9632
  '<div class="chat-body">' + bodyHtml + "</div>" +
9501
9633
  '<div class="chat-meta">' + esc(it.who || "") + " " + esc(chatTime(it.at)) +
9502
9634
  (metaBad ? " · " + t(it.status === "failed" ? "失败" : "已取消") : "") + actsA + "</div>" +
@@ -10389,7 +10521,7 @@ function codeThemeFor(mode) {
10389
10521
  return ok ? ok.id : (mode === "dark" ? "github-dark" : "github");
10390
10522
  }
10391
10523
  function codeLineNum() { return localStorage.getItem(CT_LINENUM_KEY) !== "0"; } // 缺省开
10392
- function codeWrap() { return localStorage.getItem(CT_WRAP_KEY) === "1"; }
10524
+ function codeWrap() { return localStorage.getItem(CT_WRAP_KEY) !== "0"; } // 缺省开:长行自动换行,别撑出横向滚动条
10393
10525
  function codeFontSize() {
10394
10526
  const n = parseFloat(localStorage.getItem(CT_SIZE_KEY) || "12.5");
10395
10527
  return isFinite(n) ? Math.min(22, Math.max(10, n)) : 12.5;
@@ -16070,6 +16202,9 @@ window.suRestart = suRestart;
16070
16202
  window.updDismiss = updDismiss;
16071
16203
 
16072
16204
  document.addEventListener("DOMContentLoaded", () => {
16205
+ // Initialize Hive3D controls independently of run data. A fresh workspace may have no
16206
+ // active run yet, but fullscreen/view controls must still be wired and usable.
16207
+ setupHiveSceneControls();
16073
16208
  // 远程地址里带的 ?token= 存起来并从地址栏抹掉,之后所有请求走请求头
16074
16209
  const urlTok = new URLSearchParams(location.search).get("token");
16075
16210
  if (urlTok) {
@@ -16411,6 +16546,8 @@ document.addEventListener("DOMContentLoaded", () => {
16411
16546
  localStorage.removeItem("orch.showArchived"); // 清掉旧版持久化残留,避免误解为默认选中
16412
16547
  $("f-resume-agent").addEventListener("change", loadSessions);
16413
16548
  $("f-resume-session").addEventListener("change", showResumeHint);
16549
+ $("f-sandbox-backend").addEventListener("change", syncSandboxComposer);
16550
+ syncSandboxComposer();
16414
16551
  /* 工作目录不再用「上次用过」的旧记忆预填:那样改默认保存路径后,新建任务仍会被
16415
16552
  * 旧目录盖过。首屏默认选中设置里的默认路径(loadSettings 落值),克隆/右键新建
16416
16553
  * 等显式入口各自直接写字段值,不跨刷新持久化。清掉旧版残留键,避免误解为默认选中。 */