Merge remote-tracking branch 'origin/master' into xtr/agent-loop-message-machine

This commit is contained in:
_Kerman
2026-07-27 17:18:12 +08:00
144 changed files with 3233 additions and 827 deletions

View File

@@ -21,8 +21,9 @@
flex-direction: column;
align-items: center;
/* figma Input_Bottom: pad L32/R32/B12; the bottom gradient mask is owned by
the chat scroller. Top 8 hosts the error strip's breathing room. */
padding: 8px 32px 12px;
the chat scroller. Top 6 is the gap under the dock todo strip (12px todo
margin + 6px here); error/status strips still carry their own margin. */
padding: 6px 32px 12px;
}
.hero {

View File

@@ -1,55 +1,53 @@
/* Plan strip pinned above the composer: bordered card on the composer card's
axis (776px column inside 32px side padding). Colors resolve through
--dsw-alias-* tokens only; the active row rides the business blue, done
rows fade to tertiary. */
/* Todo strip above the composer (figma 772:51905 / 772:52972 / 772:53419):
tip surface, 14px radius, status icons + secondary item labels. Column is
calc(100% - 88px) / max 776, centered; InputBar top pad supplies the gap. */
.root {
flex: none;
overflow: hidden;
margin: 8px auto 0;
width: calc(100% - 64px);
margin: 0 auto;
width: calc(100% - 88px);
max-width: 776px;
border: 1px solid var(--dsw-alias-border-l2);
border-radius: 12px;
background: var(--dsw-alias-bg-base);
border: 1px solid var(--dsw-alias-border-l1);
border-radius: 14px;
background: var(--dsw-specific-tip);
}
.body {
display: flex;
flex-direction: column;
gap: 10px;
padding: 10px 16px;
}
.header {
display: flex;
align-items: center;
gap: 8px;
gap: 10px;
width: 100%;
padding: 8px 12px;
padding: 0;
border: none;
background: transparent;
text-align: left;
cursor: pointer;
}
.header:hover {
background: var(--dsw-alias-interactive-bg-hover);
}
.title {
font-size: 13px;
line-height: 16px;
font-weight: 510;
flex: none;
font-size: 14px;
line-height: 24px;
font-weight: 500;
color: var(--dsw-alias-label-primary);
}
.progress {
font-size: 12px;
line-height: 16px;
color: var(--dsw-alias-label-tertiary);
}
.activeHint {
flex: 1;
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
font-size: 12px;
line-height: 16px;
color: var(--dsw-alias-label-secondary);
font-size: 13px;
line-height: 20px;
font-weight: 400;
color: var(--dsw-alias-label-tertiary);
text-overflow: ellipsis;
white-space: nowrap;
}
@@ -58,13 +56,15 @@
display: grid;
flex: none;
place-items: center;
margin-left: auto;
color: var(--dsw-alias-label-secondary);
color: var(--dsw-alias-label-tertiary);
}
.list {
display: flex;
flex-direction: column;
gap: 8px;
margin: 0;
padding: 0 12px 8px;
padding: 0;
list-style: none;
max-height: 180px;
overflow-y: auto;
@@ -72,40 +72,45 @@
.item {
display: flex;
align-items: baseline;
gap: 8px;
padding: 2px 0;
align-items: center;
gap: 10px;
min-width: 0;
font-size: 13px;
line-height: 20px;
color: var(--dsw-alias-label-secondary);
}
.glyph {
display: grid;
flex: none;
width: 14px;
text-align: center;
color: var(--dsw-alias-label-tertiary);
place-items: center;
width: 16px;
height: 16px;
}
.item[data-status='completed'] .content {
color: var(--dsw-alias-label-tertiary);
text-decoration: line-through;
}
.item[data-status='completed'] .glyph {
.glyphCompleted {
color: var(--dsw-alias-state-success-primary);
}
.item[data-status='in_progress'] .content {
font-weight: 510;
color: var(--dsw-alias-label-primary);
}
.item[data-status='in_progress'] .glyph {
.glyphProgress {
color: var(--dsw-alias-state-business-primary);
animation: todo-progress-spin 1s linear infinite;
}
.glyphPending {
color: var(--dsw-alias-label-caption);
}
@keyframes todo-progress-spin {
to {
transform: rotate(360deg);
}
}
/* Figma strip is single-line; long items ellipsize with no inline expand. */
.content {
min-width: 0;
overflow-wrap: anywhere;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

View File

@@ -3,8 +3,9 @@
// no data of its own, hidden while the list is empty. Mounted through the
// 'conversation.input.dock' slot (QueueDock posture): the dock adapter does
// the selecting, so the panel takes the plain list and stays framework-free.
// Visual: figma 772:51905 (states) / 772:52972 (collapsed) / 772:53419 (expanded).
import { useState } from 'react'
import { useId, useState } from 'react'
import type { Context } from 'cordis'
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
import type { TodoItem } from '@deepseek-ai/dsh-client-runtime/client'
@@ -16,45 +17,97 @@ export interface TodoPanelProps {
todos: readonly TodoItem[]
}
/** Status glyphs mirror the TUI plan panel (✓ done / ● active / ○ pending). */
const STATUS_GLYPHS: Record<TodoItem['status'], string> = {
completed: '✓', in_progress: '●', pending: '○',
/** Local exhaustiveness helper — client packages do not depend on `dsh-llm`. */
/* v8 ignore next 3 -- closed-union backstop; only reached if status is forged */
function assertNever(value: never): never {
throw new Error(`unreachable todo status: ${String(value)}`)
}
/** Status glyphs share the figma 14×14 artboard; the 16×16 `.glyph` cell centers them. */
function CompletedGlyph() {
return (
<svg width={14} height={14} viewBox="0 0 14 14" fill="none" aria-hidden="true" className={css.glyphCompleted}>
<circle cx="7" cy="7" r="6.4" stroke="currentColor" strokeWidth="1.2" />
<path
d="M10.9631 5.71411L7.70154 8.97571C7.48011 9.19714 7.27736 9.40099 7.09229 9.54993C6.89742 9.70669 6.66314 9.85279 6.3634 9.90027C6.2049 9.92534 6.04339 9.92534 5.88489 9.90027C5.58515 9.85279 5.35087 9.70669 5.15601 9.54993C4.97093 9.40099 4.76818 9.19714 4.54675 8.97571L3.03516 7.46411L3.96313 6.53613L5.47473 8.04773C5.7169 8.28989 5.86196 8.43389 5.97888 8.52795C6.08597 8.61409 6.10875 8.60701 6.08997 8.604C6.11259 8.60758 6.13571 8.60758 6.15833 8.604C6.13954 8.60701 6.16232 8.61409 6.26941 8.52795C6.38633 8.43389 6.53139 8.28989 6.77356 8.04773L10.0352 4.78613L10.9631 5.71411Z"
fill="currentColor"
/>
</svg>
)
}
/** In-progress: business-blue ring fading out; CSS spins the svg. */
function ProgressGlyph() {
const gradientId = useId()
return (
<svg width={14} height={14} viewBox="0 0 14 14" fill="none" aria-hidden="true" className={css.glyphProgress}>
<defs>
<linearGradient id={gradientId} x1="2.5" y1="12" x2="10.5" y2="3.5" gradientUnits="userSpaceOnUse">
<stop stopColor="currentColor" />
<stop offset="1" stopColor="currentColor" stopOpacity="0" />
</linearGradient>
</defs>
<circle cx="7" cy="7" r="6.4" stroke={`url(#${gradientId})`} strokeWidth="1.2" />
</svg>
)
}
/** Pending: dashed unstarted ring (figma dash 2.4 2.4). */
function PendingGlyph() {
return (
<svg width={14} height={14} viewBox="0 0 14 14" fill="none" aria-hidden="true" className={css.glyphPending}>
<circle cx="7" cy="7" r="6.4" stroke="currentColor" strokeWidth="1.2" strokeDasharray="2.4 2.4" />
</svg>
)
}
function StatusGlyph({ status }: { status: TodoItem['status'] }) {
switch (status) {
case 'completed': return <CompletedGlyph />
case 'in_progress': return <ProgressGlyph />
case 'pending': return <PendingGlyph />
/* v8 ignore next -- closed TodoItem status union */
default: return assertNever(status)
}
}
/** Header summary: "<done>/<total> tasks · <n> in progress". */
function progressLabel(todos: readonly TodoItem[]): string {
const done = todos.filter(t => t.status === 'completed').length
const active = todos.filter(t => t.status === 'in_progress').length
return `${done}/${todos.length} tasks · ${active} in progress`
}
export function TodoPanel({ todos }: TodoPanelProps) {
const [collapsed, setCollapsed] = useState(false)
if (todos.length === 0) return null
const done = todos.filter(t => t.status === 'completed').length
const active = todos.find(t => t.status === 'in_progress')
return (
<section className={css.root} data-testid="todo-panel" aria-label="任务清单">
<button
type="button"
className={css.header}
aria-expanded={!collapsed}
onClick={() => { setCollapsed(v => !v) }}
>
<span className={css.title}>Plan</span>
<span className={css.progress}>{done}/{todos.length}</span>
{collapsed && active !== undefined && (
<span className={css.activeHint}>{active.content}</span>
<section className={css.root} data-testid="todo-panel" aria-label="To-dos">
<div className={css.body}>
<button
type="button"
className={css.header}
aria-expanded={!collapsed}
onClick={() => { setCollapsed(v => !v) }}
>
<span className={css.title}>To-dos</span>
<span className={css.progress}>{progressLabel(todos)}</span>
<span className={css.chevron} aria-hidden>
{collapsed ? <IconChevronUpOutline14 /> : <IconChevronDownOutline14 />}
</span>
</button>
{!collapsed && (
<ul className={css.list}>
{todos.map(item => (
<li key={item.content} className={css.item} data-status={item.status}>
<span className={css.glyph} aria-hidden><StatusGlyph status={item.status} /></span>
<span className={css.content}>{item.content}</span>
</li>
))}
</ul>
)}
<span className={css.chevron} aria-hidden>
{collapsed ? <IconChevronUpOutline14 /> : <IconChevronDownOutline14 />}
</span>
</button>
{!collapsed && (
<ul className={css.list}>
{todos.map(item => (
<li key={item.content} className={css.item} data-status={item.status}>
<span className={css.glyph} aria-hidden>{STATUS_GLYPHS[item.status]}</span>
<span className={css.content}>{item.content}</span>
</li>
))}
</ul>
)}
</div>
</section>
)
}