dsh-conversation-overview 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +122 -0
- package/README.md +29 -122
- package/client.js +2279 -146
- package/docs/CONFIGURATION.md +40 -0
- package/locale/zh.json +1 -1
- package/package.json +4 -6
- package/.scratch/test.mjs +0 -1556
package/client.js
CHANGED
|
@@ -24,6 +24,40 @@ window.__ModuleLoader__.load({
|
|
|
24
24
|
as clickable chips and external links open in a new tab; borrow the provider too. */
|
|
25
25
|
const ChatMarkdownProvider = primitives !== null && typeof primitives.MarkdownDelegateProvider === "function" ? primitives.MarkdownDelegateProvider : null;
|
|
26
26
|
|
|
27
|
+
/* Floating layers must not live inside the overview's sticky toolbar: that wrapper
|
|
28
|
+
is transform-pinned (and carries a backdrop-filter once stuck), and both make a
|
|
29
|
+
containing block for `position: fixed` children, so viewport-anchored popovers
|
|
30
|
+
land in the wrong place after the conversation scrolls. The host chat renders its
|
|
31
|
+
own popovers through react-dom portals for the same reason. */
|
|
32
|
+
const ReactDOM = (() => {
|
|
33
|
+
try {
|
|
34
|
+
const module = require("react-dom");
|
|
35
|
+
return module && typeof module.createPortal === "function" ? module : null;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
})();
|
|
40
|
+
const createPortal = ReactDOM !== null ? ReactDOM.createPortal : null;
|
|
41
|
+
let overlayHost = null;
|
|
42
|
+
|
|
43
|
+
const overlayNode = () => {
|
|
44
|
+
if (createPortal === null) return null;
|
|
45
|
+
if (overlayHost !== null && overlayHost.isConnected !== false) return overlayHost;
|
|
46
|
+
if (typeof document === "undefined" || !document.body || typeof document.body.appendChild !== "function") return null;
|
|
47
|
+
const host = document.createElement("div");
|
|
48
|
+
host.className = "dsov-overlayHost";
|
|
49
|
+
host.setAttribute("data-dsov-overlay", "true");
|
|
50
|
+
document.body.appendChild(host);
|
|
51
|
+
overlayHost = host;
|
|
52
|
+
return host;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** Portal a fixed-position popover to the body, or render it in place without a DOM. */
|
|
56
|
+
const renderOverlay = (node) => {
|
|
57
|
+
const host = overlayNode();
|
|
58
|
+
return host === null ? node : createPortal(node, host);
|
|
59
|
+
};
|
|
60
|
+
|
|
27
61
|
function cx(...parts) {
|
|
28
62
|
return parts.filter((part) => typeof part === "string" && part.length > 0).join(" ");
|
|
29
63
|
}
|
|
@@ -43,7 +77,7 @@ window.__ModuleLoader__.load({
|
|
|
43
77
|
const RELEASE_TAGS_API_URL = "https://api.github.com/repos/LingF/dsh-conversation-overview/tags?per_page=100";
|
|
44
78
|
const TAGS_URL = "https://github.com/LingF/dsh-conversation-overview/tags";
|
|
45
79
|
/* keep in sync with package.json */
|
|
46
|
-
const PLUGIN_VERSION = "1.
|
|
80
|
+
const PLUGIN_VERSION = "1.7.0";
|
|
47
81
|
const LATEST_CACHE_KEY = "dsh.conversationOverview.latestRelease";
|
|
48
82
|
const LATEST_CACHE_TTL = 6 * 60 * 60 * 1000;
|
|
49
83
|
const latestStore = { tag: null, via: "release", loaded: false, inflight: null };
|
|
@@ -204,7 +238,7 @@ window.__ModuleLoader__.load({
|
|
|
204
238
|
const zh = {
|
|
205
239
|
"view.tab": "概览",
|
|
206
240
|
"panel.title": "概览",
|
|
207
|
-
"guide.title": "
|
|
241
|
+
"guide.title": "对话概览",
|
|
208
242
|
"guide.description": "进展、产物与引用",
|
|
209
243
|
"section.progress": "进展",
|
|
210
244
|
"section.artifacts": "产物",
|
|
@@ -234,12 +268,19 @@ window.__ModuleLoader__.load({
|
|
|
234
268
|
"progress.todoCopied": "未找到对话框,提示词已复制到剪贴板",
|
|
235
269
|
"progress.todoFailed": "填入失败,请重试",
|
|
236
270
|
"progress.waiting": "等待确认",
|
|
271
|
+
"progress.live": "进行中",
|
|
272
|
+
"progress.liveThinking": "正在生成…",
|
|
273
|
+
"progress.liveElapsed": "已 {v}",
|
|
274
|
+
"progress.liveElapsedTitle": "当前步骤已用时;没有执行中的步骤时为本轮用时",
|
|
237
275
|
"progress.counts": "{completed}/{total} 已完成",
|
|
276
|
+
"progress.meterTitle": "{completed}/{total} 已完成 · {running} 进行中 · {pending} 待处理",
|
|
277
|
+
"progress.leftover": "{running} 进行中 · {pending} 待处理",
|
|
278
|
+
"progress.staleHint": "待办状态取自执行方的最后一次更新;本轮结束时仍有未完成项",
|
|
238
279
|
"artifacts.empty": "本次对话产出的文件将在此展示",
|
|
239
280
|
"artifacts.viewAll": "查看所有改动",
|
|
240
281
|
"settings.summary": "控制概览的板块与显示位置",
|
|
241
282
|
"settings.nav": "概览",
|
|
242
|
-
"settings.pageTitle": "
|
|
283
|
+
"settings.pageTitle": "对话概览(dsh-conversation-overview)",
|
|
243
284
|
"settings.sections": "板块显示",
|
|
244
285
|
"settings.showProgress": "进展(任务与待办)",
|
|
245
286
|
"settings.showArtifacts": "产物(本次对话产出)",
|
|
@@ -367,6 +408,49 @@ window.__ModuleLoader__.load({
|
|
|
367
408
|
"stats.errors.list": "失败详情({n})",
|
|
368
409
|
"stats.errors.truncated": "仅列出步骤窗口内的失败",
|
|
369
410
|
"stats.errors.close": "关闭",
|
|
411
|
+
"stats.more": "更多",
|
|
412
|
+
"stats.more.title": "本轮统计:步数、模型、用时与 Token 用量",
|
|
413
|
+
"stats.more.head": "本轮统计",
|
|
414
|
+
"stats.more.headAll": "会话统计",
|
|
415
|
+
"stats.more.turn": "第 {n} 轮",
|
|
416
|
+
"stats.more.allTurns": "全部轮次",
|
|
417
|
+
"stats.more.turns": "轮数",
|
|
418
|
+
"stats.more.turnsValue": "{n} 轮",
|
|
419
|
+
"stats.more.turnsHint": "轮数:至少有一个已结束步(step/end)的轮次数。",
|
|
420
|
+
"stats.more.steps": "步数",
|
|
421
|
+
"stats.more.stepsValue": "{n} 步",
|
|
422
|
+
"stats.more.stepsHint": "步数:该轮 step/end 事件数,与宿主统计中的「步」一致。",
|
|
423
|
+
"stats.more.model": "模型",
|
|
424
|
+
"stats.more.modelHint": "模型:该轮 assistant/message 上报的提供方/模型,按首次出现顺序去重。",
|
|
425
|
+
"stats.more.unknown": "—",
|
|
426
|
+
"stats.more.firstToken": "本轮首字延迟",
|
|
427
|
+
"stats.more.firstTokenHint": "本轮首字延迟:本轮第一个 step/start → 本轮第一个输出 token(文本、推理或工具调用)的时间,包含请求组装与排队。",
|
|
428
|
+
"stats.more.llmTime": "模型用时",
|
|
429
|
+
"stats.more.llmTimeHint": "模型用时:各步 step/start → assistant/message 的累计时长;未落定消息的步不计。",
|
|
430
|
+
"stats.more.toolTime": "工具用时",
|
|
431
|
+
"stats.more.toolTimeHint": "工具用时:tool/call → tool/result 按 callId 配对后的累计时长。",
|
|
432
|
+
"stats.more.ttft": "首 token 平均",
|
|
433
|
+
"stats.more.ttftHint": "首 token 平均:各步 step/start → 该步第一个输出 token(文本、推理或工具调用)的平均等待,只计有首 token 的步,与宿主同口径。",
|
|
434
|
+
"stats.more.speed": "输出速度",
|
|
435
|
+
"stats.more.speedValue": "{v} tok/s",
|
|
436
|
+
"stats.more.speedHint": "输出速度:首 token → 消息落定期间,输出 token ÷ 解码时长。",
|
|
437
|
+
"stats.more.tokens": "Token 用量",
|
|
438
|
+
"stats.more.tokenTotal": "总量",
|
|
439
|
+
"stats.more.tokenValue": "{count} tok",
|
|
440
|
+
"stats.more.tokensHint": "Token 用量:提供方上报的 usage 累计;总量 = 未缓存输入 + 缓存读取 + 缓存写入 + 输出。",
|
|
441
|
+
"stats.more.cacheHit": "缓存命中",
|
|
442
|
+
"stats.more.cacheHitHint": "缓存命中:缓存读取 ÷(未缓存输入 + 缓存读取 + 缓存写入)。",
|
|
443
|
+
"stats.more.input": "未缓存输入",
|
|
444
|
+
"stats.more.inputHint": "未缓存输入:每次请求中未被缓存命中的 prompt token。",
|
|
445
|
+
"stats.more.cacheRead": "缓存读取",
|
|
446
|
+
"stats.more.cacheReadHint": "缓存读取:命中前缀缓存、按缓存价计费的 prompt token。",
|
|
447
|
+
"stats.more.cacheWrite": "缓存写入",
|
|
448
|
+
"stats.more.cacheWriteHint": "缓存写入:写入缓存的 prompt token,部分提供方不上报。",
|
|
449
|
+
"stats.more.output": "输出",
|
|
450
|
+
"stats.more.outputHint": "输出:模型生成的 completion token,含推理。",
|
|
451
|
+
"stats.more.reasoning": "其中推理",
|
|
452
|
+
"stats.more.reasoningHint": "推理:输出 token 中属于推理过程的部分,部分提供方不上报。",
|
|
453
|
+
"stats.more.close": "关闭统计",
|
|
370
454
|
"reference.types": "类型",
|
|
371
455
|
"reference.types.title": "按类型筛选",
|
|
372
456
|
"reference.types.all": "全部类型",
|
|
@@ -446,6 +530,53 @@ window.__ModuleLoader__.load({
|
|
|
446
530
|
"role.parentPrefix": "·",
|
|
447
531
|
"state.noSession": "当前没有打开的会话",
|
|
448
532
|
"state.error": "概览渲染失败",
|
|
533
|
+
"planWorkbench.action": "批注并修改",
|
|
534
|
+
"planWorkbench.actionHint": "逐段批注或直接改写计划,提交后请求模型修订;与「要求修改」同级的细粒度方式",
|
|
535
|
+
"planWorkbench.openFailed": "侧边栏不可用,无法打开计划工作台",
|
|
536
|
+
"planWorkbench.tabTitle": "计划工作台",
|
|
537
|
+
"planWorkbench.reviewPending": "待审阅",
|
|
538
|
+
"planWorkbench.reviewClosed": "审阅已结束",
|
|
539
|
+
"planWorkbench.modes": "工作台模式",
|
|
540
|
+
"planWorkbench.modeAnnotate": "批注",
|
|
541
|
+
"planWorkbench.modeEdit": "改写",
|
|
542
|
+
"planWorkbench.openOriginal": "打开原计划",
|
|
543
|
+
"planWorkbench.open": "计划操作",
|
|
544
|
+
"planWorkbench.whole": "整体意见",
|
|
545
|
+
"planWorkbench.preface": "开头",
|
|
546
|
+
"planWorkbench.addHere": "在此处批注",
|
|
547
|
+
"planWorkbench.addAria": "在「{heading}」处批注",
|
|
548
|
+
"planWorkbench.commentPlaceholder": "这一节要怎么改…",
|
|
549
|
+
"planWorkbench.commentAria": "批注「{heading}」",
|
|
550
|
+
"planWorkbench.save": "保存批注",
|
|
551
|
+
"planWorkbench.cancel": "取消",
|
|
552
|
+
"planWorkbench.remove": "删除批注",
|
|
553
|
+
"planWorkbench.editHint": "直接改写全文;提交时以这份修订稿为准。",
|
|
554
|
+
"planWorkbench.editAria": "编辑计划全文",
|
|
555
|
+
"planWorkbench.editSave": "保存草稿",
|
|
556
|
+
"planWorkbench.editSaved": "草稿已保存",
|
|
557
|
+
"planWorkbench.revert": "恢复原文",
|
|
558
|
+
"planWorkbench.count": "{n} 条批注",
|
|
559
|
+
"planWorkbench.empty": "还没有批注或改写",
|
|
560
|
+
"planWorkbench.submitReview": "提交批注并请求修订",
|
|
561
|
+
"planWorkbench.submitDraft": "带入输入框",
|
|
562
|
+
"planWorkbench.submitComposer": "插入输入框",
|
|
563
|
+
"planWorkbench.submitting": "提交中…",
|
|
564
|
+
"planWorkbench.copyFeedback": "复制反馈",
|
|
565
|
+
"planWorkbench.submitted": "已提交,等待模型修订计划",
|
|
566
|
+
"planWorkbench.drafted": "已带入输入框,检查后可发送",
|
|
567
|
+
"planWorkbench.copied": "已复制",
|
|
568
|
+
"planWorkbench.copyFailed": "复制失败,请手动选择文本",
|
|
569
|
+
"planWorkbench.failed": "提交失败",
|
|
570
|
+
"planWorkbench.unavailable": "计划内容不可用(宿主计划页或会话记录缺失),可打开原计划查看。",
|
|
571
|
+
"planWorkbench.untitled": "未命名计划",
|
|
572
|
+
"planWorkbench.menuAnnotate": "批注修改",
|
|
573
|
+
"planWorkbench.menuCopy": "复制计划",
|
|
574
|
+
"planWorkbench.feedbackIntro": "我在计划《{title}》上做了批注,请据此修订计划;若仍处于计划模式,修订后请重新调用 exit_plan_mode 提交,先不要执行。",
|
|
575
|
+
"planWorkbench.feedbackHead": "{n}. 「{heading}」",
|
|
576
|
+
"planWorkbench.feedbackQuote": "原文:{quote}",
|
|
577
|
+
"planWorkbench.feedbackComment": "批注:{comment}",
|
|
578
|
+
"planWorkbench.feedbackEdit": "另外,我直接改写了计划,请以这份修订稿为准(并回应上面的批注):",
|
|
579
|
+
"planWorkbench.feedbackOutro": "未涉及的部分保持不变。",
|
|
449
580
|
};
|
|
450
581
|
|
|
451
582
|
const en = {
|
|
@@ -476,12 +607,19 @@ window.__ModuleLoader__.load({
|
|
|
476
607
|
"progress.todoCopied": "Composer not found — prompt copied to the clipboard",
|
|
477
608
|
"progress.todoFailed": "Could not fill the composer, try again",
|
|
478
609
|
"progress.waiting": "Waiting for confirmation",
|
|
610
|
+
"progress.live": "Live",
|
|
611
|
+
"progress.liveThinking": "Generating…",
|
|
612
|
+
"progress.liveElapsed": "{v} in",
|
|
613
|
+
"progress.liveElapsedTitle": "Elapsed for the current step; for this turn while no step is running",
|
|
479
614
|
"team.phase.provisioning": "Provisioning",
|
|
480
615
|
"team.phase.active": "Active",
|
|
481
616
|
"team.phase.inactive": "Inactive",
|
|
482
617
|
"team.phase.failed": "Failed",
|
|
483
618
|
"team.blocked": "Blocked {n}",
|
|
484
619
|
"progress.counts": "{completed} of {total} completed",
|
|
620
|
+
"progress.meterTitle": "{completed} of {total} completed · {running} in progress · {pending} pending",
|
|
621
|
+
"progress.leftover": "{running} in progress · {pending} pending",
|
|
622
|
+
"progress.staleHint": "Todo state follows the agent's last update; items were still open when the turn ended",
|
|
485
623
|
"artifacts.empty": "Files produced in this conversation appear here",
|
|
486
624
|
"artifacts.viewAll": "Review all changes",
|
|
487
625
|
"settings.summary": "Control the overview sections and where they appear",
|
|
@@ -614,6 +752,49 @@ window.__ModuleLoader__.load({
|
|
|
614
752
|
"stats.errors.list": "Failure details ({n})",
|
|
615
753
|
"stats.errors.truncated": "Only failures within the step window are listed",
|
|
616
754
|
"stats.errors.close": "Close",
|
|
755
|
+
"stats.more": "More",
|
|
756
|
+
"stats.more.title": "Turn statistics: steps, model, timings and token usage",
|
|
757
|
+
"stats.more.head": "Turn statistics",
|
|
758
|
+
"stats.more.headAll": "Session statistics",
|
|
759
|
+
"stats.more.turn": "Turn {n}",
|
|
760
|
+
"stats.more.allTurns": "All turns",
|
|
761
|
+
"stats.more.turns": "Turns",
|
|
762
|
+
"stats.more.turnsValue": "{n} turns",
|
|
763
|
+
"stats.more.turnsHint": "Turns: turns with at least one closed step (step/end).",
|
|
764
|
+
"stats.more.steps": "Steps",
|
|
765
|
+
"stats.more.stepsValue": "{n} steps",
|
|
766
|
+
"stats.more.stepsHint": "Steps: step/end events in this scope, the harness's step count.",
|
|
767
|
+
"stats.more.model": "Model",
|
|
768
|
+
"stats.more.modelHint": "Models: provider/model reported by assistant/message, deduplicated in first-seen order.",
|
|
769
|
+
"stats.more.unknown": "—",
|
|
770
|
+
"stats.more.firstToken": "First token",
|
|
771
|
+
"stats.more.firstTokenHint": "Time from this turn's first step/start to its first output token (text, reasoning or tool call), including request assembly and queueing.",
|
|
772
|
+
"stats.more.llmTime": "Model time",
|
|
773
|
+
"stats.more.llmTimeHint": "Summed step/start to assistant/message wall time; steps whose message never settled are not counted.",
|
|
774
|
+
"stats.more.toolTime": "Tool time",
|
|
775
|
+
"stats.more.toolTimeHint": "Summed tool/call to tool/result wall time, paired by callId.",
|
|
776
|
+
"stats.more.ttft": "Avg first token",
|
|
777
|
+
"stats.more.ttftHint": "Average step/start to first output token per step (text, reasoning or tool call), over steps that reported a first token; matches the harness rule.",
|
|
778
|
+
"stats.more.speed": "Output speed",
|
|
779
|
+
"stats.more.speedValue": "{v} tok/s",
|
|
780
|
+
"stats.more.speedHint": "Output tokens divided by decode time from the first token to the assembled message.",
|
|
781
|
+
"stats.more.tokens": "Token usage",
|
|
782
|
+
"stats.more.tokenTotal": "Total",
|
|
783
|
+
"stats.more.tokenValue": "{count} tok",
|
|
784
|
+
"stats.more.tokensHint": "Provider-reported usage; total = uncached input + cache read + cache write + output.",
|
|
785
|
+
"stats.more.cacheHit": "Cache hit",
|
|
786
|
+
"stats.more.cacheHitHint": "Cache read divided by (uncached input + cache read + cache write).",
|
|
787
|
+
"stats.more.input": "Uncached input",
|
|
788
|
+
"stats.more.inputHint": "Prompt tokens that were not served from cache.",
|
|
789
|
+
"stats.more.cacheRead": "Cache read",
|
|
790
|
+
"stats.more.cacheReadHint": "Prompt tokens served from the prefix cache, billed at the cache rate.",
|
|
791
|
+
"stats.more.cacheWrite": "Cache write",
|
|
792
|
+
"stats.more.cacheWriteHint": "Prompt tokens written to cache; not every provider reports them.",
|
|
793
|
+
"stats.more.output": "Output",
|
|
794
|
+
"stats.more.outputHint": "Generated completion tokens, including reasoning.",
|
|
795
|
+
"stats.more.reasoning": "Reasoning",
|
|
796
|
+
"stats.more.reasoningHint": "Output tokens spent on reasoning; not every provider reports them.",
|
|
797
|
+
"stats.more.close": "Close statistics",
|
|
617
798
|
"reference.types": "Types",
|
|
618
799
|
"reference.types.title": "Filter by type",
|
|
619
800
|
"reference.types.all": "All types",
|
|
@@ -693,6 +874,53 @@ window.__ModuleLoader__.load({
|
|
|
693
874
|
"role.parentPrefix": "·",
|
|
694
875
|
"state.noSession": "No session is open",
|
|
695
876
|
"state.error": "Overview failed to render",
|
|
877
|
+
"planWorkbench.action": "Annotate & revise",
|
|
878
|
+
"planWorkbench.actionHint": "Annotate each section or rewrite the plan, then request the revision; a finer-grained alternative to Request changes",
|
|
879
|
+
"planWorkbench.openFailed": "No sidebar available to open the plan workbench",
|
|
880
|
+
"planWorkbench.tabTitle": "Plan workbench",
|
|
881
|
+
"planWorkbench.reviewPending": "Awaiting review",
|
|
882
|
+
"planWorkbench.reviewClosed": "Review closed",
|
|
883
|
+
"planWorkbench.modes": "Workbench mode",
|
|
884
|
+
"planWorkbench.modeAnnotate": "Annotate",
|
|
885
|
+
"planWorkbench.modeEdit": "Rewrite",
|
|
886
|
+
"planWorkbench.openOriginal": "Open plan",
|
|
887
|
+
"planWorkbench.open": "Plan actions",
|
|
888
|
+
"planWorkbench.whole": "Whole plan",
|
|
889
|
+
"planWorkbench.preface": "Opening",
|
|
890
|
+
"planWorkbench.addHere": "Annotate here",
|
|
891
|
+
"planWorkbench.addAria": "Annotate “{heading}”",
|
|
892
|
+
"planWorkbench.commentPlaceholder": "How should this section change…",
|
|
893
|
+
"planWorkbench.commentAria": "Annotation for “{heading}”",
|
|
894
|
+
"planWorkbench.save": "Save annotation",
|
|
895
|
+
"planWorkbench.cancel": "Cancel",
|
|
896
|
+
"planWorkbench.remove": "Delete annotation",
|
|
897
|
+
"planWorkbench.editHint": "Rewrite the plan directly; submitting sends this revision as the plan.",
|
|
898
|
+
"planWorkbench.editAria": "Edit the plan markdown",
|
|
899
|
+
"planWorkbench.editSave": "Save draft",
|
|
900
|
+
"planWorkbench.editSaved": "Draft saved",
|
|
901
|
+
"planWorkbench.revert": "Revert to original",
|
|
902
|
+
"planWorkbench.count": "{n} annotation(s)",
|
|
903
|
+
"planWorkbench.empty": "No annotations or rewrite yet",
|
|
904
|
+
"planWorkbench.submitReview": "Submit for revision",
|
|
905
|
+
"planWorkbench.submitDraft": "Put in composer",
|
|
906
|
+
"planWorkbench.submitComposer": "Insert into composer",
|
|
907
|
+
"planWorkbench.submitting": "Submitting…",
|
|
908
|
+
"planWorkbench.copyFeedback": "Copy feedback",
|
|
909
|
+
"planWorkbench.submitted": "Submitted; waiting for the revised plan",
|
|
910
|
+
"planWorkbench.drafted": "Put in the composer; review before sending",
|
|
911
|
+
"planWorkbench.copied": "Copied",
|
|
912
|
+
"planWorkbench.copyFailed": "Copy failed; select the text manually",
|
|
913
|
+
"planWorkbench.failed": "Submit failed",
|
|
914
|
+
"planWorkbench.unavailable": "Plan text unavailable (host plan page or session log missing). Open the original plan instead.",
|
|
915
|
+
"planWorkbench.untitled": "Untitled plan",
|
|
916
|
+
"planWorkbench.menuAnnotate": "Annotate",
|
|
917
|
+
"planWorkbench.menuCopy": "Copy plan",
|
|
918
|
+
"planWorkbench.feedbackIntro": "I annotated the plan “{title}”. Please revise the plan accordingly; if still in plan mode, call exit_plan_mode again with the revision and do not start executing yet.",
|
|
919
|
+
"planWorkbench.feedbackHead": "{n}. “{heading}”",
|
|
920
|
+
"planWorkbench.feedbackQuote": "Quote: {quote}",
|
|
921
|
+
"planWorkbench.feedbackComment": "Annotation: {comment}",
|
|
922
|
+
"planWorkbench.feedbackEdit": "I also rewrote the plan directly; treat this revision as the source of truth (and address the annotations above):",
|
|
923
|
+
"planWorkbench.feedbackOutro": "Keep everything else unchanged.",
|
|
696
924
|
};
|
|
697
925
|
|
|
698
926
|
/* ------------------------------------------------------------------ */
|
|
@@ -1622,22 +1850,12 @@ window.__ModuleLoader__.load({
|
|
|
1622
1850
|
opens. The plan plugin re-resolves the markdown from the session log by
|
|
1623
1851
|
tool-call id, so this address stays valid across reloads. */
|
|
1624
1852
|
function openNativePlan(sessionId, callId) {
|
|
1625
|
-
|
|
1853
|
+
const address = planResourceAddress(sessionId, callId);
|
|
1854
|
+
if (address === undefined) return false;
|
|
1626
1855
|
const sidebar = runtime && runtime.ctx ? runtime.ctx.sidebarRight : undefined;
|
|
1627
1856
|
if (sidebar === undefined || typeof sidebar.openResource !== "function") return false;
|
|
1628
|
-
let segments;
|
|
1629
|
-
try {
|
|
1630
|
-
const sessions = runtime.ctx.sessions;
|
|
1631
|
-
const child = sessions !== undefined && typeof sessions.subagentAddress === "function" ? sessions.subagentAddress(sessionId) : undefined;
|
|
1632
|
-
segments =
|
|
1633
|
-
child === undefined
|
|
1634
|
-
? [sessionId, callId]
|
|
1635
|
-
: ["subagent", child.parentSessionId, child.childSessionId, child.mode, callId];
|
|
1636
|
-
} catch {
|
|
1637
|
-
segments = [sessionId, callId]; /* uninjected ctx access throws; plain sessions are the common case */
|
|
1638
|
-
}
|
|
1639
1857
|
try {
|
|
1640
|
-
sidebar.openResource(
|
|
1858
|
+
sidebar.openResource(address);
|
|
1641
1859
|
return true;
|
|
1642
1860
|
} catch {
|
|
1643
1861
|
return false;
|
|
@@ -1883,7 +2101,7 @@ window.__ModuleLoader__.load({
|
|
|
1883
2101
|
}
|
|
1884
2102
|
|
|
1885
2103
|
function createTurnBucket() {
|
|
1886
|
-
return { artifacts: new Map(), refs: new Map(), refOrder: [], changes: [], steps: [], anchors: [], members: new Map(), teamTasks: new Map(), subagents: new Map(), stats: createStats(), conclusion: null, lastReply: null, ended: false };
|
|
2104
|
+
return { artifacts: new Map(), refs: new Map(), refOrder: [], changes: [], steps: [], anchors: [], members: new Map(), teamTasks: new Map(), subagents: new Map(), stats: createStats(), metrics: createTurnMetrics(), conclusion: null, lastReply: null, ended: false };
|
|
1887
2105
|
}
|
|
1888
2106
|
|
|
1889
2107
|
const EMPTY_BUCKET = Object.freeze({ artifacts: new Map(), refs: new Map(), refOrder: [], changes: [], steps: [], anchors: [], members: new Map(), teamTasks: new Map(), subagents: new Map(), stats: Object.freeze(createStats()) });
|
|
@@ -1897,6 +2115,315 @@ window.__ModuleLoader__.load({
|
|
|
1897
2115
|
return bucket;
|
|
1898
2116
|
}
|
|
1899
2117
|
|
|
2118
|
+
/* ------------------------------------------------------------------ */
|
|
2119
|
+
/* Per-turn session statistics (the 「更多」 panel) */
|
|
2120
|
+
/* ------------------------------------------------------------------ */
|
|
2121
|
+
|
|
2122
|
+
/** The readings behind the chat's session-statistics pill, folded per turn:
|
|
2123
|
+
closed steps, model/tool wall time, first-token latency, decode throughput
|
|
2124
|
+
and provider-reported token usage. The rules mirror the host's
|
|
2125
|
+
`sessionStats` / `tokenUsage` folds so both surfaces agree. */
|
|
2126
|
+
function createTurnMetrics() {
|
|
2127
|
+
return {
|
|
2128
|
+
steps: 0,
|
|
2129
|
+
llmMs: 0,
|
|
2130
|
+
toolMs: 0,
|
|
2131
|
+
ttftMs: 0,
|
|
2132
|
+
ttftSteps: 0,
|
|
2133
|
+
decodeMs: 0,
|
|
2134
|
+
decodeTokens: 0,
|
|
2135
|
+
/* the turn's first step/start and the first token after it: 「本轮首字延迟」 */
|
|
2136
|
+
firstStepStartTime: null,
|
|
2137
|
+
firstTokenMs: null,
|
|
2138
|
+
tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, samples: 0 },
|
|
2139
|
+
routes: [],
|
|
2140
|
+
openStep: null,
|
|
2141
|
+
pendingCalls: new Map(),
|
|
2142
|
+
};
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
/** Record the turn's very first output token once; later steps never overwrite it.
|
|
2146
|
+
The clock starts at the turn's first `step/start`, so the wait includes that
|
|
2147
|
+
step's request assembly, not the pause between turns. */
|
|
2148
|
+
function recordTurnFirstToken(metrics, tokenTime) {
|
|
2149
|
+
if (tokenTime === null || metrics.firstTokenMs !== null || metrics.firstStepStartTime === null) return;
|
|
2150
|
+
metrics.firstTokenMs = Math.max(0, tokenTime - metrics.firstStepStartTime);
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
/** Whether one stream chunk carries the model's first output token (the host's
|
|
2154
|
+
`isTokenDelta`): non-empty text/reasoning delta, or a tool-call delta that
|
|
2155
|
+
already names its tool. */
|
|
2156
|
+
function isTokenDelta(chunk) {
|
|
2157
|
+
switch (chunk.type) {
|
|
2158
|
+
case "text-delta":
|
|
2159
|
+
case "reasoning-delta":
|
|
2160
|
+
return chunk.text !== "";
|
|
2161
|
+
case "tool-call-delta":
|
|
2162
|
+
return chunk.argumentsDelta !== "" || chunk.name !== undefined;
|
|
2163
|
+
default:
|
|
2164
|
+
return false;
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
/** Time of the first token in one compact durable stream, expanding packed
|
|
2169
|
+
delta runs through their `dt` gaps exactly like the host helper. */
|
|
2170
|
+
function streamFirstTokenTime(stream) {
|
|
2171
|
+
if (!Array.isArray(stream)) return null;
|
|
2172
|
+
for (const record of stream) {
|
|
2173
|
+
if (!isRecord(record)) continue;
|
|
2174
|
+
if (record.type === "chunk") {
|
|
2175
|
+
if (isRecord(record.chunk) && isTokenDelta(record.chunk) && typeof record.time === "number") return record.time;
|
|
2176
|
+
continue;
|
|
2177
|
+
}
|
|
2178
|
+
if (record.type === "text-chunks" || record.type === "reasoning-chunks") {
|
|
2179
|
+
const texts = Array.isArray(record.texts) ? record.texts : [];
|
|
2180
|
+
const dt = Array.isArray(record.dt) ? record.dt : [];
|
|
2181
|
+
let time = typeof record.time0 === "number" ? record.time0 : null;
|
|
2182
|
+
for (let index = 0; index < texts.length && time !== null; index += 1) {
|
|
2183
|
+
if (index > 0) time += typeof dt[index - 1] === "number" ? dt[index - 1] : 0;
|
|
2184
|
+
if (texts[index] !== "") return time;
|
|
2185
|
+
}
|
|
2186
|
+
continue;
|
|
2187
|
+
}
|
|
2188
|
+
if (record.type === "tool-call-chunks") {
|
|
2189
|
+
if (typeof record.time0 !== "number") continue;
|
|
2190
|
+
if (record.name !== undefined) return record.time0;
|
|
2191
|
+
const args = Array.isArray(record.args) ? record.args : [];
|
|
2192
|
+
const dt = Array.isArray(record.dt) ? record.dt : [];
|
|
2193
|
+
let time = record.time0;
|
|
2194
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
2195
|
+
if (index > 0) time += typeof dt[index - 1] === "number" ? dt[index - 1] : 0;
|
|
2196
|
+
if (args[index] !== "") return time;
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
return null;
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
/** The last raw chunk of one never-packed type in a compact stream. */
|
|
2204
|
+
function lastStreamChunk(stream, type) {
|
|
2205
|
+
if (!Array.isArray(stream)) return null;
|
|
2206
|
+
for (let index = stream.length - 1; index >= 0; index -= 1) {
|
|
2207
|
+
const record = stream[index];
|
|
2208
|
+
if (isRecord(record) && record.type === "chunk" && isRecord(record.chunk) && record.chunk.type === type) return record.chunk;
|
|
2209
|
+
}
|
|
2210
|
+
return null;
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
/** A provider count, or null when the adapter did not report a usable value. */
|
|
2214
|
+
function usableCount(value) {
|
|
2215
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
/** Fold one provider usage sample into a turn's token buckets. */
|
|
2219
|
+
function addTurnTokens(metrics, usage) {
|
|
2220
|
+
if (!isRecord(usage)) return;
|
|
2221
|
+
const input = usableCount(usage.inputTokens);
|
|
2222
|
+
const output = usableCount(usage.outputTokens);
|
|
2223
|
+
if (input === null || output === null) return;
|
|
2224
|
+
metrics.tokens.input += input;
|
|
2225
|
+
metrics.tokens.output += output;
|
|
2226
|
+
const cacheRead = usableCount(usage.cacheReadTokens);
|
|
2227
|
+
const cacheWrite = usableCount(usage.cacheWriteTokens);
|
|
2228
|
+
const reasoning = usableCount(usage.reasoningTokens);
|
|
2229
|
+
if (cacheRead !== null) metrics.tokens.cacheRead += cacheRead;
|
|
2230
|
+
if (cacheWrite !== null) metrics.tokens.cacheWrite += cacheWrite;
|
|
2231
|
+
if (reasoning !== null) metrics.tokens.reasoning += reasoning;
|
|
2232
|
+
metrics.tokens.samples += 1;
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
/** Read one stream's trailing usage chunk, when the adapter embedded one. */
|
|
2236
|
+
function streamUsage(stream) {
|
|
2237
|
+
const chunk = lastStreamChunk(stream, "usage");
|
|
2238
|
+
return chunk !== null && isRecord(chunk.usage) ? chunk.usage : null;
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
/** Fold the boundary events that carry per-turn session statistics. Runs beside
|
|
2242
|
+
the display fold: it only reads event data, never changes the message view. */
|
|
2243
|
+
function foldTurnMetrics(folder, event) {
|
|
2244
|
+
if (!isRecord(event) || !isRecord(event.data)) return;
|
|
2245
|
+
const data = event.data;
|
|
2246
|
+
const time = typeof event.time === "number" && Number.isFinite(event.time) ? event.time : null;
|
|
2247
|
+
const turnOf = () => {
|
|
2248
|
+
if (typeof data.turn === "number" && Number.isSafeInteger(data.turn) && data.turn >= 1) return data.turn;
|
|
2249
|
+
return folder.currentTurn >= 1 ? folder.currentTurn : null;
|
|
2250
|
+
};
|
|
2251
|
+
switch (event.type) {
|
|
2252
|
+
case "step/start": {
|
|
2253
|
+
const turn = turnOf();
|
|
2254
|
+
if (turn === null || time === null) return;
|
|
2255
|
+
const metrics = turnBucket(folder, turn).metrics;
|
|
2256
|
+
metrics.openStep = { turn, step: typeof data.step === "number" ? data.step : 0, startTime: time, firstTokenTime: null };
|
|
2257
|
+
if (metrics.firstStepStartTime === null) metrics.firstStepStartTime = time;
|
|
2258
|
+
touch(folder);
|
|
2259
|
+
return;
|
|
2260
|
+
}
|
|
2261
|
+
case "assistant/attempt": {
|
|
2262
|
+
const turn = turnOf();
|
|
2263
|
+
if (turn === null) return;
|
|
2264
|
+
const metrics = turnBucket(folder, turn).metrics;
|
|
2265
|
+
const open = metrics.openStep;
|
|
2266
|
+
if (open === null || open.turn !== turn || open.step !== data.step) return;
|
|
2267
|
+
if (open.firstTokenTime === null) open.firstTokenTime = streamFirstTokenTime(data.stream);
|
|
2268
|
+
recordTurnFirstToken(metrics, open.firstTokenTime);
|
|
2269
|
+
return;
|
|
2270
|
+
}
|
|
2271
|
+
case "assistant/message": {
|
|
2272
|
+
const turn = turnOf();
|
|
2273
|
+
if (turn === null) return;
|
|
2274
|
+
const metrics = turnBucket(folder, turn).metrics;
|
|
2275
|
+
const sample = isRecord(data.usage) ? data.usage : streamUsage(data.stream);
|
|
2276
|
+
addTurnTokens(metrics, sample);
|
|
2277
|
+
const body = isRecord(data.message) ? data.message : null;
|
|
2278
|
+
const source = body !== null && isRecord(body.source) ? body.source : null;
|
|
2279
|
+
if (source !== null && typeof source.provider === "string" && source.provider !== "" && typeof source.model === "string" && source.model !== "") {
|
|
2280
|
+
const route = `${source.provider}/${source.model}`;
|
|
2281
|
+
if (!metrics.routes.includes(route)) metrics.routes.push(route);
|
|
2282
|
+
}
|
|
2283
|
+
const open = metrics.openStep;
|
|
2284
|
+
if (open !== null && open.turn === turn && open.step === data.step && time !== null) {
|
|
2285
|
+
metrics.llmMs += Math.max(0, time - open.startTime);
|
|
2286
|
+
const firstToken = open.firstTokenTime !== null ? open.firstTokenTime : streamFirstTokenTime(data.stream);
|
|
2287
|
+
recordTurnFirstToken(metrics, firstToken);
|
|
2288
|
+
if (firstToken !== null) {
|
|
2289
|
+
metrics.ttftMs += Math.max(0, firstToken - open.startTime);
|
|
2290
|
+
metrics.ttftSteps += 1;
|
|
2291
|
+
const outputTokens = sample === null ? null : usableCount(sample.outputTokens);
|
|
2292
|
+
if (outputTokens !== null) {
|
|
2293
|
+
metrics.decodeMs += Math.max(0, time - firstToken);
|
|
2294
|
+
metrics.decodeTokens += outputTokens;
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
metrics.openStep = null;
|
|
2298
|
+
}
|
|
2299
|
+
touch(folder);
|
|
2300
|
+
return;
|
|
2301
|
+
}
|
|
2302
|
+
case "tool/call": {
|
|
2303
|
+
const turn = turnOf();
|
|
2304
|
+
const callId = typeof data.callId === "string" && data.callId !== "" ? data.callId : null;
|
|
2305
|
+
if (turn === null || callId === null || time === null) return;
|
|
2306
|
+
turnBucket(folder, turn).metrics.pendingCalls.set(callId, time);
|
|
2307
|
+
return;
|
|
2308
|
+
}
|
|
2309
|
+
case "tool/result": {
|
|
2310
|
+
const turn = turnOf();
|
|
2311
|
+
const message = isRecord(data.message) ? data.message : null;
|
|
2312
|
+
const source = message !== null && isRecord(message.source) ? message.source : null;
|
|
2313
|
+
const callId =
|
|
2314
|
+
source !== null && typeof source.callId === "string" && source.callId !== ""
|
|
2315
|
+
? source.callId
|
|
2316
|
+
: typeof data.callId === "string" && data.callId !== ""
|
|
2317
|
+
? data.callId
|
|
2318
|
+
: null;
|
|
2319
|
+
if (turn === null || callId === null || time === null) return;
|
|
2320
|
+
const metrics = turnBucket(folder, turn).metrics;
|
|
2321
|
+
const dispatched = metrics.pendingCalls.get(callId);
|
|
2322
|
+
if (dispatched === undefined) return;
|
|
2323
|
+
metrics.pendingCalls.delete(callId);
|
|
2324
|
+
metrics.toolMs += Math.max(0, time - dispatched);
|
|
2325
|
+
return;
|
|
2326
|
+
}
|
|
2327
|
+
case "step/end": {
|
|
2328
|
+
const turn = turnOf();
|
|
2329
|
+
if (turn === null) return;
|
|
2330
|
+
const metrics = turnBucket(folder, turn).metrics;
|
|
2331
|
+
metrics.steps += 1;
|
|
2332
|
+
metrics.openStep = null;
|
|
2333
|
+
touch(folder);
|
|
2334
|
+
return;
|
|
2335
|
+
}
|
|
2336
|
+
case "turn/end": {
|
|
2337
|
+
const turn = turnOf();
|
|
2338
|
+
if (turn === null) return;
|
|
2339
|
+
const metrics = turnBucket(folder, turn).metrics;
|
|
2340
|
+
/* A call whose result never landed belongs to a cancelled turn. */
|
|
2341
|
+
metrics.pendingCalls.clear();
|
|
2342
|
+
metrics.openStep = null;
|
|
2343
|
+
return;
|
|
2344
|
+
}
|
|
2345
|
+
default:
|
|
2346
|
+
return;
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
/** Snapshot one turn's metrics for the 「更多」 panel. */
|
|
2351
|
+
function turnDetailOf(turn, metrics) {
|
|
2352
|
+
if (!isRecord(metrics)) return null;
|
|
2353
|
+
const tokens =
|
|
2354
|
+
metrics.tokens.samples > 0
|
|
2355
|
+
? {
|
|
2356
|
+
input: metrics.tokens.input,
|
|
2357
|
+
output: metrics.tokens.output,
|
|
2358
|
+
cacheRead: metrics.tokens.cacheRead,
|
|
2359
|
+
cacheWrite: metrics.tokens.cacheWrite,
|
|
2360
|
+
reasoning: metrics.tokens.reasoning,
|
|
2361
|
+
total: metrics.tokens.input + metrics.tokens.output + metrics.tokens.cacheRead + metrics.tokens.cacheWrite,
|
|
2362
|
+
}
|
|
2363
|
+
: null;
|
|
2364
|
+
return {
|
|
2365
|
+
scope: "turn",
|
|
2366
|
+
turn,
|
|
2367
|
+
turns: 1,
|
|
2368
|
+
steps: metrics.steps,
|
|
2369
|
+
llmMs: metrics.llmMs,
|
|
2370
|
+
toolMs: metrics.toolMs,
|
|
2371
|
+
ttftMs: metrics.ttftMs,
|
|
2372
|
+
ttftSteps: metrics.ttftSteps,
|
|
2373
|
+
firstTokenMs: metrics.firstTokenMs,
|
|
2374
|
+
decodeMs: metrics.decodeMs,
|
|
2375
|
+
decodeTokens: metrics.decodeTokens,
|
|
2376
|
+
models: metrics.routes.slice(),
|
|
2377
|
+
tokens,
|
|
2378
|
+
};
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
/** Sum every observed turn into one session-wide detail (the 「全部轮次」 scope).
|
|
2382
|
+
A turn counts once it has a closed step, matching the host's counter. */
|
|
2383
|
+
function aggregateTurnDetail(entries) {
|
|
2384
|
+
const totals = { scope: "all", turn: null, turns: 0, steps: 0, llmMs: 0, toolMs: 0, ttftMs: 0, ttftSteps: 0, firstTokenMs: null, decodeMs: 0, decodeTokens: 0, models: [], tokens: null };
|
|
2385
|
+
let input = 0;
|
|
2386
|
+
let output = 0;
|
|
2387
|
+
let cacheRead = 0;
|
|
2388
|
+
let cacheWrite = 0;
|
|
2389
|
+
let reasoning = 0;
|
|
2390
|
+
let samples = 0;
|
|
2391
|
+
const models = [];
|
|
2392
|
+
for (const metrics of entries) {
|
|
2393
|
+
if (!isRecord(metrics)) continue;
|
|
2394
|
+
totals.steps += metrics.steps;
|
|
2395
|
+
if (metrics.steps > 0) totals.turns += 1;
|
|
2396
|
+
totals.llmMs += metrics.llmMs;
|
|
2397
|
+
totals.toolMs += metrics.toolMs;
|
|
2398
|
+
totals.ttftMs += metrics.ttftMs;
|
|
2399
|
+
totals.ttftSteps += metrics.ttftSteps;
|
|
2400
|
+
totals.decodeMs += metrics.decodeMs;
|
|
2401
|
+
totals.decodeTokens += metrics.decodeTokens;
|
|
2402
|
+
input += metrics.tokens.input;
|
|
2403
|
+
output += metrics.tokens.output;
|
|
2404
|
+
cacheRead += metrics.tokens.cacheRead;
|
|
2405
|
+
cacheWrite += metrics.tokens.cacheWrite;
|
|
2406
|
+
reasoning += metrics.tokens.reasoning;
|
|
2407
|
+
samples += metrics.tokens.samples;
|
|
2408
|
+
for (const route of metrics.routes) if (!models.includes(route)) models.push(route);
|
|
2409
|
+
}
|
|
2410
|
+
totals.models = models;
|
|
2411
|
+
totals.tokens = samples > 0 ? { input, output, cacheRead, cacheWrite, reasoning, total: input + output + cacheRead + cacheWrite } : null;
|
|
2412
|
+
return totals;
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
/** The 「更多」 detail for the current selection: one turn, or every loaded turn. */
|
|
2416
|
+
function turnDetailFor(folder, selection, latestTurn) {
|
|
2417
|
+
if (selection === "all") {
|
|
2418
|
+
const entries = [...folder.byTurn.entries()].filter(([turn, bucket]) => turn >= 1 && isRecord(bucket) && isRecord(bucket.metrics)).map(([, bucket]) => bucket.metrics);
|
|
2419
|
+
return entries.length > 0 ? aggregateTurnDetail(entries) : null;
|
|
2420
|
+
}
|
|
2421
|
+
const turn = selection.startsWith("turn:") ? Number.parseInt(selection.slice(5), 10) : latestTurn;
|
|
2422
|
+
if (typeof turn !== "number" || !Number.isSafeInteger(turn) || turn < 1) return null;
|
|
2423
|
+
const bucket = folder.byTurn.get(turn);
|
|
2424
|
+
return bucket === undefined || !isRecord(bucket.metrics) ? null : turnDetailOf(turn, bucket.metrics);
|
|
2425
|
+
}
|
|
2426
|
+
|
|
1900
2427
|
/** Snapshot text for context-injection previews, keyed by session plus reference key. */
|
|
1901
2428
|
const contextPreviews = new Map();
|
|
1902
2429
|
const CONTEXT_PREVIEW_LIMIT = 400;
|
|
@@ -2416,6 +2943,86 @@ window.__ModuleLoader__.load({
|
|
|
2416
2943
|
});
|
|
2417
2944
|
}
|
|
2418
2945
|
|
|
2946
|
+
/** Stable identity of one step across renders (callless rows fall back to name|seq). */
|
|
2947
|
+
function stepIdentityOf(step) {
|
|
2948
|
+
if (!isRecord(step)) return "";
|
|
2949
|
+
if (typeof step.callId === "string" && step.callId !== "") return step.callId;
|
|
2950
|
+
return `${typeof step.name === "string" ? step.name : ""}|${typeof step.seq === "number" ? step.seq : ""}`;
|
|
2951
|
+
}
|
|
2952
|
+
|
|
2953
|
+
/** What the sliding step window gained and lost between two renders. */
|
|
2954
|
+
function stepWindowDelta(previous, current) {
|
|
2955
|
+
const before = Array.isArray(previous) ? previous.filter((step) => isRecord(step)) : [];
|
|
2956
|
+
const after = Array.isArray(current) ? current.filter((step) => isRecord(step)) : [];
|
|
2957
|
+
const beforeIds = new Set(before.map(stepIdentityOf));
|
|
2958
|
+
const afterIds = new Set(after.map(stepIdentityOf));
|
|
2959
|
+
return {
|
|
2960
|
+
added: after.filter((step) => !beforeIds.has(stepIdentityOf(step))),
|
|
2961
|
+
dropped: before.filter((step) => !afterIds.has(stepIdentityOf(step))),
|
|
2962
|
+
};
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
/** Running keys that just finished successfully (their rows earn a completion pop). */
|
|
2966
|
+
function completedStepKeys(previousKeys, currentKeys, stepByKey) {
|
|
2967
|
+
const before = Array.isArray(previousKeys) ? previousKeys : [];
|
|
2968
|
+
const now = new Set(Array.isArray(currentKeys) ? currentKeys : []);
|
|
2969
|
+
const hits = [];
|
|
2970
|
+
for (const key of before) {
|
|
2971
|
+
if (now.has(key)) continue;
|
|
2972
|
+
const step = stepByKey !== null && typeof stepByKey.get === "function" ? stepByKey.get(key) : undefined;
|
|
2973
|
+
if (!isRecord(step) || step.done !== true || step.error === true) continue;
|
|
2974
|
+
hits.push(key);
|
|
2975
|
+
}
|
|
2976
|
+
return hits;
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
/** The last step still running (approval children are rendered under their call). */
|
|
2980
|
+
function liveStepOf(steps) {
|
|
2981
|
+
const list = Array.isArray(steps) ? steps : [];
|
|
2982
|
+
for (let index = list.length - 1; index >= 0; index -= 1) {
|
|
2983
|
+
const step = list[index];
|
|
2984
|
+
if (!isRecord(step) || step.name === "approval") continue;
|
|
2985
|
+
if (step.done !== true && step.error !== true) return step;
|
|
2986
|
+
}
|
|
2987
|
+
return null;
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
/** Whether a live cue makes sense for this scope: whole session / latest / current turn. */
|
|
2991
|
+
function liveScopeMatches(scope, latestTurn) {
|
|
2992
|
+
if (scope === "all" || scope === "latest") return true;
|
|
2993
|
+
if (typeof scope !== "string" || !scope.startsWith("turn:")) return false;
|
|
2994
|
+
const turn = Number.parseInt(scope.slice(5), 10);
|
|
2995
|
+
return Number.isFinite(turn) && Number.isFinite(latestTurn) && turn === latestTurn;
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2998
|
+
/** Todo counters for the progress status row: the meter, its label and leftover hint
|
|
2999
|
+
must always describe the same snapshot, so they read from one place. */
|
|
3000
|
+
function summarizeTodos(todos) {
|
|
3001
|
+
const list = Array.isArray(todos) ? todos.filter((item) => isRecord(item)) : [];
|
|
3002
|
+
let completed = 0;
|
|
3003
|
+
let running = 0;
|
|
3004
|
+
for (const item of list) {
|
|
3005
|
+
if (item.status === "completed") completed += 1;
|
|
3006
|
+
else if (item.status === "in_progress") running += 1;
|
|
3007
|
+
}
|
|
3008
|
+
const total = list.length;
|
|
3009
|
+
const pending = total - completed - running;
|
|
3010
|
+
return { total, completed, running, pending, leftover: running + pending };
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
/** Ticking clock for the live strip; zero when inactive or without a base time. */
|
|
3014
|
+
function useLiveElapsed(active, start) {
|
|
3015
|
+
const [now, setNow] = React.useState(() => (typeof Date === "function" ? Date.now() : 0));
|
|
3016
|
+
React.useEffect(() => {
|
|
3017
|
+
if (active !== true) return undefined;
|
|
3018
|
+
setNow(Date.now());
|
|
3019
|
+
const timer = setInterval(() => setNow(Date.now()), 1000);
|
|
3020
|
+
return () => clearInterval(timer);
|
|
3021
|
+
}, [active, start]);
|
|
3022
|
+
if (active !== true || typeof start !== "number" || !Number.isFinite(start) || start <= 0) return 0;
|
|
3023
|
+
return Math.max(0, now - start);
|
|
3024
|
+
}
|
|
3025
|
+
|
|
2419
3026
|
/** One-line human target for a tool call: file, command, query, url, name… */
|
|
2420
3027
|
/** Team-record key a tool call creates: teammate name / task subject / subagent label. */
|
|
2421
3028
|
/** Word/bigram tokens of one todo or delegate text: latin words plus CJK bigrams, so
|
|
@@ -2630,6 +3237,9 @@ window.__ModuleLoader__.load({
|
|
|
2630
3237
|
function foldEvent(folder, event, cwd) {
|
|
2631
3238
|
if (!isRecord(event) || typeof event.type !== "string") return;
|
|
2632
3239
|
const data = event.data;
|
|
3240
|
+
/* Session statistics fold in parallel: boundary events (step, stream, tool
|
|
3241
|
+
and usage records) never reach the message fold below. */
|
|
3242
|
+
foldTurnMetrics(folder, event);
|
|
2633
3243
|
/* Tool work after a reply proves that reply was not the turn's closing word:
|
|
2634
3244
|
the candidate stays provisional until a fresh reply replaces it. */
|
|
2635
3245
|
if ((event.type === "tool/call" || event.type === "tool/result") && folder.currentTurn >= 1) {
|
|
@@ -3212,11 +3822,13 @@ window.__ModuleLoader__.load({
|
|
|
3212
3822
|
scope,
|
|
3213
3823
|
/* how many turns this scope spans: "all" covers the session, a turn view spans 1 */
|
|
3214
3824
|
scopeTurns: source === folder ? turns.length : 1,
|
|
3825
|
+
/* the 「更多」 panel's per-turn (or per-session) session statistics */
|
|
3826
|
+
turnDetail: turnDetailFor(folder, scope, latestTurn),
|
|
3215
3827
|
conclusion,
|
|
3216
3828
|
};
|
|
3217
3829
|
}
|
|
3218
3830
|
|
|
3219
|
-
const EMPTY_STATE = { todos: null, todosTime: 0, goal: null, artifacts: [], references: [], steps: [], stepsAll: [], team: { members: [], tasks: [], subagents: [] }, stats: { durationMs: 0, toolCalls: 0, commands: 0, errors: 0, files: 0 }, turnStart: null, turnEnd: null, milestones: [], todoHistory: [], changeTurns: [], hasMore: false, localeRev: 0, turns: [], latestTurn: null, scope: "latest", scopeTurns: 1, conclusion: null };
|
|
3831
|
+
const EMPTY_STATE = { todos: null, todosTime: 0, goal: null, artifacts: [], references: [], steps: [], stepsAll: [], team: { members: [], tasks: [], subagents: [] }, stats: { durationMs: 0, toolCalls: 0, commands: 0, errors: 0, files: 0 }, turnStart: null, turnEnd: null, milestones: [], todoHistory: [], changeTurns: [], hasMore: false, localeRev: 0, turns: [], latestTurn: null, scope: "latest", scopeTurns: 1, turnDetail: null, conclusion: null };
|
|
3220
3832
|
|
|
3221
3833
|
function createSummaryStore(binding) {
|
|
3222
3834
|
const source = binding.eventSource;
|
|
@@ -3224,7 +3836,12 @@ window.__ModuleLoader__.load({
|
|
|
3224
3836
|
const views = new Map();
|
|
3225
3837
|
let folder = createFolder();
|
|
3226
3838
|
folder.sessionId = typeof binding.sessionId === "string" ? binding.sessionId : null;
|
|
3227
|
-
|
|
3839
|
+
/* The feed snapshot's `change` is a delta against the snapshot it was derived
|
|
3840
|
+
from, not against this folder: a store mounted mid-turn (or a missed publish)
|
|
3841
|
+
must not fold a delta into an empty/older window. `lastRevision` lets a single
|
|
3842
|
+
skipped publish fall back to a full rebuild instead of appending out of order. */
|
|
3843
|
+
let lastWindow = null;
|
|
3844
|
+
let lastRevision = null;
|
|
3228
3845
|
let localeRev = 0;
|
|
3229
3846
|
|
|
3230
3847
|
const store = {
|
|
@@ -3261,6 +3878,7 @@ window.__ModuleLoader__.load({
|
|
|
3261
3878
|
for (const entry of windowSnapshot.entries) foldEntry(folder, entry, cwd);
|
|
3262
3879
|
folder.changed = false;
|
|
3263
3880
|
lastWindow = windowSnapshot;
|
|
3881
|
+
lastRevision = typeof windowSnapshot.revision === "number" ? windowSnapshot.revision : null;
|
|
3264
3882
|
views.clear();
|
|
3265
3883
|
notify();
|
|
3266
3884
|
};
|
|
@@ -3269,16 +3887,37 @@ window.__ModuleLoader__.load({
|
|
|
3269
3887
|
const windowSnapshot = source.getSnapshot();
|
|
3270
3888
|
if (windowSnapshot === lastWindow) return;
|
|
3271
3889
|
const previous = lastWindow;
|
|
3272
|
-
const change = windowSnapshot.change;
|
|
3273
|
-
const
|
|
3274
|
-
|
|
3890
|
+
const change = isRecord(windowSnapshot.change) ? windowSnapshot.change : null;
|
|
3891
|
+
const revision = typeof windowSnapshot.revision === "number" ? windowSnapshot.revision : null;
|
|
3892
|
+
/* A delta is only usable when this store consumed the snapshot it was derived
|
|
3893
|
+
from: first read, replace, or a revision gap means rebuild from entries. */
|
|
3894
|
+
const contiguous =
|
|
3895
|
+
previous !== null &&
|
|
3896
|
+
previous.entries !== undefined &&
|
|
3897
|
+
change !== null &&
|
|
3898
|
+
change.kind !== "replace" &&
|
|
3899
|
+
(revision === null || lastRevision === null || revision === lastRevision + 1);
|
|
3900
|
+
if (contiguous === true && change.kind === "append") {
|
|
3901
|
+
const appended = Array.isArray(change.entries) ? change.entries : [];
|
|
3275
3902
|
// Live streaming rows carry nothing durable; skip them without a rebuild.
|
|
3276
|
-
if (appended.every((entry) => isRecord(entry) && entry.type === "transient"))
|
|
3277
|
-
if (appended.every((entry) => isRecord(entry) && entry.type !== "transient" && isRecord(entry.event))) {
|
|
3903
|
+
if (appended.length > 0 && appended.every((entry) => isRecord(entry) && entry.type === "transient")) {
|
|
3278
3904
|
lastWindow = windowSnapshot;
|
|
3905
|
+
if (revision !== null) lastRevision = revision;
|
|
3906
|
+
return;
|
|
3907
|
+
}
|
|
3908
|
+
if (appended.length > 0 && appended.every((entry) => isRecord(entry) && entry.type !== "transient" && isRecord(entry.event))) {
|
|
3279
3909
|
const cwd = sessionCwd(binding.sessionId);
|
|
3280
|
-
|
|
3281
|
-
|
|
3910
|
+
try {
|
|
3911
|
+
for (const entry of appended) foldEntry(folder, entry, cwd);
|
|
3912
|
+
} catch {
|
|
3913
|
+
/* A half-folded folder cannot be trusted; rebuild from the snapshot. */
|
|
3914
|
+
fullRebuild(windowSnapshot);
|
|
3915
|
+
return;
|
|
3916
|
+
}
|
|
3917
|
+
const hasMoreChanged = (previous.hasMore === true) !== (windowSnapshot.hasMore === true);
|
|
3918
|
+
lastWindow = windowSnapshot;
|
|
3919
|
+
if (revision !== null) lastRevision = revision;
|
|
3920
|
+
if (folder.changed || hasMoreChanged) {
|
|
3282
3921
|
folder.changed = false;
|
|
3283
3922
|
views.clear();
|
|
3284
3923
|
notify();
|
|
@@ -3286,10 +3925,6 @@ window.__ModuleLoader__.load({
|
|
|
3286
3925
|
return;
|
|
3287
3926
|
}
|
|
3288
3927
|
}
|
|
3289
|
-
if (windowSnapshot.entries.length === 0 && previous !== null && previous.entries !== undefined && previous.entries.length === 0) {
|
|
3290
|
-
lastWindow = windowSnapshot;
|
|
3291
|
-
return;
|
|
3292
|
-
}
|
|
3293
3928
|
fullRebuild(windowSnapshot);
|
|
3294
3929
|
};
|
|
3295
3930
|
|
|
@@ -4541,7 +5176,7 @@ function FileTypeIcon(props) {
|
|
|
4541
5176
|
return { introAttr: introSeq % 2 === 0 ? "even" : "odd", introDelay: delayMs };
|
|
4542
5177
|
}
|
|
4543
5178
|
|
|
4544
|
-
function Section({ id, icon, title, badge, empty, action, children, introAttr, introDelay }) {
|
|
5179
|
+
function Section({ id, icon, title, badge, empty, action, children, introAttr, introDelay, live, bodyMode }) {
|
|
4545
5180
|
const [open, setOpen] = React.useState(() => collapsedSections.get(id) !== true);
|
|
4546
5181
|
const cfg = useConfig();
|
|
4547
5182
|
const kids = Array.isArray(children) ? children.filter((child) => child !== null && child !== undefined && child !== false) : children ? [children] : [];
|
|
@@ -4555,6 +5190,8 @@ function FileTypeIcon(props) {
|
|
|
4555
5190
|
{
|
|
4556
5191
|
className: "dsov-section",
|
|
4557
5192
|
"data-full": cfg.sectionFullHeight === true ? "true" : "false",
|
|
5193
|
+
"data-live": live === true ? "true" : undefined,
|
|
5194
|
+
"data-body": bodyMode,
|
|
4558
5195
|
"data-intro": introAttr,
|
|
4559
5196
|
style: typeof introDelay === "number" ? { animationDelay: `${introDelay}ms` } : undefined,
|
|
4560
5197
|
},
|
|
@@ -4603,7 +5240,7 @@ function FileTypeIcon(props) {
|
|
|
4603
5240
|
complete: "success",
|
|
4604
5241
|
};
|
|
4605
5242
|
|
|
4606
|
-
function ProgressSection({ summary, goal, t, waiting, cfg, asOf, introSeq }) {
|
|
5243
|
+
function ProgressSection({ summary, goal, t, waiting, running, cfg, asOf, introSeq }) {
|
|
4607
5244
|
const fmtTime = (ts) => formatTime(ts, cfg.timeMode, summary.turnStart);
|
|
4608
5245
|
const timeTitle = (ts) => (cfg.timeMode === "elapsed" ? formatTime(ts) : undefined);
|
|
4609
5246
|
/* timeline scrubbing: only what already happened at `asOf` is shown */
|
|
@@ -4666,6 +5303,24 @@ function FileTypeIcon(props) {
|
|
|
4666
5303
|
/* steps: newest-first window; keep the evicted bottom row as a fading ghost */
|
|
4667
5304
|
const stepsRaw = Array.isArray(summary.steps) ? summary.steps.filter((step) => isRecord(step)) : [];
|
|
4668
5305
|
const stepsAllRaw = Array.isArray(summary.stepsAll) ? summary.stepsAll.filter((step) => isRecord(step)) : [];
|
|
5306
|
+
/* Live cues only describe work happening now: never while scrubbing the
|
|
5307
|
+
timeline, viewing a historical session, or looking at an older turn. */
|
|
5308
|
+
const liveRunning = running === true && asOfTime === null && liveScopeMatches(summary.scope, summary.latestTurn);
|
|
5309
|
+
const liveWaiting = waiting === true && liveScopeMatches(summary.scope, summary.latestTurn);
|
|
5310
|
+
const liveVisible = liveRunning || liveWaiting;
|
|
5311
|
+
const liveState = liveWaiting ? "waiting" : "running";
|
|
5312
|
+
/* clock base: the running step's own start, else the latest turn's start; the
|
|
5313
|
+
session-wide scope has no single turn, so it only clocks a running step */
|
|
5314
|
+
const liveClockStep = liveRunning ? liveStepOf(stepsRaw) : null;
|
|
5315
|
+
const liveClockBase =
|
|
5316
|
+
liveClockStep !== null && typeof liveClockStep.time === "number" && liveClockStep.time > 0
|
|
5317
|
+
? liveClockStep.time
|
|
5318
|
+
: summary.scope === "all"
|
|
5319
|
+
? null
|
|
5320
|
+
: typeof summary.turnStart === "number" && summary.turnStart > 0
|
|
5321
|
+
? summary.turnStart
|
|
5322
|
+
: null;
|
|
5323
|
+
const liveElapsed = useLiveElapsed(liveRunning && liveClockBase !== null, liveClockBase);
|
|
4669
5324
|
const stepAt = (step) => asOfTime === null || (typeof step.time === "number" ? step.time <= asOfTime : true);
|
|
4670
5325
|
/* a step whose result landed after the scrub point reads as still running */
|
|
4671
5326
|
const stepClamped = (step) => {
|
|
@@ -4690,7 +5345,10 @@ function FileTypeIcon(props) {
|
|
|
4690
5345
|
})();
|
|
4691
5346
|
const [ghost, setGhost] = React.useState(null);
|
|
4692
5347
|
const [stepsExpanded, setStepsExpanded] = React.useState(false);
|
|
5348
|
+
const [listTick, setListTick] = React.useState(0);
|
|
5349
|
+
const [doneHits, setDoneHits] = React.useState(null);
|
|
4693
5350
|
const ghostTimer = React.useRef(undefined);
|
|
5351
|
+
const hitTimer = React.useRef(undefined);
|
|
4694
5352
|
const prevSteps = React.useRef([]);
|
|
4695
5353
|
const prevScope = React.useRef(summary.scope);
|
|
4696
5354
|
/* enter animation only for rows that arrive while mounted; first paint per scope is silent */
|
|
@@ -4756,9 +5414,12 @@ function FileTypeIcon(props) {
|
|
|
4756
5414
|
if (step.done !== true && step.error !== true) runningKeys.push(key);
|
|
4757
5415
|
});
|
|
4758
5416
|
const runningSig = runningKeys.join("|");
|
|
5417
|
+
const liveStep = liveVisible ? liveStepOf(renderSteps) : null;
|
|
4759
5418
|
const prevRunningKeys = React.useRef(new Set());
|
|
4760
5419
|
React.useEffect(() => {
|
|
4761
5420
|
const now = new Set(runningKeys);
|
|
5421
|
+
const wasRunning = [...prevRunningKeys.current];
|
|
5422
|
+
const finished = completedStepKeys(wasRunning, [...now], stepByKey);
|
|
4762
5423
|
for (const key of prevRunningKeys.current) {
|
|
4763
5424
|
if (now.has(key)) continue;
|
|
4764
5425
|
const step = stepByKey.get(key);
|
|
@@ -4766,6 +5427,17 @@ function FileTypeIcon(props) {
|
|
|
4766
5427
|
if (overrideOf(`step:${key}`, false)) toggleKey(`step:${key}`, false);
|
|
4767
5428
|
}
|
|
4768
5429
|
prevRunningKeys.current = now;
|
|
5430
|
+
if (finished.length === 0) return;
|
|
5431
|
+
setDoneHits((current) => {
|
|
5432
|
+
const next = { ...(current !== null && isRecord(current) ? current : {}) };
|
|
5433
|
+
for (const key of finished) next[key] = true;
|
|
5434
|
+
return next;
|
|
5435
|
+
});
|
|
5436
|
+
if (hitTimer.current !== undefined) clearTimeout(hitTimer.current);
|
|
5437
|
+
hitTimer.current = setTimeout(() => {
|
|
5438
|
+
hitTimer.current = undefined;
|
|
5439
|
+
setDoneHits(null);
|
|
5440
|
+
}, 800);
|
|
4769
5441
|
}, [runningSig]);
|
|
4770
5442
|
if (seenSteps.current === null || seenSteps.current.scope !== summary.scope) {
|
|
4771
5443
|
seenSteps.current = { scope: summary.scope, keys: new Set(renderSteps.map(stepKeyOf)) };
|
|
@@ -4773,6 +5445,7 @@ function FileTypeIcon(props) {
|
|
|
4773
5445
|
React.useEffect(
|
|
4774
5446
|
() => () => {
|
|
4775
5447
|
if (ghostTimer.current !== undefined) clearTimeout(ghostTimer.current);
|
|
5448
|
+
if (hitTimer.current !== undefined) clearTimeout(hitTimer.current);
|
|
4776
5449
|
},
|
|
4777
5450
|
[]
|
|
4778
5451
|
);
|
|
@@ -4784,15 +5457,17 @@ function FileTypeIcon(props) {
|
|
|
4784
5457
|
return;
|
|
4785
5458
|
}
|
|
4786
5459
|
if (asOfTime !== null || previous.length === 0) return;
|
|
4787
|
-
const
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
}
|
|
5460
|
+
const delta = stepWindowDelta(previous, steps);
|
|
5461
|
+
if (delta.dropped.length > 0) {
|
|
5462
|
+
setGhost(delta.dropped[delta.dropped.length - 1]);
|
|
5463
|
+
if (ghostTimer.current !== undefined) clearTimeout(ghostTimer.current);
|
|
5464
|
+
ghostTimer.current = setTimeout(() => {
|
|
5465
|
+
ghostTimer.current = undefined;
|
|
5466
|
+
setGhost(null);
|
|
5467
|
+
}, 520);
|
|
5468
|
+
}
|
|
5469
|
+
/* alternate two identical keyframes so the conveyor nudge replays on every slide */
|
|
5470
|
+
if (delta.added.length > 0 || delta.dropped.length > 0) setListTick((value) => value + 1);
|
|
4796
5471
|
}, [steps]);
|
|
4797
5472
|
/* team data + todos drive both the merged list and the step nesting */
|
|
4798
5473
|
const team = isRecord(summary.team) ? summary.team : null;
|
|
@@ -5091,6 +5766,16 @@ function FileTypeIcon(props) {
|
|
|
5091
5766
|
const attached = stepAttachments.get(key);
|
|
5092
5767
|
const approvals = approvalChildren.get(key);
|
|
5093
5768
|
const dying = extraProps !== undefined && extraProps["data-exit"] === "true";
|
|
5769
|
+
const activeRow =
|
|
5770
|
+
dying !== true &&
|
|
5771
|
+
liveRunning === true &&
|
|
5772
|
+
liveState !== "waiting" &&
|
|
5773
|
+
stepRunning === true &&
|
|
5774
|
+
failed !== true &&
|
|
5775
|
+
waitingRow !== true &&
|
|
5776
|
+
liveStep !== null &&
|
|
5777
|
+
stepIdentityOf(step) === stepIdentityOf(liveStep);
|
|
5778
|
+
const hitRow = dying !== true && doneHits !== null && isRecord(doneHits) && doneHits[key] === true;
|
|
5094
5779
|
const body = dying ? null : stepBodyOf(step);
|
|
5095
5780
|
const gated = !dying && Array.isArray(approvals) && approvals.length > 0;
|
|
5096
5781
|
const pendingApproval = gated && approvals.some((child) => approvalToneOf(child) === "warn");
|
|
@@ -5169,6 +5854,8 @@ function FileTypeIcon(props) {
|
|
|
5169
5854
|
key,
|
|
5170
5855
|
"data-error": failed ? "true" : "false",
|
|
5171
5856
|
"data-waiting": waitingRow ? "true" : "false",
|
|
5857
|
+
"data-active": activeRow ? "true" : undefined,
|
|
5858
|
+
"data-hit": hitRow ? "true" : undefined,
|
|
5172
5859
|
"data-flash": flash === "ok" ? "ok" : flash === "err" ? "err" : undefined,
|
|
5173
5860
|
title: full === "" ? undefined : waitingRow ? `${t("progress.waiting")} · ${full}` : full,
|
|
5174
5861
|
...(extraProps === undefined ? {} : extraProps),
|
|
@@ -5273,7 +5960,119 @@ function FileTypeIcon(props) {
|
|
|
5273
5960
|
if (Array.isArray(rendered)) target.push(...rendered);
|
|
5274
5961
|
else target.push(rendered);
|
|
5275
5962
|
};
|
|
5276
|
-
|
|
5963
|
+
/* Live HUD: one persistent status row above the goal/todos/steps so a long turn
|
|
5964
|
+
stays visibly alive between durable events (a step's own row can be off-window). */
|
|
5965
|
+
const liveName =
|
|
5966
|
+
liveStep === null
|
|
5967
|
+
? ""
|
|
5968
|
+
: liveStep.name === "compact-checkpoint"
|
|
5969
|
+
? t("step.compact")
|
|
5970
|
+
: liveStep.name === "approval"
|
|
5971
|
+
? t("step.approval")
|
|
5972
|
+
: typeof liveStep.name === "string"
|
|
5973
|
+
? liveStep.name
|
|
5974
|
+
: "";
|
|
5975
|
+
const liveDetail =
|
|
5976
|
+
liveStep !== null && typeof liveStep.detail === "string" && liveStep.detail !== "" ? liveStep.detail : "";
|
|
5977
|
+
const liveFull =
|
|
5978
|
+
liveStep !== null && typeof liveStep.full === "string" && liveStep.full !== "" ? liveStep.full : liveDetail;
|
|
5979
|
+
const liveNowText =
|
|
5980
|
+
liveState === "waiting"
|
|
5981
|
+
? [liveName, liveDetail].filter((part) => part !== "").join(" · ")
|
|
5982
|
+
: liveStep !== null
|
|
5983
|
+
? [liveName, liveDetail].filter((part) => part !== "").join(" · ")
|
|
5984
|
+
: t("progress.liveThinking");
|
|
5985
|
+
/* The status row is the section's only chrome: live while the turn runs or
|
|
5986
|
+
waits, and a neutral summary once it ends — the todo counter lives inside it
|
|
5987
|
+
(meter) instead of a second, differently styled progress bar. */
|
|
5988
|
+
const todoSummary = hasTodos ? summarizeTodos(todos) : null;
|
|
5989
|
+
const statusLive = liveVisible === true;
|
|
5990
|
+
const statusState = statusLive ? liveState : "summary";
|
|
5991
|
+
const meterText =
|
|
5992
|
+
todoSummary === null
|
|
5993
|
+
? ""
|
|
5994
|
+
: t("progress.meterTitle", {
|
|
5995
|
+
completed: todoSummary.completed,
|
|
5996
|
+
total: todoSummary.total,
|
|
5997
|
+
running: todoSummary.running,
|
|
5998
|
+
pending: todoSummary.pending,
|
|
5999
|
+
});
|
|
6000
|
+
const summaryLeftover =
|
|
6001
|
+
todoSummary !== null && todoSummary.leftover > 0
|
|
6002
|
+
? t("progress.leftover", { running: todoSummary.running, pending: todoSummary.pending })
|
|
6003
|
+
: "";
|
|
6004
|
+
const statusTitle =
|
|
6005
|
+
todoSummary !== null
|
|
6006
|
+
? summaryLeftover !== "" && !statusLive
|
|
6007
|
+
? `${meterText} · ${t("progress.staleHint")}`
|
|
6008
|
+
: meterText
|
|
6009
|
+
: liveFull === ""
|
|
6010
|
+
? undefined
|
|
6011
|
+
: liveFull;
|
|
6012
|
+
const statusLabel = statusLive
|
|
6013
|
+
? liveState === "waiting"
|
|
6014
|
+
? t("progress.waiting")
|
|
6015
|
+
: t("progress.live")
|
|
6016
|
+
: todoSummary !== null
|
|
6017
|
+
? t("progress.counts", { completed: todoSummary.completed, total: todoSummary.total })
|
|
6018
|
+
: "";
|
|
6019
|
+
const statusNow = statusLive ? liveNowText : summaryLeftover;
|
|
6020
|
+
const meterPercent =
|
|
6021
|
+
todoSummary === null || todoSummary.total === 0 ? 0 : Math.round((todoSummary.completed / todoSummary.total) * 100);
|
|
6022
|
+
const liveMeter =
|
|
6023
|
+
todoSummary !== null
|
|
6024
|
+
? h(
|
|
6025
|
+
"span",
|
|
6026
|
+
{
|
|
6027
|
+
className: "dsov-liveMeter",
|
|
6028
|
+
role: "progressbar",
|
|
6029
|
+
"aria-valuemin": 0,
|
|
6030
|
+
"aria-valuemax": todoSummary.total,
|
|
6031
|
+
"aria-valuenow": todoSummary.completed,
|
|
6032
|
+
"aria-valuetext": meterText,
|
|
6033
|
+
},
|
|
6034
|
+
h("span", { className: "dsov-liveMeterTrack" }, h("span", { className: "dsov-liveMeterFill", style: { width: `${meterPercent}%` } })),
|
|
6035
|
+
statusLive
|
|
6036
|
+
? h("span", { className: "dsov-liveMeterCount", "aria-hidden": "true" }, `${todoSummary.completed}/${todoSummary.total}`)
|
|
6037
|
+
: null
|
|
6038
|
+
)
|
|
6039
|
+
: null;
|
|
6040
|
+
const liveStrip =
|
|
6041
|
+
statusLive || todoSummary !== null
|
|
6042
|
+
? h(
|
|
6043
|
+
"div",
|
|
6044
|
+
{
|
|
6045
|
+
className: "dsov-live",
|
|
6046
|
+
key: "live",
|
|
6047
|
+
"data-state": statusState,
|
|
6048
|
+
"data-complete": todoSummary !== null && todoSummary.leftover === 0 ? "true" : undefined,
|
|
6049
|
+
role: statusLive ? "status" : undefined,
|
|
6050
|
+
title: statusTitle,
|
|
6051
|
+
},
|
|
6052
|
+
h("span", { className: "dsov-livePulse", "aria-hidden": "true" }, h("span", { className: "dsov-liveDot" })),
|
|
6053
|
+
h(
|
|
6054
|
+
"span",
|
|
6055
|
+
{ className: "dsov-liveText", "aria-live": statusLive ? "polite" : undefined },
|
|
6056
|
+
h("span", { className: "dsov-liveLabel" }, statusLabel),
|
|
6057
|
+
statusNow !== "" ? h("span", { className: "dsov-liveNow" }, statusNow) : null
|
|
6058
|
+
),
|
|
6059
|
+
liveMeter !== null
|
|
6060
|
+
? liveMeter
|
|
6061
|
+
: h(
|
|
6062
|
+
"span",
|
|
6063
|
+
{ className: "dsov-liveBars", "aria-hidden": "true" },
|
|
6064
|
+
[0, 1, 2, 3, 4].map((index) => h("i", { key: `bar-${index}` }))
|
|
6065
|
+
),
|
|
6066
|
+
statusLive && liveElapsed > 0
|
|
6067
|
+
? h(
|
|
6068
|
+
"span",
|
|
6069
|
+
{ className: "dsov-liveClock", title: t("progress.liveElapsedTitle"), "aria-hidden": "true" },
|
|
6070
|
+
t("progress.liveElapsed", { v: formatElapsed(liveElapsed) })
|
|
6071
|
+
)
|
|
6072
|
+
: null
|
|
6073
|
+
)
|
|
6074
|
+
: null;
|
|
6075
|
+
const kids = liveStrip !== null ? [liveStrip] : [];
|
|
5277
6076
|
if (goal) {
|
|
5278
6077
|
/* A session goal is a mode, not a todo: rounds are re-driven automatically.
|
|
5279
6078
|
The banner says so, carries the phase and the round budget, and names the
|
|
@@ -5310,16 +6109,6 @@ function FileTypeIcon(props) {
|
|
|
5310
6109
|
);
|
|
5311
6110
|
}
|
|
5312
6111
|
if (hasTodos) {
|
|
5313
|
-
const total = todos.length;
|
|
5314
|
-
const done = todos.filter((item) => item.status === "completed").length;
|
|
5315
|
-
kids.push(
|
|
5316
|
-
h(
|
|
5317
|
-
"div",
|
|
5318
|
-
{ className: "dsov-progress", key: "bar" },
|
|
5319
|
-
h("div", { className: "dsov-progressTrack" }, h("div", { className: "dsov-progressFill", style: { width: `${Math.round((done / total) * 100)}%` } })),
|
|
5320
|
-
h("div", { className: "dsov-progressMeta" }, t("progress.counts", { completed: done, total }))
|
|
5321
|
-
)
|
|
5322
|
-
);
|
|
5323
6112
|
const todoRows = [];
|
|
5324
6113
|
todos.forEach((todo, index) => {
|
|
5325
6114
|
const status = TODO_KEYS[todo.status] ? todo.status : "pending";
|
|
@@ -5449,31 +6238,51 @@ function FileTypeIcon(props) {
|
|
|
5449
6238
|
pushStepRow(rows, ghost, `ghost-${ghost.callId}`, { "data-exit": "true" }, false);
|
|
5450
6239
|
}
|
|
5451
6240
|
if (hasTeam) pushFlatTeam(rows);
|
|
6241
|
+
const stepsHeadText = t(stepsOpen ? "progress.allSteps" : "progress.recentSteps");
|
|
6242
|
+
const stepsToggle = (suffix) =>
|
|
6243
|
+
canExpand
|
|
6244
|
+
? h(
|
|
6245
|
+
"button",
|
|
6246
|
+
{
|
|
6247
|
+
type: "button",
|
|
6248
|
+
key: `stepsToggle-${suffix}`,
|
|
6249
|
+
className: "dsov-moreSteps",
|
|
6250
|
+
title: stepsOpen ? t("progress.hideEarlierSteps") : t("progress.moreStepsCount", { n: expandedSteps.length - steps.length }),
|
|
6251
|
+
"aria-expanded": stepsOpen ? "true" : "false",
|
|
6252
|
+
onClick: () => setStepsExpanded(!stepsOpen),
|
|
6253
|
+
},
|
|
6254
|
+
t(stepsOpen ? "progress.hideEarlierSteps" : "progress.showEarlierSteps"),
|
|
6255
|
+
h(IconHistory, { size: 12, className: "dsov-moreIcon" })
|
|
6256
|
+
)
|
|
6257
|
+
: null;
|
|
5452
6258
|
kids.push(
|
|
5453
6259
|
h(
|
|
5454
6260
|
"div",
|
|
5455
6261
|
{ className: "dsov-stepsHead", key: "stepsHead" },
|
|
5456
|
-
h("span",
|
|
5457
|
-
|
|
5458
|
-
? h(
|
|
5459
|
-
"button",
|
|
5460
|
-
{
|
|
5461
|
-
type: "button",
|
|
5462
|
-
className: "dsov-moreSteps",
|
|
5463
|
-
title: stepsOpen ? t("progress.hideEarlierSteps") : t("progress.moreStepsCount", { n: expandedSteps.length - steps.length }),
|
|
5464
|
-
onClick: () => setStepsExpanded(!stepsOpen),
|
|
5465
|
-
},
|
|
5466
|
-
t(stepsOpen ? "progress.hideEarlierSteps" : "progress.showEarlierSteps"),
|
|
5467
|
-
h(IconHistory, { size: 12, className: "dsov-moreIcon" })
|
|
5468
|
-
)
|
|
5469
|
-
: null
|
|
6262
|
+
h("span", { className: "dsov-stepsHeadLabel", title: stepsHeadText }, stepsHeadText),
|
|
6263
|
+
stepsToggle("head")
|
|
5470
6264
|
)
|
|
5471
6265
|
);
|
|
5472
|
-
kids.push(h("div", { className: "dsov-steps", key: "steps" }, rows));
|
|
6266
|
+
kids.push(h("div", { className: "dsov-steps", key: "steps", "data-nudge": listTick === 0 ? undefined : listTick % 2 === 0 ? "even" : "odd" }, rows));
|
|
6267
|
+
/* An expanded list is long enough to scroll away from its own header control,
|
|
6268
|
+
so the collapse action is repeated where the reading ends. */
|
|
6269
|
+
if (stepsOpen && canExpand) kids.push(h("div", { className: "dsov-stepsFoot", key: "stepsFoot" }, stepsToggle("foot")));
|
|
5473
6270
|
}
|
|
5474
6271
|
return h(
|
|
5475
6272
|
Section,
|
|
5476
|
-
{
|
|
6273
|
+
{
|
|
6274
|
+
id: "progress",
|
|
6275
|
+
...sectionIntroProps(introSeq, 0),
|
|
6276
|
+
icon: h(IconProgress, { size: 15 }),
|
|
6277
|
+
title: t("section.progress"),
|
|
6278
|
+
empty: t(summary.scope === "all" ? "progress.empty" : "progress.emptyTurn"),
|
|
6279
|
+
live: liveVisible,
|
|
6280
|
+
/* the collapsed list never overflows (six-step window / full todo list), so the
|
|
6281
|
+
body keeps its natural height and can never flash a scrollbar; the expanded
|
|
6282
|
+
list is pinned to one height so arriving steps scroll inside it instead of
|
|
6283
|
+
resizing the section */
|
|
6284
|
+
bodyMode: stepsOpen ? "fixed" : "auto",
|
|
6285
|
+
},
|
|
5477
6286
|
kids
|
|
5478
6287
|
);
|
|
5479
6288
|
}
|
|
@@ -5488,9 +6297,13 @@ function FileTypeIcon(props) {
|
|
|
5488
6297
|
const cwd = sessionCwd(sessionId);
|
|
5489
6298
|
const canFileMenu = hostSessionRemote() !== null;
|
|
5490
6299
|
const [fileMenu, setFileMenu] = React.useState(null);
|
|
6300
|
+
const [planMenu, setPlanMenu] = React.useState(null);
|
|
5491
6301
|
React.useEffect(() => {
|
|
5492
|
-
if (fileMenu === null) return undefined;
|
|
5493
|
-
const close = () =>
|
|
6302
|
+
if (fileMenu === null && planMenu === null) return undefined;
|
|
6303
|
+
const close = () => {
|
|
6304
|
+
setFileMenu(null);
|
|
6305
|
+
setPlanMenu(null);
|
|
6306
|
+
};
|
|
5494
6307
|
const onKey = (event) => {
|
|
5495
6308
|
if (event.key === "Escape") close();
|
|
5496
6309
|
};
|
|
@@ -5504,7 +6317,7 @@ function FileTypeIcon(props) {
|
|
|
5504
6317
|
window.removeEventListener("scroll", close, true);
|
|
5505
6318
|
window.removeEventListener("resize", close);
|
|
5506
6319
|
};
|
|
5507
|
-
}, [fileMenu]);
|
|
6320
|
+
}, [fileMenu, planMenu]);
|
|
5508
6321
|
const turns = summary.changeTurns;
|
|
5509
6322
|
const latest = turns.length > 0 ? turns[turns.length - 1] : null;
|
|
5510
6323
|
const action =
|
|
@@ -5523,22 +6336,20 @@ function FileTypeIcon(props) {
|
|
|
5523
6336
|
const isConclusion = artifact.kind === "conclusion";
|
|
5524
6337
|
const isPlan = artifact.plan === true;
|
|
5525
6338
|
const planName = isPlan ? (typeof artifact.planTitle === "string" && artifact.planTitle !== "" ? artifact.planTitle : t("artifact.plan")) : "";
|
|
5526
|
-
|
|
5527
|
-
"button",
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
: undefined,
|
|
5541
|
-
},
|
|
6339
|
+
const buttonProps = {
|
|
6340
|
+
type: "button",
|
|
6341
|
+
className: "dsov-row",
|
|
6342
|
+
title: isConclusion ? `${artifact.title} · ${t("artifact.conclusionHint")}` : isPlan ? planName : artifact.description ? `${artifact.path} — ${artifact.description}` : artifact.path,
|
|
6343
|
+
onClick: () => openArtifact(binding, artifact, stats),
|
|
6344
|
+
onContextMenu:
|
|
6345
|
+
canFileMenu && !isMemory && !isPlan && !isConclusion
|
|
6346
|
+
? (event) => {
|
|
6347
|
+
event.preventDefault();
|
|
6348
|
+
setFileMenu({ x: event.clientX, y: event.clientY, path: absPath(cwd, artifact.path) });
|
|
6349
|
+
}
|
|
6350
|
+
: undefined,
|
|
6351
|
+
};
|
|
6352
|
+
const children = [
|
|
5542
6353
|
h(
|
|
5543
6354
|
"span",
|
|
5544
6355
|
{ className: "dsov-fileIcon" },
|
|
@@ -5564,10 +6375,39 @@ function FileTypeIcon(props) {
|
|
|
5564
6375
|
isPlan ? h("span", { className: "dsov-badge", "data-tone": "plan" }, t("artifact.plan")) : null,
|
|
5565
6376
|
isConclusion ? h("span", { className: "dsov-badge", "data-tone": "brand" }, t("artifact.conclusion")) : null,
|
|
5566
6377
|
isConclusion && typeof artifact.chars === "number" ? h("span", { className: "dsov-badge", "data-tone": "quiet" }, t("artifact.conclusionChars", { n: artifact.chars })) : null,
|
|
5567
|
-
h("span", { className: "dsov-rowTail" }, h("span", { className: "dsov-time", title: timeTitle(artifact.time) }, fmtTime(artifact.time)))
|
|
6378
|
+
h("span", { className: "dsov-rowTail" }, h("span", { className: "dsov-time", title: timeTitle(artifact.time) }, fmtTime(artifact.time))),
|
|
6379
|
+
];
|
|
6380
|
+
const rowKey = `${artifact.path}#${index}`;
|
|
6381
|
+
if (!isPlan) return h("button", { ...buttonProps, key: rowKey }, ...children);
|
|
6382
|
+
/* Plan rows keep their click (the host plan page) and gain a hover entry:
|
|
6383
|
+
the workbench needs no new surface to reach already-approved plans. */
|
|
6384
|
+
const hasPlanIdentity =
|
|
6385
|
+
(typeof artifact.planCallId === "string" && artifact.planCallId !== "") || (typeof artifact.previewKey === "string" && artifact.previewKey !== "");
|
|
6386
|
+
return h(
|
|
6387
|
+
"div",
|
|
6388
|
+
{ className: "dsov-planRow", key: rowKey },
|
|
6389
|
+
h("button", buttonProps, ...children),
|
|
6390
|
+
hasPlanIdentity
|
|
6391
|
+
? h(
|
|
6392
|
+
"button",
|
|
6393
|
+
{
|
|
6394
|
+
type: "button",
|
|
6395
|
+
className: "dsov-planRowAction",
|
|
6396
|
+
title: t("planWorkbench.open"),
|
|
6397
|
+
"aria-label": t("planWorkbench.open"),
|
|
6398
|
+
onClick: (event) => {
|
|
6399
|
+
event.preventDefault();
|
|
6400
|
+
event.stopPropagation();
|
|
6401
|
+
setPlanMenu({ x: event.clientX, y: event.clientY, artifact, sessionId });
|
|
6402
|
+
},
|
|
6403
|
+
},
|
|
6404
|
+
h(IconPlan, { size: 14 })
|
|
6405
|
+
)
|
|
6406
|
+
: null
|
|
5568
6407
|
);
|
|
5569
6408
|
});
|
|
5570
6409
|
if (fileMenu !== null) rows.push(h(ArtifactFileMenu, { key: "dsov-file-menu", menu: fileMenu, t, onClose: () => setFileMenu(null) }));
|
|
6410
|
+
if (planMenu !== null) rows.push(h(PlanRowMenu, { key: "dsov-plan-menu", menu: planMenu, t, onClose: () => setPlanMenu(null) }));
|
|
5571
6411
|
return h(
|
|
5572
6412
|
Section,
|
|
5573
6413
|
{
|
|
@@ -6001,33 +6841,139 @@ function FileTypeIcon(props) {
|
|
|
6001
6841
|
return `${seconds}s`;
|
|
6002
6842
|
}
|
|
6003
6843
|
|
|
6004
|
-
/**
|
|
6005
|
-
function
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
const
|
|
6016
|
-
|
|
6844
|
+
/** Sub-minute durations keep one decimal so first-token latency stays readable. */
|
|
6845
|
+
function formatDurationPrecise(ms) {
|
|
6846
|
+
const seconds = Math.max(0, (Number.isFinite(ms) ? ms : 0) / 1000);
|
|
6847
|
+
if (seconds < 60) return `${Math.round(seconds * 10) / 10}s`;
|
|
6848
|
+
const whole = Math.round(seconds);
|
|
6849
|
+
return `${Math.floor(whole / 60)}m ${String(whole % 60).padStart(2, "0")}s`;
|
|
6850
|
+
}
|
|
6851
|
+
|
|
6852
|
+
/** Compact token count, matching the host pills: 517 / 12.2K / 1.2M. */
|
|
6853
|
+
function formatTokensCompact(value) {
|
|
6854
|
+
const count = Math.max(0, Math.round(Number.isFinite(value) ? value : 0));
|
|
6855
|
+
const scaled = (candidate) => (candidate >= 100 ? String(Math.round(candidate)) : String(Math.round(candidate * 10) / 10));
|
|
6856
|
+
if (count < 1e3) return String(count);
|
|
6857
|
+
if (count < 1e6) return `${scaled(count / 1e3)}K`;
|
|
6858
|
+
return `${scaled(count / 1e6)}M`;
|
|
6859
|
+
}
|
|
6860
|
+
|
|
6861
|
+
/** Exact token count with thousands separators for the breakdown rows. */
|
|
6862
|
+
function formatTokenGroups(value) {
|
|
6863
|
+
const digits = String(Math.max(0, Math.round(Number.isFinite(value) ? value : 0)));
|
|
6864
|
+
const groups = [];
|
|
6865
|
+
for (let end = digits.length; end > 0; end -= 3) groups.unshift(digits.slice(Math.max(0, end - 3), end));
|
|
6866
|
+
return groups.join(",");
|
|
6867
|
+
}
|
|
6868
|
+
|
|
6869
|
+
/** Decode throughput: whole tokens from ten up, one decimal below. */
|
|
6870
|
+
function formatTokensPerSecond(tps) {
|
|
6871
|
+
const clamped = Math.max(0, Number.isFinite(tps) ? tps : 0);
|
|
6872
|
+
return clamped >= 10 ? String(Math.round(clamped)) : String(Math.round(clamped * 10) / 10);
|
|
6873
|
+
}
|
|
6874
|
+
|
|
6875
|
+
/** Cache-read share of prompt-side input; partial hits never round up to 100%. */
|
|
6876
|
+
function cacheHitPercentOf(tokens) {
|
|
6877
|
+
const prompt = tokens.input + tokens.cacheRead + tokens.cacheWrite;
|
|
6878
|
+
if (prompt <= 0 || tokens.cacheRead <= 0) return null;
|
|
6879
|
+
if (tokens.cacheRead >= prompt) return 100;
|
|
6880
|
+
const rounded = Math.round((tokens.cacheRead / prompt) * 1000) / 10;
|
|
6881
|
+
return rounded >= 100 ? 99.9 : rounded;
|
|
6882
|
+
}
|
|
6883
|
+
|
|
6884
|
+
/** Rows of the 「更多」 panel: one turn's session statistics in reading order.
|
|
6885
|
+
Section rows carry `section: true` and render as a group heading. */
|
|
6886
|
+
function turnDetailRows(detail, t) {
|
|
6887
|
+
if (!isRecord(detail)) return [];
|
|
6888
|
+
const rows = [];
|
|
6889
|
+
if (detail.scope === "all") {
|
|
6890
|
+
rows.push({ key: "turns", label: t("stats.more.turns"), value: t("stats.more.turnsValue", { n: detail.turns }), hint: t("stats.more.turnsHint") });
|
|
6891
|
+
}
|
|
6892
|
+
rows.push({ key: "steps", label: t("stats.more.steps"), value: t("stats.more.stepsValue", { n: detail.steps }), hint: t("stats.more.stepsHint") });
|
|
6893
|
+
rows.push({
|
|
6894
|
+
key: "model",
|
|
6895
|
+
label: t("stats.more.model"),
|
|
6896
|
+
value: detail.models.length > 0 ? detail.models.join(", ") : t("stats.more.unknown"),
|
|
6897
|
+
hint: t("stats.more.modelHint"),
|
|
6898
|
+
});
|
|
6899
|
+
/* turn-only: no single start point exists once the scope aggregates turns */
|
|
6900
|
+
if (detail.scope === "turn" && detail.firstTokenMs !== null) {
|
|
6901
|
+
rows.push({ key: "firstToken", label: t("stats.more.firstToken"), value: formatDurationPrecise(detail.firstTokenMs), hint: t("stats.more.firstTokenHint") });
|
|
6902
|
+
}
|
|
6903
|
+
if (detail.llmMs > 0) rows.push({ key: "llm", label: t("stats.more.llmTime"), value: formatDurationPrecise(detail.llmMs), hint: t("stats.more.llmTimeHint") });
|
|
6904
|
+
if (detail.toolMs > 0) rows.push({ key: "tool", label: t("stats.more.toolTime"), value: formatDurationPrecise(detail.toolMs), hint: t("stats.more.toolTimeHint") });
|
|
6905
|
+
if (detail.ttftSteps > 0) {
|
|
6906
|
+
rows.push({ key: "ttft", label: t("stats.more.ttft"), value: formatDurationPrecise(detail.ttftMs / detail.ttftSteps), hint: t("stats.more.ttftHint") });
|
|
6907
|
+
}
|
|
6908
|
+
if (detail.decodeMs > 0 && detail.decodeTokens > 0) {
|
|
6909
|
+
rows.push({
|
|
6910
|
+
key: "speed",
|
|
6911
|
+
label: t("stats.more.speed"),
|
|
6912
|
+
value: t("stats.more.speedValue", { v: formatTokensPerSecond(detail.decodeTokens / (detail.decodeMs / 1000)) }),
|
|
6913
|
+
hint: t("stats.more.speedHint"),
|
|
6914
|
+
});
|
|
6915
|
+
}
|
|
6916
|
+
if (isRecord(detail.tokens)) {
|
|
6917
|
+
const tokens = detail.tokens;
|
|
6918
|
+
rows.push({ key: "tokensHead", section: true, label: t("stats.more.tokens"), hint: t("stats.more.tokensHint") });
|
|
6919
|
+
rows.push({
|
|
6920
|
+
key: "tokenTotal",
|
|
6921
|
+
label: t("stats.more.tokenTotal"),
|
|
6922
|
+
value: t("stats.more.tokenValue", { count: formatTokensCompact(tokens.total) }),
|
|
6923
|
+
hint: t("stats.more.tokensHint"),
|
|
6924
|
+
});
|
|
6925
|
+
const cacheHit = cacheHitPercentOf(tokens);
|
|
6926
|
+
if (cacheHit !== null) rows.push({ key: "cacheHit", label: t("stats.more.cacheHit"), value: `${cacheHit}%`, hint: t("stats.more.cacheHitHint") });
|
|
6927
|
+
rows.push({ key: "input", label: t("stats.more.input"), value: t("stats.more.tokenValue", { count: formatTokenGroups(tokens.input) }), hint: t("stats.more.inputHint") });
|
|
6928
|
+
if (tokens.cacheRead > 0) {
|
|
6929
|
+
rows.push({ key: "cacheRead", label: t("stats.more.cacheRead"), value: t("stats.more.tokenValue", { count: formatTokenGroups(tokens.cacheRead) }), hint: t("stats.more.cacheReadHint") });
|
|
6930
|
+
}
|
|
6931
|
+
if (tokens.cacheWrite > 0) {
|
|
6932
|
+
rows.push({ key: "cacheWrite", label: t("stats.more.cacheWrite"), value: t("stats.more.tokenValue", { count: formatTokenGroups(tokens.cacheWrite) }), hint: t("stats.more.cacheWriteHint") });
|
|
6933
|
+
}
|
|
6934
|
+
rows.push({ key: "output", label: t("stats.more.output"), value: t("stats.more.tokenValue", { count: formatTokenGroups(tokens.output) }), hint: t("stats.more.outputHint") });
|
|
6935
|
+
if (tokens.reasoning > 0) {
|
|
6936
|
+
rows.push({ key: "reasoning", label: t("stats.more.reasoning"), value: t("stats.more.tokenValue", { count: formatTokenGroups(tokens.reasoning) }), hint: t("stats.more.reasoningHint") });
|
|
6937
|
+
}
|
|
6938
|
+
}
|
|
6939
|
+
return rows;
|
|
6940
|
+
}
|
|
6941
|
+
|
|
6942
|
+
/** One fixed-position popover anchored under a pill. The toolbar wrappers clip
|
|
6943
|
+
absolutely positioned children, so this anchors to the viewport, follows its
|
|
6944
|
+
pill while the chat auto-scrolls, and closes on outside pointerdown/Escape. */
|
|
6945
|
+
function useAnchoredPopover(heightGuess) {
|
|
6946
|
+
const [open, setOpen] = React.useState(false);
|
|
6947
|
+
const [pos, setPos] = React.useState(null);
|
|
6948
|
+
const anchorRef = React.useRef(null);
|
|
6949
|
+
const popRef = React.useRef(null);
|
|
6950
|
+
const place = React.useCallback(() => {
|
|
6951
|
+
const button = anchorRef.current;
|
|
6017
6952
|
if (button === null || button.isConnected !== true) return false;
|
|
6018
6953
|
const rect = button.getBoundingClientRect();
|
|
6019
6954
|
if (rect.bottom < 0 || rect.top > window.innerHeight) return false;
|
|
6020
|
-
|
|
6021
|
-
|
|
6955
|
+
/* After the first paint the mounted popover knows its own height, which is
|
|
6956
|
+
what keeps a long list (every row of the 全部轮次 scope) inside the view. */
|
|
6957
|
+
const height = popRef.current !== null && popRef.current.offsetHeight > 0 ? popRef.current.offsetHeight : heightGuess;
|
|
6958
|
+
setPos({
|
|
6959
|
+
top: Math.min(rect.bottom + 5, Math.max(8, window.innerHeight - height)),
|
|
6022
6960
|
right: Math.max(8, window.innerWidth - rect.right),
|
|
6023
6961
|
});
|
|
6024
6962
|
return true;
|
|
6963
|
+
}, [heightGuess]);
|
|
6964
|
+
const close = React.useCallback(() => {
|
|
6965
|
+
setOpen(false);
|
|
6966
|
+
setPos(null);
|
|
6025
6967
|
}, []);
|
|
6026
6968
|
React.useEffect(() => {
|
|
6027
|
-
if (!
|
|
6028
|
-
|
|
6969
|
+
if (!open) return undefined;
|
|
6970
|
+
/* the popover is mounted by now: re-place with its measured height */
|
|
6971
|
+
if (!place()) {
|
|
6972
|
+
close();
|
|
6973
|
+
return undefined;
|
|
6974
|
+
}
|
|
6029
6975
|
const onDown = (event) => {
|
|
6030
|
-
if (
|
|
6976
|
+
if (popRef.current && !popRef.current.contains(event.target)) close();
|
|
6031
6977
|
};
|
|
6032
6978
|
const onKey = (event) => {
|
|
6033
6979
|
if (event.key === "Escape") close();
|
|
@@ -6039,9 +6985,13 @@ function FileTypeIcon(props) {
|
|
|
6039
6985
|
let frame = 0;
|
|
6040
6986
|
const follow = () => {
|
|
6041
6987
|
if (frame !== 0) return;
|
|
6988
|
+
if (typeof requestAnimationFrame !== "function") {
|
|
6989
|
+
if (!place()) close();
|
|
6990
|
+
return;
|
|
6991
|
+
}
|
|
6042
6992
|
frame = requestAnimationFrame(() => {
|
|
6043
6993
|
frame = 0;
|
|
6044
|
-
if (!
|
|
6994
|
+
if (!place()) close();
|
|
6045
6995
|
});
|
|
6046
6996
|
};
|
|
6047
6997
|
document.addEventListener("mousedown", onDown);
|
|
@@ -6049,13 +6999,22 @@ function FileTypeIcon(props) {
|
|
|
6049
6999
|
window.addEventListener("scroll", follow, true);
|
|
6050
7000
|
window.addEventListener("resize", follow);
|
|
6051
7001
|
return () => {
|
|
6052
|
-
if (frame !== 0) cancelAnimationFrame(frame);
|
|
7002
|
+
if (frame !== 0 && typeof cancelAnimationFrame === "function") cancelAnimationFrame(frame);
|
|
6053
7003
|
document.removeEventListener("mousedown", onDown);
|
|
6054
7004
|
document.removeEventListener("keydown", onKey);
|
|
6055
7005
|
window.removeEventListener("scroll", follow, true);
|
|
6056
7006
|
window.removeEventListener("resize", follow);
|
|
6057
7007
|
};
|
|
6058
|
-
}, [
|
|
7008
|
+
}, [open, place, close]);
|
|
7009
|
+
return { open, setOpen, pos, anchorRef, popRef, place, close };
|
|
7010
|
+
}
|
|
7011
|
+
|
|
7012
|
+
/** Left-hand stat pills on the scope row: duration, tool calls, commands, files, failures. */
|
|
7013
|
+
function StatsBar({ stats, t, canTimeline, timelineOpen, scrubbing, onToggleTimeline, prevJump, onPrevTurn, failedSteps, detail }) {
|
|
7014
|
+
/* both popovers keep their hooks above every early return */
|
|
7015
|
+
const failed = Array.isArray(failedSteps) ? failedSteps.filter((step) => isRecord(step)) : [];
|
|
7016
|
+
const err = useAnchoredPopover(292);
|
|
7017
|
+
const more = useAnchoredPopover(360);
|
|
6059
7018
|
if (!isRecord(stats)) return null;
|
|
6060
7019
|
const duration = typeof stats.durationMs === "number" ? stats.durationMs : 0;
|
|
6061
7020
|
const toolCalls = typeof stats.toolCalls === "number" ? stats.toolCalls : 0;
|
|
@@ -6075,6 +7034,8 @@ function FileTypeIcon(props) {
|
|
|
6075
7034
|
if (duration === 0 && toolCalls === 0) return jumpBtn === null ? null : h("div", { className: "dsov-stats" }, jumpBtn);
|
|
6076
7035
|
const pill = (key, text, title, tone) => h("span", { className: "dsov-stat", key, title, ...(tone === undefined ? {} : { "data-tone": tone }) }, text);
|
|
6077
7036
|
const scrubable = canTimeline === true && typeof onToggleTimeline === "function";
|
|
7037
|
+
/* the aggregate scope is a session reading, not a single turn's */
|
|
7038
|
+
const moreHead = detail !== null && isRecord(detail) && detail.scope === "all" ? t("stats.more.headAll") : t("stats.more.head");
|
|
6078
7039
|
return h(
|
|
6079
7040
|
"div",
|
|
6080
7041
|
{ className: "dsov-stats" },
|
|
@@ -6102,39 +7063,41 @@ function FileTypeIcon(props) {
|
|
|
6102
7063
|
errors > 0
|
|
6103
7064
|
? h(
|
|
6104
7065
|
"span",
|
|
6105
|
-
{ className: "dsov-statWrap", key: "errors", ref:
|
|
7066
|
+
{ className: "dsov-statWrap", key: "errors", ref: err.popRef },
|
|
6106
7067
|
h(
|
|
6107
7068
|
"button",
|
|
6108
7069
|
{
|
|
6109
7070
|
type: "button",
|
|
6110
7071
|
className: "dsov-stat dsov-statBtn",
|
|
6111
7072
|
"data-tone": "err",
|
|
6112
|
-
"data-active":
|
|
6113
|
-
ref:
|
|
7073
|
+
"data-active": err.open ? "true" : undefined,
|
|
7074
|
+
ref: err.anchorRef,
|
|
6114
7075
|
disabled: failed.length === 0,
|
|
6115
7076
|
title: failed.length > 0 ? t("stats.errors.popTitle") : t("stats.errors.title"),
|
|
6116
7077
|
onClick:
|
|
6117
7078
|
failed.length > 0
|
|
6118
7079
|
? () => {
|
|
6119
|
-
if (
|
|
6120
|
-
|
|
7080
|
+
if (err.open) {
|
|
7081
|
+
err.close();
|
|
6121
7082
|
return;
|
|
6122
7083
|
}
|
|
6123
|
-
|
|
7084
|
+
err.setOpen(true);
|
|
7085
|
+
if (!err.place()) err.close();
|
|
6124
7086
|
}
|
|
6125
7087
|
: undefined,
|
|
6126
7088
|
},
|
|
6127
7089
|
t("stats.errors", { n: errors })
|
|
6128
7090
|
),
|
|
6129
|
-
|
|
6130
|
-
?
|
|
7091
|
+
err.open && err.pos !== null && failed.length > 0
|
|
7092
|
+
? renderOverlay(
|
|
7093
|
+
h(
|
|
6131
7094
|
"div",
|
|
6132
|
-
{ className: "dsov-errPop", style: { top: `${
|
|
7095
|
+
{ className: "dsov-errPop", style: { top: `${err.pos.top}px`, right: `${err.pos.right}px` } },
|
|
6133
7096
|
h(
|
|
6134
7097
|
"div",
|
|
6135
7098
|
{ className: "dsov-errPopHead" },
|
|
6136
7099
|
h("span", null, t("stats.errors.list", { n: failed.length }), errors > failed.length ? h("span", { className: "dsov-errPopHint" }, t("stats.errors.truncated")) : null),
|
|
6137
|
-
h("button", { type: "button", className: "dsov-errPopClose", title: t("stats.errors.close"), onClick:
|
|
7100
|
+
h("button", { type: "button", className: "dsov-errPopClose", title: t("stats.errors.close"), onClick: err.close }, "\u00d7")
|
|
6138
7101
|
),
|
|
6139
7102
|
...failed
|
|
6140
7103
|
.slice()
|
|
@@ -6158,6 +7121,61 @@ function FileTypeIcon(props) {
|
|
|
6158
7121
|
)
|
|
6159
7122
|
)
|
|
6160
7123
|
)
|
|
7124
|
+
)
|
|
7125
|
+
: null
|
|
7126
|
+
)
|
|
7127
|
+
: null,
|
|
7128
|
+
detail !== null && isRecord(detail)
|
|
7129
|
+
? h(
|
|
7130
|
+
"span",
|
|
7131
|
+
{ className: "dsov-statWrap", key: "more", ref: more.popRef },
|
|
7132
|
+
h(
|
|
7133
|
+
"button",
|
|
7134
|
+
{
|
|
7135
|
+
type: "button",
|
|
7136
|
+
className: "dsov-stat dsov-statBtn dsov-moreBtn",
|
|
7137
|
+
"aria-haspopup": "dialog",
|
|
7138
|
+
"aria-expanded": more.open ? "true" : "false",
|
|
7139
|
+
"data-active": more.open ? "true" : undefined,
|
|
7140
|
+
ref: more.anchorRef,
|
|
7141
|
+
title: t("stats.more.title"),
|
|
7142
|
+
onClick: () => {
|
|
7143
|
+
if (more.open) {
|
|
7144
|
+
more.close();
|
|
7145
|
+
return;
|
|
7146
|
+
}
|
|
7147
|
+
more.setOpen(true);
|
|
7148
|
+
if (!more.place()) more.close();
|
|
7149
|
+
},
|
|
7150
|
+
},
|
|
7151
|
+
t("stats.more"),
|
|
7152
|
+
h(IconChevron, { size: 10, className: "dsov-moreChevron" })
|
|
7153
|
+
),
|
|
7154
|
+
more.open && more.pos !== null
|
|
7155
|
+
? renderOverlay(
|
|
7156
|
+
h(
|
|
7157
|
+
"div",
|
|
7158
|
+
{ className: "dsov-morePop", style: { top: `${more.pos.top}px`, right: `${more.pos.right}px` }, role: "dialog", "aria-label": moreHead },
|
|
7159
|
+
h(
|
|
7160
|
+
"div",
|
|
7161
|
+
{ className: "dsov-moreHead" },
|
|
7162
|
+
h("span", { className: "dsov-moreTitle" }, moreHead),
|
|
7163
|
+
h("span", { className: "dsov-moreSub" }, detail.scope === "all" ? t("stats.more.allTurns") : t("stats.more.turn", { n: detail.turn })),
|
|
7164
|
+
h("button", { type: "button", className: "dsov-errPopClose", title: t("stats.more.close"), onClick: more.close }, "\u00d7")
|
|
7165
|
+
),
|
|
7166
|
+
h("div", { className: "dsov-moreRule" }),
|
|
7167
|
+
...turnDetailRows(detail, t).map((row) =>
|
|
7168
|
+
row.section === true
|
|
7169
|
+
? h("div", { className: "dsov-moreSection", key: row.key, title: row.hint }, row.label)
|
|
7170
|
+
: h(
|
|
7171
|
+
"div",
|
|
7172
|
+
{ className: "dsov-moreRow", key: row.key, title: row.hint },
|
|
7173
|
+
h("span", { className: "dsov-moreLabel" }, row.label),
|
|
7174
|
+
h("span", { className: "dsov-moreValue" }, row.value)
|
|
7175
|
+
)
|
|
7176
|
+
)
|
|
7177
|
+
)
|
|
7178
|
+
)
|
|
6161
7179
|
: null
|
|
6162
7180
|
)
|
|
6163
7181
|
: null
|
|
@@ -6582,6 +7600,8 @@ function FileTypeIcon(props) {
|
|
|
6582
7600
|
timelineOpen,
|
|
6583
7601
|
scrubbing: asOf !== null,
|
|
6584
7602
|
failedSteps,
|
|
7603
|
+
/* the 「更多」 panel: per-turn session statistics for this scope */
|
|
7604
|
+
detail: summary.turnDetail,
|
|
6585
7605
|
prevJump: asOf !== null && asOf <= summary.turnStart ? prevTurn : null,
|
|
6586
7606
|
onPrevTurn: () => {
|
|
6587
7607
|
setSelection(`turn:${prevTurn}`);
|
|
@@ -6762,7 +7782,18 @@ function FileTypeIcon(props) {
|
|
|
6762
7782
|
)
|
|
6763
7783
|
: null,
|
|
6764
7784
|
introBanner !== null ? h("div", { className: "dsov-turnStart", key: `turnStart-${introSeq}` }, h("span", { className: "dsov-turnStartDot" }), t("intro.newTurn", { n: introBanner })) : null,
|
|
6765
|
-
cfg.showProgress
|
|
7785
|
+
cfg.showProgress
|
|
7786
|
+
? h(ProgressSection, {
|
|
7787
|
+
summary,
|
|
7788
|
+
goal,
|
|
7789
|
+
t,
|
|
7790
|
+
waiting: sessionWaiting && asOf === null,
|
|
7791
|
+
running: sessionRunning && asOf === null,
|
|
7792
|
+
cfg,
|
|
7793
|
+
asOf,
|
|
7794
|
+
introSeq,
|
|
7795
|
+
})
|
|
7796
|
+
: null,
|
|
6766
7797
|
cfg.showArtifacts ? h(ArtifactsSection, { summary, t, binding, stats, asOf, introSeq }) : null,
|
|
6767
7798
|
cfg.showReferences ? h(ReferencesSection, { summary, t, sessionId, asOf, introSeq }) : null
|
|
6768
7799
|
);
|
|
@@ -7585,18 +8616,18 @@ function FileTypeIcon(props) {
|
|
|
7585
8616
|
|
|
7586
8617
|
const CSS = `
|
|
7587
8618
|
.dsov-view{box-sizing:border-box;width:100%;max-width:880px;margin:0 auto;padding:20px 28px 64px;display:flex;flex-direction:column;gap:14px}
|
|
7588
|
-
.dsov-panel{box-sizing:border-box;height:100%;min-height:0;overflow-y:auto;padding:14px 16px 28px;display:flex;flex-direction:column;gap:12px}
|
|
7589
|
-
.dsov-panel,.dsov-viking,.dsov-secBody,.dsov-typeMenu,.dsov-md-pre,.dsov-turnbarScroll,.dsov-toolbarScroll{scrollbar-width:thin;scrollbar-color:color-mix(in srgb,var(--dsw-alias-label-secondary) 35%,transparent) transparent}
|
|
7590
|
-
.dsov-panel::-webkit-scrollbar,.dsov-viking::-webkit-scrollbar,.dsov-secBody::-webkit-scrollbar,.dsov-typeMenu::-webkit-scrollbar,.dsov-md-pre::-webkit-scrollbar,.dsov-turnbarScroll::-webkit-scrollbar,.dsov-toolbarScroll::-webkit-scrollbar{width:6px;height:5px}
|
|
7591
|
-
.dsov-panel::-webkit-scrollbar-thumb,.dsov-viking::-webkit-scrollbar-thumb,.dsov-secBody::-webkit-scrollbar-thumb,.dsov-typeMenu::-webkit-scrollbar-thumb,.dsov-md-pre::-webkit-scrollbar-thumb,.dsov-turnbarScroll::-webkit-scrollbar-thumb,.dsov-toolbarScroll::-webkit-scrollbar-thumb{background:color-mix(in srgb,var(--dsw-alias-label-secondary) 35%,transparent);border-radius:3px}
|
|
7592
|
-
.dsov-panel::-webkit-scrollbar-track,.dsov-viking::-webkit-scrollbar-track,.dsov-secBody::-webkit-scrollbar-track,.dsov-typeMenu::-webkit-scrollbar-track,.dsov-md-pre::-webkit-scrollbar-track,.dsov-turnbarScroll::-webkit-scrollbar-track,.dsov-toolbarScroll::-webkit-scrollbar-track{background:transparent}
|
|
8619
|
+
.dsov-panel{box-sizing:border-box;height:100%;min-height:0;overflow-y:auto;scrollbar-gutter:stable;padding:14px 16px 28px;display:flex;flex-direction:column;gap:12px}
|
|
8620
|
+
.dsov-panel,.dsov-viking,.dsov-secBody,.dsov-planWbBody,.dsov-typeMenu,.dsov-md-pre,.dsov-turnbarScroll,.dsov-toolbarScroll{scrollbar-width:thin;scrollbar-color:color-mix(in srgb,var(--dsw-alias-label-secondary) 35%,transparent) transparent}
|
|
8621
|
+
.dsov-panel::-webkit-scrollbar,.dsov-viking::-webkit-scrollbar,.dsov-secBody::-webkit-scrollbar,.dsov-planWbBody::-webkit-scrollbar,.dsov-typeMenu::-webkit-scrollbar,.dsov-md-pre::-webkit-scrollbar,.dsov-turnbarScroll::-webkit-scrollbar,.dsov-toolbarScroll::-webkit-scrollbar{width:6px;height:5px}
|
|
8622
|
+
.dsov-panel::-webkit-scrollbar-thumb,.dsov-viking::-webkit-scrollbar-thumb,.dsov-secBody::-webkit-scrollbar-thumb,.dsov-planWbBody::-webkit-scrollbar-thumb,.dsov-typeMenu::-webkit-scrollbar-thumb,.dsov-md-pre::-webkit-scrollbar-thumb,.dsov-turnbarScroll::-webkit-scrollbar-thumb,.dsov-toolbarScroll::-webkit-scrollbar-thumb{background:color-mix(in srgb,var(--dsw-alias-label-secondary) 35%,transparent);border-radius:3px}
|
|
8623
|
+
.dsov-panel::-webkit-scrollbar-track,.dsov-viking::-webkit-scrollbar-track,.dsov-secBody::-webkit-scrollbar-track,.dsov-planWbBody::-webkit-scrollbar-track,.dsov-typeMenu::-webkit-scrollbar-track,.dsov-md-pre::-webkit-scrollbar-track,.dsov-turnbarScroll::-webkit-scrollbar-track,.dsov-toolbarScroll::-webkit-scrollbar-track{background:transparent}
|
|
7593
8624
|
.dsov-toolbar{display:flex;align-items:center;gap:8px}
|
|
7594
8625
|
.dsov-toolbarScroll{flex:1 1 auto;min-width:0;display:flex;align-items:center;overflow-x:auto;overflow-y:hidden;padding-bottom:5px}
|
|
7595
8626
|
.dsov-toolbarFixed{flex:none;display:flex;align-items:center;gap:8px;margin-left:auto;padding-bottom:5px}
|
|
7596
8627
|
.dsov-iconBtn{display:inline-flex;align-items:center;gap:6px;height:28px;padding:0 10px;border-radius:8px;border:.5px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;cursor:pointer;white-space:nowrap;flex:none}
|
|
7597
8628
|
.dsov-turnSelect{flex:none}
|
|
7598
8629
|
.dsov-iconBtn:hover{background:color-mix(in srgb,var(--dsw-alias-label-primary) 7%,transparent);color:var(--dsw-alias-label-primary)}
|
|
7599
|
-
.dsov-section{background:var(--dsw-alias-bg-layer-1);border:.5px solid var(--dsw-alias-border-l1);border-radius:12px;padding:12px 14px;display:flex;flex-direction:column;gap:8px}
|
|
8630
|
+
.dsov-section{position:relative;background:var(--dsw-alias-bg-layer-1);border:.5px solid var(--dsw-alias-border-l1);border-radius:12px;padding:12px 14px;display:flex;flex-direction:column;gap:8px}
|
|
7600
8631
|
.dsov-sectionHead{display:flex;align-items:center;gap:6px}
|
|
7601
8632
|
.dsov-sectionToggle{flex:1;min-width:0;display:flex;align-items:center;gap:8px;background:transparent;border:none;margin:0;padding:2px 4px;border-radius:8px;font:inherit;color:inherit;text-align:left;cursor:pointer}
|
|
7602
8633
|
.dsov-sectionToggle:hover{background:color-mix(in srgb,var(--dsw-alias-label-primary) 6%,transparent)}
|
|
@@ -7604,6 +8635,7 @@ function FileTypeIcon(props) {
|
|
|
7604
8635
|
.dsov-chevron[data-open=true]{transform:rotate(90deg)}
|
|
7605
8636
|
.dsov-moreSteps{appearance:none;background:transparent;border:none;cursor:pointer;display:inline-flex;align-items:center;gap:4px;flex:none;margin-left:auto;padding:1px 6px;border-radius:8px;font-size:10.5px;line-height:16px;color:var(--dsw-alias-label-secondary);white-space:nowrap}
|
|
7606
8637
|
.dsov-moreSteps:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
|
|
8638
|
+
.dsov-moreSteps:focus-visible{outline:1.5px solid var(--dsw-alias-brand-primary);outline-offset:1px}
|
|
7607
8639
|
.dsov-moreIcon{flex:none;display:inline-flex;opacity:.75}
|
|
7608
8640
|
.dsov-sectionIcon{flex:none;display:inline-flex;color:var(--dsw-alias-label-secondary)}
|
|
7609
8641
|
.dsov-sectionTitle{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);white-space:nowrap}
|
|
@@ -7611,10 +8643,14 @@ function FileTypeIcon(props) {
|
|
|
7611
8643
|
.dsov-headAction{flex:none;display:inline-flex;align-items:center;gap:5px;height:26px;padding:0 9px;border-radius:8px;border:.5px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;cursor:pointer;white-space:nowrap}
|
|
7612
8644
|
.dsov-headAction:hover{color:var(--dsw-alias-brand-primary);border-color:color-mix(in srgb,var(--dsw-alias-brand-primary) 45%,transparent);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 8%,transparent)}
|
|
7613
8645
|
.dsov-empty{font-size:12.5px;line-height:1.6;color:var(--dsw-alias-label-secondary);padding:4px 2px}
|
|
7614
|
-
.dsov-secBody{max-height:320px;overflow-y:auto;overflow-x:hidden;margin:0 -12px 0 -8px;padding:0 12px 0 8px}
|
|
8646
|
+
.dsov-secBody{max-height:320px;overflow-y:auto;overflow-x:hidden;scrollbar-gutter:stable;margin:0 -12px 0 -8px;padding:0 12px 0 8px}
|
|
7615
8647
|
.dsov-section[data-full=true] .dsov-secBody{max-height:none;overflow:visible;margin:0;padding:0}
|
|
8648
|
+
.dsov-section[data-body=auto] .dsov-secBody{max-height:none;overflow:visible}
|
|
8649
|
+
.dsov-section[data-body=auto] .dsov-live{position:relative;top:0}
|
|
8650
|
+
.dsov-section[data-body=fixed] .dsov-secBody{height:320px;max-height:320px;overflow-y:auto}
|
|
8651
|
+
.dsov-section[data-full=true][data-body=fixed] .dsov-secBody{height:auto;max-height:none;overflow:visible}
|
|
7616
8652
|
.dsov-loading{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:12px;color:var(--dsw-alias-label-secondary)}
|
|
7617
|
-
.dsov-goal{display:flex;flex-direction:column;gap:3px;box-sizing:border-box;border:.5px solid color-mix(in srgb,var(--dsw-alias-brand-primary) 32%,transparent);border-radius:10px;background:color-mix(in srgb,var(--dsw-alias-brand-primary) 6%,transparent);padding:7px 9px 8px;color:var(--dsw-alias-label-primary)}
|
|
8653
|
+
.dsov-goal{display:flex;flex-direction:column;gap:3px;margin:0 0 6px;box-sizing:border-box;border:.5px solid color-mix(in srgb,var(--dsw-alias-brand-primary) 32%,transparent);border-radius:10px;background:color-mix(in srgb,var(--dsw-alias-brand-primary) 6%,transparent);padding:7px 9px 8px;color:var(--dsw-alias-label-primary)}
|
|
7618
8654
|
.dsov-goal[data-phase=paused]{border-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#ff9f0a) 40%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#ff9f0a) 7%,transparent)}
|
|
7619
8655
|
.dsov-goal[data-phase=blocked]{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary,#ff453a) 40%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#ff453a) 7%,transparent)}
|
|
7620
8656
|
.dsov-goal[data-phase=complete]{border-color:color-mix(in srgb,var(--dsw-alias-state-success-primary,#2ea36a) 40%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-success-primary,#2ea36a) 7%,transparent)}
|
|
@@ -7643,10 +8679,6 @@ function FileTypeIcon(props) {
|
|
|
7643
8679
|
.dsov-badge[data-tone=warn]{color:var(--dsw-alias-state-warn-primary);border-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 45%,transparent)}
|
|
7644
8680
|
.dsov-badge[data-tone=error]{color:var(--dsw-alias-state-error-primary);border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 45%,transparent)}
|
|
7645
8681
|
.dsov-badge[data-tone=quiet]{border-color:transparent;background:var(--dsw-alias-bg-layer-2)}
|
|
7646
|
-
.dsov-progress{display:flex;flex-direction:column;gap:6px;margin-top:2px}
|
|
7647
|
-
.dsov-progressTrack{height:4px;border-radius:2px;background:var(--dsw-alias-bg-layer-2);overflow:hidden}
|
|
7648
|
-
.dsov-progressFill{height:100%;border-radius:2px;background:var(--dsw-alias-state-success-primary);transition:width .25s ease}
|
|
7649
|
-
.dsov-progressMeta{font-size:11.5px;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums}
|
|
7650
8682
|
.dsov-todo{display:flex;align-items:flex-start;gap:8px;font-size:13px;line-height:1.55;color:var(--dsw-alias-label-primary);padding:4px 6px;border-radius:8px;cursor:pointer;transition:background-color .18s ease,color .18s ease}
|
|
7651
8683
|
.dsov-todo+.dsov-todo{margin-top:3px}
|
|
7652
8684
|
.dsov-todo:hover{background:color-mix(in srgb,var(--dsw-alias-label-primary) 6%,transparent)}
|
|
@@ -7660,7 +8692,9 @@ function FileTypeIcon(props) {
|
|
|
7660
8692
|
.dsov-todoText{flex:1;min-width:0;overflow-wrap:anywhere}
|
|
7661
8693
|
.dsov-todo[data-status=completed] .dsov-todoText{color:var(--dsw-alias-label-secondary)}
|
|
7662
8694
|
.dsov-todoStatus{flex:none;font-size:11px;line-height:20px;color:var(--dsw-alias-label-secondary)}
|
|
7663
|
-
.dsov-stepsHead{display:flex;align-items:center;gap:8px;font-size:10.5px;line-height:16px;color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));letter-spacing:.02em;padding:
|
|
8695
|
+
.dsov-stepsHead{display:flex;align-items:center;gap:8px;font-size:10.5px;line-height:16px;color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));letter-spacing:.02em;padding:2px 2px 6px}
|
|
8696
|
+
.dsov-stepsHeadLabel{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
8697
|
+
.dsov-stepsFoot{display:flex;justify-content:flex-end;padding:4px 2px 0}
|
|
7664
8698
|
.dsov-steps{position:relative;display:flex;flex-direction:column;gap:3px}
|
|
7665
8699
|
.dsov-step{display:flex;align-items:center;gap:6px;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-primary);padding:3px 6px;min-width:0;border-radius:8px;cursor:pointer;transition:background-color .18s ease}
|
|
7666
8700
|
.dsov-step:hover{background:color-mix(in srgb,var(--dsw-alias-label-primary) 5%,transparent)}
|
|
@@ -7857,7 +8891,8 @@ html[data-dsov-quicknav="off"] nav:has(button[data-index][aria-label]),html[data
|
|
|
7857
8891
|
.dsov-fileMenuErr{padding:5px 8px;font-size:11px;color:var(--dsw-alias-state-error-primary,#ff453a)}
|
|
7858
8892
|
.dsov-statWrap{position:relative;display:inline-flex}
|
|
7859
8893
|
.dsov-statBtn[disabled]{cursor:default}
|
|
7860
|
-
.dsov-
|
|
8894
|
+
.dsov-overlayHost{position:fixed;top:0;left:0;width:0;height:0;z-index:80;pointer-events:none}
|
|
8895
|
+
.dsov-errPop{position:fixed;z-index:80;pointer-events:auto;width:min(360px,78vw);max-height:280px;overflow-y:auto;padding:4px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);border:.5px solid var(--dsw-alias-border-l1);box-shadow:0 8px 22px rgba(0,0,0,.20)}
|
|
7861
8896
|
.dsov-errPopHead{display:flex;justify-content:space-between;align-items:center;gap:8px;padding:2px 4px 4px;font-size:11px;color:var(--dsw-alias-label-secondary)}
|
|
7862
8897
|
.dsov-errPopHint{margin-left:6px;font-size:10px;color:var(--dsw-alias-label-tertiary)}
|
|
7863
8898
|
.dsov-errPopClose{border:0;background:none;cursor:pointer;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:1;padding:2px 5px;border-radius:4px}
|
|
@@ -7869,6 +8904,20 @@ html[data-dsov-quicknav="off"] nav:has(button[data-index][aria-label]),html[data
|
|
|
7869
8904
|
.dsov-errPopTime{color:var(--dsw-alias-label-tertiary);font-size:10px;white-space:nowrap}
|
|
7870
8905
|
.dsov-errPopDetail{color:var(--dsw-alias-label-secondary);word-break:break-all;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
7871
8906
|
.dsov-errPopMsg{margin-top:2px;color:var(--dsw-alias-state-error-primary,#ff453a);white-space:pre-wrap;word-break:break-all;font-size:10.5px;max-height:76px;overflow-y:auto}
|
|
8907
|
+
.dsov-moreBtn{display:inline-flex;align-items:center;gap:3px}
|
|
8908
|
+
.dsov-moreChevron{flex:none;opacity:.7;transition:transform .15s ease}
|
|
8909
|
+
.dsov-moreBtn[data-active=true] .dsov-moreChevron{transform:rotate(180deg)}
|
|
8910
|
+
.dsov-morePop{position:fixed;z-index:80;pointer-events:auto;width:min(300px,82vw);max-height:min(72vh,430px);overflow-y:auto;padding:7px 9px 8px;border-radius:10px;background:var(--dsw-alias-bg-layer-2);border:.5px solid var(--dsw-alias-border-l1);box-shadow:0 8px 22px rgba(0,0,0,.20);color:var(--dsw-alias-label-primary);font-size:11.5px;line-height:1.5}
|
|
8911
|
+
.dsov-moreHead{display:flex;align-items:center;gap:8px;padding:1px 1px 3px}
|
|
8912
|
+
.dsov-moreTitle{flex:none;font-weight:600;font-size:12px;color:var(--dsw-alias-label-primary)}
|
|
8913
|
+
.dsov-moreSub{flex:1 1 auto;min-width:0;color:var(--dsw-alias-label-tertiary);font-size:10.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
8914
|
+
.dsov-moreRule{height:.5px;background:var(--dsw-alias-border-l1);margin:1px 0 5px}
|
|
8915
|
+
.dsov-moreSection{margin:7px 0 3px;font-size:10px;line-height:16px;letter-spacing:.04em;color:var(--dsw-alias-label-tertiary);font-weight:600}
|
|
8916
|
+
.dsov-moreSection:first-of-type{margin-top:2px}
|
|
8917
|
+
.dsov-moreRow{display:flex;align-items:baseline;gap:10px;padding:2px 2px;border-radius:5px}
|
|
8918
|
+
.dsov-moreRow:hover{background:var(--dsw-alias-bg-layer-3)}
|
|
8919
|
+
.dsov-moreLabel{flex:none;color:var(--dsw-alias-label-secondary);white-space:nowrap}
|
|
8920
|
+
.dsov-moreValue{flex:1 1 auto;min-width:0;text-align:right;color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;overflow-wrap:anywhere;word-break:break-word}
|
|
7872
8921
|
.dsov-role{display:flex;align-items:center;gap:6px;flex-wrap:wrap;padding:0 2px;font-size:10.5px;line-height:16px;color:var(--dsw-alias-label-secondary)}
|
|
7873
8922
|
.dsov-roleBadge{flex:none;display:inline-flex;align-items:center;padding:1px 7px;border-radius:999px;font-weight:500;color:var(--dsw-alias-brand-primary,#247bbf);background:color-mix(in srgb,var(--dsw-alias-brand-primary,#247bbf) 12%,transparent)}
|
|
7874
8923
|
.dsov-role[data-kind=sub] .dsov-roleBadge{color:var(--dsw-alias-state-warn-primary,#ff9f0a);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#ff9f0a) 15%,transparent)}
|
|
@@ -7919,15 +8968,68 @@ html[data-dsov-quicknav="off"] nav:has(button[data-index][aria-label]),html[data
|
|
|
7919
8968
|
.dsov-turnSelect:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l3,var(--dsw-alias-border-l2))}
|
|
7920
8969
|
@keyframes dsov-spin{to{transform:rotate(360deg)}}
|
|
7921
8970
|
@keyframes dsov-pulse{0%,100%{opacity:1}50%{opacity:.3}}
|
|
7922
|
-
@keyframes dsov-stepIn{
|
|
7923
|
-
@keyframes dsov-stepOut{
|
|
8971
|
+
@keyframes dsov-stepIn{0%{opacity:0;transform:translateY(14px) scale(.97);filter:blur(1.5px)}60%{opacity:1;filter:blur(0)}100%{opacity:1;transform:none;filter:blur(0)}}
|
|
8972
|
+
@keyframes dsov-stepOut{0%{opacity:1;transform:none;filter:blur(0)}100%{opacity:0;transform:translateY(-16px) scale(.96);filter:blur(2px)}}
|
|
7924
8973
|
@keyframes dsov-refIn{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:none}}
|
|
8974
|
+
@keyframes dsov-liveSweep{0%{transform:translateX(-120%)}55%,100%{transform:translateX(120%)}}
|
|
8975
|
+
@keyframes dsov-liveHalo{0%{box-shadow:0 0 0 0 color-mix(in srgb,var(--dsw-alias-brand-primary,#247bbf) 45%,transparent)}70%{box-shadow:0 0 0 7px transparent}100%{box-shadow:0 0 0 0 transparent}}
|
|
8976
|
+
@keyframes dsov-liveBar{0%,100%{transform:scaleY(.35)}50%{transform:scaleY(1)}}
|
|
8977
|
+
@keyframes dsov-liveRim{0%,100%{opacity:0}35%{opacity:.55}65%{opacity:.22}}
|
|
8978
|
+
@keyframes dsov-stepScan{0%{background-position:180% 0}100%{background-position:-80% 0}}
|
|
8979
|
+
@keyframes dsov-stepRailPulse{0%,100%{opacity:.4}50%{opacity:1}}
|
|
8980
|
+
@keyframes dsov-stepsNudgeA{0%{transform:translateY(5px)}55%{transform:translateY(-2px)}100%{transform:none}}
|
|
8981
|
+
@keyframes dsov-stepsNudgeB{0%{transform:translateY(5px)}55%{transform:translateY(-2px)}100%{transform:none}}
|
|
8982
|
+
@keyframes dsov-stepHit{0%{background:color-mix(in srgb,var(--dsw-alias-state-success-primary,#2ea36a) 22%,transparent);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dsw-alias-state-success-primary,#2ea36a) 75%,transparent),0 0 14px color-mix(in srgb,var(--dsw-alias-state-success-primary,#2ea36a) 40%,transparent)}100%{background:transparent;box-shadow:0 0 0 0 transparent}}
|
|
8983
|
+
@keyframes dsov-hitPop{0%{transform:scale(1)}40%{transform:scale(1.35)}100%{transform:scale(1)}}
|
|
7925
8984
|
.dsov-row[data-enter=true]{animation:dsov-refIn .32s ease both}
|
|
7926
|
-
.dsov-step[data-enter=true]{animation:dsov-stepIn .
|
|
7927
|
-
.dsov-step[data-exit=true]{position:absolute;top:0;left:0;right:0;animation:dsov-stepOut .
|
|
8985
|
+
.dsov-step[data-enter=true]{animation:dsov-stepIn .42s cubic-bezier(.22,.61,.36,1) both}
|
|
8986
|
+
.dsov-step[data-exit=true]{position:absolute;top:0;left:0;right:0;animation:dsov-stepOut .5s cubic-bezier(.22,.61,.36,1) both;pointer-events:none;z-index:1}
|
|
8987
|
+
.dsov-steps[data-nudge=odd]{animation:dsov-stepsNudgeA .34s cubic-bezier(.22,.61,.36,1)}
|
|
8988
|
+
.dsov-steps[data-nudge=even]{animation:dsov-stepsNudgeB .34s cubic-bezier(.22,.61,.36,1)}
|
|
8989
|
+
.dsov-step[data-active=true]{position:relative;isolation:isolate;background:color-mix(in srgb,var(--dsw-alias-brand-primary,#247bbf) 9%,transparent)}
|
|
8990
|
+
.dsov-step[data-active=true]::before{content:"";position:absolute;top:3px;bottom:3px;left:1px;width:2px;border-radius:1px;background:var(--dsw-alias-brand-primary,#247bbf);animation:dsov-stepRailPulse 1.8s ease-in-out infinite;pointer-events:none}
|
|
8991
|
+
.dsov-step[data-active=true]:hover{background:color-mix(in srgb,var(--dsw-alias-brand-primary,#247bbf) 13%,transparent)}
|
|
8992
|
+
.dsov-step[data-active=true]::after{content:"";position:absolute;inset:0;z-index:-1;border-radius:inherit;background:linear-gradient(95deg,color-mix(in srgb,var(--dsw-alias-brand-primary,#247bbf) 20%,transparent),transparent 55%);background-size:220% 100%;animation:dsov-stepScan 2.2s linear infinite;pointer-events:none}
|
|
8993
|
+
.dsov-step[data-active=true] .dsov-stepName{font-weight:600;color:var(--dsw-alias-brand-primary,#247bbf)}
|
|
8994
|
+
.dsov-step[data-hit=true]{animation:dsov-stepHit .8s ease-out}
|
|
8995
|
+
.dsov-step[data-hit=true] .dsov-todoIcon{animation:dsov-hitPop .45s cubic-bezier(.2,1.6,.4,1)}
|
|
8996
|
+
.dsov-live{position:sticky;top:-1px;z-index:3;isolation:isolate;display:flex;align-items:center;gap:8px;min-width:0;box-sizing:border-box;margin:0 0 6px;padding:6px 9px;border-radius:10px;border:.5px solid color-mix(in srgb,var(--dsw-alias-brand-primary,#247bbf) 42%,transparent);background:color-mix(in srgb,var(--dsw-alias-brand-primary,#247bbf) 7%,var(--dsw-alias-bg-layer-1));color:var(--dsw-alias-label-primary);font-size:11.5px;line-height:1.5;overflow:hidden}
|
|
8997
|
+
.dsov-live::before{content:"";position:absolute;inset:0;z-index:-1;background:linear-gradient(100deg,transparent 0%,color-mix(in srgb,var(--dsw-alias-brand-primary,#247bbf) 22%,transparent) 45%,transparent 70%);transform:translateX(-120%);animation:dsov-liveSweep 2.4s ease-in-out infinite;pointer-events:none}
|
|
8998
|
+
.dsov-section[data-full=true] .dsov-live{position:relative;top:0}
|
|
8999
|
+
.dsov-live[data-state=waiting]{border-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#ff9f0a) 48%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#ff9f0a) 9%,var(--dsw-alias-bg-layer-1))}
|
|
9000
|
+
.dsov-live[data-state=waiting]::before{background:linear-gradient(100deg,transparent 0%,color-mix(in srgb,var(--dsw-alias-state-warn-primary,#ff9f0a) 22%,transparent) 45%,transparent 70%);animation:none;transform:translateX(-120%)}
|
|
9001
|
+
.dsov-livePulse{position:relative;flex:none;display:inline-flex;align-items:center;justify-content:center;width:8px;height:8px}
|
|
9002
|
+
.dsov-liveDot{width:8px;height:8px;border-radius:50%;background:var(--dsw-alias-brand-primary,#247bbf);animation:dsov-liveHalo 1.6s ease-out infinite}
|
|
9003
|
+
.dsov-live[data-state=waiting] .dsov-liveDot{background:var(--dsw-alias-state-warn-primary,#ff9f0a);animation-duration:2.6s}
|
|
9004
|
+
.dsov-liveText{display:flex;align-items:baseline;gap:7px;min-width:0;flex:1}
|
|
9005
|
+
.dsov-liveLabel{flex:none;font-weight:600;color:var(--dsw-alias-brand-primary,#247bbf);letter-spacing:.02em}
|
|
9006
|
+
.dsov-live[data-state=waiting] .dsov-liveLabel{color:var(--dsw-alias-state-warn-primary,#ff9f0a)}
|
|
9007
|
+
.dsov-liveNow{min-width:0;color:var(--dsw-alias-label-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-variant-numeric:tabular-nums}
|
|
9008
|
+
.dsov-liveBars{flex:none;display:inline-flex;align-items:flex-end;gap:1.5px;height:12px}
|
|
9009
|
+
.dsov-liveBars i{width:2.5px;height:100%;border-radius:1px;background:var(--dsw-alias-brand-primary,#247bbf);transform-origin:bottom;animation:dsov-liveBar 1s ease-in-out infinite}
|
|
9010
|
+
.dsov-liveBars i:nth-child(2){animation-delay:.12s}
|
|
9011
|
+
.dsov-liveBars i:nth-child(3){animation-delay:.24s}
|
|
9012
|
+
.dsov-liveBars i:nth-child(4){animation-delay:.36s}
|
|
9013
|
+
.dsov-liveBars i:nth-child(5){animation-delay:.48s}
|
|
9014
|
+
.dsov-live[data-state=waiting] .dsov-liveBars i{background:var(--dsw-alias-state-warn-primary,#ff9f0a);animation:none;transform:scaleY(.42);opacity:.55}
|
|
9015
|
+
.dsov-liveClock{flex:none;font-size:10.5px;color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));font-variant-numeric:tabular-nums;padding-left:7px;border-left:.5px solid color-mix(in srgb,var(--dsw-alias-label-primary) 12%,transparent)}
|
|
9016
|
+
.dsov-liveMeter{flex:none;display:inline-flex;align-items:center;gap:5px;min-width:0}
|
|
9017
|
+
.dsov-liveMeterTrack{flex:none;width:44px;height:5px;border-radius:3px;background:color-mix(in srgb,var(--dsw-alias-label-primary) 12%,transparent);overflow:hidden}
|
|
9018
|
+
.dsov-liveMeterFill{display:block;height:100%;border-radius:3px;background:var(--dsw-alias-brand-primary,#247bbf);transition:width .3s ease}
|
|
9019
|
+
.dsov-live[data-state=waiting] .dsov-liveMeterFill{background:var(--dsw-alias-state-warn-primary,#ff9f0a)}
|
|
9020
|
+
.dsov-live[data-state=summary] .dsov-liveMeterFill{background:var(--dsw-alias-state-success-primary,#2ea36a)}
|
|
9021
|
+
.dsov-liveMeterCount{flex:none;font-size:10.5px;line-height:1;color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));font-variant-numeric:tabular-nums}
|
|
9022
|
+
.dsov-live[data-state=summary]{border-color:var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary)}
|
|
9023
|
+
.dsov-live[data-state=summary]::before{display:none}
|
|
9024
|
+
.dsov-live[data-state=summary] .dsov-liveDot{background:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary));animation:none}
|
|
9025
|
+
.dsov-live[data-state=summary][data-complete=true] .dsov-liveDot{background:var(--dsw-alias-state-success-primary,#2ea36a)}
|
|
9026
|
+
.dsov-live[data-state=summary] .dsov-liveLabel{color:var(--dsw-alias-label-secondary);font-weight:600}
|
|
9027
|
+
.dsov-live[data-state=summary] .dsov-liveNow{color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary))}
|
|
9028
|
+
.dsov-section[data-live=true]::after{content:"";position:absolute;inset:-1px;border-radius:13px;border:1px solid color-mix(in srgb,var(--dsw-alias-brand-primary,#247bbf) 55%,transparent);opacity:0;pointer-events:none;animation:dsov-liveRim 2.6s ease-in-out infinite}
|
|
9029
|
+
.dsov-section[data-live=true] .dsov-sectionIcon{animation:dsov-pulse 2s ease-in-out infinite}
|
|
7928
9030
|
.dsov-todoSpin{transform-origin:7.5px 7.5px;animation:dsov-spin 1.1s linear infinite}
|
|
7929
9031
|
.dsov-todoPulse{animation:dsov-pulse 1.4s ease-in-out infinite}
|
|
7930
|
-
@media (prefers-reduced-motion: reduce){.dsov-todoSpin,.dsov-todoPulse,.dsov-step[data-enter=true],.dsov-row[data-enter=true],.dsov-section[data-intro],.dsov-turnStart{animation:none}.dsov-step[data-exit=true]{animation:none;opacity:0}}
|
|
9032
|
+
@media (prefers-reduced-motion: reduce){.dsov-todoSpin,.dsov-todoPulse,.dsov-step[data-enter=true],.dsov-row[data-enter=true],.dsov-section[data-intro],.dsov-turnStart{animation:none}.dsov-step[data-exit=true]{animation:none;opacity:0}.dsov-live::before,.dsov-liveDot,.dsov-liveBars i,.dsov-step[data-active=true],.dsov-step[data-active=true]::before,.dsov-step[data-active=true]::after,.dsov-steps[data-nudge],.dsov-step[data-hit=true],.dsov-step[data-hit=true] .dsov-todoIcon,.dsov-section[data-live=true]::after,.dsov-section[data-live=true] .dsov-sectionIcon{animation:none}.dsov-liveMeterFill{transition:none}}
|
|
7931
9033
|
.dsov-vikingMode{align-self:flex-start;margin-left:auto}
|
|
7932
9034
|
.dsov-seg{flex:none;display:inline-flex;gap:2px;padding:2px;border-radius:8px;background:var(--dsw-alias-bg-layer-2)}
|
|
7933
9035
|
.dsov-segBtn{border:none;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:11px;line-height:18px;padding:0 8px;border-radius:6px;cursor:pointer;white-space:nowrap}
|
|
@@ -7979,19 +9081,946 @@ html[data-dsov-quicknav="off"] nav:has(button[data-index][aria-label]),html[data
|
|
|
7979
9081
|
.dsov-ft-ppt{--dsh-file-type-default-color:var(--dsw-static-amber-500)}
|
|
7980
9082
|
.dsov-ft-video{--dsh-file-type-default-color:var(--dsh-file-type-violet)}
|
|
7981
9083
|
.dsov-ft-word{--dsh-file-type-default-color:var(--dsw-static-deepseek-450)}
|
|
9084
|
+
.dsov-planActionWrap{display:inline-flex;align-items:center;gap:6px;flex-wrap:wrap}
|
|
9085
|
+
.dsov-planActionHost{display:flex;align-items:center;gap:6px;min-width:0}
|
|
9086
|
+
.dsov-planAction{display:inline-flex;align-items:center;gap:6px;height:30px;padding:0 12px;border-radius:8px;border:.5px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12.5px;cursor:pointer}
|
|
9087
|
+
.dsov-planAction:hover{background:var(--dsw-alias-bg-layer-2)}
|
|
9088
|
+
.dsov-planActionError{font-size:11.5px;color:var(--dsw-alias-state-error-primary)}
|
|
9089
|
+
.dsov-planRow{position:relative;display:flex;align-items:center;margin:0 -8px}
|
|
9090
|
+
.dsov-planRow .dsov-row{flex:1;min-width:0;margin:0}
|
|
9091
|
+
.dsov-planRowAction{position:absolute;right:0;top:50%;transform:translateY(-50%);width:26px;height:26px;border-radius:7px;border:.5px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-label-secondary);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;opacity:0;transition:opacity .12s ease;z-index:1}
|
|
9092
|
+
.dsov-planRow:hover .dsov-planRowAction,.dsov-planRow:focus-within .dsov-planRowAction{opacity:1}
|
|
9093
|
+
.dsov-planRow:hover .dsov-rowTail{visibility:hidden}
|
|
9094
|
+
.dsov-planWb{box-sizing:border-box;height:100%;min-height:0;overflow:hidden;padding:0 16px;display:flex;flex-direction:column;font-size:13px}
|
|
9095
|
+
.dsov-planWbHead{flex:none;display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:14px 0 10px;border-bottom:.5px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1)}
|
|
9096
|
+
.dsov-planWbTitle{font-size:13.5px;font-weight:600;color:var(--dsw-alias-label-primary);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
|
|
9097
|
+
.dsov-planWbState{font-size:11px;line-height:18px;padding:0 7px;border-radius:999px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2);white-space:nowrap;flex:none}
|
|
9098
|
+
.dsov-planWbState[data-tone=live]{color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 12%,transparent)}
|
|
9099
|
+
.dsov-planWbModes{display:inline-flex;flex:none;padding:2px;gap:2px;border-radius:8px;background:var(--dsw-alias-bg-layer-2)}
|
|
9100
|
+
.dsov-planWbMode{border:none;background:transparent;cursor:pointer;font:inherit;font-size:12px;line-height:20px;padding:0 9px;border-radius:6px;color:var(--dsw-alias-label-secondary)}
|
|
9101
|
+
.dsov-planWbMode[data-active=true]{background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-label-primary);font-weight:600}
|
|
9102
|
+
.dsov-planWbBody{flex:1;min-height:0;overflow-y:auto;scrollbar-gutter:stable;padding:2px 0 12px}
|
|
9103
|
+
.dsov-planWbSections{display:flex;flex-direction:column;gap:2px;padding:6px 0 10px}
|
|
9104
|
+
.dsov-planWbSection{position:relative;padding:4px 30px 4px 0;border-radius:8px}
|
|
9105
|
+
.dsov-planWbSection:hover{background:color-mix(in srgb,var(--dsw-alias-label-primary) 4%,transparent)}
|
|
9106
|
+
.dsov-planWbSectionBody{min-width:0;overflow-wrap:anywhere}
|
|
9107
|
+
.dsov-planWbWhole{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--dsw-alias-label-secondary);border-bottom:.5px dashed var(--dsw-alias-border-l2);padding-bottom:2px;margin:2px 0 6px}
|
|
9108
|
+
.dsov-planWbAdd{position:absolute;top:6px;right:2px;width:22px;height:22px;border-radius:6px;border:.5px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1,transparent);color:var(--dsw-alias-label-secondary);cursor:pointer;font-size:14px;line-height:1;display:inline-flex;align-items:center;justify-content:center;opacity:0;transition:opacity .12s ease}
|
|
9109
|
+
.dsov-planWbSection:hover .dsov-planWbAdd,.dsov-planWbSection:focus-within .dsov-planWbAdd{opacity:1}
|
|
9110
|
+
.dsov-planWbNote{margin:6px 0 4px 12px;padding:6px 10px;border-left:2px solid var(--dsw-alias-brand-primary);border-radius:0 8px 8px 0;background:color-mix(in srgb,var(--dsw-alias-brand-primary) 6%,transparent)}
|
|
9111
|
+
.dsov-planWbNoteHead{display:flex;align-items:flex-start;gap:6px}
|
|
9112
|
+
.dsov-planWbNoteQuote{flex:1;min-width:0;font-size:11.5px;color:var(--dsw-alias-label-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
9113
|
+
.dsov-planWbNoteDel{flex:none;border:none;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;font-size:13px;line-height:16px;padding:0 2px;border-radius:4px}
|
|
9114
|
+
.dsov-planWbNoteDel:hover{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-bg-layer-2)}
|
|
9115
|
+
.dsov-planWbNoteText{font-size:12.5px;line-height:1.55;color:var(--dsw-alias-label-primary);white-space:pre-wrap;overflow-wrap:anywhere;margin-top:2px}
|
|
9116
|
+
.dsov-planWbEditor{margin:6px 0 6px 12px;display:flex;flex-direction:column;gap:6px}
|
|
9117
|
+
.dsov-planWbTextarea,.dsov-planWbEditText{width:100%;box-sizing:border-box;resize:vertical;border:.5px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-1,transparent);color:var(--dsw-alias-label-primary);font:inherit;font-size:12.5px;line-height:1.55;padding:7px 9px;outline:none}
|
|
9118
|
+
.dsov-planWbTextarea:focus,.dsov-planWbEditText:focus{border-color:var(--dsw-alias-brand-primary)}
|
|
9119
|
+
.dsov-planWbEdit{display:flex;flex-direction:column;gap:8px;padding:8px 0}
|
|
9120
|
+
.dsov-planWbEditText{min-height:52vh;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px}
|
|
9121
|
+
.dsov-planWbEditorFoot{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
9122
|
+
.dsov-planWbHint{flex:1;min-width:120px;font-size:11.5px;color:var(--dsw-alias-label-secondary)}
|
|
9123
|
+
.dsov-planWbPrimary{flex:none;border:none;border-radius:8px;padding:0 12px;height:28px;font:inherit;font-size:12.5px;cursor:pointer;background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-onbrand,#fff)}
|
|
9124
|
+
.dsov-planWbPrimary:disabled{opacity:.5;cursor:default}
|
|
9125
|
+
.dsov-planWbGhost{flex:none;border:.5px solid var(--dsw-alias-border-l1);border-radius:8px;padding:0 10px;height:28px;font:inherit;font-size:12.5px;cursor:pointer;background:transparent;color:var(--dsw-alias-label-secondary)}
|
|
9126
|
+
.dsov-planWbGhost:disabled{opacity:.5;cursor:default}
|
|
9127
|
+
.dsov-planWbFoot{flex:none;display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:10px 0 14px;border-top:.5px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1)}
|
|
9128
|
+
.dsov-planWbCount{font-size:12px;color:var(--dsw-alias-label-secondary);flex:none}
|
|
9129
|
+
.dsov-planWbErr{flex:1;min-width:120px;font-size:12px;color:var(--dsw-alias-state-error-primary);overflow-wrap:anywhere}
|
|
9130
|
+
.dsov-planWbNotice{flex:1;min-width:120px;font-size:12px;color:var(--dsw-alias-state-success-primary)}
|
|
9131
|
+
.dsov-planWbUnavailable{flex:1;min-height:0;overflow-y:auto;display:flex;flex-direction:column;align-items:flex-start;gap:10px;font-size:13px;color:var(--dsw-alias-label-secondary);padding:14px 2px}
|
|
7982
9132
|
`;
|
|
7983
9133
|
|
|
7984
9134
|
/* ------------------------------------------------------------------ */
|
|
7985
|
-
/*
|
|
9135
|
+
/* Plan workbench (extractable) */
|
|
9136
|
+
/* */
|
|
9137
|
+
/* Review-card annotation + revision for plan-mode plans. This whole */
|
|
9138
|
+
/* region reaches the rest of the module only through planTitleOf, */
|
|
9139
|
+
/* rememberContextPreview / readContextPreview, MarkdownBody, */
|
|
9140
|
+
/* ChatMarkdown, insertIntoComposer, openNativePlan and translate, so */
|
|
9141
|
+
/* it can be lifted into its own plugin without touching anything */
|
|
9142
|
+
/* else. DocumentWorkbench (render/annotate/rewrite) stays free of */
|
|
9143
|
+
/* plan-mode knowledge; the delivery helpers below are the plan half. */
|
|
7986
9144
|
/* ------------------------------------------------------------------ */
|
|
7987
9145
|
|
|
7988
|
-
const
|
|
7989
|
-
|
|
7990
|
-
|
|
7991
|
-
|
|
7992
|
-
|
|
9146
|
+
const PLAN_TAB_ID = "dsh-conversation-overview/plan";
|
|
9147
|
+
const PLAN_TAB_KIND = "overview-plan";
|
|
9148
|
+
const PLAN_REVIEW_ACTION_ID = "dsh-conversation-overview/plan-review";
|
|
9149
|
+
const PLAN_ADDRESS_PREFIX = "dsh-resource://overview-plan/";
|
|
9150
|
+
const PLAN_WB_REF_PREFIX = "planwb|";
|
|
9151
|
+
/* a resource address no protocol claims: the body then keeps to its snapshot */
|
|
9152
|
+
const PLAN_NO_RESOURCE = "sidebar://no-plan-resource";
|
|
9153
|
+
const PLAN_ANNOTATION_STORE_KEY = "dsh.conversationOverview.planAnnotations.v1";
|
|
9154
|
+
const PLAN_ANNOTATION_STORE_VERSION = 1;
|
|
9155
|
+
const PLAN_ANNOTATION_PLANS_LIMIT = 40;
|
|
9156
|
+
const PLAN_ANNOTATION_PER_PLAN_LIMIT = 50;
|
|
9157
|
+
const PLAN_COMMENT_LIMIT = 4000;
|
|
9158
|
+
const PLAN_EDIT_TEXT_LIMIT = 40000;
|
|
9159
|
+
const PLAN_SECTION_QUOTE_LIMIT = 200;
|
|
9160
|
+
/* sectionIndex used by an opinion on the whole plan */
|
|
9161
|
+
const PLAN_WHOLE = -1;
|
|
9162
|
+
|
|
9163
|
+
/** Resource address of one logged plan; mirrors the plan plugin's own scheme. */
|
|
9164
|
+
function planResourceAddress(sessionId, callId) {
|
|
9165
|
+
if (typeof sessionId !== "string" || sessionId === "" || typeof callId !== "string" || callId === "") return undefined;
|
|
9166
|
+
let segments;
|
|
9167
|
+
try {
|
|
9168
|
+
const sessions = runtime && runtime.ctx ? runtime.ctx.sessions : undefined;
|
|
9169
|
+
const child = sessions !== undefined && sessions !== null && typeof sessions.subagentAddress === "function" ? sessions.subagentAddress(sessionId) : undefined;
|
|
9170
|
+
segments = child === undefined || child === null ? [sessionId, callId] : ["subagent", child.parentSessionId, child.childSessionId, child.mode, callId];
|
|
9171
|
+
} catch {
|
|
9172
|
+
segments = [sessionId, callId]; /* uninjected ctx access throws; plain sessions are the common case */
|
|
9173
|
+
}
|
|
9174
|
+
return `dsh-resource://plan/${segments.map(encodeURIComponent).join("/")}`;
|
|
9175
|
+
}
|
|
7993
9176
|
|
|
7994
|
-
|
|
9177
|
+
/* ---------------------------------------------------------------- */
|
|
9178
|
+
/* Identity + address */
|
|
9179
|
+
/* ---------------------------------------------------------------- */
|
|
9180
|
+
|
|
9181
|
+
/** Stable per-plan identity: the logged call id wins, then the review request, then the snapshot. */
|
|
9182
|
+
function planWorkbenchIdentity(callId, requestKey, previewKey) {
|
|
9183
|
+
if (typeof callId === "string" && callId !== "") return `call:${callId}`;
|
|
9184
|
+
if (typeof requestKey === "string" && requestKey !== "") return `review:${requestKey}`;
|
|
9185
|
+
if (typeof previewKey === "string" && previewKey !== "") return `artifact:${previewKey}`;
|
|
9186
|
+
return undefined;
|
|
9187
|
+
}
|
|
9188
|
+
|
|
9189
|
+
function planWorkbenchKey(sessionId, identity) {
|
|
9190
|
+
return `${sessionId}|${PLAN_WB_REF_PREFIX}${identity}`;
|
|
9191
|
+
}
|
|
9192
|
+
|
|
9193
|
+
function parsePlanWorkbenchKey(key) {
|
|
9194
|
+
if (typeof key !== "string") return undefined;
|
|
9195
|
+
const separator = key.indexOf("|");
|
|
9196
|
+
if (separator <= 0) return undefined;
|
|
9197
|
+
const sessionId = key.slice(0, separator);
|
|
9198
|
+
const rest = key.slice(separator + 1);
|
|
9199
|
+
if (!rest.startsWith(PLAN_WB_REF_PREFIX)) return undefined;
|
|
9200
|
+
const identity = rest.slice(PLAN_WB_REF_PREFIX.length);
|
|
9201
|
+
const colon = identity.indexOf(":");
|
|
9202
|
+
if (colon <= 0) return undefined;
|
|
9203
|
+
const kind = identity.slice(0, colon);
|
|
9204
|
+
const id = identity.slice(colon + 1);
|
|
9205
|
+
if (id === "" || (kind !== "call" && kind !== "review" && kind !== "artifact")) return undefined;
|
|
9206
|
+
return { sessionId, kind, id };
|
|
9207
|
+
}
|
|
9208
|
+
|
|
9209
|
+
function planWorkbenchAddress(key) {
|
|
9210
|
+
return `${PLAN_ADDRESS_PREFIX}${encodeURIComponent(key)}`;
|
|
9211
|
+
}
|
|
9212
|
+
|
|
9213
|
+
function parsePlanWorkbenchAddress(address) {
|
|
9214
|
+
if (typeof address !== "string" || !address.startsWith(PLAN_ADDRESS_PREFIX)) return undefined;
|
|
9215
|
+
try {
|
|
9216
|
+
const key = decodeURIComponent(address.slice(PLAN_ADDRESS_PREFIX.length));
|
|
9217
|
+
return key === "" ? undefined : key;
|
|
9218
|
+
} catch {
|
|
9219
|
+
return undefined;
|
|
9220
|
+
}
|
|
9221
|
+
}
|
|
9222
|
+
|
|
9223
|
+
/* ---------------------------------------------------------------- */
|
|
9224
|
+
/* Sectioning */
|
|
9225
|
+
/* ---------------------------------------------------------------- */
|
|
9226
|
+
|
|
9227
|
+
/** Split a plan into heading-anchored sections; fences never open a section. */
|
|
9228
|
+
function planSections(text) {
|
|
9229
|
+
const source = typeof text === "string" ? text : "";
|
|
9230
|
+
const lines = source.split("\n");
|
|
9231
|
+
const sections = [];
|
|
9232
|
+
let start = 0;
|
|
9233
|
+
let level = 0;
|
|
9234
|
+
let heading = "";
|
|
9235
|
+
let fence = null;
|
|
9236
|
+
const push = (endLine) => {
|
|
9237
|
+
const body = lines.slice(start, endLine + 1).join("\n").replace(/^\s*\n+/, "").replace(/\n+\s*$/, "");
|
|
9238
|
+
if (body.trim() === "") return;
|
|
9239
|
+
const section = { index: sections.length, level, heading, startLine: start, endLine };
|
|
9240
|
+
section.text = body;
|
|
9241
|
+
section.quote = planSectionQuote(section);
|
|
9242
|
+
sections.push(section);
|
|
9243
|
+
};
|
|
9244
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
9245
|
+
const trimmed = lines[index].trim();
|
|
9246
|
+
if (fence !== null) {
|
|
9247
|
+
if (trimmed.startsWith(fence)) fence = null;
|
|
9248
|
+
continue;
|
|
9249
|
+
}
|
|
9250
|
+
if (trimmed.startsWith("```") || trimmed.startsWith("~~~")) {
|
|
9251
|
+
fence = trimmed.slice(0, 3);
|
|
9252
|
+
continue;
|
|
9253
|
+
}
|
|
9254
|
+
const match = /^(#{1,6})[ \t]+(.*)$/.exec(trimmed);
|
|
9255
|
+
if (match === null) continue;
|
|
9256
|
+
push(index - 1);
|
|
9257
|
+
start = index;
|
|
9258
|
+
level = match[1].length;
|
|
9259
|
+
heading = match[2].replace(/[#*`]/g, "").trim().slice(0, 120);
|
|
9260
|
+
}
|
|
9261
|
+
push(lines.length - 1);
|
|
9262
|
+
return sections;
|
|
9263
|
+
}
|
|
9264
|
+
|
|
9265
|
+
/** First body line of a section, collapsed for the feedback message and note cards. */
|
|
9266
|
+
function planSectionQuote(section) {
|
|
9267
|
+
if (!isRecord(section)) return "";
|
|
9268
|
+
const heading = typeof section.heading === "string" ? section.heading : "";
|
|
9269
|
+
const lines = (typeof section.text === "string" ? section.text : "").split("\n");
|
|
9270
|
+
for (const line of lines) {
|
|
9271
|
+
const trimmed = line.trim();
|
|
9272
|
+
if (trimmed === "" || /^#{1,6}[ \t]+/.test(trimmed)) continue;
|
|
9273
|
+
return trimmed.replace(/\s+/g, " ").slice(0, PLAN_SECTION_QUOTE_LIMIT);
|
|
9274
|
+
}
|
|
9275
|
+
return heading.slice(0, PLAN_SECTION_QUOTE_LIMIT);
|
|
9276
|
+
}
|
|
9277
|
+
|
|
9278
|
+
/* ---------------------------------------------------------------- */
|
|
9279
|
+
/* Annotation storage (browser-local) */
|
|
9280
|
+
/* ---------------------------------------------------------------- */
|
|
9281
|
+
|
|
9282
|
+
function emptyPlanBook() {
|
|
9283
|
+
return { annotations: [], edited: "" };
|
|
9284
|
+
}
|
|
9285
|
+
|
|
9286
|
+
function sanitizePlanBook(value) {
|
|
9287
|
+
const source = isRecord(value) ? value : {};
|
|
9288
|
+
const raw = Array.isArray(source.annotations) ? source.annotations : [];
|
|
9289
|
+
const annotations = [];
|
|
9290
|
+
for (const item of raw) {
|
|
9291
|
+
if (annotations.length >= PLAN_ANNOTATION_PER_PLAN_LIMIT) break;
|
|
9292
|
+
if (!isRecord(item)) continue;
|
|
9293
|
+
const comment = typeof item.comment === "string" ? item.comment.trim().slice(0, PLAN_COMMENT_LIMIT) : "";
|
|
9294
|
+
if (comment === "") continue;
|
|
9295
|
+
annotations.push({
|
|
9296
|
+
id: typeof item.id === "string" && item.id !== "" ? item.id : `a${annotations.length}`,
|
|
9297
|
+
sectionIndex: Number.isSafeInteger(item.sectionIndex) ? item.sectionIndex : PLAN_WHOLE,
|
|
9298
|
+
heading: typeof item.heading === "string" ? item.heading.slice(0, 120) : "",
|
|
9299
|
+
quote: typeof item.quote === "string" ? item.quote.slice(0, PLAN_SECTION_QUOTE_LIMIT) : "",
|
|
9300
|
+
comment,
|
|
9301
|
+
createdAt: typeof item.createdAt === "number" ? item.createdAt : 0,
|
|
9302
|
+
});
|
|
9303
|
+
}
|
|
9304
|
+
const edited = typeof source.edited === "string" ? source.edited.slice(0, PLAN_EDIT_TEXT_LIMIT) : "";
|
|
9305
|
+
return { annotations, edited };
|
|
9306
|
+
}
|
|
9307
|
+
|
|
9308
|
+
function readPlanAnnotationStore() {
|
|
9309
|
+
try {
|
|
9310
|
+
const raw = window.localStorage.getItem(PLAN_ANNOTATION_STORE_KEY);
|
|
9311
|
+
const parsed = typeof raw === "string" ? JSON.parse(raw) : null;
|
|
9312
|
+
if (!isRecord(parsed) || !isRecord(parsed.byPlan)) return { version: PLAN_ANNOTATION_STORE_VERSION, byPlan: {} };
|
|
9313
|
+
return { version: PLAN_ANNOTATION_STORE_VERSION, byPlan: parsed.byPlan };
|
|
9314
|
+
} catch {
|
|
9315
|
+
return { version: PLAN_ANNOTATION_STORE_VERSION, byPlan: {} };
|
|
9316
|
+
}
|
|
9317
|
+
}
|
|
9318
|
+
|
|
9319
|
+
function readPlanBook(key) {
|
|
9320
|
+
if (typeof key !== "string" || key === "") return emptyPlanBook();
|
|
9321
|
+
return sanitizePlanBook(readPlanAnnotationStore().byPlan[key]);
|
|
9322
|
+
}
|
|
9323
|
+
|
|
9324
|
+
function writePlanBook(key, book) {
|
|
9325
|
+
if (typeof key !== "string" || key === "") return;
|
|
9326
|
+
try {
|
|
9327
|
+
const store = readPlanAnnotationStore();
|
|
9328
|
+
const next = sanitizePlanBook(book);
|
|
9329
|
+
if (next.annotations.length === 0 && next.edited === "") delete store.byPlan[key];
|
|
9330
|
+
else store.byPlan[key] = { ...next, updatedAt: Date.now() };
|
|
9331
|
+
const keys = Object.keys(store.byPlan);
|
|
9332
|
+
if (keys.length > PLAN_ANNOTATION_PLANS_LIMIT) {
|
|
9333
|
+
keys.sort((a, b) => (Number(store.byPlan[a].updatedAt) || 0) - (Number(store.byPlan[b].updatedAt) || 0));
|
|
9334
|
+
for (const stale of keys.slice(0, keys.length - PLAN_ANNOTATION_PLANS_LIMIT)) delete store.byPlan[stale];
|
|
9335
|
+
}
|
|
9336
|
+
window.localStorage.setItem(PLAN_ANNOTATION_STORE_KEY, JSON.stringify({ version: PLAN_ANNOTATION_STORE_VERSION, byPlan: store.byPlan }));
|
|
9337
|
+
} catch {
|
|
9338
|
+
/* storage full or unavailable: the in-memory book still serves this tab */
|
|
9339
|
+
}
|
|
9340
|
+
}
|
|
9341
|
+
|
|
9342
|
+
/* ---------------------------------------------------------------- */
|
|
9343
|
+
/* Feedback + delivery */
|
|
9344
|
+
/* ---------------------------------------------------------------- */
|
|
9345
|
+
|
|
9346
|
+
/** Structured revision request, or null when there is nothing to send. */
|
|
9347
|
+
function buildPlanFeedback(data, translator) {
|
|
9348
|
+
const t = typeof translator === "function" ? translator : translate;
|
|
9349
|
+
const record = isRecord(data) ? data : {};
|
|
9350
|
+
const annotations = Array.isArray(record.annotations) ? record.annotations : [];
|
|
9351
|
+
const edited = typeof record.edited === "string" ? record.edited.trim() : "";
|
|
9352
|
+
if (annotations.length === 0 && edited === "") return null;
|
|
9353
|
+
const title = typeof record.title === "string" && record.title !== "" ? record.title : t("planWorkbench.untitled");
|
|
9354
|
+
const ordered = annotations.slice().sort((a, b) => {
|
|
9355
|
+
const left = a.sectionIndex === PLAN_WHOLE ? -1 : a.sectionIndex;
|
|
9356
|
+
const right = b.sectionIndex === PLAN_WHOLE ? -1 : b.sectionIndex;
|
|
9357
|
+
return left - right;
|
|
9358
|
+
});
|
|
9359
|
+
const lines = [t("planWorkbench.feedbackIntro", { title })];
|
|
9360
|
+
ordered.forEach((annotation, index) => {
|
|
9361
|
+
const heading = typeof annotation.heading === "string" && annotation.heading !== "" ? annotation.heading : t("planWorkbench.whole");
|
|
9362
|
+
lines.push("");
|
|
9363
|
+
lines.push(t("planWorkbench.feedbackHead", { n: index + 1, heading }));
|
|
9364
|
+
if (typeof annotation.quote === "string" && annotation.quote !== "") lines.push(t("planWorkbench.feedbackQuote", { quote: annotation.quote }));
|
|
9365
|
+
lines.push(t("planWorkbench.feedbackComment", { comment: annotation.comment }));
|
|
9366
|
+
});
|
|
9367
|
+
if (edited !== "") {
|
|
9368
|
+
lines.push("");
|
|
9369
|
+
lines.push(t("planWorkbench.feedbackEdit"));
|
|
9370
|
+
lines.push("");
|
|
9371
|
+
lines.push(edited);
|
|
9372
|
+
}
|
|
9373
|
+
lines.push("");
|
|
9374
|
+
lines.push(t("planWorkbench.feedbackOutro"));
|
|
9375
|
+
return lines.join("\n");
|
|
9376
|
+
}
|
|
9377
|
+
|
|
9378
|
+
/** The live plan-review interaction this workbench may answer, or null. */
|
|
9379
|
+
function findLivePlanReview(pending, key, text) {
|
|
9380
|
+
if (!isRecord(pending) || pending.kind !== "plan-review" || typeof pending.answer !== "function") return null;
|
|
9381
|
+
try {
|
|
9382
|
+
const snapshot = typeof pending.snapshot === "function" ? pending.snapshot() : null;
|
|
9383
|
+
if (isRecord(snapshot) && snapshot.channel === "none") return null;
|
|
9384
|
+
} catch {
|
|
9385
|
+
/* snapshot is best-effort; answer() reports the real channel */
|
|
9386
|
+
}
|
|
9387
|
+
const questions = Array.isArray(pending.questions) ? pending.questions : [];
|
|
9388
|
+
const question = questions.find((item) => isRecord(item) && typeof item.id === "string" && item.id !== "");
|
|
9389
|
+
if (question === undefined) return null;
|
|
9390
|
+
const intent = isRecord(question.intent) ? question.intent : null;
|
|
9391
|
+
const callId = intent !== null && intent.kind === "plan-review" && typeof intent.callId === "string" && intent.callId !== "" ? intent.callId : undefined;
|
|
9392
|
+
const identity = parsePlanWorkbenchKey(key);
|
|
9393
|
+
if (callId !== undefined && identity !== undefined && identity.kind === "call") {
|
|
9394
|
+
return callId === identity.id ? { interaction: pending, reviewId: question.id } : null;
|
|
9395
|
+
}
|
|
9396
|
+
/* no call id on either side (or a snapshot identity): fall back to the plan text */
|
|
9397
|
+
const detail = typeof question.detail === "string" ? question.detail : "";
|
|
9398
|
+
if (detail === "" || text === "") return null;
|
|
9399
|
+
const same = detail === text || (text.length >= 200 && detail.startsWith(text.slice(0, Math.min(text.length, 2000))));
|
|
9400
|
+
return same ? { interaction: pending, reviewId: question.id } : null;
|
|
9401
|
+
}
|
|
9402
|
+
|
|
9403
|
+
/** Where the feedback can go: the live review, the composer draft, or the clipboard. */
|
|
9404
|
+
function planDelivery(options) {
|
|
9405
|
+
const record = isRecord(options) ? options : {};
|
|
9406
|
+
if (record.live !== null && record.live !== undefined) return "review";
|
|
9407
|
+
if (record.hasInputActions === true) return "draft";
|
|
9408
|
+
return "copy";
|
|
9409
|
+
}
|
|
9410
|
+
|
|
9411
|
+
function planErrorMessage(cause) {
|
|
9412
|
+
if (cause instanceof Error && cause.message !== "") return cause.message;
|
|
9413
|
+
return typeof cause === "string" && cause !== "" ? cause : translate("planWorkbench.failed");
|
|
9414
|
+
}
|
|
9415
|
+
|
|
9416
|
+
function useSessionPendingInteraction(sessionId) {
|
|
9417
|
+
const source = readSessionStatusSource();
|
|
9418
|
+
const subscribe = React.useCallback(
|
|
9419
|
+
(onChange) => (source !== undefined && typeof source.subscribe === "function" ? source.subscribe(onChange) : () => {}),
|
|
9420
|
+
[source]
|
|
9421
|
+
);
|
|
9422
|
+
const read = React.useCallback(() => {
|
|
9423
|
+
if (source === undefined || typeof source.getSnapshot !== "function" || typeof sessionId !== "string") return undefined;
|
|
9424
|
+
try {
|
|
9425
|
+
const snapshot = source.getSnapshot();
|
|
9426
|
+
const status = snapshot !== null && snapshot !== undefined && typeof snapshot.get === "function" ? snapshot.get(sessionId) : undefined;
|
|
9427
|
+
return status !== null && status !== undefined ? status.pendingInteraction : undefined;
|
|
9428
|
+
} catch {
|
|
9429
|
+
return undefined;
|
|
9430
|
+
}
|
|
9431
|
+
}, [source, sessionId]);
|
|
9432
|
+
return React.useSyncExternalStore(subscribe, read);
|
|
9433
|
+
}
|
|
9434
|
+
|
|
9435
|
+
/* ---------------------------------------------------------------- */
|
|
9436
|
+
/* Open entry */
|
|
9437
|
+
/* ---------------------------------------------------------------- */
|
|
9438
|
+
|
|
9439
|
+
/** Capture the plan snapshot and open its workbench tab; false when no sidebar can hold it. */
|
|
9440
|
+
function openPlanWorkbench(payload) {
|
|
9441
|
+
const record = isRecord(payload) ? payload : {};
|
|
9442
|
+
const sessionId = typeof record.sessionId === "string" && record.sessionId !== "" ? record.sessionId : undefined;
|
|
9443
|
+
if (sessionId === undefined) return false;
|
|
9444
|
+
const identity = planWorkbenchIdentity(record.callId, record.requestKey, record.previewKey);
|
|
9445
|
+
if (identity === undefined) return false;
|
|
9446
|
+
const markdown = typeof record.markdown === "string" ? record.markdown : "";
|
|
9447
|
+
const title = typeof record.title === "string" && record.title !== "" ? record.title : planTitleOf(markdown);
|
|
9448
|
+
const stored = rememberContextPreview(sessionId, `${PLAN_WB_REF_PREFIX}${identity}`, title, markdown, "plan");
|
|
9449
|
+
const key = typeof stored === "string" && stored !== "" ? stored : planWorkbenchKey(sessionId, identity);
|
|
9450
|
+
const sidebar = contextSidebar();
|
|
9451
|
+
if (sidebar === null) return false;
|
|
9452
|
+
try {
|
|
9453
|
+
sidebar.openResource(planWorkbenchAddress(key));
|
|
9454
|
+
return true;
|
|
9455
|
+
} catch {
|
|
9456
|
+
return false;
|
|
9457
|
+
}
|
|
9458
|
+
}
|
|
9459
|
+
|
|
9460
|
+
/** Leave the workbench once its review feedback has landed: replace this tab with
|
|
9461
|
+
the overview page so the approval tab closes and the column returns to 概览.
|
|
9462
|
+
Falls back to closing alone, and reports whether anything happened so the
|
|
9463
|
+
caller can keep the inline notice when no tab surface can act. */
|
|
9464
|
+
function finishPlanReviewTab(tab) {
|
|
9465
|
+
const actions = isRecord(tab) && isRecord(tab.actions) ? tab.actions : null;
|
|
9466
|
+
if (actions === null) return false;
|
|
9467
|
+
try {
|
|
9468
|
+
if (typeof actions.openTab === "function") {
|
|
9469
|
+
actions.openTab(TAB_KIND, { replaceTab: true });
|
|
9470
|
+
return true;
|
|
9471
|
+
}
|
|
9472
|
+
} catch {
|
|
9473
|
+
/* the overview page type is not registered in this placement: close instead */
|
|
9474
|
+
}
|
|
9475
|
+
try {
|
|
9476
|
+
if (typeof actions.close === "function") {
|
|
9477
|
+
actions.close();
|
|
9478
|
+
return true;
|
|
9479
|
+
}
|
|
9480
|
+
} catch {
|
|
9481
|
+
/* nothing left to act on */
|
|
9482
|
+
}
|
|
9483
|
+
return false;
|
|
9484
|
+
}
|
|
9485
|
+
|
|
9486
|
+
/** The review card's footer button row, found without knowing the host's hashed
|
|
9487
|
+
CSS-module prefix. The card root carries `data-plan-review-key`. */
|
|
9488
|
+
function planReviewFooterActions(card) {
|
|
9489
|
+
if (card === null || typeof card !== "object") return null;
|
|
9490
|
+
if (typeof card.querySelector === "function") {
|
|
9491
|
+
const direct = card.querySelector('[class*="_footer"] [class*="_actions"]');
|
|
9492
|
+
if (direct !== null && direct !== undefined) return direct;
|
|
9493
|
+
}
|
|
9494
|
+
const groups = typeof card.querySelectorAll === "function" ? card.querySelectorAll('[class*="_actions"]') : null;
|
|
9495
|
+
if (groups !== null && typeof groups[Symbol.iterator] === "function") {
|
|
9496
|
+
for (const group of groups) {
|
|
9497
|
+
const buttons = typeof group.querySelectorAll === "function" ? group.querySelectorAll("button") : null;
|
|
9498
|
+
if (buttons !== null && buttons !== undefined && typeof buttons.length === "number" && buttons.length >= 2) return group;
|
|
9499
|
+
}
|
|
9500
|
+
}
|
|
9501
|
+
return null;
|
|
9502
|
+
}
|
|
9503
|
+
|
|
9504
|
+
/** Review-card action: a pill in the shipped plan-review footer row. The slot seat
|
|
9505
|
+
itself is the warning strip's preview-actions area, so the pill is portalled into
|
|
9506
|
+
the card footer to sit beside 「要求修改」/「同意执行」 with the host's own button
|
|
9507
|
+
styling (falls back to the strip pill when that footer cannot be located). */
|
|
9508
|
+
function PlanReviewAnnotateAction(props) {
|
|
9509
|
+
const t = typeof props.t === "function" ? props.t : translate;
|
|
9510
|
+
const review = isRecord(props.review) ? props.review : null;
|
|
9511
|
+
const requestKey = typeof props.requestKey === "string" ? props.requestKey : "";
|
|
9512
|
+
const [failed, setFailed] = React.useState(false);
|
|
9513
|
+
/* false = render in the slot seat (first paint and no-DOM fallback) */
|
|
9514
|
+
const [mount, setMount] = React.useState(false);
|
|
9515
|
+
const useIsoLayoutEffect = typeof React.useLayoutEffect === "function" ? React.useLayoutEffect : React.useEffect;
|
|
9516
|
+
const open = () => {
|
|
9517
|
+
const ok = openPlanWorkbench({
|
|
9518
|
+
sessionId: props.sessionId,
|
|
9519
|
+
callId: review !== null && typeof review.callId === "string" ? review.callId : undefined,
|
|
9520
|
+
requestKey: props.requestKey,
|
|
9521
|
+
title: review !== null ? planTitleOf(review.plan) : "",
|
|
9522
|
+
markdown: review !== null && typeof review.plan === "string" ? review.plan : "",
|
|
9523
|
+
});
|
|
9524
|
+
setFailed(ok !== true);
|
|
9525
|
+
};
|
|
9526
|
+
useIsoLayoutEffect(() => {
|
|
9527
|
+
if (createPortal === null || requestKey === "" || typeof document === "undefined" || !document.body || typeof document.querySelectorAll !== "function") {
|
|
9528
|
+
return undefined;
|
|
9529
|
+
}
|
|
9530
|
+
let card = null;
|
|
9531
|
+
for (const node of document.querySelectorAll("[data-plan-review-key]")) {
|
|
9532
|
+
if (node.getAttribute("data-plan-review-key") === requestKey) {
|
|
9533
|
+
card = node;
|
|
9534
|
+
break;
|
|
9535
|
+
}
|
|
9536
|
+
}
|
|
9537
|
+
if (card === null) return undefined;
|
|
9538
|
+
const actions = planReviewFooterActions(card);
|
|
9539
|
+
if (actions === null || typeof actions.insertBefore !== "function") return undefined;
|
|
9540
|
+
const host = document.createElement("div");
|
|
9541
|
+
host.className = "dsov-planActionHost";
|
|
9542
|
+
try {
|
|
9543
|
+
actions.insertBefore(host, actions.lastElementChild);
|
|
9544
|
+
} catch {
|
|
9545
|
+
return undefined;
|
|
9546
|
+
}
|
|
9547
|
+
setMount(host);
|
|
9548
|
+
return () => {
|
|
9549
|
+
if (typeof host.remove === "function") host.remove();
|
|
9550
|
+
else if (host.parentNode !== null) host.parentNode.removeChild(host);
|
|
9551
|
+
};
|
|
9552
|
+
}, [requestKey]);
|
|
9553
|
+
const pill =
|
|
9554
|
+
primitives !== null && primitives.Button !== undefined && primitives.Button !== null
|
|
9555
|
+
? h(
|
|
9556
|
+
primitives.Button,
|
|
9557
|
+
{ type: "button", variant: "outline", icon: h(IconPlan, { size: 14 }), title: t("planWorkbench.actionHint"), onClick: open },
|
|
9558
|
+
t("planWorkbench.action")
|
|
9559
|
+
)
|
|
9560
|
+
: h(
|
|
9561
|
+
"button",
|
|
9562
|
+
{ type: "button", className: "dsov-planAction", onClick: open, title: t("planWorkbench.actionHint") },
|
|
9563
|
+
h(IconPlan, { size: 14 }),
|
|
9564
|
+
h("span", null, t("planWorkbench.action"))
|
|
9565
|
+
);
|
|
9566
|
+
const wrap = h(
|
|
9567
|
+
"span",
|
|
9568
|
+
{ className: "dsov-planActionWrap", "data-in-footer": mount === false ? undefined : "true" },
|
|
9569
|
+
pill,
|
|
9570
|
+
failed ? h("span", { className: "dsov-planActionError" }, t("planWorkbench.openFailed")) : null
|
|
9571
|
+
);
|
|
9572
|
+
return mount === false ? wrap : createPortal(wrap, mount);
|
|
9573
|
+
}
|
|
9574
|
+
|
|
9575
|
+
/** Row menu beside one overview plan artifact. */
|
|
9576
|
+
function PlanRowMenu({ menu, t, onClose }) {
|
|
9577
|
+
const clamp = (value, max) => Math.max(8, Math.min(value, max));
|
|
9578
|
+
const style = {
|
|
9579
|
+
left: `${clamp(menu.x, (typeof window !== "undefined" ? window.innerWidth : 1200) - 220)}px`,
|
|
9580
|
+
top: `${clamp(menu.y, (typeof window !== "undefined" ? window.innerHeight : 800) - 170)}px`,
|
|
9581
|
+
};
|
|
9582
|
+
const artifact = isRecord(menu.artifact) ? menu.artifact : {};
|
|
9583
|
+
const run = (action) => {
|
|
9584
|
+
try {
|
|
9585
|
+
action();
|
|
9586
|
+
} finally {
|
|
9587
|
+
onClose();
|
|
9588
|
+
}
|
|
9589
|
+
};
|
|
9590
|
+
const entry = typeof artifact.previewKey === "string" && artifact.previewKey !== "" ? readContextPreview(artifact.previewKey) : null;
|
|
9591
|
+
return h(
|
|
9592
|
+
"div",
|
|
9593
|
+
{ className: "dsov-fileMenu", style, onMouseDown: (event) => event.stopPropagation(), onContextMenu: (event) => event.preventDefault() },
|
|
9594
|
+
h(
|
|
9595
|
+
"button",
|
|
9596
|
+
{
|
|
9597
|
+
type: "button",
|
|
9598
|
+
className: "dsov-fileMenuItem",
|
|
9599
|
+
onClick: () =>
|
|
9600
|
+
run(() =>
|
|
9601
|
+
openPlanWorkbench({
|
|
9602
|
+
sessionId: menu.sessionId,
|
|
9603
|
+
callId: artifact.planCallId,
|
|
9604
|
+
previewKey: artifact.previewKey,
|
|
9605
|
+
title: artifact.planTitle,
|
|
9606
|
+
markdown: entry !== null ? entry.text : "",
|
|
9607
|
+
})
|
|
9608
|
+
),
|
|
9609
|
+
},
|
|
9610
|
+
t("planWorkbench.menuAnnotate")
|
|
9611
|
+
),
|
|
9612
|
+
h("div", { className: "dsov-fileMenuSep" }),
|
|
9613
|
+
h(
|
|
9614
|
+
"button",
|
|
9615
|
+
{
|
|
9616
|
+
type: "button",
|
|
9617
|
+
className: "dsov-fileMenuItem",
|
|
9618
|
+
disabled: entry === null,
|
|
9619
|
+
onClick: () =>
|
|
9620
|
+
run(() => {
|
|
9621
|
+
if (entry === null) return;
|
|
9622
|
+
const result = insertIntoComposer(entry.text);
|
|
9623
|
+
void result;
|
|
9624
|
+
}),
|
|
9625
|
+
},
|
|
9626
|
+
t("planWorkbench.menuCopy")
|
|
9627
|
+
),
|
|
9628
|
+
h(
|
|
9629
|
+
"button",
|
|
9630
|
+
{ type: "button", className: "dsov-fileMenuItem", onClick: () => run(() => openNativePlan(menu.sessionId, artifact.planCallId)) },
|
|
9631
|
+
t("planWorkbench.openOriginal")
|
|
9632
|
+
)
|
|
9633
|
+
);
|
|
9634
|
+
}
|
|
9635
|
+
|
|
9636
|
+
/* ---------------------------------------------------------------- */
|
|
9637
|
+
/* Workbench tab body */
|
|
9638
|
+
/* ---------------------------------------------------------------- */
|
|
9639
|
+
|
|
9640
|
+
function PlanWorkbenchBody(props) {
|
|
9641
|
+
const t = typeof props.t === "function" ? props.t : translate;
|
|
9642
|
+
const info = typeof props.useTabInfo === "function" ? props.useTabInfo() : null;
|
|
9643
|
+
const address = info !== null && isRecord(info.tab) ? info.tab.contentId : undefined;
|
|
9644
|
+
const key = parsePlanWorkbenchAddress(address);
|
|
9645
|
+
const identity = key === undefined ? undefined : parsePlanWorkbenchKey(key);
|
|
9646
|
+
const snapshot = key === undefined ? null : readContextPreview(key);
|
|
9647
|
+
const callId = identity !== undefined && identity.kind === "call" ? identity.id : undefined;
|
|
9648
|
+
const resourceAddress = identity !== undefined && callId !== undefined ? planResourceAddress(identity.sessionId, callId) : PLAN_NO_RESOURCE;
|
|
9649
|
+
const resource = typeof props.useResource === "function" ? props.useResource(resourceAddress) : null;
|
|
9650
|
+
const resourceValue = isRecord(resource) && resource.status === "live" && isRecord(resource.value) ? resource.value : null;
|
|
9651
|
+
const text = resourceValue !== null && typeof resourceValue.markdown === "string" ? resourceValue.markdown : snapshot !== null ? snapshot.text : "";
|
|
9652
|
+
const title =
|
|
9653
|
+
resourceValue !== null && typeof resourceValue.title === "string" && resourceValue.title !== ""
|
|
9654
|
+
? resourceValue.title
|
|
9655
|
+
: snapshot !== null
|
|
9656
|
+
? snapshot.title
|
|
9657
|
+
: t("planWorkbench.tabTitle");
|
|
9658
|
+
const foreign =
|
|
9659
|
+
identity !== undefined && typeof props.sessionId === "string" && props.sessionId !== "" && identity.sessionId !== props.sessionId;
|
|
9660
|
+
const pending = useSessionPendingInteraction(identity === undefined ? undefined : identity.sessionId);
|
|
9661
|
+
const live = findLivePlanReview(pending, key === undefined ? "" : key, text);
|
|
9662
|
+
const hasInputActions = isRecord(props.inputActions) && typeof props.inputActions.setDraft === "function";
|
|
9663
|
+
const delivery = planDelivery({ live, hasInputActions });
|
|
9664
|
+
|
|
9665
|
+
const [mode, setMode] = React.useState("annotate");
|
|
9666
|
+
const [book, setBook] = React.useState(() => (key === undefined ? emptyPlanBook() : readPlanBook(key)));
|
|
9667
|
+
const [draftEdit, setDraftEdit] = React.useState(null);
|
|
9668
|
+
const [editingAt, setEditingAt] = React.useState(null);
|
|
9669
|
+
const [commentDraft, setCommentDraft] = React.useState("");
|
|
9670
|
+
const [busy, setBusy] = React.useState(false);
|
|
9671
|
+
const [error, setError] = React.useState(null);
|
|
9672
|
+
const [notice, setNotice] = React.useState(null);
|
|
9673
|
+
|
|
9674
|
+
React.useEffect(() => {
|
|
9675
|
+
setBook(key === undefined ? emptyPlanBook() : readPlanBook(key));
|
|
9676
|
+
setDraftEdit(null);
|
|
9677
|
+
setEditingAt(null);
|
|
9678
|
+
setCommentDraft("");
|
|
9679
|
+
setBusy(false);
|
|
9680
|
+
setError(null);
|
|
9681
|
+
setNotice(null);
|
|
9682
|
+
}, [key]);
|
|
9683
|
+
|
|
9684
|
+
const sections = React.useMemo(() => planSections(text), [text]);
|
|
9685
|
+
const editedValue = draftEdit !== null ? draftEdit : book.edited !== "" ? book.edited : text;
|
|
9686
|
+
const editedText = draftEdit !== null ? draftEdit : book.edited;
|
|
9687
|
+
const annotationCount = book.annotations.length;
|
|
9688
|
+
const saveBook = (next) => {
|
|
9689
|
+
const clean = sanitizePlanBook(next);
|
|
9690
|
+
setBook(clean);
|
|
9691
|
+
if (key !== undefined) writePlanBook(key, clean);
|
|
9692
|
+
};
|
|
9693
|
+
|
|
9694
|
+
const openOriginal = () => {
|
|
9695
|
+
if (identity === undefined || identity.kind !== "call") return false;
|
|
9696
|
+
return openNativePlan(identity.sessionId, identity.id);
|
|
9697
|
+
};
|
|
9698
|
+
|
|
9699
|
+
const markdownOf = (value) => (ChatMarkdown !== null ? h(ChatMarkdown, { text: value, labels: chatMarkdownLabels(t) }) : h(MarkdownBody, { text: value }));
|
|
9700
|
+
|
|
9701
|
+
const addAnnotation = (sectionIndex, heading, quote) => {
|
|
9702
|
+
const comment = commentDraft.trim();
|
|
9703
|
+
if (comment === "") return;
|
|
9704
|
+
const item = {
|
|
9705
|
+
id: `a${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`,
|
|
9706
|
+
sectionIndex,
|
|
9707
|
+
heading,
|
|
9708
|
+
quote,
|
|
9709
|
+
comment,
|
|
9710
|
+
createdAt: Date.now(),
|
|
9711
|
+
};
|
|
9712
|
+
saveBook({ annotations: [...book.annotations, item], edited: book.edited });
|
|
9713
|
+
setCommentDraft("");
|
|
9714
|
+
setEditingAt(null);
|
|
9715
|
+
setError(null);
|
|
9716
|
+
setNotice(null);
|
|
9717
|
+
};
|
|
9718
|
+
|
|
9719
|
+
const removeAnnotation = (id) => {
|
|
9720
|
+
saveBook({ annotations: book.annotations.filter((item) => item.id !== id), edited: book.edited });
|
|
9721
|
+
};
|
|
9722
|
+
|
|
9723
|
+
const submit = () => {
|
|
9724
|
+
const feedback = buildPlanFeedback({ title, annotations: book.annotations, edited: editedText }, t);
|
|
9725
|
+
if (feedback === null) {
|
|
9726
|
+
setError(t("planWorkbench.empty"));
|
|
9727
|
+
return;
|
|
9728
|
+
}
|
|
9729
|
+
setError(null);
|
|
9730
|
+
setNotice(null);
|
|
9731
|
+
if (live !== null) {
|
|
9732
|
+
setBusy(true);
|
|
9733
|
+
let answer;
|
|
9734
|
+
try {
|
|
9735
|
+
answer = live.interaction.answer({ answers: [{ id: live.reviewId, selected: [], custom: feedback }] });
|
|
9736
|
+
} catch (cause) {
|
|
9737
|
+
setBusy(false);
|
|
9738
|
+
setError(planErrorMessage(cause));
|
|
9739
|
+
return;
|
|
9740
|
+
}
|
|
9741
|
+
Promise.resolve(answer).then(
|
|
9742
|
+
() => {
|
|
9743
|
+
setBusy(false);
|
|
9744
|
+
/* the feedback reached the live review: retire the approval tab and land
|
|
9745
|
+
back on the overview instead of leaving the stale plan on screen */
|
|
9746
|
+
const tab = info !== null && isRecord(info.tab) ? info.tab : null;
|
|
9747
|
+
if (!finishPlanReviewTab(tab)) setNotice(t("planWorkbench.submitted"));
|
|
9748
|
+
},
|
|
9749
|
+
(cause) => {
|
|
9750
|
+
setBusy(false);
|
|
9751
|
+
setError(planErrorMessage(cause));
|
|
9752
|
+
}
|
|
9753
|
+
);
|
|
9754
|
+
return;
|
|
9755
|
+
}
|
|
9756
|
+
if (hasInputActions) {
|
|
9757
|
+
try {
|
|
9758
|
+
props.inputActions.setDraft(feedback);
|
|
9759
|
+
if (typeof props.inputActions.persistDraft === "function") props.inputActions.persistDraft();
|
|
9760
|
+
setNotice(t("planWorkbench.drafted"));
|
|
9761
|
+
} catch (cause) {
|
|
9762
|
+
setError(planErrorMessage(cause));
|
|
9763
|
+
}
|
|
9764
|
+
return;
|
|
9765
|
+
}
|
|
9766
|
+
const result = insertIntoComposer(feedback);
|
|
9767
|
+
if (result === "failed") setError(t("planWorkbench.copyFailed"));
|
|
9768
|
+
else setNotice(result === "copied" ? t("planWorkbench.copied") : t("planWorkbench.drafted"));
|
|
9769
|
+
};
|
|
9770
|
+
|
|
9771
|
+
const copyFeedback = () => {
|
|
9772
|
+
const feedback = buildPlanFeedback({ title, annotations: book.annotations, edited: editedText }, t);
|
|
9773
|
+
if (feedback === null) {
|
|
9774
|
+
setError(t("planWorkbench.empty"));
|
|
9775
|
+
return;
|
|
9776
|
+
}
|
|
9777
|
+
setError(null);
|
|
9778
|
+
try {
|
|
9779
|
+
if (typeof navigator !== "undefined" && navigator.clipboard !== undefined && typeof navigator.clipboard.writeText === "function") {
|
|
9780
|
+
navigator.clipboard.writeText(feedback).then(
|
|
9781
|
+
() => setNotice(t("planWorkbench.copied")),
|
|
9782
|
+
() => setError(t("planWorkbench.copyFailed"))
|
|
9783
|
+
);
|
|
9784
|
+
return;
|
|
9785
|
+
}
|
|
9786
|
+
} catch {
|
|
9787
|
+
/* fall through to the error notice */
|
|
9788
|
+
}
|
|
9789
|
+
setError(t("planWorkbench.copyFailed"));
|
|
9790
|
+
};
|
|
9791
|
+
|
|
9792
|
+
if (key === undefined || identity === undefined || text === "") {
|
|
9793
|
+
return h(
|
|
9794
|
+
"div",
|
|
9795
|
+
{ className: "dsov-planWb" },
|
|
9796
|
+
h("div", { className: "dsov-planWbHead" }, h("span", { className: "dsov-planWbTitle" }, title)),
|
|
9797
|
+
h(
|
|
9798
|
+
"div",
|
|
9799
|
+
{ className: "dsov-planWbUnavailable" },
|
|
9800
|
+
h("span", null, t("planWorkbench.unavailable")),
|
|
9801
|
+
identity !== undefined && identity.kind === "call"
|
|
9802
|
+
? h("button", { type: "button", className: "dsov-headAction", onClick: openOriginal }, h(IconPlan, { size: 13 }), t("planWorkbench.openOriginal"))
|
|
9803
|
+
: null
|
|
9804
|
+
)
|
|
9805
|
+
);
|
|
9806
|
+
}
|
|
9807
|
+
|
|
9808
|
+
const label = (section) => (section.heading !== "" ? section.heading : t("planWorkbench.preface")) || t("planWorkbench.whole");
|
|
9809
|
+
|
|
9810
|
+
const noteCards = (sectionIndex) =>
|
|
9811
|
+
book.annotations
|
|
9812
|
+
.filter((item) => item.sectionIndex === sectionIndex)
|
|
9813
|
+
.map((item) =>
|
|
9814
|
+
h(
|
|
9815
|
+
"div",
|
|
9816
|
+
{ className: "dsov-planWbNote", key: item.id },
|
|
9817
|
+
h(
|
|
9818
|
+
"div",
|
|
9819
|
+
{ className: "dsov-planWbNoteHead" },
|
|
9820
|
+
h("span", { className: "dsov-planWbNoteQuote" }, item.quote !== "" ? item.quote : item.heading !== "" ? item.heading : t("planWorkbench.whole")),
|
|
9821
|
+
h(
|
|
9822
|
+
"button",
|
|
9823
|
+
{ type: "button", className: "dsov-planWbNoteDel", title: t("planWorkbench.remove"), "aria-label": t("planWorkbench.remove"), onClick: () => removeAnnotation(item.id) },
|
|
9824
|
+
"×"
|
|
9825
|
+
)
|
|
9826
|
+
),
|
|
9827
|
+
h("div", { className: "dsov-planWbNoteText" }, item.comment)
|
|
9828
|
+
)
|
|
9829
|
+
);
|
|
9830
|
+
|
|
9831
|
+
const editorFor = (sectionIndex, heading, quote) =>
|
|
9832
|
+
h(
|
|
9833
|
+
"div",
|
|
9834
|
+
{ className: "dsov-planWbEditor" },
|
|
9835
|
+
h("textarea", {
|
|
9836
|
+
className: "dsov-planWbTextarea",
|
|
9837
|
+
rows: 3,
|
|
9838
|
+
value: commentDraft,
|
|
9839
|
+
placeholder: t("planWorkbench.commentPlaceholder"),
|
|
9840
|
+
"aria-label": t("planWorkbench.commentAria", { heading: heading !== "" ? heading : t("planWorkbench.whole") }),
|
|
9841
|
+
onChange: (event) => setCommentDraft(event.target.value),
|
|
9842
|
+
onKeyDown: (event) => {
|
|
9843
|
+
if (event.key === "Escape") {
|
|
9844
|
+
setEditingAt(null);
|
|
9845
|
+
setCommentDraft("");
|
|
9846
|
+
}
|
|
9847
|
+
},
|
|
9848
|
+
}),
|
|
9849
|
+
h(
|
|
9850
|
+
"div",
|
|
9851
|
+
{ className: "dsov-planWbEditorFoot" },
|
|
9852
|
+
h("button", { type: "button", className: "dsov-planWbPrimary", disabled: commentDraft.trim() === "", onClick: () => addAnnotation(sectionIndex, heading, quote) }, t("planWorkbench.save")),
|
|
9853
|
+
h(
|
|
9854
|
+
"button",
|
|
9855
|
+
{
|
|
9856
|
+
type: "button",
|
|
9857
|
+
className: "dsov-planWbGhost",
|
|
9858
|
+
onClick: () => {
|
|
9859
|
+
setEditingAt(null);
|
|
9860
|
+
setCommentDraft("");
|
|
9861
|
+
},
|
|
9862
|
+
},
|
|
9863
|
+
t("planWorkbench.cancel")
|
|
9864
|
+
)
|
|
9865
|
+
)
|
|
9866
|
+
);
|
|
9867
|
+
|
|
9868
|
+
const addButton = (sectionIndex, heading, quote) =>
|
|
9869
|
+
h(
|
|
9870
|
+
"button",
|
|
9871
|
+
{
|
|
9872
|
+
type: "button",
|
|
9873
|
+
className: "dsov-planWbAdd",
|
|
9874
|
+
title: t("planWorkbench.addHere"),
|
|
9875
|
+
"aria-label": t("planWorkbench.addAria", { heading: heading !== "" ? heading : t("planWorkbench.whole") }),
|
|
9876
|
+
onClick: () => {
|
|
9877
|
+
setEditingAt(editingAt === sectionIndex ? null : sectionIndex);
|
|
9878
|
+
setCommentDraft("");
|
|
9879
|
+
},
|
|
9880
|
+
},
|
|
9881
|
+
"+"
|
|
9882
|
+
);
|
|
9883
|
+
|
|
9884
|
+
const wholeLabel = t("planWorkbench.whole");
|
|
9885
|
+
const sectionsNode = h(
|
|
9886
|
+
"div",
|
|
9887
|
+
{ className: "dsov-planWbSections" },
|
|
9888
|
+
h(
|
|
9889
|
+
"section",
|
|
9890
|
+
{ className: "dsov-planWbSection", "data-whole": "true" },
|
|
9891
|
+
h("div", { className: "dsov-planWbSectionBody" }, h("div", { className: "dsov-planWbWhole" }, wholeLabel)),
|
|
9892
|
+
addButton(PLAN_WHOLE, wholeLabel, ""),
|
|
9893
|
+
noteCards(PLAN_WHOLE),
|
|
9894
|
+
editingAt === PLAN_WHOLE ? editorFor(PLAN_WHOLE, wholeLabel, "") : null
|
|
9895
|
+
),
|
|
9896
|
+
sections.map((section) =>
|
|
9897
|
+
h(
|
|
9898
|
+
"section",
|
|
9899
|
+
{ className: "dsov-planWbSection", key: `s${section.index}`, "data-level": String(section.level) },
|
|
9900
|
+
h("div", { className: "dsov-planWbSectionBody" }, markdownOf(section.text)),
|
|
9901
|
+
addButton(section.index, section.heading, section.quote),
|
|
9902
|
+
noteCards(section.index),
|
|
9903
|
+
editingAt === section.index ? editorFor(section.index, section.heading, section.quote) : null
|
|
9904
|
+
)
|
|
9905
|
+
)
|
|
9906
|
+
);
|
|
9907
|
+
const annotateBody =
|
|
9908
|
+
ChatMarkdown !== null && ChatMarkdownProvider !== null
|
|
9909
|
+
? h(ChatMarkdownProvider, {
|
|
9910
|
+
openFile: (path) => openDocLinkedFile(identity.sessionId, path),
|
|
9911
|
+
openExternalLink: openDocExternalLink,
|
|
9912
|
+
children: sectionsNode,
|
|
9913
|
+
})
|
|
9914
|
+
: sectionsNode;
|
|
9915
|
+
|
|
9916
|
+
const editorBody = h(
|
|
9917
|
+
"div",
|
|
9918
|
+
{ className: "dsov-planWbEdit" },
|
|
9919
|
+
h("textarea", {
|
|
9920
|
+
className: "dsov-planWbEditText",
|
|
9921
|
+
value: editedValue,
|
|
9922
|
+
readOnly: foreign,
|
|
9923
|
+
"aria-label": t("planWorkbench.editAria"),
|
|
9924
|
+
onChange: (event) => setDraftEdit(event.target.value),
|
|
9925
|
+
}),
|
|
9926
|
+
h(
|
|
9927
|
+
"div",
|
|
9928
|
+
{ className: "dsov-planWbEditorFoot" },
|
|
9929
|
+
h("span", { className: "dsov-planWbHint" }, t("planWorkbench.editHint")),
|
|
9930
|
+
h(
|
|
9931
|
+
"button",
|
|
9932
|
+
{
|
|
9933
|
+
type: "button",
|
|
9934
|
+
className: "dsov-planWbGhost",
|
|
9935
|
+
disabled: book.edited === "",
|
|
9936
|
+
onClick: () => {
|
|
9937
|
+
setDraftEdit("");
|
|
9938
|
+
saveBook({ annotations: book.annotations, edited: "" });
|
|
9939
|
+
},
|
|
9940
|
+
},
|
|
9941
|
+
t("planWorkbench.revert")
|
|
9942
|
+
),
|
|
9943
|
+
h(
|
|
9944
|
+
"button",
|
|
9945
|
+
{
|
|
9946
|
+
type: "button",
|
|
9947
|
+
className: "dsov-planWbPrimary",
|
|
9948
|
+
onClick: () => {
|
|
9949
|
+
saveBook({ annotations: book.annotations, edited: editedValue });
|
|
9950
|
+
setDraftEdit(null);
|
|
9951
|
+
setNotice(t("planWorkbench.editSaved"));
|
|
9952
|
+
},
|
|
9953
|
+
},
|
|
9954
|
+
t("planWorkbench.editSave")
|
|
9955
|
+
)
|
|
9956
|
+
)
|
|
9957
|
+
);
|
|
9958
|
+
|
|
9959
|
+
const submitLabel =
|
|
9960
|
+
delivery === "review" ? t("planWorkbench.submitReview") : delivery === "draft" ? t("planWorkbench.submitDraft") : t("planWorkbench.submitComposer");
|
|
9961
|
+
const canSubmit = foreign !== true && busy !== true && (annotationCount > 0 || editedText !== "");
|
|
9962
|
+
|
|
9963
|
+
return h(
|
|
9964
|
+
"div",
|
|
9965
|
+
{ className: "dsov-planWb", "data-foreign": foreign === true ? "true" : "false" },
|
|
9966
|
+
h(
|
|
9967
|
+
"div",
|
|
9968
|
+
{ className: "dsov-planWbHead" },
|
|
9969
|
+
h("span", { className: "dsov-planWbTitle", title }, title),
|
|
9970
|
+
h("span", { className: "dsov-planWbState", "data-tone": live !== null ? "live" : "quiet" }, live !== null ? t("planWorkbench.reviewPending") : t("planWorkbench.reviewClosed")),
|
|
9971
|
+
h(
|
|
9972
|
+
"div",
|
|
9973
|
+
{ className: "dsov-planWbModes", role: "group", "aria-label": t("planWorkbench.modes") },
|
|
9974
|
+
h(
|
|
9975
|
+
"button",
|
|
9976
|
+
{ type: "button", className: "dsov-planWbMode", "data-active": mode === "annotate" ? "true" : "false", onClick: () => setMode("annotate") },
|
|
9977
|
+
t("planWorkbench.modeAnnotate")
|
|
9978
|
+
),
|
|
9979
|
+
h("button", { type: "button", className: "dsov-planWbMode", "data-active": mode === "edit" ? "true" : "false", onClick: () => setMode("edit") }, t("planWorkbench.modeEdit"))
|
|
9980
|
+
),
|
|
9981
|
+
identity.kind === "call"
|
|
9982
|
+
? h("button", { type: "button", className: "dsov-headAction", onClick: () => void openOriginal() }, h(IconPlan, { size: 13 }), t("planWorkbench.openOriginal"))
|
|
9983
|
+
: null
|
|
9984
|
+
),
|
|
9985
|
+
h("div", { className: "dsov-planWbBody" }, mode === "edit" ? editorBody : annotateBody),
|
|
9986
|
+
h(
|
|
9987
|
+
"div",
|
|
9988
|
+
{ className: "dsov-planWbFoot" },
|
|
9989
|
+
h("span", { className: "dsov-planWbCount" }, t("planWorkbench.count", { n: annotationCount })),
|
|
9990
|
+
error !== null ? h("span", { className: "dsov-planWbErr" }, error) : notice !== null ? h("span", { className: "dsov-planWbNotice" }, notice) : null,
|
|
9991
|
+
h("button", { type: "button", className: "dsov-planWbPrimary", disabled: canSubmit !== true, onClick: submit }, busy === true ? t("planWorkbench.submitting") : submitLabel),
|
|
9992
|
+
h("button", { type: "button", className: "dsov-planWbGhost", onClick: copyFeedback }, t("planWorkbench.copyFeedback"))
|
|
9993
|
+
)
|
|
9994
|
+
);
|
|
9995
|
+
}
|
|
9996
|
+
|
|
9997
|
+
/** Tab chip for one plan workbench: the plan glyph plus the plan title. */
|
|
9998
|
+
function PlanTabTitle(props) {
|
|
9999
|
+
const t = typeof props.t === "function" ? props.t : translate;
|
|
10000
|
+
const info = typeof props.useTabInfo === "function" ? props.useTabInfo() : null;
|
|
10001
|
+
const address = info !== null && isRecord(info.tab) ? info.tab.contentId : undefined;
|
|
10002
|
+
const key = parsePlanWorkbenchAddress(address);
|
|
10003
|
+
const entry = key === undefined ? null : readContextPreview(key);
|
|
10004
|
+
const title = entry !== null ? entry.title : t("planWorkbench.tabTitle");
|
|
10005
|
+
return h(
|
|
10006
|
+
"span",
|
|
10007
|
+
{ className: "dsov-vikingHead" },
|
|
10008
|
+
h("span", { className: "dsov-vikingIcon" }, h(IconPlan, { size: 14 })),
|
|
10009
|
+
h("span", { className: "dsov-vikingHeadText" }, h("span", { className: "dsov-vikingName" }, title))
|
|
10010
|
+
);
|
|
10011
|
+
}
|
|
10012
|
+
|
|
10013
|
+
/* ------------------------------------------------------------------ */
|
|
10014
|
+
/* Plugin body */
|
|
10015
|
+
/* ------------------------------------------------------------------ */
|
|
10016
|
+
|
|
10017
|
+
const inject = ["slots", "locale", "sessions", "sidebarRight", "sidebarRightTabs", "uiSession"];
|
|
10018
|
+
|
|
10019
|
+
function apply(ctx) {
|
|
10020
|
+
runtime = { ctx, t: ctx.locale.bind(NS) };
|
|
10021
|
+
mountDesktopRemote(ctx);
|
|
10022
|
+
|
|
10023
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "conversation-overview: dictionaries");
|
|
7995
10024
|
|
|
7996
10025
|
ctx.effect(() => {
|
|
7997
10026
|
if (typeof document === "undefined") return undefined;
|
|
@@ -8003,6 +10032,8 @@ html[data-dsov-quicknav="off"] nav:has(button[data-index][aria-label]),html[data
|
|
|
8003
10032
|
document.head.appendChild(style);
|
|
8004
10033
|
return () => {
|
|
8005
10034
|
style.remove();
|
|
10035
|
+
if (overlayHost !== null && typeof overlayHost.remove === "function") overlayHost.remove();
|
|
10036
|
+
overlayHost = null;
|
|
8006
10037
|
};
|
|
8007
10038
|
}, "conversation-overview: styles");
|
|
8008
10039
|
|
|
@@ -8216,6 +10247,72 @@ html[data-dsov-quicknav="off"] nav:has(button[data-index][aria-label]),html[data
|
|
|
8216
10247
|
"conversation-overview: context tab title"
|
|
8217
10248
|
);
|
|
8218
10249
|
|
|
10250
|
+
ctx.effect(
|
|
10251
|
+
() =>
|
|
10252
|
+
ctx.sidebarRightTabs.register({
|
|
10253
|
+
id: PLAN_TAB_ID,
|
|
10254
|
+
kind: PLAN_TAB_KIND,
|
|
10255
|
+
patterns: [`${PLAN_ADDRESS_PREFIX}**`],
|
|
10256
|
+
priority: "builtin",
|
|
10257
|
+
multiple: true,
|
|
10258
|
+
canOpen: (address) => parsePlanWorkbenchAddress(address) !== undefined,
|
|
10259
|
+
title: (address) => {
|
|
10260
|
+
const key = parsePlanWorkbenchAddress(address);
|
|
10261
|
+
if (key === undefined) return String(address);
|
|
10262
|
+
const entry = readContextPreview(key);
|
|
10263
|
+
return entry !== null ? entry.title : translate("planWorkbench.tabTitle");
|
|
10264
|
+
},
|
|
10265
|
+
}),
|
|
10266
|
+
"conversation-overview: plan workbench tab type"
|
|
10267
|
+
);
|
|
10268
|
+
|
|
10269
|
+
ctx.effect(
|
|
10270
|
+
() =>
|
|
10271
|
+
ctx.slots.inject(
|
|
10272
|
+
"sidebar.right.pane.tab",
|
|
10273
|
+
() =>
|
|
10274
|
+
ctx.slots.register(
|
|
10275
|
+
{
|
|
10276
|
+
name: "sidebar.right.pane.tab",
|
|
10277
|
+
key: PLAN_TAB_ID,
|
|
10278
|
+
locale: NS,
|
|
10279
|
+
},
|
|
10280
|
+
PlanWorkbenchBody
|
|
10281
|
+
)
|
|
10282
|
+
),
|
|
10283
|
+
"conversation-overview: plan workbench tab body"
|
|
10284
|
+
);
|
|
10285
|
+
|
|
10286
|
+
ctx.effect(
|
|
10287
|
+
() =>
|
|
10288
|
+
ctx.slots.inject(
|
|
10289
|
+
"sidebar.right.pane.tab.title",
|
|
10290
|
+
() => ctx.slots.register({ name: "sidebar.right.pane.tab.title", key: PLAN_TAB_ID }, PlanTabTitle)
|
|
10291
|
+
),
|
|
10292
|
+
"conversation-overview: plan workbench tab title"
|
|
10293
|
+
);
|
|
10294
|
+
|
|
10295
|
+
/* The shipped plan-review footer keeps 「要求修改」(dismiss) and 「同意执行」;
|
|
10296
|
+
this pill opens the annotation workbench, whose submit path feeds the same
|
|
10297
|
+
PendingQuestion so the model revises inside the pending turn. */
|
|
10298
|
+
ctx.effect(
|
|
10299
|
+
() =>
|
|
10300
|
+
ctx.slots.inject(
|
|
10301
|
+
"conversation.plan-review.actions",
|
|
10302
|
+
() =>
|
|
10303
|
+
ctx.slots.register(
|
|
10304
|
+
{
|
|
10305
|
+
name: "conversation.plan-review.actions",
|
|
10306
|
+
id: PLAN_REVIEW_ACTION_ID,
|
|
10307
|
+
order: 10,
|
|
10308
|
+
locale: NS,
|
|
10309
|
+
},
|
|
10310
|
+
PlanReviewAnnotateAction
|
|
10311
|
+
)
|
|
10312
|
+
),
|
|
10313
|
+
"conversation-overview: plan review annotation action"
|
|
10314
|
+
);
|
|
10315
|
+
|
|
8219
10316
|
ctx.effect(() => {
|
|
8220
10317
|
if (typeof document === "undefined" || !document.documentElement) return undefined;
|
|
8221
10318
|
const applyAttr = () => {
|
|
@@ -8347,6 +10444,7 @@ html[data-dsov-quicknav="off"] nav:has(button[data-index][aria-label]),html[data
|
|
|
8347
10444
|
zh,
|
|
8348
10445
|
en,
|
|
8349
10446
|
createFolder,
|
|
10447
|
+
createSummaryStore,
|
|
8350
10448
|
foldEvent,
|
|
8351
10449
|
foldEntry,
|
|
8352
10450
|
buildState,
|
|
@@ -8355,6 +10453,14 @@ html[data-dsov-quicknav="off"] nav:has(button[data-index][aria-label]),html[data
|
|
|
8355
10453
|
planTokens,
|
|
8356
10454
|
planActiveAt,
|
|
8357
10455
|
recentStepWindow,
|
|
10456
|
+
stepIdentityOf,
|
|
10457
|
+
stepWindowDelta,
|
|
10458
|
+
completedStepKeys,
|
|
10459
|
+
TAB_KIND,
|
|
10460
|
+
liveStepOf,
|
|
10461
|
+
liveScopeMatches,
|
|
10462
|
+
summarizeTodos,
|
|
10463
|
+
finishPlanReviewTab,
|
|
8358
10464
|
vikingLabel,
|
|
8359
10465
|
vikingRefKind,
|
|
8360
10466
|
vikingIsDir,
|
|
@@ -8417,6 +10523,24 @@ html[data-dsov-quicknav="off"] nav:has(button[data-index][aria-label]),html[data
|
|
|
8417
10523
|
preferSessionOverview,
|
|
8418
10524
|
planTitleOf,
|
|
8419
10525
|
openNativePlan,
|
|
10526
|
+
planResourceAddress,
|
|
10527
|
+
planWorkbenchIdentity,
|
|
10528
|
+
planWorkbenchKey,
|
|
10529
|
+
parsePlanWorkbenchKey,
|
|
10530
|
+
planWorkbenchAddress,
|
|
10531
|
+
parsePlanWorkbenchAddress,
|
|
10532
|
+
planSections,
|
|
10533
|
+
planSectionQuote,
|
|
10534
|
+
sanitizePlanBook,
|
|
10535
|
+
readPlanBook,
|
|
10536
|
+
writePlanBook,
|
|
10537
|
+
buildPlanFeedback,
|
|
10538
|
+
findLivePlanReview,
|
|
10539
|
+
planDelivery,
|
|
10540
|
+
openPlanWorkbench,
|
|
10541
|
+
PlanWorkbenchBody,
|
|
10542
|
+
PlanReviewAnnotateAction,
|
|
10543
|
+
planReviewFooterActions,
|
|
8420
10544
|
documentAddress,
|
|
8421
10545
|
openNativeDocument,
|
|
8422
10546
|
openArtifact,
|
|
@@ -8447,6 +10571,15 @@ html[data-dsov-quicknav="off"] nav:has(button[data-index][aria-label]),html[data
|
|
|
8447
10571
|
stepFull,
|
|
8448
10572
|
teamStepKey,
|
|
8449
10573
|
formatDuration,
|
|
10574
|
+
formatDurationPrecise,
|
|
10575
|
+
formatTokensCompact,
|
|
10576
|
+
formatTokenGroups,
|
|
10577
|
+
formatTokensPerSecond,
|
|
10578
|
+
cacheHitPercentOf,
|
|
10579
|
+
turnDetailRows,
|
|
10580
|
+
turnDetailFor,
|
|
10581
|
+
turnDetailOf,
|
|
10582
|
+
foldTurnMetrics,
|
|
8450
10583
|
rerunPrompt,
|
|
8451
10584
|
insertIntoComposer,
|
|
8452
10585
|
requestNavScope,
|