site: simplify landing messaging and refresh collaboration stepper

- Drop the 'V4' suffix from Auto Discussion branding across copy and structured data
- Remove the Capabilities section; fold its boundary/shared-context messaging into existing sections
- Rework the scenario stepper with round-by-round agent activity states (Waiting/Thinking/Working/Done) and bilingual runtime labels
- Bump asset cache versions; update landing tests (13 pass via node --test)
This commit is contained in:
Ryder Sun committed 2026-10-28 13:43:40 +08:00
1 parent d11deef490
commit 4f10fd14d9
5 files changed
+263 -144

No files matched your search

+10 -5
View File
@@ -14,12 +14,13 @@
return { el: el, en: el.innerHTML, zh: el.dataset.zh };
});
var labels = {
"Agent activity": "Agent 协作状态",
"Meldwork home": "Meldwork 首页", "Primary": "主导航", "Mobile": "移动端导航",
"Toggle theme": "切换明暗主题", "Menu": "菜单", "Playback mode": "播放模式",
"Previous step": "上一步", "Next step": "下一步", "Scroll to workflow": "查看工作流程",
"Meldwork detecting local Agent CLIs": "Meldwork 检测本地 Agent CLI",
"Agents discussing and reviewing a shared task": "Agent 讨论并检查同一个任务",
"Auto Discussion V4 group collaboration": "自动讨论 V4 群组协作",
"Auto Discussion group collaboration": "自动讨论 群组协作",
"Direct multimodal session with local files": "引用本地文件的多模态直接会话"
};
var attributes = [];
@@ -39,9 +40,13 @@
var englishSchema = JSON.parse(schemaEl.textContent);
var faq = Array.from(doc.querySelectorAll(".acc-item"));
var runtimeCopy = {
"Round 1 · Propose": "第 1 轮 · 提案", "Round 1 · Critique": "第 1 轮 · 质询",
"Round 2 · Review": "第 2 轮 · 评审", "Round 2 · Reconcile": "第 2 轮 · 协调",
"Adoption · Human Gate": "用户决策 · 人工确认",
"Waiting": "等待", "Thinking": "思考中", "Working": "运行中", "Done": "已完成",
"Task": "任务", "Round 1 · Request": "第 1 轮 · 请求协作",
"Round 1 · Analysis": "第 1 轮 · 分析", "Round 1 · Review": "第 1 轮 · 评审",
"Round 2 · Synthesis": "第 2 轮 · 汇总",
"Codex is working…": "Codex 正在分析…", "Hermes and OpenCodeReview are working…": "Hermes 和 OpenCodeReview 正在协作…",
"Hermes and OpenCodeReview are thinking…": "Hermes 和 OpenCodeReview 正在思考…",
"OpenCodeReview is working…": "OpenCodeReview 正在评审…", "Codex is bringing the feedback together…": "Codex 正在汇总反馈…",
"Pause": "暂停播放", "Resume": "继续播放", "Replay": "重新播放",
"Auto": "自动播放", "Sequence": "完整记录"
};
@@ -69,7 +74,7 @@
if (locale === "zh") {
var app = schema["@graph"].find(function (entry) { return entry["@type"] === "SoftwareApplication"; });
app.description = chineseDescription;
app.featureList = "自主协作、共享任务上下文、同伴贡献与评审、授权执行、并发回复、自动讨论 V4、本地协作记录";
app.featureList = "自主协作、共享任务上下文、同伴贡献与评审、授权执行、并发回复、自动讨论、本地协作记录";
app.inLanguage = "zh-CN";
var questions = schema["@graph"].find(function (entry) { return entry["@type"] === "FAQPage"; });
questions.mainEntity = faq.map(function (el) {
+38 -87
View File
@@ -31,7 +31,7 @@
"softwareVersion": "1.0.8",
"license": "https://www.apache.org/licenses/LICENSE-2.0",
"description": "Meldwork is a local-first multi-Agent collaboration workspace for macOS. Selected Agents share context, discuss approaches, carry out authorized work, and check results with less manual coordination. Users set goals and permissions and step in when information or key decisions are needed.",
"featureList": "Autonomous Agent collaboration, shared task context, peer contributions and reviews, authorized task execution, Concurrent Responses, Auto Discussion V4, opt-in workspace writes, user decisions when needed, local collaboration history, multimodal attachments, selected knowledge sources",
"featureList": "Autonomous Agent collaboration, shared task context, peer contributions and reviews, authorized task execution, Concurrent Responses, Auto Discussion, opt-in workspace writes, user decisions when needed, local collaboration history, multimodal attachments, selected knowledge sources",
"offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" },
"author": { "@type": "Organization", "name": "Meldwork", "url": "https://github.com/Ryder-Sun/Meldwork" },
"downloadUrl": "https://github.com/Ryder-Sun/Meldwork/releases/tag/Meldwork-V1.0.8",
@@ -62,7 +62,7 @@
},
{
"@type": "Question",
"name": "What does Auto Discussion V4 actually do?",
"name": "What does Auto Discussion actually do?",
"acceptedAnswer": { "@type": "Answer", "text": "Selected Agents start with independent proposals, then discuss differences, request peer contributions, carry out authorized work, and check deliverables. A delivery owner judges progress and whether more work or a user decision is needed. You do not need to direct every round, but missing information, permissions, or key decisions can still require your input." }
},
{
@@ -104,7 +104,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="styles.css?v=20261027.1" />
<link rel="stylesheet" href="styles.css?v=20261028.4" />
</head>
<body>
@@ -121,7 +121,6 @@
<nav class="nav-pill" aria-label="Primary">
<a href="#workflow" class="nav-link" data-spy="workflow"><span data-zh="工作流程">Workflow</span></a>
<a href="#modes" class="nav-link" data-spy="modes"><span data-zh="协作模式">Modes</span></a>
<a href="#capabilities" class="nav-link" data-spy="capabilities"><span data-zh="产品能力">Capabilities</span></a>
<a href="#scenario" class="nav-link" data-spy="scenario"><span data-zh="真实协作">In a run</span></a>
<a href="#compare" class="nav-link" data-spy="compare"><span data-zh="产品特点">How it differs</span></a>
<a href="#faq" class="nav-link" data-spy="faq"><span data-zh="常见问题">FAQ</span></a>
@@ -143,7 +142,6 @@
<nav class="mobile-menu" id="mobileMenu" hidden aria-label="Mobile">
<a href="#workflow" class="m-link"><span data-zh="工作流程">Workflow</span></a>
<a href="#modes" class="m-link"><span data-zh="协作模式">Modes</span></a>
<a href="#capabilities" class="m-link"><span data-zh="产品能力">Capabilities</span></a>
<a href="#scenario" class="m-link"><span data-zh="真实协作">In a run</span></a>
<a href="#compare" class="m-link"><span data-zh="产品特点">How it differs</span></a>
<a href="#faq" class="m-link"><span data-zh="常见问题">FAQ</span></a>
@@ -262,7 +260,7 @@
<article class="geo-tile spotlight reveal" data-reveal style="--d:0.16s">
<span class="geo-kicker" data-zh="协作模式">Modes</span>
<span class="geo-num">3</span>
<p class="geo-fact-value" data-zh="直接会话 · 并发回复 · 自动讨论 V4">Direct · Concurrent Responses · Auto Discussion V4</p>
<p class="geo-fact-value" data-zh="直接会话 · 并发回复 · 自动讨论">Direct · Concurrent Responses · Auto Discussion</p>
</article>
<article class="geo-tile geo-tile--wide spotlight reveal" data-reveal style="--d:0.24s">
<span class="geo-kicker" data-zh="共同推进">Working together</span>
@@ -341,61 +339,27 @@
</article>
<article class="mode-card spotlight reveal" data-reveal style="--d:0.24s">
<span class="mode-glyph-wrap"><span class="mode-glyph">#</span></span>
<h3 data-zh="自动讨论 V4">Auto Discussion V4</h3>
<h3 data-zh="自动讨论">Auto Discussion</h3>
<p data-zh="Agent 提出方案、讨论分歧、请求同伴贡献,执行授权工作并检查成果。">Agents propose, discuss differences, request peer contributions, carry out authorized work, and check results.</p>
<span class="mode-best" data-zh="适合希望减少人工协调的多步骤任务。">Best for multi-step work with less manual coordination.</span>
</article>
</div>
</section>
<!-- ================= CAPABILITIES ================= -->
<section class="section capabilities" id="capabilities">
<p class="eyebrow reveal" data-reveal data-zh="让协作持续推进">WHAT SUPPORTS THE WORK</p>
<h2 class="section-title reveal" data-reveal data-zh="少一些协调,让工作继续向前。">Less coordinating. More work moving forward.</h2>
<p class="section-sub reveal" data-reveal data-zh="共享上下文与同伴贡献帮助 Agent 推进任务,边界和关键决策仍由你掌握。">Shared context and peer contributions help Agents advance the task. You keep control of the boundaries and key decisions.</p>
<div class="cap-grid">
<article class="cap-card spotlight tilt reveal" data-reveal style="--d:0s">
<div class="cap-icon"><span class="cap-gate"></span></div>
<h3 data-zh="由你设定边界">You Set the Boundaries</h3>
<p data-zh="选择目标与权限。工作区写入需主动授权;信息缺失或需要关键决策时,任务会交回给你。">Choose the goal and permissions. Workspace writes are opt-in; requests for information or key decisions come back to you.</p>
<span class="cap-tag" data-zh="需要时作出决策">Decide when needed</span>
</article>
<article class="cap-card spotlight tilt reveal" data-reveal style="--d:0.08s">
<div class="cap-icon"><span class="cap-snap"></span></div>
<h3 data-zh="共享上下文">Shared Context</h3>
<p data-zh="Agent 从同一份任务快照开始,并在讨论推进时接收同伴的贡献。">Agents start from the same task snapshot and receive peer contributions as discussion progresses.</p>
<span class="cap-tag" data-zh="减少上下文转述">Less context relaying</span>
</article>
<article class="cap-card spotlight tilt reveal" data-reveal style="--d:0.16s">
<div class="cap-icon"><span class="cap-trail"></span></div>
<h3 data-zh="Agent 自主推进">Agents Advance the Work</h3>
<p data-zh="Agent 请求同伴补充、接续授权工作并检查交付物。需要时可回看回复与运行状态。">Agents request peer input, continue authorized work, and review deliverables. Revisit replies and run status when you need to.</p>
<span class="cap-tag" data-zh="贡献、检查、完善">Contribute, check, refine</span>
</article>
<article class="cap-card spotlight tilt reveal" data-reveal style="--d:0.24s">
<div class="cap-icon"><span class="cap-lock"></span></div>
<h3 data-zh="本地优先">Local-first Boundary</h3>
<p data-zh="在你的电脑上运行 Electron 工作空间,对话保存在本地,无需远程 Agent 集群。">Electron work cell on your machine. No remote Agent fleet. Conversations stay local.</p>
<span class="cap-tag" data-zh="已有 CLI,本地记录">Your CLI, your data</span>
</article>
</div>
</section>
<!-- ================= SCENARIO (animated stepper) ================= -->
<section class="section scenario" id="scenario">
<p class="eyebrow reveal" data-reveal data-zh="一次真实协作">INSIDE A REAL RUN</p>
<h2 class="section-title reveal" data-reveal data-zh="一次发布评审,从提案到用户决策。">A release review, from proposals to a user decision.</h2>
<p class="section-sub reveal" data-reveal data-zh="V1.0.3 历史会话摘录:Codex、Hermes 和 OpenCodeReview 讨论内测发布,用户介入关键取舍。这段案例展示讨论与人工决策,不代表完整的自动执行任务。">Historical V1.0.3 session excerpts: Codex, Hermes, and OpenCodeReview discussed a private-beta release, with the user steering a key tradeoff. This example shows discussion and human input, rather than an end-to-end execution task.</p>
<p class="eyebrow reveal" data-reveal data-zh="一次协作演示">INSIDE AUTO DISCUSSION</p>
<h2 class="section-title reveal" data-reveal data-zh="把任务交给一个协作小组。">Give the task a team.</h2>
<p class="section-sub reveal" data-reveal data-zh="从 @ 同伴、并行分析到汇总建议,看看 Agent 如何接续推进一次代码评审。">From a mention to a shared plan. Watch Agents take a code review forward.</p>
<div class="scenario-stage reveal" data-reveal data-scenario>
<div class="mac-titlebar">
<span class="mac-lights" aria-hidden="true"><i class="light-r"></i><i class="light-y"></i><i class="light-g"></i></span>
<div class="scenario-meta">
<span class="meta-pill" data-zh="&lt;span class=&quot;meta-dot&quot;&gt;&lt;/span&gt; 自动讨论 V4"><span class="meta-dot"></span> Auto Discussion V4</span>
<span class="meta-name" data-zh="发布准备评审小组">Launch Readiness Council</span>
<span class="meta-pill" data-zh="&lt;span class=&quot;meta-dot&quot;&gt;&lt;/span&gt; 自动讨论"><span class="meta-dot"></span> Auto Discussion</span>
<span class="meta-name" data-zh="代码变更评审">Code Change Review</span>
</div>
<div class="titlebar-right">
<span class="meta-round" data-round data-zh="第 1 轮 · 提案">Round 1 · Propose</span>
<span class="meta-round" data-round data-zh="任务">Task</span>
<div class="seg-control" role="group" aria-label="Playback mode">
<button type="button" class="seg-btn is-on" data-mode="auto" aria-pressed="true" data-zh="自动播放">Auto</button>
<button type="button" class="seg-btn" data-mode="order" aria-pressed="false" data-zh="完整记录">Sequence</button>
@@ -404,61 +368,48 @@
</div>
<div class="scenario-body">
<aside class="agent-roster" aria-label="Agent activity">
<span class="roster-label" data-zh="参与协作">THE TEAM</span>
<div class="agent-seat" data-agent="codex"><img src="assets/agents/codex.svg" alt="" /><div><strong>Codex</strong><span data-zh="协调与汇总">Coordinate &amp; synthesize</span></div><small data-agent-status></small><i aria-hidden="true"></i></div>
<div class="agent-seat" data-agent="hermes"><img src="assets/agents/hermes.svg" alt="" /><div><strong>Hermes</strong><span data-zh="需求与边界">Requirements &amp; edge cases</span></div><small data-agent-status></small><i aria-hidden="true"></i></div>
<div class="agent-seat" data-agent="review"><img src="assets/agents/opencodereview.svg" alt="" /><div><strong>OpenCodeReview</strong><span data-zh="实现与测试">Implementation &amp; tests</span></div><small data-agent-status></small><i aria-hidden="true"></i></div>
<div class="demo-note" data-zh="协作流程示意&lt;br&gt;实际分工由任务和参与的 Agent 决定。">Illustrative workflow<br>Actual collaboration depends on the task and participating Agents.</div>
</aside>
<div class="conversation-panel">
<div class="task-brief"><span data-zh="你的任务">YOUR TASK</span><p data-zh="评审这次代码变更,找出风险并提出改进建议。">Review this code change. Find risks and suggest improvements.</p></div>
<div class="timeline-track" data-timeline>
<div class="timeline-rail" aria-hidden="true">
<span class="rail-line"></span>
<span class="rail-progress" data-rail></span>
</div>
<ol class="timeline" data-timeline-list>
<li class="t-step is-active" data-step="0">
<span class="t-node" data-node></span>
<span class="t-time" data-zh="第 1 轮 · 05:34">R1 · 05:34</span>
<div class="t-bubble t-bubble-codex">
<header><img src="assets/agents/codex.svg" alt="" /><strong>Codex</strong><span class="t-role" data-zh="方案提议">Proposal</span></header>
<p data-zh="先发布&lt;strong&gt;仅限受邀用户的内测版&lt;/strong&gt;,只承诺一件事:用户在不同 Agent 之间切换时,同一个任务仍能连贯推进。暂不宣传工作流自动化、通用兼容或已具备公开发行条件。">Ship a <strong>private, invitation-only preview</strong> with one promise: keep a real task coherent while users move between Agents. Don't market workflow automation, universal compatibility, or public distribution readiness yet.</p>
</div>
<div class="t-bubble t-bubble-you"><header><img src="assets/agents/codex.svg" alt="" /><strong>Codex</strong><span class="t-role" data-zh="开始分析">Starting</span></header>
<p data-zh="我先梳理变更,再请两位同伴分别检查边界情况和实现。">I’ll map the change and bring in two peers to check edge cases and implementation.</p></div>
</li>
<li class="t-step" data-step="1">
<span class="t-node" data-node></span>
<span class="t-time" data-zh="第 1 轮 · 05:38">R1 · 05:38</span>
<div class="t-bubble t-bubble-hermes">
<header><img src="assets/agents/hermes.svg" alt="" /><strong>Hermes</strong><span class="t-role" data-zh="依据质询">Evidence challenge</span></header>
<p data-zh="内测应当&lt;strong&gt;验证行为&lt;/strong&gt;,而非热情。观察用户是否继续同一任务、是否有明确理由引入第二个 Agent,以及是否在接受结果前回看过程。没有这些信号,产品定位仍有待验证。">The beta should <strong>test behavior</strong>, not enthusiasm. Measure whether users resume the same task, bring in a second Agent for a clear reason, and use the trace before accepting a result. Without those signals, the category story is still unproven.</p>
</div>
<div class="t-bubble t-bubble-codex"><header><img src="assets/agents/codex.svg" alt="" /><strong>Codex</strong><span class="t-role" data-zh="请求同伴">Peer request</span></header>
<p data-zh="&lt;strong&gt;@Hermes&lt;/strong&gt; 请检查需求与边界情况;&lt;strong&gt;@OpenCodeReview&lt;/strong&gt; 请评审实现和测试。"><strong>@Hermes</strong>, check requirements and edge cases. <strong>@OpenCodeReview</strong>, review the implementation and tests.</p></div>
</li>
<li class="t-step" data-step="2">
<span class="t-node" data-node></span>
<span class="t-time" data-zh="第 2 轮 · 05:44">R2 · 05:44</span>
<div class="t-bubble t-bubble-ocr">
<header><img src="assets/agents/opencodereview.svg" alt="" /><strong>OpenCodeReview</strong><span class="t-role" data-zh="工程评审">Engineering review</span></header>
<p data-zh="MVP &lt;strong&gt;已能支持受控评估&lt;/strong&gt;,但还不适合公开下载。测试和打包通过,阻碍在于可信的 macOS 签名与公证。首版继续保持内测,并明确标出不支持的平台。">The MVP <strong>is</strong> stable enough for a controlled evaluation, but not for a public download. Tests and packaging pass; the blocker is trusted macOS signing and notarization. Keep the first release private and label unsupported platforms explicitly.</p>
</div>
<div class="t-bubble t-bubble-hermes"><header><img src="assets/agents/hermes.svg" alt="" /><strong>Hermes</strong><span class="t-role" data-zh="边界分析">Edge cases</span></header>
<p data-zh="建议补充空输入和失败重试的处理,避免异常情况下任务停滞。">Check empty input and retry behavior so the task can recover from failures.</p></div>
</li>
<li class="t-step" data-step="3">
<span class="t-node" data-node></span>
<span class="t-time" data-zh="第 2 轮 · 05:52">R2 · 05:52</span>
<div class="t-bubble t-bubble-you">
<header><span class="you-dot" aria-hidden="true">Y</span><strong data-zh="你">You</strong><span class="t-role" data-zh="协调分歧">Reconciliation</span></header>
<p data-zh="第 2 轮:协调分歧。只保留本周能验证的决定,并给出&lt;strong&gt;一项验收条件&lt;/strong&gt;。">Round 2: reconcile the disagreement. Keep only decisions we can verify this week and give me <strong>one acceptance gate</strong>.</p>
</div>
<div class="t-bubble t-bubble-ocr"><header><img src="assets/agents/opencodereview.svg" alt="" /><strong>OpenCodeReview</strong><span class="t-role" data-zh="实现评审">Implementation review</span></header>
<p data-zh="实现方向可行;建议增加失败重试的测试,并检查错误信息是否清晰。">The approach looks viable. Add a retry test and check that errors are clear.</p></div>
</li>
<li class="t-step" data-step="4">
<span class="t-node" data-node></span>
<span class="t-time" data-zh="用户决策">Adoption</span>
<div class="t-bubble t-bubble-gate">
<header><span class="gate-dot"></span><strong data-zh="人工决策">Human Gate</strong><span class="t-role" data-zh="最终决定">Decision</span></header>
<p data-zh="V1.0.3 仅向内测用户发布,&lt;strong&gt;暂不公开发行。&lt;/strong&gt;待 Developer ID 签名与公证在干净的 Mac 环境通过后重新评估。">Ship V1.0.3 to the private preview only. <strong>No-Go for public release.</strong> Reopen after Developer ID signing and notarization pass on a clean Mac.</p>
</div>
<div class="t-bubble t-bubble-codex"><header><img src="assets/agents/codex.svg" alt="" /><strong>Codex</strong><span class="t-role" data-zh="下一步">Next steps</span></header>
<p data-zh="汇总建议:补齐空输入处理、失败重试测试和错误提示,再由你检查结果。">Next: cover empty input, add a retry test, and improve error messages for your review.</p></div>
</li>
</ol>
</div>
<div class="activity-slot"><p class="run-status" data-run-status role="status" hidden></p></div>
<div class="scenario-controls">
<button type="button" class="rail-btn" data-prev aria-label="Previous step">‹</button>
<span class="rail-status" data-status>1 / 5</span>
<button type="button" class="rail-btn" data-next aria-label="Next step">›</button>
<button type="button" class="playback-toggle" data-pause aria-pressed="false">Pause</button>
</div>
</div>
</div>
</div>
</section>
@@ -478,7 +429,7 @@
<p data-zh="Agent 在同一个任务上下文中比较思路,检查彼此的贡献。">Agents compare approaches and check each other's contributions in the same task context.</p>
</div>
<div class="cs-info-item" data-cs-info="2">
<h3 data-zh="自动讨论 V4">Auto Discussion V4</h3>
<h3 data-zh="自动讨论">Auto Discussion</h3>
<p data-zh="Agent 分轮讨论、请求同伴贡献并推进授权工作。需要你作出决策时再介入。">Agents discuss, request contributions, and advance authorized work across rounds. Step in when a decision needs you.</p>
</div>
<div class="cs-info-item" data-cs-info="3">
@@ -497,8 +448,8 @@
<div class="cs-caption" data-zh="多 Agent 评审">Multi-agent review</div>
</div>
<div class="cs-card" data-card-idx="2">
<img src="assets/screenshots/meldwork-group-collaboration.png" alt="Auto Discussion V4 group collaboration" loading="lazy" />
<div class="cs-caption" data-zh="自动讨论 V4">Auto Discussion V4</div>
<img src="assets/screenshots/meldwork-group-collaboration.png" alt="Auto Discussion group collaboration" loading="lazy" />
<div class="cs-caption" data-zh="自动讨论">Auto Discussion</div>
</div>
<div class="cs-card" data-card-idx="3">
<img src="assets/screenshots/meldwork-direct-multimodal.png" alt="Direct multimodal session with local files" loading="lazy" />
@@ -643,7 +594,7 @@
</li>
<li class="acc-item reveal" data-reveal style="--d:0.12s">
<button type="button" class="acc-trigger" aria-expanded="false">
<span data-zh="自动讨论 V4 如何工作?">What does Auto Discussion V4 actually do?</span>
<span data-zh="自动讨论 如何工作?">What does Auto Discussion actually do?</span>
<span class="acc-icon" aria-hidden="true"></span>
</button>
<div class="acc-panel">
@@ -716,8 +667,8 @@
</div>
</footer>
<script src="i18n.js?v=20261027.1" defer></script>
<script src="i18n.js?v=20261028.4" defer></script>
<script src="glyph-embers.js?v=20261027.1" defer></script>
<script src="main.js?v=20261027.1" defer></script>
<script src="main.js?v=20261028.4" defer></script>
</body>
</html>
+79 -35
View File
@@ -327,66 +327,109 @@
var total = steps.length;
var current = 0;
var isInView = false;
var hovered = false;
var focused = false;
var paused = false;
var autoTimer = null;
var scnMode = reduced ? "order" : "auto";
var railFill = scenario.querySelector("[data-rail]");
var feed = scenario.querySelector("[data-timeline]");
var seats = scenario.querySelectorAll("[data-agent]");
var railStatus = scenario.querySelector("[data-status]");
var roundLabel = scenario.querySelector("[data-round]");
var pauseButton = scenario.querySelector("[data-pause]");
var runStatus = scenario.querySelector("[data-run-status]");
var segBtns = scenario.querySelectorAll(".seg-btn");
var roundNames = ["Round 1 · Propose", "Round 1 · Critique", "Round 2 · Review",
"Round 2 · Reconcile", "Adoption · Human Gate"];
var roundNames = ["Task", "Round 1 · Request", "Round 1 · Analysis", "Round 1 · Review", "Round 2 · Synthesis"];
var runningNames = ["Codex is working…", "Hermes and OpenCodeReview are working…",
"OpenCodeReview is working…", "Codex is bringing the feedback together…", "Codex is bringing the feedback together…"];
var paragraphs = Array.from(steps, function (step) { return step.querySelector("p"); });
var originals = paragraphs.map(function (p) { return { en: p.innerHTML, zh: p.dataset.zh }; });
var typed = 0;
var stage = "typing";
function translate(text) { return win.meldworkI18n.t(text); }
function setStep(idx) {
function markup(i) { return originals[i][doc.documentElement.lang === "zh-CN" ? "zh" : "en"]; }
function renderText(i, count) {
var p = paragraphs[i];
p.innerHTML = markup(i);
var walker = doc.createTreeWalker(p, NodeFilter.SHOW_TEXT);
var nodes = [];
while (walker.nextNode()) nodes.push(walker.currentNode);
nodes.forEach(function (node) {
var chars = Array.from(node.nodeValue);
node.nodeValue = chars.slice(0, count).join("");
count = Math.max(0, count - chars.length);
});
}
function textLength(i) {
var template = doc.createElement("template");
template.innerHTML = markup(i);
return Array.from(template.content.textContent).length;
}
function setStep(idx, animate) {
current = Math.max(0, Math.min(total - 1, idx));
typed = animate ? 0 : textLength(current);
stage = animate ? "typing" : "reading";
render();
}
function render() {
steps.forEach(function (step, i) {
step.classList.toggle("is-visible", i <= current);
step.classList.toggle("is-active", i === current);
step.hidden = scnMode !== "order" && i > current;
if (i === current) step.setAttribute("aria-current", "step");
else step.removeAttribute("aria-current");
renderText(i, scnMode === "order" || i !== current ? textLength(i) : typed);
});
var node = steps[current].querySelector(".t-node");
if (railFill) railFill.style.height = (steps[current].offsetTop + node.offsetTop + node.offsetHeight / 2) + "px";
if (railStatus) railStatus.textContent = (current + 1) + " / " + total;
if (roundLabel) roundLabel.textContent = translate(roundNames[current]);
var states = [["Working", "Waiting", "Waiting"], ["Working", "Waiting", "Waiting"],
["Waiting", "Working", "Working"], ["Waiting", "Done", "Working"], ["Working", "Done", "Done"]][current].slice();
if (current === 1 && stage !== "typing") states = ["Waiting", stage === "thinking" ? "Thinking" : "Working", stage === "thinking" ? "Thinking" : "Working"];
if (current === 2 && stage !== "typing") states[1] = "Done";
if (current === 3 && stage !== "typing") states = ["Working", "Done", "Done"];
if (current === 4 && stage !== "typing") states = ["Done", "Done", "Done"];
if (scnMode === "order") states = ["Done", "Done", "Done"];
seats.forEach(function (seat, i) {
seat.dataset.state = states[i].toLowerCase();
seat.querySelector("[data-agent-status]").textContent = translate(states[i]);
});
scenario.dataset.phase = stage;
feed.scrollTop = scnMode === "order" ? 0 : feed.scrollHeight;
railStatus.textContent = (current + 1) + " / " + total;
roundLabel.textContent = translate(roundNames[current]);
scenario.querySelector("[data-prev]").disabled = current === 0;
scenario.querySelector("[data-next]").disabled = current === total - 1;
pauseButton.textContent = translate(current === total - 1 ? "Replay" : paused ? "Resume" : "Pause");
pauseButton.textContent = translate(paused ? "Resume" : "Pause");
pauseButton.setAttribute("aria-pressed", String(paused));
}
function stopAuto() {
clearTimeout(autoTimer);
autoTimer = null;
scenario.dataset.playback = "paused";
runStatus.hidden = true;
}
function scheduleNext() {
stopAuto();
if (!isInView || hovered || focused || paused || reduced || doc.hidden || scnMode !== "auto" || current === total - 1) return;
if (!isInView || paused || reduced || doc.hidden || scnMode !== "auto") return;
scenario.dataset.playback = "playing";
// Allow time to read each contribution. Language switches preserve the step.
var length = steps[current].querySelector("p").textContent.length;
var delay = doc.documentElement.lang === "zh-CN" ? Math.max(7000, length * 90) : Math.max(7000, length * 32);
autoTimer = setTimeout(function () { setStep(current + 1); scheduleNext(); }, delay);
runStatus.textContent = translate(current === 1 && stage === "typing" ? "Codex is working…" :
current === 1 && stage === "thinking" ? "Hermes and OpenCodeReview are thinking…" : runningNames[current]);
runStatus.hidden = false;
var delay = stage === "typing" ? 35 : stage === "thinking" ? 1300 : current === total - 1 ? 4200 : current === 1 ? 1800 : 1600;
autoTimer = setTimeout(function () {
if (stage === "typing") {
typed = Math.min(textLength(current), typed + (doc.documentElement.lang === "zh-CN" ? 1 : 2));
if (typed >= textLength(current)) stage = current === 1 ? "thinking" : "reading";
render();
} else if (stage === "thinking") {
stage = "reading";
render();
} else {
setStep(current === total - 1 ? 0 : current + 1, true);
}
scheduleNext();
}, delay);
}
function manualStep(idx) { paused = true; setStep(idx); scheduleNext(); }
function manualStep(idx) { paused = true; setStep(idx, false); scheduleNext(); }
scenario.querySelector("[data-prev]").addEventListener("click", function () { manualStep(current - 1); });
scenario.querySelector("[data-next]").addEventListener("click", function () { manualStep(current + 1); });
pauseButton.addEventListener("click", function () {
if (current === total - 1) { paused = false; setStep(0); }
else { paused = !paused; setStep(current); }
scheduleNext();
});
scenario.addEventListener("mouseenter", function () { hovered = true; stopAuto(); });
scenario.addEventListener("mouseleave", function () { hovered = false; scheduleNext(); });
scenario.addEventListener("focusin", function () { focused = true; stopAuto(); });
scenario.addEventListener("focusout", function (event) {
focused = scenario.contains(event.relatedTarget);
scheduleNext();
});
pauseButton.addEventListener("click", function () { paused = !paused; render(); scheduleNext(); });
scenario.addEventListener("keydown", function (event) {
if (scnMode !== "auto" || !["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) return;
event.preventDefault();
@@ -400,17 +443,18 @@
button.classList.toggle("is-on", selected);
button.setAttribute("aria-pressed", String(selected));
});
setStep(current);
if (mode === "auto") setStep(current, !paused && !reduced);
else render();
scheduleNext();
}
segBtns.forEach(function (button) {
button.addEventListener("click", function () { setMode(button.dataset.mode); });
});
doc.addEventListener("visibilitychange", scheduleNext);
doc.addEventListener("meldwork:localechange", function () { setStep(current); scheduleNext(); });
if ("ResizeObserver" in win) {
new ResizeObserver(function () { setStep(current); }).observe(scenario.querySelector(".timeline"));
}
doc.addEventListener("meldwork:localechange", function () {
typed = stage === "typing" ? Math.min(typed, textLength(current)) : textLength(current);
render(); scheduleNext();
});
if ("IntersectionObserver" in win) {
new IntersectionObserver(function (entries) {
isInView = entries[0].isIntersecting;
+80 -2
View File
@@ -1262,7 +1262,7 @@ html:lang(zh-CN) .t-bubble p { line-height: 1.9; }
letter-spacing: 0.02em;
}
/* Scenario transcript: every contribution remains readable during playback. */
/* Scenario contributions appear in order; transcript mode reveals all. */
.scenario-stage {
margin-top: clamp(32px, 5vh, 56px);
position: relative;
@@ -1302,6 +1302,7 @@ html:lang(zh-CN) .t-bubble p { line-height: 1.9; }
.rail-progress { position: absolute; top: 0; left: 0; width: 1px; height: 0; background: var(--coral); transform-origin: top; transition: height 0.65s var(--ease-out); }
.timeline { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 12px; }
.t-step { display: flex; align-items: flex-start; gap: 16px; position: relative; opacity: 1; min-width: 0; }
.t-step[hidden] { display: none; }
.t-node { position: absolute; left: -23px; top: 23px; width: 9px; height: 9px; border-radius: 50%; background: var(--surface-hover); box-shadow: 0 0 0 4px var(--surface); transition: background 0.3s, box-shadow 0.3s; z-index: 1; }
.t-step.is-visible .t-node { background: var(--muted); }
.t-step.is-active .t-node { background: var(--coral); box-shadow: 0 0 0 4px var(--surface), 0 0 0 5px var(--coral-soft); }
@@ -1310,7 +1311,7 @@ html:lang(zh-CN) .t-bubble p { line-height: 1.9; }
.t-bubble { --agent-color: var(--coral-text); position: relative; flex: 1; min-width: 0; padding: 20px 24px; border-radius: 12px; background: var(--bg-deep); transition: background 0.4s, box-shadow 0.4s, transform 0.4s var(--ease-out); }
.t-bubble:hover { background: var(--surface-raised); }
.t-step.is-active .t-bubble { background: var(--surface-raised); box-shadow: 0 6px 18px rgb(0 0 0 / 0.1); transform: translateX(3px); animation: contributionEnter 0.55s var(--ease-out); }
@keyframes contributionEnter { from { transform: translateX(0) translateY(4px); } to { transform: translateX(3px) translateY(0); } }
@keyframes contributionEnter { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateX(3px) translateY(0); } }
.t-bubble header { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin-bottom: 12px; }
.t-bubble header img { width: 30px; height: 30px; object-fit: contain; padding: 5px; border-radius: 8px; background: #ffffff; }
.t-bubble header strong { font-size: 13px; font-weight: 600; color: var(--ink); }
@@ -1322,6 +1323,9 @@ html:lang(zh-CN) .t-bubble p { line-height: 1.9; }
.t-bubble-ocr { --agent-color: #86bba6; }
.you-dot, .gate-dot { width: 22px; height: 22px; border-radius: 5px; display: grid; place-items: center; flex-shrink: 0; background: var(--coral-glow); color: var(--coral-text); font-size: 11px; font-weight: 600; }
.gate-dot::after { content: "✓"; }
.run-status { margin: 18px 0 0 40px; color: var(--muted); font-size: 12px; }
.run-status::before { content: ""; display: inline-block; width: 6px; height: 6px; margin-right: 9px; border-radius: 50%; background: var(--coral); animation: runPulse 1.3s ease-in-out infinite; }
@keyframes runPulse { 50% { opacity: .3; } }
.scenario-controls { display: flex; align-items: center; justify-content: center; gap: 12px; margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--line); }
.rail-status { font-family: var(--font-display); font-size: 12px; color: var(--muted); min-width: 50px; text-align: center; }
.rail-btn { display: grid; place-items: center; width: 34px; height: 34px; border-radius: 6px; color: var(--ink-soft); background: var(--bg-deep); font-size: 22px; transition: color 0.2s, background 0.2s; }
@@ -1798,3 +1802,77 @@ html:lang(zh-CN) .t-bubble p { line-height: 1.9; }
.t-bubble::after, .logo-badge::before { display: none; }
.logo-badge, .nav-pill, .cta-dark, .theme-toggle { opacity: 1 !important; transform: none !important; }
}
/* Auto Discussion: a stable team workspace and a live conversation. */
.scenario-stage { max-width: 1080px; margin-inline: auto; border: 1px solid var(--line-strong); border-radius: 16px; background: var(--surface); }
.scenario .section-sub { max-width: 660px; }
.scenario-stage .mac-titlebar { padding: 18px 24px; background: transparent; }
.scenario-stage .meta-round { display: none; }
.scenario-stage .scenario-body { display: grid; grid-template-columns: 260px minmax(0,1fr); padding: 0; }
.agent-roster { padding: 28px 20px; background: var(--bg-deep); border-right: 1px solid var(--line); display: flex; flex-direction: column; gap: 12px; }
.roster-label { color: var(--muted); font-size: 10px; letter-spacing: .14em; margin-bottom: 12px; }
.agent-seat { display: grid; grid-template-columns: 36px 1fr; gap: 10px; position: relative; padding: 14px 10px; border-radius: 10px; transition: background .4s; }
.agent-seat img { width: 36px; height: 36px; padding: 6px; background: #fff; border-radius: 9px; }
.agent-seat strong { display: block; color: var(--ink); font-size: 12px; line-height: 1.6; }
.agent-seat div span { display: block; font-size: 10px; color: var(--muted); line-height: 1.6; }
.agent-seat small { grid-column: 2; color: var(--muted); font-size: 10px; }
.agent-seat i { position: absolute; right: 10px; bottom: 17px; width: 5px; height: 5px; border-radius: 50%; background: var(--muted); }
.agent-seat[data-state="working"] { background: rgba(var(--coral-rgb), .09); }
.agent-seat[data-state="working"] small { color: var(--coral-text); }
.agent-seat[data-state="working"] i { background: var(--coral); box-shadow: 0 0 0 4px rgba(var(--coral-rgb), .1); animation: runPulse 1.4s infinite; }
.agent-seat[data-state="done"] i { background: #86bba6; }
.demo-note { margin-top: auto; padding: 24px 10px 0; font-size: 10px; line-height: 1.8; color: var(--muted); }
.conversation-panel { min-width: 0; }
.task-brief { margin: 24px 28px 0; padding-bottom: 20px; border-bottom: 1px solid var(--line); }
.task-brief > span { color: var(--coral-text); font-size: 10px; letter-spacing: .1em; }
.task-brief p { font-size: 14px; line-height: 1.7; margin: 8px 0 0; color: var(--ink); }
.scenario-stage .timeline-track { height: 330px; overflow-y: auto; overscroll-behavior: contain; padding: 24px 28px 8px; scrollbar-width: thin; scrollbar-color: var(--line-strong) transparent; }
.scenario-stage .timeline { gap: 22px; }
.scenario-stage .t-step { display: block; }
.scenario-stage .t-step[hidden] { display: none; }
.scenario-stage .t-bubble { padding: 0; background: transparent; border: 0; border-radius: 0; box-shadow: none; transform: none; }
.scenario-stage .t-bubble header { gap: 9px; margin-bottom: 8px; }
.scenario-stage .t-bubble header img { width: 26px; height: 26px; border-radius: 7px; padding: 4px; }
.scenario-stage .t-bubble header strong { font-size: 12px; }
.scenario-stage .t-bubble p { padding-left: 35px; font-size: 13px; line-height: 1.8; }
.scenario-stage .t-bubble p strong { color: var(--coral-text); background: rgba(var(--coral-rgb), .08); border-radius: 4px; padding: 1px 3px; }
.scenario-stage .t-role { font-size: 9px; letter-spacing: .02em; text-transform: none; }
.scenario-stage .t-step.is-active .t-bubble { background: transparent; box-shadow: none; animation: messageArrive .5s var(--ease-out) both; }
@keyframes messageArrive { from { opacity: 0; transform: translateY(9px); } to { opacity: 1; transform: translateY(0); } }
.activity-slot { height: 36px; padding: 6px 28px; }
.scenario-stage .run-status { margin: 0; font-size: 11px; }
.scenario-stage .scenario-controls { margin: 0; padding: 12px 24px; gap: 8px; justify-content: flex-end; }
.scenario-stage .rail-status { margin-right: auto; order: -1; }
.scenario-stage .rail-btn { width: 28px; height: 28px; background: transparent; }
.scenario-stage .playback-toggle { min-width: 78px; padding: 6px 10px; font-size: 11px; }
.scenario-stage.is-order .t-step.is-active .t-bubble { animation: none; }
.scenario-stage.is-order .activity-slot { display: none; }
.scenario-stage.is-order .timeline-track { height: 414px; }
@media (max-width: 700px) {
.scenario-stage .scenario-body { grid-template-columns: 1fr; }
.agent-roster { display: grid; grid-template-columns: repeat(3,minmax(0,1fr)); padding: 12px; gap: 4px; border-right: 0; border-bottom: 1px solid var(--line); }
.roster-label, .demo-note, .agent-seat div span { display: none; }
.agent-seat { grid-template-columns: 1fr; justify-items: center; padding: 10px 2px; gap: 5px; }
.agent-seat strong { font-size: 9px; }
.agent-seat small { grid-column: 1; font-size: 9px; }
.agent-seat i { display: none; }
.agent-seat img { width: 30px; height: 30px; }
.scenario-stage .mac-titlebar { padding: 14px; gap: 10px; }
.scenario-stage .meta-name { display: none; }
.scenario-stage .seg-btn { padding: 6px 9px; }
.task-brief { margin: 18px 18px 0; padding-bottom: 16px; }
.scenario-stage .timeline-track { padding: 18px; height: 310px; }
.scenario-stage .t-bubble p { padding-left: 0; }
.scenario-stage .t-role { font-size: 8px; }
.activity-slot { padding-inline: 18px; }
}
@media (prefers-reduced-motion: reduce) {
.scenario-stage .t-step.is-active .t-bubble, .agent-seat i { animation: none; }
}
/* Typing and thinking cues in the illustrative collaboration. */
.scenario-stage[data-phase="typing"] .t-step.is-active p::after { content: ""; display: inline-block; width: 2px; height: 1em; margin-left: 3px; vertical-align: -.15em; background: var(--coral-text); animation: runPulse .8s infinite; }
.agent-seat[data-state="thinking"] { background: rgba(var(--coral-rgb), .06); }
.agent-seat[data-state="thinking"] small { color: var(--coral-text); }
.agent-seat[data-state="thinking"] i { background: var(--coral); animation: runPulse .8s infinite; }
@media (prefers-reduced-motion: reduce) { .scenario-stage[data-phase="typing"] .t-step.is-active p::after, .agent-seat[data-state="thinking"] i { animation: none; } }
+56 -15
View File
@@ -92,12 +92,18 @@ function page({ language = 'en', saved, reduced = false, blockedStorage = false
const observer = observers.findLast(entry => entry.targets.includes(scenario))
observer.callback([{ target: scenario, isIntersecting: value }])
}
function tick() {
const timer = [...timers.entries()].find(([, value]) => value.delay >= 7000)
assert.ok(timer, 'a reading-paced playback timer should exist')
function pulse() {
const timer = [...timers.entries()].find(([, value]) => value.delay === 35) ||
[...timers.entries()].find(([, value]) => [1300, 1600, 1800, 4200].includes(value.delay))
assert.ok(timer, 'a playback timer should exist')
timers.delete(timer[0])
timer[1].callback()
}
function tick() {
const initial = scenario.querySelector('[aria-current="step"]').dataset.step
let guard = 0
do { pulse(); assert.ok(++guard < 300) } while (scenario.querySelector('[aria-current="step"]').dataset.step === initial)
}
function runFrame(now) {
const pending = [...frames.values()]
frames.clear()
@@ -106,7 +112,7 @@ function page({ language = 'en', saved, reduced = false, blockedStorage = false
function setMotion(value) {
mediaQueries.forEach(query => { query.matches = value; query.onChange?.() })
}
return { dom, window, doc, scenario, inView, tick, timers, runFrame, setMotion,
return { dom, window, doc, scenario, inView, tick, pulse, timers, runFrame, setMotion,
emberDraws: () => emberDraws, glyphSamples: () => glyphSamples }
}
@@ -147,7 +153,7 @@ test('unavailable storage still permits language switching', () => {
test('full-page copy has Chinese alternatives while links and emphasis survive switching', () => {
const p = page()
try {
for (const el of p.doc.querySelectorAll('h1 .hl-line,h2,h3,p,.cta-ghost,.nav-link span,.m-link span,.stat-label,.meta-pill,.meta-name,.seg-btn,.t-time,.t-role,.cs-caption,.cmp-row > div,.bnd-col li,.acc-trigger > span:first-child,.foot-meta > span')) {
for (const el of p.doc.querySelectorAll('h1 .hl-line,h2,h3,p:not([data-run-status]),.cta-ghost,.nav-link span,.m-link span,.stat-label,.meta-pill,.meta-name,.seg-btn,.t-time,.t-role,.cs-caption,.cmp-row > div,.bnd-col li,.acc-trigger > span:first-child,.foot-meta > span')) {
assert.ok(el.hasAttribute('data-zh') || el.closest('[data-zh]'), el.outerHTML)
}
const linkCount = p.doc.querySelectorAll('a').length
@@ -170,7 +176,7 @@ test('manual playback pauses and language switches preserve the current contribu
assert.equal(p.scenario.dataset.playback, 'paused')
p.window.meldworkI18n.setLocale('zh')
assert.equal(p.scenario.querySelector('[aria-current="step"]').dataset.step, '2')
assert.equal(p.doc.querySelector('[data-round]').textContent, '第 2 轮 · 评审')
assert.equal(p.doc.querySelector('[data-round]').textContent, '第 1 轮 · 分析')
assert.equal(p.doc.querySelector('[data-pause]').textContent, '继续播放')
p.doc.querySelector('[data-pause]').click()
assert.equal(p.scenario.dataset.playback, 'playing')
@@ -179,26 +185,61 @@ test('manual playback pauses and language switches preserve the current contribu
} finally { p.dom.window.close() }
})
test('autoplay stops at the last contribution and replay starts from the beginning', () => {
test('autoplay types each contribution and loops after the synthesis', () => {
const p = page()
try {
p.inView(true)
const first = p.scenario.querySelector('[data-step="0"] p')
assert.equal(first.textContent, '')
p.pulse()
assert.ok(first.textContent.length > 0 && first.textContent.length < 20)
for (let i = 0; i < 4; i++) p.tick()
assert.equal(p.doc.querySelector('[data-status]').textContent, '5 / 5')
assert.equal(p.scenario.dataset.playback, 'paused')
assert.equal(p.doc.querySelector('[data-pause]').textContent, 'Replay')
p.doc.querySelector('[data-pause]').click()
assert.equal(p.scenario.dataset.playback, 'playing')
p.tick()
assert.equal(p.doc.querySelector('[data-status]').textContent, '1 / 5')
assert.equal(first.textContent, '')
} finally { p.dom.window.close() }
})
test('hover and document visibility suspend playback without losing progress', () => {
test('mentions precede thinking and both Agents respond in order', () => {
const p = page()
try {
p.inView(true)
const steps = [...p.scenario.querySelectorAll('.t-step')]
const seats = [...p.scenario.querySelectorAll('[data-agent]')]
p.tick()
assert.deepEqual(seats.map(seat => seat.dataset.state), ['working', 'waiting', 'waiting'])
assert.equal(steps[1].querySelector('p').textContent, '')
while (p.scenario.dataset.phase === 'typing') p.pulse()
assert.match(steps[1].textContent, /@Hermes/)
assert.match(steps[1].textContent, /@OpenCodeReview/)
assert.deepEqual(seats.map(seat => seat.dataset.state), ['waiting', 'thinking', 'thinking'])
p.pulse()
assert.deepEqual(seats.map(seat => seat.dataset.state), ['waiting', 'working', 'working'])
p.tick()
assert.deepEqual(steps.map(step => step.hidden), [false, false, false, true, true])
assert.match(steps[2].textContent, /Hermes/)
p.tick()
assert.deepEqual(seats.map(seat => seat.dataset.state), ['waiting', 'done', 'working'])
assert.match(steps[3].textContent, /OpenCodeReview/)
p.doc.querySelector('[data-mode="order"]').click()
assert.ok(steps.every(step => !step.hidden))
p.window.meldworkI18n.setLocale('zh')
assert.match(steps[1].textContent, /@Hermes/)
assert.doesNotMatch(p.doc.body.textContent, /自动讨论\s*V4|Auto Discussion\s*V4/)
assert.equal(p.doc.querySelector('#capabilities'), null)
assert.equal(p.doc.querySelector('a[href="#capabilities"]'), null)
} finally { p.dom.window.close() }
})
test('hover keeps playback running while document visibility suspends it', () => {
const p = page()
try {
p.inView(true)
p.tick()
p.scenario.dispatchEvent(new p.window.MouseEvent('mouseenter'))
assert.equal(p.scenario.dataset.playback, 'paused')
assert.equal(p.scenario.dataset.playback, 'playing')
p.scenario.dispatchEvent(new p.window.MouseEvent('mouseleave'))
assert.equal(p.scenario.dataset.playback, 'playing')
Object.defineProperty(p.doc, 'hidden', { value: true, configurable: true })
@@ -211,12 +252,12 @@ test('hover and document visibility suspend playback without losing progress', (
} finally { p.dom.window.close() }
})
test('keyboard navigation, focus pause and transcript mode keep playback under user control', () => {
test('keyboard navigation, focus and transcript mode keep playback under user control', () => {
const p = page()
try {
p.inView(true)
p.doc.querySelector('[data-next]').focus()
assert.equal(p.scenario.dataset.playback, 'paused')
assert.equal(p.scenario.dataset.playback, 'playing')
p.scenario.dispatchEvent(new p.window.KeyboardEvent('keydown', { key: 'End', bubbles: true }))
assert.equal(p.doc.querySelector('[data-status]').textContent, '5 / 5')
p.doc.querySelector('[data-mode="order"]').click()
@@ -233,7 +274,7 @@ test('reduced motion defaults to a readable transcript without automatic playbac
assert.ok(p.scenario.classList.contains('is-order'))
p.doc.querySelector('[data-mode="auto"]').click()
assert.equal(p.scenario.dataset.playback, 'paused')
assert.equal([...p.timers.values()].some(value => value.delay >= 7000), false)
assert.equal([...p.timers.values()].some(value => value.delay >= 2800), false)
} finally { p.dom.window.close() }
})