fix(B01): retain native card state and restore mode and confirmation styles

This commit is contained in:
Codex committed 2026-10-21 13:41:18 +09:00
1 parent ddd677bae3
commit 1ed71b63b5
14 files changed
+407 -37

No files matched your search

@@ -1,4 +1,5 @@
import { useEffect, useId, useRef, useState } from 'react'
import { Button } from '../ui/Button'
import { Popover } from '../ui/Popover'
import './task-popconfirm.css'
@@ -49,12 +50,11 @@ export function TaskPopconfirm({ kind, running, anchor, onConfirm, onClose }: Ta
<div aria-busy={busy} data-task-confirm-kind={kind} data-task-confirm-running={running || undefined}>
<h2 className="task-popconfirm__title" id={titleId}>{copy.title}</h2>
<p className="task-popconfirm__description" id={descriptionId}>{copy.description}</p>
{kind === 'archive' && <p className="task-popconfirm__compatibility">YCode 暂无此设置入口,请从侧栏“已归档”查看和恢复。</p>}
{error && <div className="task-popconfirm__error" id={errorId} role="alert">{error}</div>}
<div className="task-popconfirm__actions">
<button type="button" className="wb-button wb-button--secondary wb-button--small task-popconfirm__cancel" disabled={busy} onClick={close}>取消</button>
<button type="button" className={`wb-button wb-button--primary wb-button--small task-popconfirm__confirm${kind === 'delete' ? ' wb-button--danger' : ''}`}
disabled={busy} onClick={() => void confirm()}>{busy ? '处理中…' : copy.confirm}</button>
<Button variant="secondary" size="sm" className="task-popconfirm__cancel" disabled={busy} onClick={close}>取消</Button>
<Button variant={kind === 'delete' ? 'danger' : 'primary'} size="sm" className="task-popconfirm__confirm"
disabled={busy} onClick={() => void confirm()}>{busy ? '处理中…' : copy.confirm}</Button>
</div>
</div>
</Popover>
@@ -4,7 +4,7 @@
width: calc(var(--yc-space-10) * 5);
max-width: calc(100vw - var(--yc-space-5));
padding: var(--yc-space-5);
border-radius: var(--yc-radius-xl);
border-radius: var(--yc-radius-2xl);
overflow: visible;
color: var(--wb-color-text-primary);
background: var(--wb-bg-popover);
@@ -14,17 +14,20 @@
margin: 0 0 var(--yc-space-3);
font-size: var(--yc-font-size-5);
line-height: var(--yc-line-height-5-2);
font-weight: 500;
font-weight: var(--yc-font-weight-600);
}
.task-popconfirm__description,
.task-popconfirm__compatibility {
.task-popconfirm__description {
margin: 0;
color: var(--wb-color-text-secondary);
font-size: var(--yc-font-size-4);
line-height: var(--yc-line-height-5);
overflow-wrap: anywhere;
}
.task-popconfirm__compatibility { margin-top: var(--yc-space-3); color: var(--wb-color-text-tertiary); }
.task-popconfirm__error { margin-top: var(--yc-space-3); color: var(--wb-color-text-error-default); font-size: var(--yc-font-size-4); line-height: var(--yc-line-height-5); }
.task-popconfirm__actions { display: flex; justify-content: flex-end; gap: var(--yc-space-3); margin-top: var(--yc-space-5); }
.task-popconfirm-source .task-popconfirm__actions .wb-button { min-width: calc(var(--yc-space-10) + var(--yc-space-2)); height: var(--yc-line-height-7); padding: 0 var(--yc-space-3); }
.task-popconfirm-source .task-popconfirm__actions .yc-button {
/* Recorded minimum width and inset, composed from existing tokens. */
min-width: calc(var(--yc-size-17) - var(--yc-border-width) * 2);
height: var(--yc-line-height-7);
padding: 0 calc(var(--yc-space-6) - var(--yc-border-width));
}
@@ -5,10 +5,14 @@ import './mode-chip.css'
export function ModeChip({ mode, disabled, onClear }: { mode: AgentMode; disabled?: boolean; onClear(): void | Promise<void> }): React.JSX.Element | null {
if (mode === 'craft') return null
const label = mode === 'plan' ? '计划' : '仅问答'
return <span className="yc-mode-chip" role="group" tabIndex={0} aria-label={`${label}模式`}>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.2" aria-hidden="true">
{mode === 'plan' ? <><rect x="3" y="3" width="10" height="11" rx="1.5" /><rect x="5.5" y="1.5" width="5" height="3" rx="1" /><path d="M5.5 7h5M5.5 10h5" /></> : <path d="M13.5 7.5a5.5 5.5 0 0 1-5.5 5.5H3l-1.5 1v-6.5A5.5 5.5 0 0 1 7 2h1a5.5 5.5 0 0 1 5.5 5.5Z" />}
</svg><span>{label}</span>
{!disabled && <button type="button" className="yc-mode-chip__clear" aria-label={`退出${label}模式`} title="切换到工作模式" onClick={() => void onClear()}>×</button>}
return <span className="yc-mode-chip" role="group" tabIndex={disabled ? -1 : 0} aria-disabled={disabled || undefined} aria-label={`${label}模式`}>
<span className="yc-mode-chip__icon-slot">
<svg className="yc-mode-chip__mode-icon" width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.2" aria-hidden="true">
{mode === 'plan' ? <><rect x="3" y="3" width="10" height="11" rx="1.5" /><rect x="5.5" y="1.5" width="5" height="3" rx="1" /><path d="M5.5 7h5M5.5 10h5" /></> : <path d="M13.5 7.5a5.5 5.5 0 0 1-5.5 5.5H3l-1.5 1v-6.5A5.5 5.5 0 0 1 7 2h1a5.5 5.5 0 0 1 5.5 5.5Z" />}
</svg>
{!disabled && <button type="button" className="yc-mode-chip__clear" aria-label={`退出${label}模式`} title="切换到工作模式" onClick={() => void onClear()}>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.2" aria-hidden="true"><path d="m4 4 8 8M12 4l-8 8" /></svg>
</button>}
</span><span>{label}</span>
</span>
}
@@ -1,6 +1,9 @@
.yc-mode-chip { position:relative; display:inline-flex; flex-shrink:0; align-items:center; gap:var(--yc-space-2); height:var(--yc-space-8); padding:0 var(--yc-space-3); border-radius:var(--yc-space-3); background:var(--cr-bg-tertiary); color:var(--cr-text-primary); font-size:var(--yc-font-size-4); line-height:var(--yc-line-height-5); font-weight:500; }
.yc-mode-chip > svg { flex-shrink:0; width:var(--yc-space-5); height:var(--yc-space-5); }
.yc-mode-chip { display:inline-flex; flex-shrink:0; align-items:center; gap:var(--yc-space-2); height:var(--yc-space-8); padding:0 var(--yc-space-3); border-radius:var(--yc-radius-full); background:transparent; color:var(--cr-text-secondary); font-size:var(--yc-font-size-4); line-height:var(--yc-line-height-5); font-weight:500; }
.yc-mode-chip__icon-slot { display:grid; flex-shrink:0; width:var(--yc-space-5); height:var(--yc-space-5); }
.yc-mode-chip__mode-icon, .yc-mode-chip__clear { grid-area:1 / 1; width:var(--yc-space-5); height:var(--yc-space-5); }
.yc-mode-chip__clear > svg { width:100%; height:100%; }
.yc-mode-chip:focus-visible { outline:var(--yc-space-1) solid var(--cr-text-secondary); outline-offset:var(--yc-space-1); }
.yc-mode-chip__clear { display:inline-flex; align-items:center; justify-content:center; width:var(--yc-space-5); height:var(--yc-space-5); padding:0; border:0; border-radius:50%; background:var(--cr-bg-tertiary); color:inherit; font-size:var(--yc-space-5); cursor:pointer; opacity:0; position:absolute; right:calc(-1 * var(--wb-spacing-2-5)); top:calc(-1.5 * var(--yc-space-1)); }
.yc-mode-chip:hover .yc-mode-chip__clear, .yc-mode-chip:focus-within .yc-mode-chip__clear { opacity:1; }
.yc-mode-chip__clear { display:inline-flex; align-items:center; justify-content:center; padding:0; border:0; border-radius:var(--yc-radius-full); background:transparent; color:inherit; cursor:pointer; opacity:0; }
.yc-mode-chip:not([aria-disabled="true"]):hover .yc-mode-chip__mode-icon, .yc-mode-chip:not([aria-disabled="true"]):focus-within .yc-mode-chip__mode-icon { opacity:0; }
.yc-mode-chip:not([aria-disabled="true"]):hover .yc-mode-chip__clear, .yc-mode-chip:not([aria-disabled="true"]):focus-within .yc-mode-chip__clear { opacity:1; }
.yc-mode-chip__clear:focus-visible { outline:var(--yc-space-1) solid var(--cr-text-secondary); }
@@ -4,6 +4,7 @@ import { SourceFoldChevronIcon } from '../message-actions/SourceActionIcons'
import {
buildCompletedFoldModel,
classifyCompletedFoldPart,
completedFoldPartIdentity,
type CompletedFoldOptions,
type CompletedFoldPart,
type CompletedFoldSegment,
@@ -26,7 +27,7 @@ function renderPart(
summaryGroup?: CompletedFoldSummaryGroup
): ReactNode {
const classification = unit.classification ?? classifyCompletedFoldPart(unit.part, unit.index)
return <DisclosureStateScope key={unit.key} id={partDisclosureId(unit.part, unit.index)}>{render(unit.part, unit.index, {
return <DisclosureStateScope key={unit.key} id={completedFoldPartIdentity(unit.part, unit.index)}>{render(unit.part, unit.index, {
kind: summaryGroup ? 'summary-member' : 'part',
classification,
presentationGroupId: classification.presentationGroupId,
@@ -42,7 +43,7 @@ function renderPresentationUnit(
if (unit.kind === 'part') return renderPart(unit.unit, render)
const children = unit.group.units.map(member => renderPart(member, render, unit.group))
return (
<DisclosureStateScope key={unit.group.key} id={`summary:${partDisclosureId(unit.group.units[0].part, unit.group.units[0].index)}`}>
<DisclosureStateScope key={unit.group.key} id={`summary:${completedFoldPartIdentity(unit.group.units[0].part, unit.group.units[0].index)}`}>
{renderGroup ? renderGroup(unit.group, children) : <>{children}</>}
</DisclosureStateScope>
)
@@ -146,10 +147,6 @@ export function LiveEventDisclosure({
)
}
function partDisclosureId(part: CompletedFoldPart, index: number): string {
const id = 'id' in part && typeof part.id === 'string' ? part.id : part.type === 'tool' && typeof part.toolId === 'string' ? part.toolId : index
return JSON.stringify([part.type, id])
}
export function CompletedFold(props: CompletedFoldProps) {
return <DisclosureStateProvider key={props.foldId}><CompletedFoldContent {...props} /></DisclosureStateProvider>
@@ -183,7 +180,7 @@ function CompletedFoldContent({
if (!model) return <>{parts.map((part, index) => {
const classification = classifyCompletedFoldPart(part, index, classifyToolCall)
if (!classification.hasRootBlock) return null
return <DisclosureStateScope key={`${part.type}:${index}`} id={partDisclosureId(part, index)}>{render(part, index, {
return <DisclosureStateScope key={completedFoldPartIdentity(part, index)} id={completedFoldPartIdentity(part, index)}>{render(part, index, {
kind: 'part',
classification,
presentationGroupId: classification.presentationGroupId
@@ -208,7 +205,7 @@ function CompletedFoldContent({
) : parts.map((part, index) => {
const classification = classifyCompletedFoldPart(part, index, classifyToolCall)
if (!classification.hasRootBlock) return null
return <DisclosureStateScope key={`${part.type}:${index}`} id={partDisclosureId(part, index)}>{render(part, index, { kind: 'part', classification })}</DisclosureStateScope>
return <DisclosureStateScope key={completedFoldPartIdentity(part, index)} id={completedFoldPartIdentity(part, index)}>{render(part, index, { kind: 'part', classification })}</DisclosureStateScope>
})
})()}
</CollapseSection>
@@ -241,13 +241,24 @@ export function formatCompletedFoldDuration(durationMs: number): string {
return `${seconds}s`
}
/** Business identity survives terminal text coalescing and presentation reordering.
* Old histories without an ID intentionally retain positional fallback semantics.
*/
export function completedFoldPartIdentity(part: CompletedFoldPart, index: number): string {
let id: unknown = 'id' in part ? part.id : undefined
if (part.type === 'tool') id = part.toolId
let content: unknown
if (part.type === 'plan') content = part.plan
if (part.type === 'task') content = part.task
if (part.type === 'artifact') content = part.artifact
if (content && typeof content === 'object' && 'id' in content) id = content.id
return typeof id === 'string' && id.length > 0
? JSON.stringify([part.type, 'id', id])
: JSON.stringify([part.type, 'index', index])
}
function makeUnit(part: CompletedFoldPart, index: number): CompletedFoldUnit {
const partId = 'id' in part && typeof part.id === 'string'
? part.id
: part.type === 'tool' && typeof part.toolId === 'string'
? part.toolId
: String(index)
return { key: `${part.type}:${partId}:${index}`, index, part }
return { key: completedFoldPartIdentity(part, index), index, part }
}
function buildSourceSegments(