kimi-code/apps/kimi-web/design/queue-composer-mockup.html
qer b905dd4910
feat(web): redesign web UI and add design system (#1258)
* feat: redesign web ui & add design system

* feat(web): add motion to redesigned UI and add changesets

Animate toast enter/leave, dialog open, and workspace-list and tool-row expand/collapse instead of snapping, and add the changesets covering the web redesign.

* fix(web): remove undo message exit animation

* fix(web): route agent tools to AgentTool and focus dialog on open

- toolRegistry matched the raw 'agent' name, but normalizeToolName folds
  agent/subagent into 'task', so agent calls fell through to GenericTool
  and lost the inline Open button for the subagent detail panel.
- Dialog's focus watcher only fired on change; callers that mount with
  open already true (Login, Settings, ...) never moved focus into the
  modal. Run it immediately so initial focus and restore-on-close work.

* feat(web): add logo long-press design-system easter egg

Hold the sidebar logo for 3 seconds to open a dialog showing the design system page. Also trim and rebalance the redesign changesets.

* fix(web): silence Sidebar v-show warning by making it single-root

Nest the design-system Teleport inside the sidebar <aside> so the component has a single element root. App applies v-show to Sidebar, which needs an element root to attach to; the fragment root logged a "non-element root node" warning on every reactive update and the collapse did not take effect.

* fix(web): thinking toggle, tool-group i18n, agent detail button

- Show the default-thinking switch as on whenever thinking is effectively enabled (enabled !== false), matching the core resolver.

- Route the grouped tool-call header and status through vue-i18n.

- Hide the subagent "Open detail" button when no matching task exists (e.g. a completed foreground subagent after a refresh).

* fix(web): use strict equality in agent detail button guard

oxlint eqeqeq flagged the loose != null check; resolveAgentTaskId returns string | undefined, so compare with !== undefined.

* chore(web): remove stray design mockups and screenshots

Remove the design exploration mockups, screenshots, prompts, and notes that were accidentally committed under apps/kimi-web/design. Keep design-system.html, which the sidebar logo easter egg still references.

* fix(web): keep sessions on continuation failure, treat absent thinking as on

- Keep sessions already loaded from earlier pages when a continuation page fetch fails, instead of replacing the workspace with an empty page.

- Treat an absent thinking config as enabled in the settings toggle, matching the core resolver (thinking is on unless explicitly disabled).

* feat(web): open design-system easter egg on 10 logo clicks

Replace the 3-second long-press trigger with 10 consecutive clicks on the Kimi mark; the count resets after a short idle. The long-press was unreliable because pointerleave cancelled the timer on any drift.

* fix(web): treat cancelled swarm members as finished

phaseForTask now lets a terminal status (completed/failed/cancelled) override a stale subagentPhase, so a cancelled swarm member no longer stays live and suppresses the finished AgentSwarm card.

* feat(web): use 1s long-press for design-system easter egg

Switch the logo easter egg back to a long-press, shortened to 1 second, and make it reliable this time: use pointer capture plus touch-action:none so a slight drift no longer cancels the hold.

* fix(web): use plain Spinner for activity notices

ActivityNotice renders for non-chat loading states (e.g. compaction), so it must use Spinner per the design-system rule that reserves MoonSpinner for the chat first-response state.

* docs(web): add a11y guidance to design system

* docs(web): drop stale design README link

* fix(web): restore model search focus and define panel header weight

- Bind the model picker's search Input to searchRef so useDialogFocus moves focus into it on open instead of the dialog's close button.

- Use the defined --weight-semibold token for panel header titles (--weight-bold is not declared, so the shorthand was invalid).

* fix(web): let any open dialog own Escape over the side panel

Track open design-system Dialog instances in a shared count and include it in App.vue's anyOverlayOpen, so a dialog whose open state lives outside App.vue (such as the sidebar session search) captures Escape before the background side panel closes.

* fix(web): base dock-work flag on filtered dock task lists

Foreground subagents are excluded from the dock task lists, so a session whose only task is a foreground subagent no longer renders an empty workbar above the composer.

* fix(web): create subagent task before forwarding text deltas

A client that subscribes from a snapshot after subagent.spawned already fired never received the lifecycle taskCreated; the reducer only applies taskProgress to existing tasks, so assistant text deltas were dropped and the live subagent detail stayed blank. Emit taskCreated (via patchSubagent) before the text progress, mirroring the tool-progress path.

* fix(web): keep plan, swarm, and goal mode toggles per session

Plan, swarm, and goal modes were stored as global scalars on the web client and a single localStorage key each, so they leaked across sessions. Bind them to the active session via per-session maps, persist per session, and apply server status/events to the originating session so background sessions keep independent state.

* fix(web): keep subagent detail reachable for synthesized tasks

When the web client subscribes after a subagent already spawned, the synthesized subagent task has no parentToolCallId, so the Agent tool's Open-detail button was hidden and the panel would not open. Fall back to the single unmapped subagent task when resolving the detail target in both the button visibility and the panel open paths.

* fix(web): keep session kebab menu from being clipped

Teleport the SessionRow kebab menu to body and anchor it with fixed positioning so the collapsing group-sessions list's overflow:hidden no longer clips the dropdown.

* fix(web): apply staged modes to the created session by id

When starting the first prompt, apply the staged plan/swarm/goal modes to the just-created session's per-session maps by id instead of via the activeSessionId-based setters, so a session switch during the selectSession await can't drop the modes for this session or pollute another.

* refactor(web): unify confirmation dialogs into a single modal

Replace the inconsistent confirmation patterns (native confirm(),
two-step menu arming, hand-rolled inline strips, bare buttons) with one
modal ConfirmDialog driven by a global useConfirmDialog() composable,
and consolidate the duplicated confirm/cancel i18n keys.

* feat(web): inline message queue with separate stop button

- Send button always sends/enqueues; interrupt moves to a separate red Stop
  button shown only while running, so the two can no longer be confused.
- Queued prompts now render inline at the tail of the transcript (after the
  running turn) instead of behind the dock panel: click to edit, remove, drag
  the grip to reorder, with image thumbnails and a "next up" marker.
- Remove the dock queue panel and the QueuePane component; Steer stays on
  Ctrl/Cmd+S.

* chore: add changeset for web queue UX

* feat(web): prompt reliability, sidebar menu, and composer/markdown polish

- Fix spurious errors when question/approval/task actions were already complete
- Add loading feedback to question and approval prompts; block double-clicks
- Make the question "Other" option selectable by row click and let Enter advance/submit
- Consolidate workspace section actions into an overflow menu
- Tighten markdown prose line-height and block spacing
- Recall input history only when the caret is at text start
2026-07-01 20:47:12 +08:00

547 lines
31 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="zh-CN" data-color-scheme="light">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>kimi-web · 消息队列 UX 高保真原型</title>
<style>
/* -----------------------------------------------------------------------
设计 token抄录自 apps/kimi-web/src/style.css v2 层;浅色 + 深色)。
真实落地请直接用项目 token本沙盒为自包含而内联。
----------------------------------------------------------------------- */
:root {
--color-bg: #ffffff;
--color-surface: #fafbfc;
--color-surface-raised: #ffffff;
--color-surface-sunken: #f3f5f8;
--color-text: rgba(0, 0, 0, 0.9);
--color-text-muted: #6b7280;
--color-text-faint: #9aa3af;
--color-line: #e7eaee;
--color-line-strong: #d4d9e0;
--color-accent: #1783ff;
--color-accent-hover: #0f6fe0;
--color-accent-soft: #e8f3ff;
--color-accent-bd: #cfe6ff;
--color-success: #0e7a38;
--color-warning: #a9610a;
--color-danger: #c0392b;
--color-danger-soft: #fbeaea;
--space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px;
--radius-xs: 4px; --radius-sm: 6px; --radius-md: 8px; --radius-lg: 12px;
--radius-xl: 16px; --radius-2xl: 20px; --radius-full: 999px;
--shadow-xs: 0 1px 2px rgba(16, 24, 40, 0.04);
--shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.05), 0 1px 3px rgba(16, 24, 40, 0.06);
--shadow-md: 0 4px 12px rgba(16, 24, 40, 0.07), 0 2px 4px rgba(16, 24, 40, 0.05);
--shadow-lg: 0 12px 32px rgba(16, 24, 40, 0.12), 0 4px 10px rgba(16, 24, 40, 0.08);
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--duration-fast: 120ms; --duration-base: 160ms;
--font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
"Hiragino Sans GB", "Microsoft YaHei", sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
--text-xs: 12px; --text-sm: 13px; --text-base: 14px; --text-lg: 16px;
--leading-normal: 1.5; --leading-relaxed: 1.7;
--weight-medium: 500; --weight-semibold: 700;
}
html[data-color-scheme="dark"] {
--color-bg: #0d1117;
--color-surface: #161b22;
--color-surface-raised: #1c2128;
--color-surface-sunken: #0d1117;
--color-text: #c9cdd4;
--color-text-muted: #9aa0a8;
--color-text-faint: #6b7280;
--color-line: #2d333b;
--color-line-strong: #3d444d;
--color-accent: #58a6ff;
--color-accent-hover: #79b8ff;
--color-accent-soft: rgba(88, 166, 255, 0.14);
--color-accent-bd: rgba(88, 166, 255, 0.28);
--color-success: #3fb950;
--color-warning: #d29922;
--color-danger: #f85149;
--color-danger-soft: rgba(248, 81, 73, 0.14);
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.2);
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.22), 0 1px 3px rgba(0, 0, 0, 0.18);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.24);
--shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.34), 0 4px 10px rgba(0, 0, 0, 0.28);
}
* { box-sizing: border-box; }
html, body { margin: 0; }
body {
background: var(--color-bg);
color: var(--color-text);
font-family: var(--font-ui);
font-size: var(--text-base);
line-height: var(--leading-normal);
-webkit-font-smoothing: antialiased;
transition: background var(--duration-base) var(--ease-out), color var(--duration-base) var(--ease-out);
}
svg { display: block; }
/* ---- 页面框架 ---- */
header {
max-width: 1180px; margin: 0 auto; padding: 40px 24px 8px;
display: flex; align-items: flex-start; justify-content: space-between; gap: 16px;
}
header h1 { font-size: 20px; font-weight: 650; margin: 0 0 6px; letter-spacing: -0.01em; }
header p { margin: 0; color: var(--color-text-muted); font-size: var(--text-sm); max-width: 60ch; }
.theme-toggle {
flex: none; display: inline-flex; align-items: center; gap: 6px; height: 30px; padding: 0 12px;
border-radius: var(--radius-full); border: 1px solid var(--color-line);
background: var(--color-surface); color: var(--color-text-muted); font: inherit; font-size: var(--text-sm);
cursor: pointer; transition: background var(--duration-fast) var(--ease-out);
}
.theme-toggle:hover { background: var(--color-surface-sunken); }
.wrap { max-width: 1180px; margin: 0 auto; padding: 18px 24px 80px; }
.legend {
display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px;
margin: 0 0 22px; padding: 14px 16px;
border: 1px solid var(--color-line); border-radius: var(--radius-lg); background: var(--color-surface);
}
.legend h2 { grid-column: 1 / -1; font-size: var(--text-sm); font-weight: var(--weight-semibold); margin: 0 0 2px; }
.legend .item { display: flex; gap: 10px; align-items: flex-start; font-size: var(--text-sm); color: var(--color-text-muted); }
.legend .item b { color: var(--color-text); font-weight: var(--weight-medium); }
.legend .tag { flex: none; font-size: 11px; font-weight: 700; color: var(--color-accent-hover); background: var(--color-accent-soft); border: 1px solid var(--color-accent-bd); border-radius: var(--radius-full); padding: 1px 7px; margin-top: 1px; }
.stage-label { font-size: var(--text-xs); font-weight: var(--weight-semibold); letter-spacing: .03em; text-transform: uppercase; color: var(--color-text-faint); margin: 26px 0 8px; }
/* 带侧栏注解的舞台:手机列 + 注解列 */
.annotated { display: grid; grid-template-columns: minmax(0, 760px) 1fr; gap: 22px; align-items: start; }
.notes { display: flex; flex-direction: column; gap: 12px; position: sticky; top: 16px; padding-top: 34px; }
.note-card { padding: 11px 13px; border: 1px solid var(--color-line); border-left: 3px solid var(--color-accent-bd); border-radius: var(--radius-md); background: var(--color-surface); font-size: var(--text-xs); color: var(--color-text-muted); line-height: 1.5; }
.note-card b { display: block; color: var(--color-text); font-weight: var(--weight-semibold); margin-bottom: 3px; font-size: var(--text-sm); }
/* ---- 聊天舞台 ---- */
.phone {
border: 1px solid var(--color-line); border-radius: var(--radius-2xl); background: var(--color-bg);
box-shadow: var(--shadow-lg); overflow: hidden;
}
.chat {
display: flex; flex-direction: column; gap: 14px;
padding: 22px 18px 14px;
max-height: 560px; overflow-y: auto;
}
/* 用户气泡(与 design-system §04 一致) */
.u-bub {
align-self: flex-end; max-width: 78%;
background: var(--color-accent-soft); border: 1px solid var(--color-accent-bd);
color: var(--color-text); border-radius: 18px 18px 5px 18px; padding: 11px 15px;
font-size: 15px; line-height: var(--leading-normal); box-shadow: var(--shadow-xs);
}
.u-bub.faint { opacity: .55; } /* 历史已发送、正在处理的那条 */
/* assistant prose */
.a-msg { max-width: 760px; font-size: 15px; line-height: var(--leading-relaxed); color: var(--color-text); }
.a-msg p { margin: 0; }
.a-msg code { font-family: var(--font-mono); background: var(--color-surface-sunken); border: 1px solid var(--color-line); color: var(--color-accent-hover); padding: 1px 6px; border-radius: 5px; font-size: .9em; }
/* thinking */
.thinking { display: inline-flex; align-items: center; gap: 9px; font-size: var(--text-sm); color: var(--color-text-muted); }
.moon { font-size: 15px; line-height: 1; }
/* 紧凑工具行 */
.tool-row { display: flex; align-items: center; gap: 8px; height: 30px; padding: 0 11px; border: 1px solid var(--color-line); border-radius: var(--radius-md); font-family: var(--font-mono); font-size: var(--text-sm); color: var(--color-text); max-width: 520px; }
.tool-row .tr-name { font-weight: 600; }
.tool-row .tr-arg { color: var(--color-text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.tool-row .tr-time { margin-left: auto; color: var(--color-text-faint); font-size: var(--text-xs); }
.dot { width: 7px; height: 7px; border-radius: 50%; flex: none; background: var(--color-text-faint); }
.dot.done { background: var(--color-success); }
.dot.running { background: var(--color-accent); box-shadow: 0 0 0 0 var(--color-accent-soft); animation: pulse 1.4s ease-out infinite; }
@keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(23,131,255,.4); } 100% { box-shadow: 0 0 0 6px rgba(23,131,255,0); } }
/* ===================================================================
新增:内联队列(核心提案)
=================================================================== */
.q-stack { align-self: flex-end; width: min(78%, 520px); display: flex; flex-direction: column; gap: 7px; }
.q-head {
display: flex; align-items: center; justify-content: flex-end; gap: 8px;
padding: 0 4px; color: var(--color-text-faint); font-size: var(--text-xs);
}
.q-head .q-title { display: inline-flex; align-items: center; gap: 6px; }
.q-head .q-hint { color: var(--color-text-faint); }
.q-head .q-count { font-weight: var(--weight-semibold); color: var(--color-accent-hover); }
.q-bubble {
position: relative; display: flex; align-items: center; gap: 8px;
padding: 9px 8px 9px 6px;
background: var(--color-surface-raised);
border: 1px dashed var(--color-accent-bd);
border-radius: 16px 16px 6px 16px;
box-shadow: var(--shadow-xs);
cursor: pointer; text-align: left;
transition: border-color var(--duration-fast) var(--ease-out),
background var(--duration-fast) var(--ease-out),
transform var(--duration-fast) var(--ease-out);
animation: q-in .22s var(--ease-out);
}
@keyframes q-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
.q-bubble:hover { border-color: var(--color-accent); background: var(--color-accent-soft); }
.q-bubble:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--color-accent-soft); border-color: var(--color-accent); }
.q-bubble.leaving { animation: q-out .18s var(--ease-out) forwards; }
@keyframes q-out { to { opacity: 0; transform: translateY(-4px); } }
.q-grip { flex: none; width: 16px; display: grid; place-items: center; color: var(--color-text-faint); opacity: .7; cursor: grab; }
.q-bubble:hover .q-grip { opacity: 1; }
.q-body { flex: 1; min-width: 0; font-size: 15px; line-height: var(--leading-normal); color: var(--color-text); opacity: .82; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.q-bubble:hover .q-body { opacity: 1; }
.q-body .q-img { display: inline-flex; align-items: center; gap: 4px; color: var(--color-text-muted); }
.q-tag { flex: none; font-size: 10.5px; font-weight: 700; letter-spacing: .02em; padding: 1px 6px; border-radius: var(--radius-full); white-space: nowrap; }
.q-tag.next { color: var(--color-accent-hover); background: var(--color-accent-soft); border: 1px solid var(--color-accent-bd); }
.q-tag.idx { color: var(--color-text-faint); background: var(--color-surface-sunken); border: 1px solid var(--color-line); }
.q-rm {
flex: none; width: 22px; height: 22px; display: grid; place-items: center;
border: none; background: transparent; color: var(--color-text-faint); border-radius: var(--radius-sm);
cursor: pointer; opacity: 0; transition: opacity var(--duration-fast) var(--ease-out), background var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out);
}
.q-bubble:hover .q-rm, .q-bubble:focus-within .q-rm, .q-rm:focus-visible { opacity: 1; }
.q-rm:hover { background: var(--color-danger-soft); color: var(--color-danger); }
.q-rm:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--color-accent-soft); }
/* ===================================================================
Composer含新增的独立 Stop 按钮 + 运行态 placeholder
=================================================================== */
.dock { padding: 6px 18px 18px; background: var(--color-bg); }
.composer { background: var(--color-surface-raised); border: 1px solid var(--color-line); border-radius: var(--radius-xl); box-shadow: var(--shadow-md); overflow: hidden; }
.composer:focus-within { border-color: var(--color-accent); box-shadow: var(--shadow-md), 0 0 0 3px var(--color-accent-soft); }
.composer-ta {
display: block; width: 100%; resize: none; border: none; outline: none; background: transparent;
padding: 14px 16px 8px; font: inherit; font-size: 15px; color: var(--color-text); line-height: var(--leading-normal);
min-height: 24px; max-height: 160px;
}
.composer-ta::placeholder { color: var(--color-text-faint); }
.composer-bar { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 6px 8px 8px; }
.composer-left, .composer-right { display: flex; align-items: center; gap: 2px; }
.icon-btn { width: 30px; height: 30px; display: grid; place-items: center; border: none; background: transparent; color: var(--color-text-muted); border-radius: var(--radius-md); cursor: pointer; }
.icon-btn:hover { background: var(--color-surface-sunken); color: var(--color-text); }
.pill { display: inline-flex; align-items: center; gap: 6px; height: 28px; padding: 0 10px; border-radius: var(--radius-md); border: 1px solid transparent; background: transparent; font-size: var(--text-sm); font-weight: var(--weight-medium); color: var(--color-text); cursor: pointer; }
.pill:hover { background: var(--color-surface-sunken); }
.pill .pp-strong { font-weight: 700; }
.pill .pp-sub { color: var(--color-accent); font-weight: 600; }
.pill svg { color: var(--color-text-faint); }
.send { width: 32px; height: 32px; border-radius: 50%; display: grid; place-items: center; background: var(--color-accent); color: #fff; border: none; cursor: pointer; box-shadow: var(--shadow-xs); transition: transform var(--duration-fast) var(--ease-out), background var(--duration-base) var(--ease-out); }
.send:hover { background: var(--color-accent-hover); }
.send:active { transform: scale(.92); }
/* 独立的 Stop 按钮(新增):与 Send 平级,运行时显示 */
.stop {
width: 32px; height: 32px; border-radius: 50%; display: grid; place-items: center;
background: var(--color-surface-raised); color: var(--color-text);
border: 1px solid var(--color-line-strong); box-shadow: var(--shadow-xs);
cursor: pointer; transition: transform var(--duration-fast) var(--ease-out), background var(--duration-base) var(--ease-out), color var(--duration-base) var(--ease-out), border-color var(--duration-base) var(--ease-out);
}
.stop:hover { background: var(--color-danger-soft); color: var(--color-danger); border-color: var(--color-danger-soft); }
.stop:active { transform: scale(.92); }
/* 运行态 hint输入框下方一行小字可关 */
.composer-hint { padding: 0 16px 8px; font-size: var(--text-xs); color: var(--color-text-faint); display: flex; align-items: center; gap: 6px; }
.composer-hint kbd { font-family: var(--font-mono); font-size: 11px; padding: 0 5px; border: 1px solid var(--color-line); border-radius: var(--radius-xs); background: var(--color-surface-sunken); color: var(--color-text-muted); }
/* 演示控制条 */
.demo-ctrl { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 10px; padding: 0 2px; }
.demo-ctrl button { font: inherit; font-size: var(--text-sm); border: 1px solid var(--color-line); background: var(--color-surface); color: var(--color-text-muted); border-radius: var(--radius-full); padding: 5px 12px; cursor: pointer; }
.demo-ctrl button:hover { background: var(--color-surface-sunken); color: var(--color-text); }
.demo-ctrl .spacer { flex: 1; }
.demo-ctrl .note { font-size: var(--text-xs); color: var(--color-text-faint); }
.footer-note { margin-top: 22px; font-size: var(--text-sm); color: var(--color-text-muted); max-width: 760px; }
.footer-note h3 { font-size: var(--text-base); color: var(--color-text); margin: 18px 0 6px; }
.footer-note ul { margin: 6px 0; padding-left: 20px; }
.footer-note li { margin: 4px 0; }
.footer-note code { font-family: var(--font-mono); font-size: 12px; background: var(--color-surface-sunken); border: 1px solid var(--color-line); border-radius: 5px; padding: 1px 5px; color: var(--color-accent-hover); }
@media (max-width: 980px) {
.legend { grid-template-columns: 1fr; }
.annotated { grid-template-columns: minmax(0, 760px); }
.notes { position: static; padding-top: 8px; }
header { flex-direction: column; }
}
</style>
</head>
<body>
<header>
<div>
<h1>消息队列 UX · 高保真原型</h1>
<p>把「发送」和「中断」解耦,把队列从隐藏面板搬进对话流。下面是运行中 + 排队的核心场景,可交互(试着输入回车、点 ×、点排队项编辑、点“出队一条”)。</p>
</div>
<button class="theme-toggle" id="themeToggle" type="button">◐ 切换深 / 浅</button>
</header>
<div class="wrap">
<div class="legend">
<h2>相对现在的改动4 点)</h2>
<div class="item"><span class="tag">A</span><div><b>发送永远是发送</b> — 运行中也不再变成“中断”Enter / 按钮都是入队。</div></div>
<div class="item"><span class="tag">B</span><div><b>Stop 独立按钮</b> — 与发送平级、运行时固定显示,急停仍一键可达。</div></div>
<div class="item"><span class="tag">C</span><div><b>队列内联进对话流</b> — 作为半透明待发送气泡,看得见、点得动。</div></div>
<div class="item"><span class="tag">D</span><div><b>可编辑 / 可排序 / 可删除</b> — 点气泡编辑、拖拽排序、× 删除。</div></div>
</div>
<div class="stage-label">场景 · 运行中agent 正在改写中间件,你又追加了 3 条)</div>
<div class="annotated">
<div class="phone">
<div class="chat" id="chat">
<!-- 历史:已被处理的用户消息 -->
<div class="u-bub faint">帮我把登录接口改成 JWT并补上对应的单元测试。</div>
<div class="a-msg"><p>我看了一下 <code>src/auth</code> 的结构,当前是基于 session cookie 的实现,先读取相关文件。</p></div>
<div class="tool-row">
<span class="dot done"></span>
<span class="tr-name">read_file</span>
<span class="tr-arg">src/auth/session.ts</span>
<span class="tr-time">0.2s</span>
</div>
<div class="tool-row">
<span class="dot running"></span>
<span class="tr-name">read_file</span>
<span class="tr-arg">src/auth/middleware.ts</span>
<span class="tr-time">运行中…</span>
</div>
<div class="thinking"><span class="moon">🌔</span>正在改写认证中间件…</div>
<!-- ===== 内联队列(新增) ===== -->
<div class="q-stack" id="qStack">
<div class="q-head">
<span class="q-title">
<svg width="12" height="12" viewBox="0 0 16 16" fill="none"><path d="M2 4h12M2 8h12M2 12h8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
队列 · <span class="q-count" id="qCount">3</span>
</span>
<span class="q-hint">当前回合结束后自动逐条发送</span>
</div>
<div id="qItems"></div>
</div>
</div>
<!-- ===== Composer含独立 Stop ===== -->
<div class="dock">
<div class="composer" id="composer">
<textarea class="composer-ta" id="ta" rows="1" placeholder="输入会加入队列 · Ctrl+S 立即插入运行中的回合"></textarea>
<div class="composer-bar">
<div class="composer-left">
<button class="icon-btn" type="button" title="添加附件">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>
</button>
<span class="pill"><span style="width:7px;height:7px;border-radius:50%;background:var(--color-warning)"></span>yolo</span>
<span class="pill">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M2 4h12M2 8h8M2 12h5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>plan
</span>
</div>
<div class="composer-right">
<span class="pill"><span class="pp-strong">kimi-k2</span><span class="pp-sub">· thinking</span>
<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span>
<!-- 新增:独立的 Stop仅在 running 时显示 -->
<button class="stop" id="stopBtn" type="button" title="中断当前回合">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><rect x="4" y="4" width="8" height="8" rx="1.5" fill="currentColor"/></svg>
</button>
<!-- Send 永远是 Send -->
<button class="send" id="sendBtn" type="button" title="发送(运行中时加入队列)">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 13V3m0 0L4.5 6.5M8 3l3.5 3.5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
</div>
</div>
<div class="composer-hint">
<svg width="12" height="12" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6.5" stroke="currentColor" stroke-width="1.4"/><path d="M8 7.2v3.6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="8" cy="5.2" r=".7" fill="currentColor"/></svg>
运行中:<kbd>Enter</kbd> 加入队列 · <kbd>Ctrl</kbd>+<kbd>S</kbd> 立即插入当前回合
</div>
</div>
<div class="demo-ctrl">
<button id="drainBtn" type="button">▶ 出队一条(模拟回合结束)</button>
<button id="resetBtn" type="button">↺ 重置</button>
<span class="spacer"></span>
<span class="note">提示:在输入框里打点什么按 Enter会作为新气泡进队列。</span>
</div>
</div>
</div>
<aside class="notes">
<div class="note-card">
<b>队列内联进对话流</b>
跟在当前 turn 后面,时间线一目了然。第一条标“下一条”,其余按 FIFO 编号。点气泡 = 载入输入框编辑;⋮⋮ 拖拽排序;× 删除hover / 聚焦时显示,键盘可达)。
</div>
<div class="note-card">
<b>发送 / 中断 解耦</b>
发送按钮永远是发送(运行中 = 入队),不会误触 Stop。Stop 独立成一个 32px 圆按钮hover 变红,和发送平级,急停依然一键可达。
</div>
<div class="note-card">
<b>出队可见</b>
点“▶ 出队一条”看效果:队头气泡上浮为正式用户气泡,队列整体前移,“下一条”标签自动易主。
</div>
</aside>
</div>
<div class="stage-label">对照 · 空闲态(没有队列、没有运行中的 turn</div>
<div class="phone" style="max-width:760px">
<div class="chat" style="max-height:none; padding-bottom:18px">
<div class="u-bub faint">这个项目用的是什么测试框架?</div>
<div class="a-msg"><p>用的是 <code>vitest</code>,纯逻辑测试,没有 jsdom / 组件测试。测试文件跟被测模块放在一起。</p></div>
</div>
<div class="dock">
<div class="composer">
<textarea class="composer-ta" rows="1" placeholder="给 Kimi 发消息,/ 调用命令,@ 引用文件…"></textarea>
<div class="composer-bar">
<div class="composer-left">
<button class="icon-btn" type="button"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg></button>
<span class="pill"><span style="width:7px;height:7px;border-radius:50%;background:var(--color-warning)"></span>yolo</span>
</div>
<div class="composer-right">
<span class="pill"><span class="pp-strong">kimi-k2</span><span class="pp-sub">· thinking</span>
<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span>
<!-- 空闲:没有 Stop只有 Send -->
<button class="send" type="button"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 13V3m0 0L4.5 6.5M8 3l3.5 3.5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
</div>
</div>
</div>
</div>
</div>
<div class="footer-note">
<h3>这套设计如何回应之前的几个问题</h3>
<ul>
<li><b>误触中断</b> → Stop 与 Send 物理分离Send 永不变成 Stop。</li>
<li><b>队列看不见</b> → 直接出现在对话流末尾,半透明 + 虚线边框表达“尚未发送”,不再需要点开 Dock 面板。</li>
<li><b>不能重排 / 图片不可见</b> → ⋮⋮ 拖拽排序;带图片的项显示缩略图(示例里的 <code>图片 ×2</code> 可换为真实缩略图)。</li>
<li><b>出队不可见</b> → 点“▶ 出队一条”看效果:队头气泡上浮为正式用户气泡,队列整体前移、“下一条”标签易主。</li>
<li><b>Steer 退居二级</b> → 保留 <code>Ctrl+S</code>,在输入框 hint 里提示,不抢主路径。</li>
</ul>
<p style="margin-top:10px">文件名:<code>apps/kimi-web/design/queue-composer-mockup.html</code> · 纯静态沙盒,未触碰任何应用代码。</p>
</div>
</div>
<script>
// ----- 演示数据 -----
const initialQueue = [
{ text: '过期时间设成 7 天,刷新令牌 30 天' },
{ text: '', img: 2 }, // 纯图片排队项
{ text: '另外把 refresh token 的 rotation 也加上' },
];
let queue = structuredClone(initialQueue);
const qItems = document.getElementById('qItems');
const qCount = document.getElementById('qCount');
const ta = document.getElementById('ta');
const sendBtn = document.getElementById('sendBtn');
const drainBtn = document.getElementById('drainBtn');
const resetBtn = document.getElementById('resetBtn');
const chat = document.getElementById('chat');
const icGrip = '<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><circle cx="5.5" cy="4" r="1.1" fill="currentColor"/><circle cx="10.5" cy="4" r="1.1" fill="currentColor"/><circle cx="5.5" cy="8" r="1.1" fill="currentColor"/><circle cx="10.5" cy="8" r="1.1" fill="currentColor"/><circle cx="5.5" cy="12" r="1.1" fill="currentColor"/><circle cx="10.5" cy="12" r="1.1" fill="currentColor"/></svg>';
const icClose = '<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>';
const icImg = '<svg width="13" height="13" viewBox="0 0 16 16" fill="none"><rect x="2" y="3" width="12" height="10" rx="1.5" stroke="currentColor" stroke-width="1.4"/><circle cx="5.5" cy="6.5" r="1" fill="currentColor"/><path d="m3 11 3-2.5L9 11" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/></svg>';
function renderQueue() {
qItems.innerHTML = '';
qCount.textContent = queue.length;
queue.forEach((item, i) => {
const b = document.createElement('button');
b.className = 'q-bubble';
b.type = 'button';
b.title = '点击载入输入框编辑';
const tag = i === 0
? '<span class="q-tag next">下一条</span>'
: `<span class="q-tag idx">#${i + 1}</span>`;
const body = item.img
? `<span class="q-img">${icImg}图片 ×${item.img}</span>`
: escapeHtml(item.text);
b.innerHTML = `
<span class="q-grip" title="拖拽排序">${icGrip}</span>
<span class="q-body">${body}</span>
${tag}
<span class="q-rm" role="button" tabindex="0" title="移除" aria-label="移除">${icClose}</span>`;
b.addEventListener('click', (e) => {
if (e.target.closest('.q-rm') || e.target.closest('.q-grip')) return;
editItem(i);
});
b.querySelector('.q-rm').addEventListener('click', (e) => { e.stopPropagation(); removeItem(i); });
qItems.appendChild(b);
});
}
function escapeHtml(s){ return s.replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); }
function enqueue(text, img) {
queue.push({ text, img });
renderQueue();
scrollChat();
}
function removeItem(i) {
const el = qItems.children[i];
if (el) { el.classList.add('leaving'); setTimeout(() => { queue.splice(i,1); renderQueue(); }, 160); }
else { queue.splice(i,1); renderQueue(); }
}
function editItem(i) {
const item = queue[i];
if (!item || item.img) return; // 图片项不可编辑
ta.value = item.text;
autoGrow();
removeItem(i);
ta.focus();
}
function drainOne() {
if (queue.length === 0) return;
const head = queue.shift();
renderQueue();
const sent = document.createElement('div');
sent.className = 'u-bub';
sent.style.animation = 'q-in .22s var(--ease-out)';
sent.textContent = head.img ? `(图片 ×${head.img}` : head.text;
chat.insertBefore(sent, document.getElementById('qStack'));
scrollChat();
}
function scrollChat(){ requestAnimationFrame(() => { chat.scrollTop = chat.scrollHeight; }); }
function autoGrow() {
ta.style.height = 'auto';
ta.style.height = Math.min(ta.scrollHeight, 160) + 'px';
}
// 输入框Enter 入队
ta.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) {
e.preventDefault();
const v = ta.value.trim();
if (!v) return;
enqueue(v);
ta.value = ''; autoGrow();
}
});
ta.addEventListener('input', autoGrow);
sendBtn.addEventListener('click', () => {
const v = ta.value.trim();
if (!v) return;
enqueue(v); ta.value = ''; autoGrow(); ta.focus();
});
drainBtn.addEventListener('click', drainOne);
resetBtn.addEventListener('click', () => {
queue = structuredClone(initialQueue);
[...chat.querySelectorAll('.u-bub:not(.faint)')].forEach(n => n.remove());
renderQueue();
});
// 深 / 浅切换
document.getElementById('themeToggle').addEventListener('click', () => {
const cur = document.documentElement.getAttribute('data-color-scheme');
document.documentElement.setAttribute('data-color-scheme', cur === 'dark' ? 'light' : 'dark');
});
renderQueue();
</script>
</body>
</html>