fix(B01): retain native card state and restore mode and confirmation styles
This commit is contained in:
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(
|
||||
|
||||
Reference in new issue
Block a user