feat(app): session timeline/turn rework (#13196)

Co-authored-by: David Hill <iamdavidhill@gmail.com>
This commit is contained in:
Adam
2026-02-17 07:16:23 -06:00
committed by GitHub
parent 3dfbb70593
commit 10985671ad
85 changed files with 3158 additions and 2477 deletions

View File

@@ -4,15 +4,44 @@
display: flex;
align-items: center;
align-self: stretch;
gap: 20px;
justify-content: space-between;
gap: 0px;
justify-content: flex-start;
[data-slot="basic-tool-tool-trigger-content"] {
width: 100%;
width: auto;
display: flex;
align-items: center;
align-self: stretch;
gap: 20px;
gap: 8px;
}
[data-slot="basic-tool-tool-indicator"] {
width: 16px;
height: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
[data-component="spinner"] {
width: 16px;
height: 16px;
}
}
[data-slot="basic-tool-tool-spinner"] {
width: 16px;
height: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: var(--text-weak);
[data-component="spinner"] {
width: 16px;
height: 16px;
}
}
[data-slot="icon-svg"] {
@@ -20,16 +49,17 @@
}
[data-slot="basic-tool-tool-info"] {
flex-grow: 1;
flex: 0 1 auto;
min-width: 0;
font-size: 14px;
}
[data-slot="basic-tool-tool-info-structured"] {
width: 100%;
width: auto;
display: flex;
align-items: center;
gap: 8px;
justify-content: space-between;
justify-content: flex-start;
}
[data-slot="basic-tool-tool-info-main"] {
@@ -43,16 +73,21 @@
[data-slot="basic-tool-tool-title"] {
flex-shrink: 0;
font-family: var(--font-family-sans);
font-size: var(--font-size-small);
font-size: 14px;
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: var(--line-height-large);
letter-spacing: var(--letter-spacing-normal);
color: var(--text-base);
color: var(--text-strong);
&.capitalize {
text-transform: capitalize;
}
&.agent-title {
color: var(--text-strong);
font-weight: var(--font-weight-medium);
}
}
[data-slot="basic-tool-tool-subtitle"] {
@@ -62,12 +97,12 @@
text-overflow: ellipsis;
white-space: nowrap;
font-family: var(--font-family-sans);
font-size: var(--font-size-small);
font-size: 14px;
font-style: normal;
font-weight: var(--font-weight-medium);
font-weight: var(--font-weight-regular);
line-height: var(--line-height-large);
letter-spacing: var(--letter-spacing-normal);
color: var(--text-weak);
color: var(--text-base);
&.clickable {
cursor: pointer;
@@ -78,6 +113,26 @@
color: var(--text-base);
}
}
&.subagent-link {
color: var(--text-interactive-base);
text-decoration: none;
text-underline-offset: 2px;
font-weight: var(--font-weight-regular);
&:hover {
color: var(--text-interactive-base);
text-decoration: underline;
}
&:active {
color: var(--text-interactive-base);
}
&:visited {
color: var(--text-interactive-base);
}
}
}
[data-slot="basic-tool-tool-arg"] {
@@ -87,11 +142,11 @@
text-overflow: ellipsis;
white-space: nowrap;
font-family: var(--font-family-sans);
font-size: var(--font-size-small);
font-size: 14px;
font-style: normal;
font-weight: var(--font-weight-regular);
line-height: var(--line-height-large);
letter-spacing: var(--letter-spacing-normal);
color: var(--text-weak);
color: var(--text-base);
}
}

View File

@@ -1,6 +1,7 @@
import { createEffect, createSignal, For, Match, Show, Switch, type JSX } from "solid-js"
import { Collapsible } from "./collapsible"
import { Icon, IconProps } from "./icon"
import type { IconProps } from "./icon"
import { TextShimmer } from "./text-shimmer"
export type TriggerTitle = {
title: string
@@ -22,6 +23,7 @@ export interface BasicToolProps {
icon: IconProps["name"]
trigger: TriggerTitle | JSX.Element
children?: JSX.Element
status?: string
hideDetails?: boolean
defaultOpen?: boolean
forceOpen?: boolean
@@ -31,22 +33,23 @@ export interface BasicToolProps {
export function BasicTool(props: BasicToolProps) {
const [open, setOpen] = createSignal(props.defaultOpen ?? false)
const pending = () => props.status === "pending" || props.status === "running"
createEffect(() => {
if (props.forceOpen) setOpen(true)
})
const handleOpenChange = (value: boolean) => {
if (pending()) return
if (props.locked && !value) return
setOpen(value)
}
return (
<Collapsible open={open()} onOpenChange={handleOpenChange}>
<Collapsible open={open()} onOpenChange={handleOpenChange} class="tool-collapsible">
<Collapsible.Trigger>
<div data-component="tool-trigger">
<div data-slot="basic-tool-tool-trigger-content">
<Icon name={props.icon} size="small" />
<div data-slot="basic-tool-tool-info">
<Switch>
<Match when={isTriggerTitle(props.trigger) && props.trigger}>
@@ -59,41 +62,45 @@ export function BasicTool(props: BasicToolProps) {
[trigger().titleClass ?? ""]: !!trigger().titleClass,
}}
>
{trigger().title}
<Show when={pending()} fallback={trigger().title}>
<TextShimmer text={trigger().title} />
</Show>
</span>
<Show when={trigger().subtitle}>
<span
data-slot="basic-tool-tool-subtitle"
classList={{
[trigger().subtitleClass ?? ""]: !!trigger().subtitleClass,
clickable: !!props.onSubtitleClick,
}}
onClick={(e) => {
if (props.onSubtitleClick) {
e.stopPropagation()
props.onSubtitleClick()
}
}}
>
{trigger().subtitle}
</span>
</Show>
<Show when={trigger().args?.length}>
<For each={trigger().args}>
{(arg) => (
<span
data-slot="basic-tool-tool-arg"
classList={{
[trigger().argsClass ?? ""]: !!trigger().argsClass,
}}
>
{arg}
</span>
)}
</For>
<Show when={!pending()}>
<Show when={trigger().subtitle}>
<span
data-slot="basic-tool-tool-subtitle"
classList={{
[trigger().subtitleClass ?? ""]: !!trigger().subtitleClass,
clickable: !!props.onSubtitleClick,
}}
onClick={(e) => {
if (props.onSubtitleClick) {
e.stopPropagation()
props.onSubtitleClick()
}
}}
>
{trigger().subtitle}
</span>
</Show>
<Show when={trigger().args?.length}>
<For each={trigger().args}>
{(arg) => (
<span
data-slot="basic-tool-tool-arg"
classList={{
[trigger().argsClass ?? ""]: !!trigger().argsClass,
}}
>
{arg}
</span>
)}
</For>
</Show>
</Show>
</div>
<Show when={trigger().action}>{trigger().action}</Show>
<Show when={!pending() && trigger().action}>{trigger().action}</Show>
</div>
)}
</Match>
@@ -101,7 +108,7 @@ export function BasicTool(props: BasicToolProps) {
</Switch>
</div>
</div>
<Show when={props.children && !props.hideDetails && !props.locked}>
<Show when={props.children && !props.hideDetails && !props.locked && !pending()}>
<Collapsible.Arrow />
</Show>
</div>
@@ -113,6 +120,6 @@ export function BasicTool(props: BasicToolProps) {
)
}
export function GenericTool(props: { tool: string; hideDetails?: boolean }) {
return <BasicTool icon="mcp" trigger={{ title: props.tool }} hideDetails={props.hideDetails} />
export function GenericTool(props: { tool: string; status?: string; hideDetails?: boolean }) {
return <BasicTool icon="mcp" status={props.status} trigger={{ title: props.tool }} hideDetails={props.hideDetails} />
}

View File

@@ -170,3 +170,15 @@
outline: none;
}
}
[data-component="button"].titlebar-icon[data-variant="ghost"]:hover:not(:disabled) {
background-color: var(--surface-raised-base-active);
}
[data-component="button"].titlebar-icon[data-variant="ghost"][aria-expanded="true"] {
background-color: var(--surface-base-active);
}
[data-component="button"].titlebar-icon[data-variant="ghost"][aria-expanded="true"]:hover:not(:disabled) {
background-color: var(--surface-base-active);
}

View File

@@ -1,6 +1,6 @@
[data-component="checkbox"] {
display: flex;
align-items: center;
align-items: var(--checkbox-align, center);
gap: 12px;
cursor: default;
@@ -23,6 +23,7 @@
width: 16px;
height: 16px;
padding: 2px;
margin-top: var(--checkbox-offset, 0px);
aspect-ratio: 1;
flex-shrink: 0;
border-radius: var(--radius-sm);

View File

@@ -2,23 +2,44 @@
width: 100%;
display: flex;
flex-direction: column;
background-color: var(--surface-inset-base);
border: 1px solid var(--border-weaker-base);
background-color: transparent;
border: none;
transition: background-color 0.15s ease;
border-radius: var(--radius-md);
overflow: clip;
&.tool-collapsible {
gap: 8px;
}
[data-slot="collapsible-trigger"] {
width: 100%;
display: flex;
height: 32px;
padding: 6px 8px 6px 12px;
padding: 0;
align-items: center;
align-self: stretch;
cursor: default;
user-select: none;
color: var(--text-base);
[data-slot="collapsible-arrow"] {
opacity: 0;
transition: opacity 0.15s ease;
}
[data-slot="collapsible-arrow-icon"] {
display: none;
}
[data-slot="collapsible-arrow-icon"][data-direction="right"] {
display: inline-flex;
}
&:hover [data-slot="collapsible-arrow"] {
opacity: 1;
}
/* text-12-medium */
font-family: var(--font-family-sans);
font-size: var(--font-size-small);
@@ -48,6 +69,20 @@
}
}
[data-slot="collapsible-trigger"][aria-expanded="true"] {
[data-slot="collapsible-arrow"] {
opacity: 1;
}
[data-slot="collapsible-arrow-icon"][data-direction="right"] {
display: none;
}
[data-slot="collapsible-arrow-icon"][data-direction="down"] {
display: inline-flex;
}
}
[data-slot="collapsible-content"] {
overflow: hidden;
/* animation: slideUp 250ms ease-out; */

View File

@@ -34,7 +34,12 @@ function CollapsibleContent(props: ComponentProps<typeof Kobalte.Content>) {
function CollapsibleArrow(props?: ComponentProps<"div">) {
return (
<div data-slot="collapsible-arrow" {...(props || {})}>
<Icon name="chevron-grabber-vertical" size="small" />
<span data-slot="collapsible-arrow-icon" data-direction="right">
<Icon name="chevron-right" size="small" />
</span>
<span data-slot="collapsible-arrow-icon" data-direction="down">
<Icon name="chevron-down" size="small" />
</span>
</div>
)
}

View File

@@ -7,7 +7,7 @@
[data-slot="diff-changes-additions"] {
font-family: var(--font-family-mono);
font-feature-settings: var(--font-family-mono--font-feature-settings);
font-size: var(--font-size-small);
font-size: 14px;
font-style: normal;
font-weight: var(--font-weight-regular);
line-height: var(--line-height-large);
@@ -19,7 +19,7 @@
[data-slot="diff-changes-deletions"] {
font-family: var(--font-family-mono);
font-feature-settings: var(--font-family-mono--font-feature-settings);
font-size: var(--font-size-small);
font-size: 14px;
font-style: normal;
font-weight: var(--font-weight-regular);
line-height: var(--line-height-large);
@@ -31,11 +31,12 @@
[data-component="diff-changes"][data-variant="bars"] {
width: 18px;
height: 14px;
flex-shrink: 0;
svg {
display: block;
width: 100%;
height: auto;
height: 100%;
}
}

View File

@@ -96,10 +96,10 @@ export function DiffChanges(props: {
<div data-component="diff-changes" data-variant={variant()} classList={{ [props.class ?? ""]: true }}>
<Switch>
<Match when={variant() === "bars"}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 12" fill="none">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 14" fill="none">
<g>
<For each={visibleBlocks()}>
{(color, i) => <rect x={i() * 4} width="2" height="12" rx="1" fill={color} />}
{(color, i) => <rect x={i() * 4} width="2" height="14" rx="1" fill={color} />}
</For>
</g>
</svg>

View File

@@ -143,3 +143,39 @@
outline: none;
}
}
@media (prefers-reduced-motion: no-preference) {
[data-component="icon-button"][data-icon="stop"] [data-slot="icon-svg"] rect {
transform-origin: center;
transform-box: fill-box;
animation: stop-pulse 1.8s ease-in-out infinite;
}
}
@keyframes stop-pulse {
0%,
100% {
transform: scale(0.95);
}
50% {
transform: scale(1.12);
}
}
[data-component="icon-button"].titlebar-icon {
width: 32px;
height: 24px;
aspect-ratio: auto;
}
[data-component="icon-button"].titlebar-icon[data-variant="ghost"]:hover:not(:disabled) {
background-color: var(--surface-raised-base-active);
}
[data-component="icon-button"].titlebar-icon[data-variant="ghost"][aria-expanded="true"] {
background-color: var(--surface-base-active);
}
[data-component="icon-button"].titlebar-icon[data-variant="ghost"][aria-expanded="true"]:hover:not(:disabled) {
background-color: var(--surface-base-active);
}

View File

@@ -15,6 +15,7 @@ export function IconButton(props: ComponentProps<"button"> & IconButtonProps) {
<Kobalte
{...rest}
data-component="icon-button"
data-icon={props.icon}
data-size={split.size || "normal"}
data-variant={split.variant || "secondary"}
classList={{

View File

@@ -7,11 +7,13 @@ const icons = {
"arrow-right": `<path d="M11.6654 4.58398L17.082 10.0007L11.6654 15.4173M16.6654 10.0007H2.91536" stroke="currentColor" stroke-linecap="square"/>`,
archive: `<path d="M16.8747 6.24935H17.3747V5.74935H16.8747V6.24935ZM16.8747 16.8743V17.3743H17.3747V16.8743H16.8747ZM3.12467 16.8743H2.62467V17.3743H3.12467V16.8743ZM3.12467 6.24935V5.74935H2.62467V6.24935H3.12467ZM2.08301 2.91602V2.41602H1.58301V2.91602H2.08301ZM17.9163 2.91602H18.4163V2.41602H17.9163V2.91602ZM17.9163 6.24935V6.74935H18.4163V6.24935H17.9163ZM2.08301 6.24935H1.58301V6.74935H2.08301V6.24935ZM8.33301 9.08268H7.83301V10.0827H8.33301V9.58268V9.08268ZM11.6663 10.0827H12.1663V9.08268H11.6663V9.58268V10.0827ZM16.8747 6.24935H16.3747V16.8743H16.8747H17.3747V6.24935H16.8747ZM16.8747 16.8743V16.3743H3.12467V16.8743V17.3743H16.8747V16.8743ZM3.12467 16.8743H3.62467V6.24935H3.12467H2.62467V16.8743H3.12467ZM3.12467 6.24935V6.74935H16.8747V6.24935V5.74935H3.12467V6.24935ZM2.08301 2.91602V3.41602H17.9163V2.91602V2.41602H2.08301V2.91602ZM17.9163 2.91602H17.4163V6.24935H17.9163H18.4163V2.91602H17.9163ZM17.9163 6.24935V5.74935H2.08301V6.24935V6.74935H17.9163V6.24935ZM2.08301 6.24935H2.58301V2.91602H2.08301H1.58301V6.24935H2.08301ZM8.33301 9.58268V10.0827H11.6663V9.58268V9.08268H8.33301V9.58268Z" fill="currentColor"/>`,
"bubble-5": `<path d="M18.3327 9.99935C18.3327 5.57227 15.0919 2.91602 9.99935 2.91602C4.90676 2.91602 1.66602 5.57227 1.66602 9.99935C1.66602 11.1487 2.45505 13.1006 2.57637 13.3939C2.58707 13.4197 2.59766 13.4434 2.60729 13.4697C2.69121 13.6987 3.04209 14.9354 1.66602 16.7674C3.51787 17.6528 5.48453 16.1973 5.48453 16.1973C6.84518 16.9193 8.46417 17.0827 9.99935 17.0827C15.0919 17.0827 18.3327 14.4264 18.3327 9.99935Z" stroke="currentColor" stroke-linecap="square"/>`,
prompt: `<path d="M14.5841 12.0807H17.9193V2.91406H5.6276V6.2474M14.5859 6.2474H2.08594V15.4141H5.0026V17.4974L8.7526 15.4141H14.5859V6.2474Z" stroke="currentColor" stroke-linecap="square"/>`,
brain: `<path d="M13.332 8.7487C11.4911 8.7487 9.9987 7.25631 9.9987 5.41536M6.66536 11.2487C8.50631 11.2487 9.9987 12.7411 9.9987 14.582M9.9987 2.78209L9.9987 17.0658M16.004 15.0475C17.1255 14.5876 17.9154 13.4849 17.9154 12.1978C17.9154 11.3363 17.5615 10.5575 16.9913 9.9987C17.5615 9.43991 17.9154 8.66108 17.9154 7.79962C17.9154 6.21199 16.7136 4.90504 15.1702 4.73878C14.7858 3.21216 13.4039 2.08203 11.758 2.08203C11.1171 2.08203 10.5162 2.25337 9.9987 2.55275C9.48117 2.25337 8.88032 2.08203 8.23944 2.08203C6.59353 2.08203 5.21157 3.21216 4.82722 4.73878C3.28377 4.90504 2.08203 6.21199 2.08203 7.79962C2.08203 8.66108 2.43585 9.43991 3.00609 9.9987C2.43585 10.5575 2.08203 11.3363 2.08203 12.1978C2.08203 13.4849 2.87191 14.5876 3.99339 15.0475C4.46688 16.7033 5.9917 17.9154 7.79962 17.9154C8.61335 17.9154 9.36972 17.6698 9.9987 17.2488C10.6277 17.6698 11.384 17.9154 12.1978 17.9154C14.0057 17.9154 15.5305 16.7033 16.004 15.0475Z" stroke="currentColor"/>`,
"bullet-list": `<path d="M9.58329 13.7497H17.0833M9.58329 6.24967H17.0833M6.24996 6.24967C6.24996 7.17015 5.50377 7.91634 4.58329 7.91634C3.66282 7.91634 2.91663 7.17015 2.91663 6.24967C2.91663 5.3292 3.66282 4.58301 4.58329 4.58301C5.50377 4.58301 6.24996 5.3292 6.24996 6.24967ZM6.24996 13.7497C6.24996 14.6701 5.50377 15.4163 4.58329 15.4163C3.66282 15.4163 2.91663 14.6701 2.91663 13.7497C2.91663 12.8292 3.66282 12.083 4.58329 12.083C5.50377 12.083 6.24996 12.8292 6.24996 13.7497Z" stroke="currentColor" stroke-linecap="square"/>`,
"check-small": `<path d="M6.5 11.4412L8.97059 13.5L13.5 6.5" stroke="currentColor" stroke-linecap="square"/>`,
"chevron-down": `<path d="M6.6665 8.33325L9.99984 11.6666L13.3332 8.33325" stroke="currentColor" stroke-linecap="square"/>`,
"chevron-right": `<path d="M8.33301 13.3327L11.6663 9.99935L8.33301 6.66602" stroke="currentColor" stroke-linecap="square"/>`,
"chevron-left": `<path d="M12 15L7 10L12 5" stroke="currentColor" stroke-linecap="square"/>`,
"chevron-right": `<path d="M8 15L13 10L8 5" stroke="currentColor" stroke-linecap="square"/>`,
"chevron-grabber-vertical": `<path d="M6.66675 12.4998L10.0001 15.8332L13.3334 12.4998M6.66675 7.49984L10.0001 4.1665L13.3334 7.49984" stroke="currentColor" stroke-linecap="square"/>`,
"chevron-double-right": `<path d="M11.6654 13.3346L14.9987 10.0013L11.6654 6.66797M5.83203 13.3346L9.16536 10.0013L5.83203 6.66797" stroke="currentColor" stroke-linecap="square"/>`,
"circle-x": `<path fill-rule="evenodd" clip-rule="evenodd" d="M1.6665 10.0003C1.6665 5.39795 5.39746 1.66699 9.99984 1.66699C14.6022 1.66699 18.3332 5.39795 18.3332 10.0003C18.3332 14.6027 14.6022 18.3337 9.99984 18.3337C5.39746 18.3337 1.6665 14.6027 1.6665 10.0003ZM7.49984 6.91107L6.91058 7.50033L9.41058 10.0003L6.91058 12.5003L7.49984 13.0896L9.99984 10.5896L12.4998 13.0896L13.0891 12.5003L10.5891 10.0003L13.0891 7.50033L12.4998 6.91107L9.99984 9.41107L7.49984 6.91107Z" fill="currentColor"/>`,
@@ -28,9 +30,12 @@ const icons = {
eye: `<path d="M10 4.58325C5.83333 4.58325 2.5 9.99992 2.5 9.99992C2.5 9.99992 5.83333 15.4166 10 15.4166C14.1667 15.4166 17.5 9.99992 17.5 9.99992C17.5 9.99992 14.1667 4.58325 10 4.58325Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/><circle cx="10" cy="10" r="2.5" stroke="currentColor"/>`,
enter: `<path d="M5.83333 15.8334L2.5 12.5L5.83333 9.16671M3.33333 12.5H17.9167V4.58337H10" stroke="currentColor" stroke-linecap="square"/>`,
folder: `<path d="M2.08301 2.91675V16.2501H17.9163V5.41675H9.99967L8.33301 2.91675H2.08301Z" stroke="currentColor" stroke-linecap="round"/>`,
"file-tree": `<path d="M4.58203 16.6693L6.66536 9.58594H17.082M4.58203 16.6693H16.457L18.5404 9.58594H17.082M4.58203 16.6693H2.08203V3.33594H8.33203L9.9987 5.83594H17.082V9.58594" stroke="currentColor" stroke-linecap="round"/>`,
"file-tree-active": `<path d="M6.66536 9.58594L4.58203 16.6693H16.457L18.5404 9.58594H17.082H6.66536Z" fill="currentColor" fill-opacity="40%"/><path d="M4.58203 16.6693L6.66536 9.58594H17.082M4.58203 16.6693H16.457L18.5404 9.58594H17.082M4.58203 16.6693H2.08203V3.33594H8.33203L9.9987 5.83594H17.082V9.58594" stroke="currentColor" stroke-linecap="round"/>`,
"magnifying-glass": `<path d="M15.8332 15.8337L13.0819 13.0824M14.6143 9.39088C14.6143 12.2759 12.2755 14.6148 9.39039 14.6148C6.50532 14.6148 4.1665 12.2759 4.1665 9.39088C4.1665 6.5058 6.50532 4.16699 9.39039 4.16699C12.2755 4.16699 14.6143 6.5058 14.6143 9.39088Z" stroke="currentColor" stroke-linecap="square"/>`,
"plus-small": `<path d="M9.99984 5.41699V10.0003M9.99984 10.0003V14.5837M9.99984 10.0003H5.4165M9.99984 10.0003H14.5832" stroke="currentColor" stroke-linecap="square"/>`,
plus: `<path d="M9.9987 2.20703V9.9987M9.9987 9.9987V17.7904M9.9987 9.9987H2.20703M9.9987 9.9987H17.7904" stroke="currentColor" stroke-linecap="square"/>`,
"new-session": `<path d="M17.0827 17.0807V17.5807H17.5827V17.0807H17.0827ZM2.91602 17.0807H2.41602L2.41602 17.5807H2.91602L2.91602 17.0807ZM2.91602 2.91406V2.41406H2.41602V2.91406H2.91602ZM9.58268 3.41406H10.0827V2.41406L9.58268 2.41406V2.91406V3.41406ZM17.5827 10.4141V9.91406L16.5827 9.91406V10.4141H17.0827H17.5827ZM6.24935 11.2474L5.8958 10.8938L5.74935 11.0403V11.2474H6.24935ZM6.24935 13.7474H5.74935V14.2474H6.24935V13.7474ZM8.74935 13.7474V14.2474H8.95646L9.1029 14.101L8.74935 13.7474ZM15.2077 2.28906L15.5612 1.93551L15.2077 1.58196L14.8541 1.93551L15.2077 2.28906ZM17.7077 4.78906L18.0612 5.14262L18.4148 4.78906L18.0612 4.43551L17.7077 4.78906ZM17.0827 17.0807V16.5807H2.91602V17.0807L2.91602 17.5807H17.0827V17.0807ZM2.91602 17.0807H3.41602L3.41602 2.91406H2.91602H2.41602L2.41602 17.0807H2.91602ZM2.91602 2.91406V3.41406L9.58268 3.41406V2.91406V2.41406L2.91602 2.41406V2.91406ZM17.0827 10.4141H16.5827V17.0807H17.0827H17.5827V10.4141H17.0827ZM6.24935 11.2474H5.74935V13.7474H6.24935H6.74935V11.2474H6.24935ZM6.24935 13.7474V14.2474L8.74935 14.2474V13.7474V13.2474L6.24935 13.2474V13.7474ZM6.24935 11.2474L6.6029 11.6009L15.5612 2.64262L15.2077 2.28906L14.8541 1.93551L5.8958 10.8938L6.24935 11.2474ZM15.2077 2.28906L14.8541 2.64262L17.3541 5.14262L17.7077 4.78906L18.0612 4.43551L15.5612 1.93551L15.2077 2.28906ZM17.7077 4.78906L17.3541 4.43551L8.3958 13.3938L8.74935 13.7474L9.1029 14.101L18.0612 5.14262L17.7077 4.78906Z" fill="currentColor"/>`,
"pencil-line": `<path d="M9.58301 17.9166H17.9163M17.9163 5.83325L14.1663 2.08325L2.08301 14.1666V17.9166H5.83301L17.9163 5.83325Z" stroke="currentColor" stroke-linecap="square"/>`,
mcp: `<g><path d="M0.972656 9.37176L9.5214 1.60019C10.7018 0.527151 12.6155 0.527151 13.7957 1.60019C14.9761 2.67321 14.9761 4.41295 13.7957 5.48599L7.3397 11.3552" stroke="currentColor" stroke-linecap="round"/><path d="M7.42871 11.2747L13.7957 5.48643C14.9761 4.41338 16.8898 4.41338 18.0702 5.48643L18.1147 5.52688C19.2951 6.59993 19.2951 8.33966 18.1147 9.4127L10.3831 16.4414C9.98966 16.7991 9.98966 17.379 10.3831 17.7366L11.9707 19.1799" stroke="currentColor" stroke-linecap="round"/><path d="M11.6587 3.54346L5.33619 9.29119C4.15584 10.3642 4.15584 12.1039 5.33619 13.177C6.51649 14.25 8.43019 14.25 9.61054 13.177L15.9331 7.42923" stroke="currentColor" stroke-linecap="round"/></g>`,
glasses: `<path d="M0.416626 7.91667H1.66663M19.5833 7.91667H18.3333M11.866 7.57987C11.3165 7.26398 10.6793 7.08333 9.99996 7.08333C9.32061 7.08333 8.68344 7.26398 8.13389 7.57987M8.74996 10C8.74996 12.0711 7.07103 13.75 4.99996 13.75C2.92889 13.75 1.24996 12.0711 1.24996 10C1.24996 7.92893 2.92889 6.25 4.99996 6.25C7.07103 6.25 8.74996 7.92893 8.74996 10ZM18.75 10C18.75 12.0711 17.071 13.75 15 13.75C12.9289 13.75 11.25 12.0711 11.25 10C11.25 7.92893 12.9289 6.25 15 6.25C17.071 6.25 18.75 7.92893 18.75 10Z" stroke="currentColor" stroke-linecap="square"/>`,
@@ -41,9 +46,9 @@ const icons = {
"layout-left": `<path d="M2.91675 2.91699L2.91675 2.41699L2.41675 2.41699L2.41675 2.91699L2.91675 2.91699ZM17.0834 2.91699L17.5834 2.91699L17.5834 2.41699L17.0834 2.41699L17.0834 2.91699ZM17.0834 17.0837L17.0834 17.5837L17.5834 17.5837L17.5834 17.0837L17.0834 17.0837ZM2.91675 17.0837L2.41675 17.0837L2.41675 17.5837L2.91675 17.5837L2.91675 17.0837ZM7.41674 17.0837L7.41674 17.5837L8.41674 17.5837L8.41674 17.0837L7.91674 17.0837L7.41674 17.0837ZM8.41674 2.91699L8.41674 2.41699L7.41674 2.41699L7.41674 2.91699L7.91674 2.91699L8.41674 2.91699ZM2.91675 2.91699L2.91675 3.41699L17.0834 3.41699L17.0834 2.91699L17.0834 2.41699L2.91675 2.41699L2.91675 2.91699ZM17.0834 2.91699L16.5834 2.91699L16.5834 17.0837L17.0834 17.0837L17.5834 17.0837L17.5834 2.91699L17.0834 2.91699ZM17.0834 17.0837L17.0834 16.5837L2.91675 16.5837L2.91675 17.0837L2.91675 17.5837L17.0834 17.5837L17.0834 17.0837ZM2.91675 17.0837L3.41675 17.0837L3.41675 2.91699L2.91675 2.91699L2.41675 2.91699L2.41675 17.0837L2.91675 17.0837ZM7.91674 17.0837L8.41674 17.0837L8.41674 2.91699L7.91674 2.91699L7.41674 2.91699L7.41674 17.0837L7.91674 17.0837Z" fill="currentColor"/>`,
"layout-left-partial": `<path d="M2.91732 2.91602L7.91732 2.91602L7.91732 17.0827H2.91732L2.91732 2.91602Z" fill="currentColor" fill-opacity="40%" /><path d="M2.91732 2.91602L17.084 2.91602M2.91732 2.91602L2.91732 17.0827M2.91732 2.91602L7.91732 2.91602M17.084 2.91602L17.084 17.0827M17.084 2.91602L7.91732 2.91602M17.084 17.0827L2.91732 17.0827M17.084 17.0827L7.91732 17.0827M2.91732 17.0827H7.91732M7.91732 17.0827L7.91732 2.91602" stroke="currentColor" stroke-linecap="square"/>`,
"layout-left-full": `<path d="M2.91732 2.91602L7.91732 2.91602L7.91732 17.0827H2.91732L2.91732 2.91602Z" fill="currentColor"/><path d="M2.91732 2.91602L17.084 2.91602M2.91732 2.91602L2.91732 17.0827M2.91732 2.91602L7.91732 2.91602M17.084 2.91602L17.084 17.0827M17.084 2.91602L7.91732 2.91602M17.084 17.0827L2.91732 17.0827M17.084 17.0827L7.91732 17.0827M2.91732 17.0827H7.91732M7.91732 17.0827L7.91732 2.91602" stroke="currentColor" stroke-linecap="square"/>`,
"layout-right": `<path d="M17.0832 2.91699L17.5832 2.91699L17.5832 2.41699L17.0832 2.41699L17.0832 2.91699ZM2.91651 2.91699L2.91651 2.41699L2.41651 2.41699L2.41651 2.91699L2.91651 2.91699ZM2.9165 17.0837L2.4165 17.0837L2.4165 17.5837L2.9165 17.5837L2.9165 17.0837ZM17.0832 17.0837L17.0832 17.5837L17.5832 17.5837L17.5832 17.0837L17.0832 17.0837ZM11.5832 17.0837L11.5832 17.5837L12.5832 17.5837L12.5832 17.0837L12.0832 17.0837L11.5832 17.0837ZM12.5832 2.91699L12.5832 2.41699L11.5832 2.41699L11.5832 2.91699L12.0832 2.91699L12.5832 2.91699ZM17.0832 2.91699L17.0832 2.41699L2.91651 2.41699L2.91651 2.91699L2.91651 3.41699L17.0832 3.41699L17.0832 2.91699ZM2.91651 2.91699L2.41651 2.91699L2.4165 17.0837L2.9165 17.0837L3.4165 17.0837L3.41651 2.91699L2.91651 2.91699ZM2.9165 17.0837L2.9165 17.5837L17.0832 17.5837L17.0832 17.0837L17.0832 16.5837L2.9165 16.5837L2.9165 17.0837ZM17.0832 17.0837L17.5832 17.0837L17.5832 2.91699L17.0832 2.91699L16.5832 2.91699L16.5832 17.0837L17.0832 17.0837ZM12.0832 17.0837L12.5832 17.0837L12.5832 2.91699L12.0832 2.91699L11.5832 2.91699L11.5832 17.0837L12.0832 17.0837Z" fill="currentColor"/>`,
"layout-right-partial": `<path d="M12.0827 2.91602L2.91602 2.91602L2.91602 17.0827L12.0827 17.0827L12.0827 2.91602Z" fill="currentColor" fill-opacity="40%" /><path d="M2.91602 2.91602L17.0827 2.91602L17.0827 17.0827L2.91602 17.0827M2.91602 2.91602L2.91602 17.0827M2.91602 2.91602L12.0827 2.91602L12.0827 17.0827L2.91602 17.0827" stroke="currentColor" stroke-linecap="square"/>`,
"layout-right-full": `<path d="M12.0827 2.91602L2.91602 2.91602L2.91602 17.0827L12.0827 17.0827L12.0827 2.91602Z" fill="currentColor"/><path d="M2.91602 2.91602L17.0827 2.91602L17.0827 17.0827L2.91602 17.0827M2.91602 2.91602L2.91602 17.0827M2.91602 2.91602L12.0827 2.91602L12.0827 17.0827L2.91602 17.0827" stroke="currentColor" stroke-linecap="square"/>`,
"layout-right": `<path d="M2.91536 2.91406H2.36536V2.36406H2.91536V2.91406ZM2.91536 17.0807V17.6307H2.36536V17.0807H2.91536ZM17.082 17.0807H17.632V17.6307H17.082V17.0807ZM17.082 2.91406V2.36406H17.632V2.91406H17.082ZM6.9987 2.91406H6.4487V2.36406H6.9987V2.91406ZM6.9987 17.0807V17.6307H6.4487V17.0807H6.9987ZM2.91536 2.91406H3.46536V17.0807H2.91536H2.36536V2.91406H2.91536ZM2.91536 17.0807V16.5307H17.082V17.0807V17.6307H2.91536V17.0807ZM17.082 17.0807H16.532V2.91406H17.082H17.632V17.0807H17.082ZM17.082 2.91406V3.46406H2.91536V2.91406V2.36406H17.082V2.91406ZM6.9987 2.91406H7.5487V17.0807H6.9987H6.4487V2.91406H6.9987ZM17.082 17.0807L17.082 17.6307L6.9987 17.6307V17.0807V16.5307L17.082 16.5307L17.082 17.0807ZM6.9987 2.91406V2.36406H17.082V2.91406V3.46406H6.9987V2.91406Z" fill="currentColor"/>`,
"layout-right-partial": `<path d="M17.082 17.0807L6.9987 17.0807V2.91406H17.082V17.0807Z" fill="currentColor" fill-opacity="40%" /><path d="M2.91536 2.91406H2.36536V2.36406H2.91536V2.91406ZM2.91536 17.0807V17.6307H2.36536V17.0807H2.91536ZM17.082 17.0807H17.632V17.6307H17.082V17.0807ZM17.082 2.91406V2.36406H17.632V2.91406H17.082ZM6.9987 2.91406H6.4487V2.36406H6.9987V2.91406ZM6.9987 17.0807V17.6307H6.4487V17.0807H6.9987ZM2.91536 2.91406H3.46536V17.0807H2.91536H2.36536V2.91406H2.91536ZM2.91536 17.0807V16.5307H17.082V17.0807V17.6307H2.91536V17.0807ZM17.082 17.0807H16.532V2.91406H17.082H17.632V17.0807H17.082ZM17.082 2.91406V3.46406H2.91536V2.91406V2.36406H17.082V2.91406ZM6.9987 2.91406H7.5487V17.0807H6.9987H6.4487V2.91406H6.9987ZM17.082 17.0807L17.082 17.6307L6.9987 17.6307V17.0807V16.5307L17.082 16.5307L17.082 17.0807ZM6.9987 2.91406V2.36406H17.082V2.91406V3.46406H6.9987V2.91406Z" fill="currentColor" />`,
"layout-right-full": `<path d="M17.082 17.0807L6.9987 17.0807V2.91406H17.082V17.0807Z" fill="currentColor" /><path d="M2.91536 2.91406H2.36536V2.36406H2.91536V2.91406ZM2.91536 17.0807V17.6307H2.36536V17.0807H2.91536ZM17.082 17.0807H17.632V17.6307H17.082V17.0807ZM17.082 2.91406V2.36406H17.632V2.91406H17.082ZM6.9987 2.91406H6.4487V2.36406H6.9987V2.91406ZM6.9987 17.0807V17.6307H6.4487V17.0807H6.9987ZM2.91536 2.91406H3.46536V17.0807H2.91536H2.36536V2.91406H2.91536ZM2.91536 17.0807V16.5307H17.082V17.0807V17.6307H2.91536V17.0807ZM17.082 17.0807H16.532V2.91406H17.082H17.632V17.0807H17.082ZM17.082 2.91406V3.46406H2.91536V2.91406V2.36406H17.082V2.91406ZM6.9987 2.91406H7.5487V17.0807H6.9987H6.4487V2.91406H6.9987ZM17.082 17.0807L17.082 17.6307L6.9987 17.6307V17.0807V16.5307L17.082 16.5307L17.082 17.0807ZM6.9987 2.91406V2.36406H17.082V2.91406V3.46406H6.9987V2.91406Z" fill="currentColor" />`,
"square-arrow-top-right": `<path d="M7.91675 2.9165H2.91675V17.0832H17.0834V12.0832M12.0834 2.9165H17.0834V7.9165M9.58342 10.4165L16.6667 3.33317" stroke="currentColor" stroke-linecap="square"/>`,
"speech-bubble": `<path d="M18.3334 10.0003C18.3334 5.57324 15.0927 2.91699 10.0001 2.91699C4.90749 2.91699 1.66675 5.57324 1.66675 10.0003C1.66675 11.1497 2.45578 13.1016 2.5771 13.3949C2.5878 13.4207 2.59839 13.4444 2.60802 13.4706C2.69194 13.6996 3.04282 14.9364 1.66675 16.7684C3.5186 17.6538 5.48526 16.1982 5.48526 16.1982C6.84592 16.9202 8.46491 17.0837 10.0001 17.0837C15.0927 17.0837 18.3334 14.4274 18.3334 10.0003Z" stroke="currentColor" stroke-linecap="square"/>`,
comment: `<path d="M16.25 3.75H3.75V16.25L6.875 14.4643H16.25V3.75Z" stroke="currentColor" stroke-linecap="square"/>`,

View File

@@ -12,6 +12,6 @@
&:focus {
outline: none;
box-shadow: 0 0 0 1px var(--border-interactive-focus);
box-shadow: var(--inline-input-shadow, 0 0 0 1px var(--border-interactive-focus));
}
}

View File

@@ -6,6 +6,17 @@ export type InlineInputProps = ComponentProps<"input"> & {
}
export function InlineInput(props: InlineInputProps) {
const [local, others] = splitProps(props, ["class", "width"])
return <input data-component="inline-input" class={local.class} style={{ width: local.width }} {...others} />
const [local, others] = splitProps(props, ["class", "width", "style"])
const style = () => {
if (!local.style) return { width: local.width }
if (typeof local.style === "string") {
if (!local.width) return local.style
return `${local.style};width:${local.width}`
}
if (!local.width) return local.style
return { ...local.style, width: local.width }
}
return <input data-component="inline-input" class={local.class} style={style()} {...others} />
}

View File

@@ -3,7 +3,7 @@
min-width: 0;
max-width: 100%;
overflow-wrap: break-word;
color: var(--text-base);
color: var(--text-strong);
font-family: var(--font-family-sans);
font-size: var(--font-size-base); /* 14px */
line-height: var(--line-height-x-large);
@@ -117,7 +117,7 @@
.shiki {
font-size: 13px;
padding: 8px 12px;
border-radius: 4px;
border-radius: 6px;
border: 0.5px solid var(--border-weak-base);
}
@@ -127,11 +127,55 @@
[data-slot="markdown-copy-button"] {
position: absolute;
top: 8px;
right: 8px;
top: 4px;
right: 4px;
opacity: 0;
transition: opacity 0.15s ease;
z-index: 1;
&::after {
content: attr(data-tooltip);
position: absolute;
left: 50%;
bottom: calc(100% + 4px);
transform: translateX(-50%);
z-index: 1000;
max-width: 320px;
border-radius: var(--radius-sm);
background: var(--surface-float-base);
color: var(--text-invert-strong);
padding: 2px 8px;
border: 1px solid var(--border-weak-base, rgba(0, 0, 0, 0.07));
box-shadow: var(--shadow-md);
pointer-events: none;
white-space: nowrap;
font-family: var(--font-family-sans);
font-size: var(--font-size-small);
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: var(--line-height-large);
letter-spacing: var(--letter-spacing-normal);
opacity: 0;
transition: opacity 0.15s ease;
}
}
[data-slot="markdown-copy-button"]:hover::after,
[data-slot="markdown-copy-button"]:focus-visible::after {
opacity: 1;
}
[data-slot="markdown-copy-button"][data-variant="secondary"] {
box-shadow: none;
border: 1px solid var(--border-weak-base);
}
[data-slot="markdown-copy-button"][data-variant="secondary"] [data-slot="icon-svg"] {
color: var(--icon-base);
}
[data-component="markdown-code"]:hover [data-slot="markdown-copy-button"] {

View File

@@ -85,7 +85,7 @@ function createCopyButton(labels: CopyLabels) {
button.setAttribute("data-size", "small")
button.setAttribute("data-slot", "markdown-copy-button")
button.setAttribute("aria-label", labels.copy)
button.setAttribute("title", labels.copy)
button.setAttribute("data-tooltip", labels.copy)
button.appendChild(createIcon(iconPaths.copy, "copy-icon"))
button.appendChild(createIcon(iconPaths.check, "check-icon"))
return button
@@ -95,12 +95,12 @@ function setCopyState(button: HTMLButtonElement, labels: CopyLabels, copied: boo
if (copied) {
button.setAttribute("data-copied", "true")
button.setAttribute("aria-label", labels.copied)
button.setAttribute("title", labels.copied)
button.setAttribute("data-tooltip", labels.copied)
return
}
button.removeAttribute("data-copied")
button.setAttribute("aria-label", labels.copy)
button.setAttribute("title", labels.copy)
button.setAttribute("data-tooltip", labels.copy)
}
function setupCodeCopy(root: HTMLDivElement, labels: CopyLabels) {

View File

@@ -14,15 +14,27 @@
font-weight: var(--font-weight-regular);
line-height: var(--line-height-large);
letter-spacing: var(--letter-spacing-normal);
color: var(--text-base);
color: var(--text-strong);
display: flex;
flex-direction: column;
align-items: flex-end;
align-self: stretch;
width: 100%;
max-width: 100%;
gap: 8px;
&[data-interrupted] {
color: var(--text-weak);
}
[data-slot="user-message-attachments"] {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 8px;
width: fit-content;
max-width: min(82%, 64ch);
margin-left: auto;
}
[data-slot="user-message-attachment"] {
@@ -71,15 +83,24 @@
}
}
[data-slot="user-message-body"] {
width: fit-content;
max-width: min(82%, 64ch);
margin-left: auto;
display: flex;
flex-direction: column;
align-items: flex-end;
}
[data-slot="user-message-text"] {
position: relative;
display: inline-block;
white-space: pre-wrap;
word-break: break-word;
overflow: hidden;
background: var(--surface-weak);
background: var(--surface-base);
border: 1px solid var(--border-weak-base);
padding: 8px 12px;
border-radius: 4px;
border-radius: 6px;
[data-highlight="file"] {
color: var(--syntax-property);
@@ -89,19 +110,36 @@
color: var(--syntax-type);
}
[data-slot="user-message-copy-wrapper"] {
position: absolute;
top: 7px;
right: 7px;
opacity: 0;
transition: opacity 0.15s ease;
}
max-width: 100%;
}
&:hover [data-slot="user-message-copy-wrapper"] {
opacity: 1;
[data-slot="user-message-copy-wrapper"] {
min-height: 24px;
margin-top: 4px;
display: flex;
align-items: center;
justify-content: flex-end;
opacity: 0;
pointer-events: none;
transition: opacity 0.15s ease;
will-change: opacity;
[data-component="tooltip-trigger"] {
display: inline-flex;
width: fit-content;
}
}
[data-slot="user-message-copy-wrapper"][data-interrupted] {
gap: 12px;
}
&:hover [data-slot="user-message-copy-wrapper"],
&:focus-within [data-slot="user-message-copy-wrapper"] {
opacity: 1;
pointer-events: auto;
}
.text-text-strong {
color: var(--text-strong);
}
@@ -115,21 +153,36 @@
width: 100%;
[data-slot="text-part-body"] {
position: relative;
margin-top: 32px;
margin-top: 0;
}
[data-slot="text-part-copy-wrapper"] {
position: absolute;
top: -28px;
right: 8px;
min-height: 24px;
margin-top: 4px;
display: flex;
align-items: center;
justify-content: flex-start;
opacity: 0;
pointer-events: none;
transition: opacity 0.15s ease;
z-index: 1;
will-change: opacity;
[data-component="tooltip-trigger"] {
display: inline-flex;
width: fit-content;
}
}
[data-slot="text-part-body"]:hover [data-slot="text-part-copy-wrapper"] {
[data-slot="text-part-copy-wrapper"][data-interrupted] {
width: 100%;
justify-content: flex-end;
gap: 12px;
}
&:hover [data-slot="text-part-copy-wrapper"],
&:focus-within [data-slot="text-part-copy-wrapper"] {
opacity: 1;
pointer-events: auto;
}
[data-component="markdown"] {
@@ -146,7 +199,7 @@
[data-component="markdown"] {
margin-top: 24px;
font-style: italic !important;
font-style: normal;
p:has(strong) {
margin-top: 24px;
@@ -196,7 +249,8 @@
[data-component="tool-output"] {
white-space: pre;
padding: 8px 12px;
padding: 0;
margin-bottom: 24px;
height: fit-content;
display: flex;
flex-direction: column;
@@ -238,6 +292,79 @@
}
}
[data-slot="collapsible-content"]:has([data-component="edit-content"]),
[data-slot="collapsible-content"]:has([data-component="write-content"]),
[data-slot="collapsible-content"]:has([data-component="apply-patch-files"]) {
border: 1px solid var(--border-weak-base);
border-radius: 6px;
background: transparent;
overflow: hidden;
}
[data-component="bash-output"] {
width: 100%;
border: 1px solid var(--border-weak-base);
border-radius: 6px;
background: transparent;
position: relative;
overflow: hidden;
[data-slot="bash-copy"] {
position: absolute;
top: 4px;
right: 4px;
opacity: 0;
pointer-events: none;
transition: opacity 0.15s ease;
}
&:hover [data-slot="bash-copy"],
&:focus-within [data-slot="bash-copy"] {
opacity: 1;
pointer-events: auto;
}
[data-slot="bash-copy"] [data-component="icon-button"][data-variant="secondary"] {
box-shadow: none;
border: 1px solid var(--border-weak-base);
}
[data-slot="bash-copy"] [data-component="icon-button"][data-variant="secondary"] [data-slot="icon-svg"] {
color: var(--icon-base);
}
[data-slot="bash-scroll"] {
width: 100%;
overflow-y: auto;
overflow-x: hidden;
max-height: 240px;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
[data-slot="bash-pre"] {
margin: 0;
padding: 12px;
}
[data-slot="bash-pre"] code {
font-family: var(--font-family-mono);
font-feature-settings: var(--font-family-mono--font-feature-settings);
font-size: 13px;
line-height: var(--line-height-large);
white-space: pre-wrap;
overflow-wrap: anywhere;
}
}
[data-slot="collapsible-content"]:has([data-component="edit-content"]) [data-component="edit-content"],
[data-slot="collapsible-content"]:has([data-component="write-content"]) [data-component="write-content"] {
border-top: none;
}
[data-component="edit-trigger"],
[data-component="write-trigger"] {
display: flex;
@@ -258,9 +385,9 @@
flex-shrink: 0;
display: flex;
align-items: center;
gap: 4px;
gap: 8px;
font-family: var(--font-family-sans);
font-size: var(--font-size-base);
font-size: 14px;
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: var(--line-height-large);
@@ -268,18 +395,37 @@
color: var(--text-base);
}
[data-slot="message-part-title-spinner"] {
margin-left: 4px;
width: 16px;
height: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: var(--text-weak);
[data-component="spinner"] {
width: 16px;
height: 16px;
}
}
[data-slot="message-part-title-text"] {
text-transform: capitalize;
color: var(--text-strong);
}
[data-slot="message-part-title-filename"] {
/* No text-transform - preserve original filename casing */
font-weight: var(--font-weight-regular);
}
[data-slot="message-part-path"] {
display: flex;
flex-grow: 1;
min-width: 0;
font-weight: var(--font-weight-regular);
}
[data-slot="message-part-directory"] {
@@ -344,12 +490,19 @@
}
[data-component="todos"] {
padding: 10px 12px 24px 48px;
padding: 10px 0 24px 0;
display: flex;
flex-direction: column;
gap: 8px;
[data-component="checkbox"] {
--checkbox-align: flex-start;
--checkbox-offset: 0.5px;
}
[data-slot="message-part-todo-content"] {
line-height: var(--line-height-normal);
&[data-completed="completed"] {
text-decoration: line-through;
color: var(--text-weaker);
@@ -357,41 +510,55 @@
}
}
[data-component="task-tools"] {
padding: 8px 12px;
[data-component="context-tool-group-trigger"] {
width: 100%;
min-height: 24px;
display: flex;
flex-direction: column;
gap: 6px;
align-items: center;
justify-content: flex-start;
gap: 0px;
cursor: pointer;
[data-slot="task-tool-item"] {
[data-slot="context-tool-group-title"] {
min-width: 0;
display: flex;
align-items: center;
gap: 8px;
color: var(--text-weak);
[data-slot="icon-svg"] {
flex-shrink: 0;
color: var(--icon-weak);
}
}
[data-slot="task-tool-title"] {
font-family: var(--font-family-sans);
font-size: var(--font-size-small);
font-size: 14px;
font-weight: var(--font-weight-medium);
line-height: var(--line-height-large);
color: var(--text-weak);
color: var(--text-strong);
}
[data-slot="task-tool-subtitle"] {
font-family: var(--font-family-sans);
font-size: var(--font-size-small);
font-weight: var(--font-weight-regular);
line-height: var(--line-height-large);
color: var(--text-weaker);
[data-slot="context-tool-group-label"] {
flex-shrink: 0;
}
[data-slot="context-tool-group-summary"] {
flex-shrink: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: var(--font-weight-regular);
color: var(--text-base);
}
[data-slot="collapsible-arrow"] {
color: var(--icon-weaker);
}
}
[data-component="context-tool-group-list"] {
padding: 6px 0 4px 0;
display: flex;
flex-direction: column;
gap: 2px;
[data-slot="context-tool-group-item"] {
min-width: 0;
padding: 6px 0;
}
}
@@ -549,170 +716,322 @@
}
[data-component="question-prompt"] {
position: relative;
display: flex;
flex-direction: column;
padding: 12px;
background-color: var(--surface-inset-base);
border-radius: 0 0 6px 6px;
gap: 12px;
gap: 0;
min-height: 0;
max-height: var(--question-prompt-max-height, 100dvh);
[data-slot="question-tabs"] {
[data-slot="question-body"] {
display: flex;
gap: 4px;
flex-wrap: wrap;
flex-direction: column;
gap: 16px;
flex: 1;
min-height: 0;
padding: 8px 8px 0;
background-color: var(--surface-raised-stronger-non-alpha);
border-radius: 12px;
box-shadow: var(--shadow-xs-border);
overflow: clip;
position: relative;
z-index: 10;
}
[data-slot="question-tab"] {
padding: 4px 12px;
font-size: 13px;
border-radius: 4px;
background-color: var(--surface-base);
color: var(--text-base);
border: none;
cursor: pointer;
transition:
color 0.15s,
background-color 0.15s;
[data-slot="question-header"] {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 0 10px;
}
&:hover {
background-color: var(--surface-base-hover);
}
[data-slot="question-header-title"] {
font-family: var(--font-family-sans);
font-size: 14px;
font-weight: var(--font-weight-medium);
line-height: var(--line-height-large);
color: var(--text-strong);
min-width: 0;
}
&[data-active="true"] {
background-color: var(--surface-raised-base);
}
[data-slot="question-progress"] {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
&[data-answered="true"] {
color: var(--text-strong);
}
[data-slot="question-progress-segment"] {
width: 16px;
height: 16px;
padding: 0;
border: 0;
background: transparent;
border-radius: 999px;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
touch-action: manipulation;
&::after {
content: "";
width: 16px;
height: 2px;
border-radius: 999px;
background-color: var(--icon-weak-base);
transition: background-color 0.2s ease;
}
&[data-active="true"]::after {
background-color: var(--icon-strong-base);
}
&[data-answered="true"]::after {
background-color: var(--icon-interactive-base);
}
&:disabled {
cursor: not-allowed;
opacity: 0.6;
}
}
[data-slot="question-content"] {
display: flex;
flex-direction: column;
gap: 8px;
gap: 4px;
flex: 1;
min-height: 0;
}
[data-slot="question-text"] {
font-size: 14px;
color: var(--text-base);
line-height: 1.5;
}
[data-slot="question-text"] {
font-family: var(--font-family-sans);
font-size: 14px;
font-weight: var(--font-weight-medium);
line-height: var(--line-height-large);
color: var(--text-strong);
padding: 0 10px;
}
[data-slot="question-hint"] {
font-family: var(--font-family-sans);
font-size: 13px;
font-weight: var(--font-weight-regular);
line-height: var(--line-height-large);
color: var(--text-weak);
padding: 0 10px;
}
[data-slot="question-options"] {
display: flex;
flex-direction: column;
gap: 4px;
gap: 6px;
margin-top: 12px;
padding: 1px 1px 8px;
flex: 1;
min-height: 0;
overflow-y: auto;
scrollbar-width: none;
-ms-overflow-style: none;
[data-slot="question-option"] {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 2px;
padding: 8px 12px;
background-color: var(--surface-base);
border: 1px solid var(--border-weaker-base);
border-radius: 6px;
cursor: pointer;
text-align: left;
width: 100%;
transition:
background-color 0.15s,
border-color 0.15s;
position: relative;
&:hover {
background-color: var(--surface-base-hover);
border-color: var(--border-default);
}
&[data-picked="true"] {
[data-component="icon"] {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
color: var(--text-strong);
}
}
[data-slot="option-label"] {
font-size: 14px;
color: var(--text-base);
font-weight: 500;
}
[data-slot="option-description"] {
font-size: 12px;
color: var(--text-weak);
}
}
[data-slot="custom-input-form"] {
display: flex;
gap: 8px;
padding: 8px 0;
align-items: stretch;
[data-slot="custom-input"] {
flex: 1;
padding: 8px 12px;
font-size: 14px;
border: 1px solid var(--border-default);
border-radius: 6px;
background-color: var(--surface-base);
color: var(--text-base);
outline: none;
&:focus {
border-color: var(--border-focus);
}
&::placeholder {
color: var(--text-weak);
}
}
[data-component="button"] {
height: auto;
}
}
}
[data-slot="question-review"] {
display: flex;
flex-direction: column;
gap: 12px;
[data-slot="review-title"] {
&::-webkit-scrollbar {
display: none;
}
}
[data-slot="review-item"] {
display: flex;
flex-direction: column;
gap: 2px;
font-size: 13px;
[data-slot="question-option"] {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 8px 8px 8px 10px;
background-color: var(--surface-raised-stronger-non-alpha);
border: 1px solid var(--border-weak-base);
border-radius: 6px;
box-shadow: none;
text-align: left;
width: 100%;
cursor: pointer;
transition:
background-color 0.15s ease,
border-color 0.15s ease,
box-shadow 0.15s ease;
[data-slot="review-label"] {
color: var(--text-weak);
&:hover:not([data-picked="true"]) {
background-color: var(--background-base);
}
&[data-picked="true"] {
background-color: var(--surface-interactive-weak);
border-color: transparent;
box-shadow: var(--shadow-xs-border-hover);
}
&:disabled {
cursor: not-allowed;
opacity: 0.6;
}
}
[data-slot="question-option-check"] {
display: inline-flex;
transform: translateY(2px);
}
[data-slot="question-option-box"] {
width: 16px;
height: 16px;
padding: 2px;
border-radius: var(--radius-sm);
border: 1px solid var(--border-weak-base);
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
background-color: transparent;
transition:
background-color 0.15s ease,
border-color 0.15s ease;
[data-component="icon"] {
opacity: 0;
color: var(--icon-base);
}
&[data-type="radio"] {
border-radius: 999px;
}
[data-slot="question-option-radio-dot"] {
width: 6px;
height: 6px;
border-radius: 999px;
background-color: var(--background-stronger);
opacity: 0;
}
&[data-picked="true"] {
border-color: var(--icon-interactive-base);
[data-component="icon"] {
opacity: 1;
color: var(--icon-invert-base);
}
[data-slot="review-value"] {
color: var(--text-strong);
&[data-type="checkbox"] {
background-color: var(--icon-interactive-base);
}
&[data-answered="false"] {
color: var(--text-weak);
&[data-type="radio"] {
background-color: var(--icon-interactive-base);
[data-slot="question-option-radio-dot"] {
opacity: 1;
}
}
}
}
[data-slot="question-actions"] {
[data-slot="question-option-main"] {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
flex: 1;
}
[data-slot="option-label"] {
font-family: var(--font-family-sans);
font-size: 14px;
font-weight: var(--font-weight-medium);
line-height: var(--line-height-large);
color: var(--text-strong);
}
[data-slot="option-description"] {
font-family: var(--font-family-sans);
font-size: 14px;
font-weight: var(--font-weight-regular);
line-height: var(--line-height-large);
color: var(--text-base);
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
[data-slot="question-option"][data-custom="true"] {
[data-slot="option-description"] {
overflow: visible;
text-overflow: clip;
white-space: normal;
overflow-wrap: anywhere;
}
}
[data-slot="question-custom"] {
display: flex;
flex-direction: column;
gap: 8px;
}
[data-slot="question-custom-input-wrap"] {
padding-left: 36px;
}
[data-slot="question-custom-input"] {
width: 100%;
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
outline: none;
font-family: var(--font-family-sans);
font-size: 14px;
font-weight: var(--font-weight-regular);
line-height: var(--line-height-large);
color: var(--text-base);
min-width: 0;
cursor: text;
resize: none;
overflow: hidden;
overflow-wrap: anywhere;
&::placeholder {
color: var(--text-weak);
}
&:focus-visible {
outline: 1px solid var(--border-interactive-base);
outline-offset: 2px;
border-radius: var(--radius-xs);
}
&:disabled {
opacity: 0.6;
}
}
[data-slot="question-footer"] {
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
padding: 32px 8px 8px;
background-color: var(--background-base);
border: 1px solid var(--border-weak-base);
border-radius: 12px;
overflow: clip;
margin-top: -24px;
position: relative;
z-index: 0;
}
[data-slot="question-footer-actions"] {
display: flex;
align-items: center;
gap: 8px;
justify-content: flex-end;
}
}
@@ -720,7 +1039,7 @@
display: flex;
flex-direction: column;
gap: 12px;
padding: 8px 12px;
padding: 8px 0;
[data-slot="question-answer-item"] {
display: flex;
@@ -746,18 +1065,13 @@
[data-component="apply-patch-file"] {
display: flex;
flex-direction: column;
border-top: 1px solid var(--border-weaker-base);
&:first-child {
border-top: 1px solid var(--border-weaker-base);
}
[data-slot="apply-patch-file-header"] {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background-color: var(--surface-inset-base);
background-color: transparent;
}
[data-slot="apply-patch-file-action"] {
@@ -799,7 +1113,12 @@
}
}
[data-component="apply-patch-file"] + [data-component="apply-patch-file"] {
border-top: 1px solid var(--border-weaker-base);
}
[data-component="apply-patch-file-diff"] {
border-top: 1px solid var(--border-weaker-base);
max-height: 420px;
overflow-y: auto;
scrollbar-width: none;

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,5 @@
[data-component="session-turn"] {
--session-turn-sticky-height: 0px;
--sticky-header-height: calc(var(--session-title-height, 0px) + var(--session-turn-sticky-height, 0px) + 24px);
/* flex: 1; */
--sticky-header-height: calc(var(--session-title-height, 0px) + 24px);
height: 100%;
min-height: 0;
min-width: 0;
@@ -30,525 +28,30 @@
min-width: 0;
gap: 18px;
overflow-anchor: none;
[data-slot="session-turn-badge"] {
display: inline-flex;
align-items: center;
padding: 2px 6px;
border-radius: 4px;
font-family: var(--font-family-mono);
font-size: var(--font-size-x-small);
font-weight: var(--font-weight-medium);
line-height: var(--line-height-normal);
white-space: nowrap;
color: var(--text-base);
background: var(--surface-raised-base);
}
}
[data-slot="session-turn-attachments"] {
width: 100%;
min-width: 0;
align-self: stretch;
}
[data-slot="session-turn-sticky"] {
width: calc(100% + 9px);
position: sticky;
top: var(--session-title-height, 0px);
z-index: 20;
background-color: var(--background-stronger);
margin-left: -9px;
padding-left: 9px;
/* padding-bottom: 12px; */
display: flex;
flex-direction: column;
gap: 12px;
&::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: var(--background-stronger);
z-index: -1;
}
&::after {
content: "";
position: absolute;
top: 100%;
left: 0;
right: 0;
height: 32px;
background: linear-gradient(to bottom, var(--background-stronger), transparent);
pointer-events: none;
}
}
[data-slot="session-turn-message-header"] {
display: flex;
align-items: center;
align-self: stretch;
height: 32px;
}
[data-slot="session-turn-message-content"] {
margin-top: 0;
width: 100%;
min-width: 0;
max-width: 100%;
}
[data-component="user-message"] [data-slot="user-message-text"] {
max-height: var(--user-message-collapsed-height, 64px);
}
[data-component="user-message"][data-expanded="true"] [data-slot="user-message-text"] {
max-height: none;
}
[data-component="user-message"][data-can-expand="true"] [data-slot="user-message-text"] {
padding-right: 36px;
padding-bottom: 28px;
}
[data-component="user-message"][data-can-expand="true"]:not([data-expanded="true"])
[data-slot="user-message-text"]::after {
content: "";
position: absolute;
left: 0;
right: 0;
height: 8px;
bottom: 0px;
background:
linear-gradient(to bottom, transparent, var(--surface-weak)),
linear-gradient(to bottom, transparent, var(--surface-weak));
pointer-events: none;
}
[data-component="user-message"] [data-slot="user-message-text"] [data-slot="user-message-expand"] {
display: none;
position: absolute;
bottom: 6px;
right: 6px;
padding: 0;
}
[data-component="user-message"][data-can-expand="true"]
[data-slot="user-message-text"]
[data-slot="user-message-expand"],
[data-component="user-message"][data-expanded="true"]
[data-slot="user-message-text"]
[data-slot="user-message-expand"] {
display: inline-flex;
align-items: center;
justify-content: center;
height: 22px;
width: 22px;
border: none;
border-radius: 6px;
background: transparent;
cursor: pointer;
color: var(--text-weak);
[data-slot="icon-svg"] {
transition: transform 0.15s ease;
}
}
[data-component="user-message"][data-expanded="true"]
[data-slot="user-message-text"]
[data-slot="user-message-expand"]
[data-slot="icon-svg"] {
transform: rotate(180deg);
}
[data-component="user-message"] [data-slot="user-message-text"] [data-slot="user-message-expand"]:hover {
background: var(--surface-raised-base);
color: var(--text-base);
}
[data-slot="session-turn-user-badges"] {
display: flex;
align-items: center;
gap: 6px;
padding-left: 16px;
}
[data-slot="session-turn-message-title"] {
width: 100%;
font-size: var(--font-size-large);
font-weight: 500;
color: var(--text-strong);
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
white-space: nowrap;
}
[data-slot="session-turn-message-title"] h1 {
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
white-space: nowrap;
font-size: inherit;
font-weight: inherit;
}
[data-slot="session-turn-typewriter"] {
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
white-space: nowrap;
}
[data-slot="session-turn-summary-section"] {
width: 100%;
display: flex;
flex-direction: column;
gap: 24px;
align-items: flex-start;
align-self: stretch;
}
[data-slot="session-turn-summary-header"] {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
align-self: stretch;
[data-slot="session-turn-summary-title-row"] {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
[data-slot="session-turn-response"] {
width: 100%;
}
[data-slot="session-turn-response-copy-wrapper"] {
opacity: 0;
pointer-events: none;
transition: opacity 0.15s ease;
}
&:hover [data-slot="session-turn-response-copy-wrapper"],
&:focus-within [data-slot="session-turn-response-copy-wrapper"] {
opacity: 1;
pointer-events: auto;
}
p {
font-size: var(--font-size-base);
line-height: var(--line-height-x-large);
}
}
[data-slot="session-turn-summary-title"] {
font-size: 13px;
/* text-12-medium */
font-weight: 500;
color: var(--text-weak);
}
[data-slot="session-turn-markdown"],
[data-slot="session-turn-accordion"] [data-slot="accordion-content"] {
-webkit-user-select: text;
user-select: text;
}
[data-slot="session-turn-markdown"] {
&[data-diffs="true"] {
font-size: 15px;
}
&[data-fade="true"] > * {
animation: fadeUp 0.4s ease-out forwards;
opacity: 0;
&:nth-child(1) {
animation-delay: 0.1s;
}
&:nth-child(2) {
animation-delay: 0.2s;
}
&:nth-child(3) {
animation-delay: 0.3s;
}
&:nth-child(4) {
animation-delay: 0.4s;
}
&:nth-child(5) {
animation-delay: 0.5s;
}
&:nth-child(6) {
animation-delay: 0.6s;
}
&:nth-child(7) {
animation-delay: 0.7s;
}
&:nth-child(8) {
animation-delay: 0.8s;
}
&:nth-child(9) {
animation-delay: 0.9s;
}
&:nth-child(10) {
animation-delay: 1s;
}
&:nth-child(11) {
animation-delay: 1.1s;
}
&:nth-child(12) {
animation-delay: 1.2s;
}
&:nth-child(13) {
animation-delay: 1.3s;
}
&:nth-child(14) {
animation-delay: 1.4s;
}
&:nth-child(15) {
animation-delay: 1.5s;
}
&:nth-child(16) {
animation-delay: 1.6s;
}
&:nth-child(17) {
animation-delay: 1.7s;
}
&:nth-child(18) {
animation-delay: 1.8s;
}
&:nth-child(19) {
animation-delay: 1.9s;
}
&:nth-child(20) {
animation-delay: 2s;
}
&:nth-child(21) {
animation-delay: 2.1s;
}
&:nth-child(22) {
animation-delay: 2.2s;
}
&:nth-child(23) {
animation-delay: 2.3s;
}
&:nth-child(24) {
animation-delay: 2.4s;
}
&:nth-child(25) {
animation-delay: 2.5s;
}
&:nth-child(26) {
animation-delay: 2.6s;
}
&:nth-child(27) {
animation-delay: 2.7s;
}
&:nth-child(28) {
animation-delay: 2.8s;
}
&:nth-child(29) {
animation-delay: 2.9s;
}
&:nth-child(30) {
animation-delay: 3s;
}
}
}
[data-slot="session-turn-summary-section"] {
position: relative;
[data-slot="session-turn-summary-copy"] {
position: absolute;
top: 0;
right: 0;
opacity: 0;
transition: opacity 0.15s ease;
}
&:hover [data-slot="session-turn-summary-copy"] {
opacity: 1;
}
}
[data-slot="session-turn-accordion"] {
width: 100%;
}
[data-component="sticky-accordion-header"] {
top: var(--sticky-header-height, 0px);
}
[data-component="sticky-accordion-header"][data-expanded]::before,
[data-slot="accordion-item"][data-expanded] [data-component="sticky-accordion-header"]::before {
top: calc(-1 * var(--sticky-header-height, 0px));
}
[data-slot="session-turn-accordion-trigger-content"] {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
gap: 20px;
[data-expandable="false"] {
pointer-events: none;
}
}
[data-slot="session-turn-file-info"] {
flex-grow: 1;
display: flex;
align-items: center;
gap: 20px;
min-width: 0;
}
[data-slot="session-turn-file-icon"] {
flex-shrink: 0;
width: 16px;
height: 16px;
}
[data-slot="session-turn-file-path"] {
display: flex;
flex-grow: 1;
min-width: 0;
}
[data-slot="session-turn-directory"] {
color: var(--text-base);
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
direction: rtl;
text-align: left;
}
[data-slot="session-turn-filename"] {
color: var(--text-strong);
flex-shrink: 0;
}
[data-slot="session-turn-accordion-actions"] {
flex-shrink: 0;
display: flex;
gap: 16px;
align-items: center;
justify-content: flex-end;
}
[data-slot="session-turn-accordion-content"] {
max-height: 240px;
/* max-h-60 */
overflow-y: auto;
scrollbar-width: none;
}
[data-slot="session-turn-accordion-content"]::-webkit-scrollbar {
display: none;
}
[data-slot="session-turn-response-section"] {
width: calc(100% + 9px);
min-width: 0;
margin-left: -9px;
padding-left: 9px;
}
[data-slot="session-turn-collapsible"] {
gap: 32px;
overflow: visible;
}
[data-slot="session-turn-collapsible-trigger-content"] {
max-width: 100%;
min-width: 0;
[data-slot="session-turn-thinking"] {
display: flex;
align-items: center;
gap: 8px;
color: var(--text-weak);
[data-slot="session-turn-trigger-icon"] {
color: var(--icon-base);
}
font-family: var(--font-family-sans);
font-size: var(--font-size-base);
font-weight: var(--font-weight-medium);
line-height: var(--line-height-large);
min-height: 20px;
[data-component="spinner"] {
width: 12px;
height: 12px;
margin-right: 4px;
width: 16px;
height: 16px;
}
[data-component="icon"] {
width: 14px;
height: 14px;
}
}
[data-slot="session-turn-retry-message"] {
font-weight: 500;
color: var(--syntax-critical);
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
[data-slot="session-turn-retry-seconds"] {
color: var(--text-weak);
}
[data-slot="session-turn-retry-attempt"] {
color: var(--text-weak);
}
[data-slot="session-turn-status-text"] {
overflow: hidden;
text-overflow: ellipsis;
}
[data-slot="session-turn-details-text"] {
font-size: 13px;
/* text-12-medium */
font-weight: 500;
}
.error-card {
@@ -560,50 +63,112 @@
overflow-y: auto;
}
.retry-error-link,
.error-card-link {
color: var(--text-strong);
text-decoration: underline;
}
[data-slot="session-turn-collapsible-content-inner"] {
[data-slot="session-turn-assistant-content"] {
width: 100%;
min-width: 0;
display: flex;
flex-direction: column;
align-self: stretch;
gap: 12px;
margin-left: 12px;
padding-left: 12px;
padding-right: 12px;
border-left: 1px solid var(--border-base);
> :first-child > [data-component="markdown"]:first-child {
margin-top: 0;
}
}
[data-slot="session-turn-permission-parts"] {
[data-slot="session-turn-diffs"] {
width: 100%;
min-width: 0;
}
[data-component="session-turn-diffs-trigger"] {
width: 100%;
display: flex;
align-items: center;
justify-content: flex-start;
gap: 8px;
padding: 0;
}
[data-slot="session-turn-diffs-title"] {
display: inline-flex;
align-items: baseline;
gap: 8px;
}
[data-slot="session-turn-diffs-label"] {
color: var(--text-strong);
font-family: var(--font-family-sans);
font-size: var(--font-size-base);
font-weight: var(--font-weight-medium);
line-height: var(--line-height-large);
}
[data-slot="session-turn-diffs-count"] {
color: var(--text-base);
font-family: var(--font-family-sans);
font-size: var(--font-size-base);
font-weight: var(--font-weight-regular);
line-height: var(--line-height-x-large);
}
[data-slot="session-turn-diffs-meta"] {
display: inline-flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
[data-component="diff-changes"][data-variant="bars"] {
transform: translateY(1px);
}
}
[data-component="session-turn-diffs-content"] {
padding-top: 8px;
display: flex;
flex-direction: column;
gap: 12px;
}
[data-slot="session-turn-question-parts"] {
width: 100%;
min-width: 0;
display: flex;
flex-direction: column;
gap: 12px;
[data-component="session-turn-diff"] {
border: 1px solid var(--border-weaker-base);
border-radius: var(--radius-md);
overflow: clip;
}
[data-slot="session-turn-answered-question-parts"] {
[data-slot="session-turn-diff-header"] {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 6px 10px;
border-bottom: 1px solid var(--border-weaker-base);
}
[data-slot="session-turn-diff-path"] {
display: inline-flex;
min-width: 0;
align-items: baseline;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: var(--font-family-sans);
font-size: var(--font-size-small);
line-height: var(--line-height-large);
}
[data-slot="session-turn-diff-directory"] {
color: var(--text-weak);
}
[data-slot="session-turn-diff-filename"] {
color: var(--text-strong);
font-weight: var(--font-weight-medium);
}
[data-slot="session-turn-diff-view"] {
background-color: var(--surface-inset-base);
width: 100%;
min-width: 0;
display: flex;
flex-direction: column;
gap: 12px;
}
}

View File

@@ -1,31 +1,18 @@
import {
AssistantMessage,
FilePart,
Message as MessageType,
Part as PartType,
type PermissionRequest,
type QuestionRequest,
TextPart,
ToolPart,
} from "@opencode-ai/sdk/v2/client"
import { AssistantMessage, type FileDiff, Message as MessageType, Part as PartType } from "@opencode-ai/sdk/v2/client"
import { useData } from "../context"
import { type UiI18nKey, type UiI18nParams, useI18n } from "../context/i18n"
import { useDiffComponent } from "../context/diff"
import { Binary } from "@opencode-ai/util/binary"
import { createEffect, createMemo, createSignal, For, Match, on, onCleanup, ParentProps, Show, Switch } from "solid-js"
import { Message, Part } from "./message-part"
import { Markdown } from "./markdown"
import { IconButton } from "./icon-button"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { createMemo, createSignal, For, ParentProps, Show } from "solid-js"
import { Dynamic } from "solid-js/web"
import { Message } from "./message-part"
import { Card } from "./card"
import { Button } from "./button"
import { Spinner } from "./spinner"
import { Tooltip } from "./tooltip"
import { createStore } from "solid-js/store"
import { DateTime, DurationUnit, Interval } from "luxon"
import { Collapsible } from "./collapsible"
import { DiffChanges } from "./diff-changes"
import { TextShimmer } from "./text-shimmer"
import { createAutoScroll } from "../hooks"
import { createResizeObserver } from "@solid-primitives/resize-observer"
type Translator = (key: UiI18nKey, params?: UiI18nParams) => string
import { useI18n } from "../context/i18n"
function record(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === "object" && !Array.isArray(value)
@@ -80,117 +67,42 @@ function unwrap(message: string) {
return message
}
function computeStatusFromPart(part: PartType | undefined, t: Translator): string | undefined {
if (!part) return undefined
if (part.type === "tool") {
switch (part.tool) {
case "task":
return t("ui.sessionTurn.status.delegating")
case "todowrite":
case "todoread":
return t("ui.sessionTurn.status.planning")
case "read":
return t("ui.sessionTurn.status.gatheringContext")
case "list":
case "grep":
case "glob":
return t("ui.sessionTurn.status.searchingCodebase")
case "webfetch":
return t("ui.sessionTurn.status.searchingWeb")
case "edit":
case "write":
return t("ui.sessionTurn.status.makingEdits")
case "bash":
return t("ui.sessionTurn.status.runningCommands")
default:
return undefined
}
}
if (part.type === "reasoning") {
const text = part.text ?? ""
const match = text.trimStart().match(/^\*\*(.+?)\*\*/)
if (match) return t("ui.sessionTurn.status.thinkingWithTopic", { topic: match[1].trim() })
return t("ui.sessionTurn.status.thinking")
}
if (part.type === "text") {
return t("ui.sessionTurn.status.gatheringThoughts")
}
return undefined
}
function same<T>(a: readonly T[], b: readonly T[]) {
if (a === b) return true
if (a.length !== b.length) return false
return a.every((x, i) => x === b[i])
}
function isAttachment(part: PartType | undefined) {
if (part?.type !== "file") return false
const mime = (part as FilePart).mime ?? ""
return mime.startsWith("image/") || mime === "application/pdf"
}
function list<T>(value: T[] | undefined | null, fallback: T[]) {
if (Array.isArray(value)) return value
return fallback
}
function AssistantMessageItem(props: {
message: AssistantMessage
responsePartId: string | undefined
hideResponsePart: boolean
hideReasoning: boolean
hidden?: () => readonly { messageID: string; callID: string }[]
}) {
const hidden = new Set(["todowrite", "todoread"])
function visible(part: PartType) {
if (part.type === "tool") {
if (hidden.has(part.tool)) return false
if (part.tool === "question") return part.state.status !== "pending" && part.state.status !== "running"
return true
}
if (part.type === "text") return !!part.text?.trim()
if (part.type === "reasoning") return !!part.text?.trim()
return false
}
function AssistantMessageItem(props: { message: AssistantMessage; showAssistantCopyPartID?: string | null }) {
const data = useData()
const emptyParts: PartType[] = []
const msgParts = createMemo(() => list(data.store.part?.[props.message.id], emptyParts))
const lastTextPart = createMemo(() => {
const parts = msgParts()
for (let i = parts.length - 1; i >= 0; i--) {
const part = parts[i]
if (part?.type === "text") return part as TextPart
}
return undefined
})
const filteredParts = createMemo(() => {
let parts = msgParts()
if (props.hideReasoning) {
parts = parts.filter((part) => part?.type !== "reasoning")
}
if (props.hideResponsePart) {
const responsePartId = props.responsePartId
if (responsePartId && responsePartId === lastTextPart()?.id) {
parts = parts.filter((part) => part?.id !== responsePartId)
}
}
const hidden = props.hidden?.() ?? []
if (hidden.length === 0) return parts
const id = props.message.id
return parts.filter((part) => {
if (part?.type !== "tool") return true
const tool = part as ToolPart
return !hidden.some((h) => h.messageID === id && h.callID === tool.callID)
})
})
return <Message message={props.message} parts={filteredParts()} />
return <Message message={props.message} parts={msgParts()} showAssistantCopyPartID={props.showAssistantCopyPartID} />
}
export function SessionTurn(
props: ParentProps<{
sessionID: string
sessionTitle?: string
messageID: string
lastUserMessageID?: string
stepsExpanded?: boolean
onStepsExpandedToggle?: () => void
onUserInteracted?: () => void
classes?: {
root?: string
@@ -199,16 +111,14 @@ export function SessionTurn(
}
}>,
) {
const i18n = useI18n()
const data = useData()
const i18n = useI18n()
const diffComponent = useDiffComponent()
const emptyMessages: MessageType[] = []
const emptyParts: PartType[] = []
const emptyFiles: FilePart[] = []
const emptyAssistant: AssistantMessage[] = []
const emptyPermissions: PermissionRequest[] = []
const emptyQuestions: QuestionRequest[] = []
const emptyQuestionParts: { part: ToolPart; message: AssistantMessage }[] = []
const emptyDiffs: FileDiff[] = []
const idle = { type: "idle" as const }
const allMessages = createMemo(() => list(data.store.message?.[props.sessionID], emptyMessages))
@@ -256,18 +166,22 @@ export function SessionTurn(
return list(data.store.part?.[msg.id], emptyParts)
})
const attachmentParts = createMemo(() => {
const msgParts = parts()
if (msgParts.length === 0) return emptyFiles
return msgParts.filter((part) => isAttachment(part)) as FilePart[]
})
const diffs = createMemo(() => {
const files = message()?.summary?.diffs
if (!files?.length) return emptyDiffs
const stickyParts = createMemo(() => {
const msgParts = parts()
if (msgParts.length === 0) return emptyParts
if (attachmentParts().length === 0) return msgParts
return msgParts.filter((part) => !isAttachment(part))
const seen = new Set<string>()
return files
.reduceRight<FileDiff[]>((result, diff) => {
if (seen.has(diff.file)) return result
seen.add(diff.file)
result.push(diff)
return result
}, [])
.reverse()
})
const edited = createMemo(() => diffs().length)
const [open, setOpen] = createSignal(false)
const assistantMessages = createMemo(
() => {
@@ -291,9 +205,27 @@ export function SessionTurn(
{ equals: same },
)
const lastAssistantMessage = createMemo(() => assistantMessages().at(-1))
const interrupted = createMemo(() => assistantMessages().some((m) => m.error?.name === "MessageAbortedError"))
const error = createMemo(
() => assistantMessages().find((m) => m.error && m.error.name !== "MessageAbortedError")?.error,
)
const showAssistantCopyPartID = createMemo(() => {
const messages = assistantMessages()
const error = createMemo(() => assistantMessages().find((m) => m.error)?.error)
for (let i = messages.length - 1; i >= 0; i--) {
const message = messages[i]
if (!message) continue
const parts = list(data.store.part?.[message.id], emptyParts)
for (let j = parts.length - 1; j >= 0; j--) {
const part = parts[j]
if (!part || part.type !== "text" || !part.text?.trim()) continue
return part.id
}
}
return undefined
})
const errorText = createMemo(() => {
const msg = error()?.data?.message
if (typeof msg === "string") return unwrap(msg)
@@ -301,314 +233,29 @@ export function SessionTurn(
return unwrap(String(msg))
})
const lastTextPart = createMemo(() => {
const msgs = assistantMessages()
for (let mi = msgs.length - 1; mi >= 0; mi--) {
const msgParts = list(data.store.part?.[msgs[mi].id], emptyParts)
for (let pi = msgParts.length - 1; pi >= 0; pi--) {
const part = msgParts[pi]
if (part?.type === "text") return part as TextPart
}
}
return undefined
})
const hasSteps = createMemo(() => {
for (const m of assistantMessages()) {
const msgParts = list(data.store.part?.[m.id], emptyParts)
for (const p of msgParts) {
if (p?.type === "tool") return true
}
}
return false
})
const permissions = createMemo(() => list(data.store.permission?.[props.sessionID], emptyPermissions))
const nextPermission = createMemo(() => permissions()[0])
const questions = createMemo(() => list(data.store.question?.[props.sessionID], emptyQuestions))
const nextQuestion = createMemo(() => questions()[0])
const hidden = createMemo(() => {
const out: { messageID: string; callID: string }[] = []
const perm = nextPermission()
if (perm?.tool) out.push(perm.tool)
const question = nextQuestion()
if (question?.tool) out.push(question.tool)
return out
})
const answeredQuestionParts = createMemo(() => {
if (props.stepsExpanded) return emptyQuestionParts
if (questions().length > 0) return emptyQuestionParts
const result: { part: ToolPart; message: AssistantMessage }[] = []
for (const msg of assistantMessages()) {
const parts = list(data.store.part?.[msg.id], emptyParts)
for (const part of parts) {
if (part?.type !== "tool") continue
const tool = part as ToolPart
if (tool.tool !== "question") continue
// @ts-expect-error metadata may not exist on all tool states
const answers = tool.state?.metadata?.answers
if (answers && answers.length > 0) {
result.push({ part: tool, message: msg })
}
}
}
return result
})
const shellModePart = createMemo(() => {
const p = parts()
if (p.length === 0) return
if (!p.every((part) => part?.type === "text" && part?.synthetic)) return
const msgs = assistantMessages()
if (msgs.length !== 1) return
const msgParts = list(data.store.part?.[msgs[0].id], emptyParts)
if (msgParts.length !== 1) return
const assistantPart = msgParts[0]
if (assistantPart?.type === "tool" && assistantPart.tool === "bash") return assistantPart
})
const isShellMode = createMemo(() => !!shellModePart())
const rawStatus = createMemo(() => {
const msgs = assistantMessages()
let last: PartType | undefined
let currentTask: ToolPart | undefined
for (let mi = msgs.length - 1; mi >= 0; mi--) {
const msgParts = list(data.store.part?.[msgs[mi].id], emptyParts)
for (let pi = msgParts.length - 1; pi >= 0; pi--) {
const part = msgParts[pi]
if (!part) continue
if (!last) last = part
if (
part.type === "tool" &&
part.tool === "task" &&
part.state &&
"metadata" in part.state &&
part.state.metadata?.sessionId &&
part.state.status === "running"
) {
currentTask = part as ToolPart
break
}
}
if (currentTask) break
}
const taskSessionId =
currentTask?.state && "metadata" in currentTask.state
? (currentTask.state.metadata?.sessionId as string | undefined)
: undefined
if (taskSessionId) {
const taskMessages = list(data.store.message?.[taskSessionId], emptyMessages)
for (let mi = taskMessages.length - 1; mi >= 0; mi--) {
const msg = taskMessages[mi]
if (!msg || msg.role !== "assistant") continue
const msgParts = list(data.store.part?.[msg.id], emptyParts)
for (let pi = msgParts.length - 1; pi >= 0; pi--) {
const part = msgParts[pi]
if (part) return computeStatusFromPart(part, i18n.t)
}
}
}
return computeStatusFromPart(last, i18n.t)
})
const status = createMemo(() => data.store.session_status[props.sessionID] ?? idle)
const working = createMemo(() => status().type !== "idle" && isLastUserMessage())
const retry = createMemo(() => {
// session_status is session-scoped; only show retry on the active (last) turn
if (!isLastUserMessage()) return
const s = status()
if (s.type !== "retry") return
return s
const assistantCopyPartID = createMemo(() => {
if (!isLastUserMessage()) return null
if (status().type !== "idle") return null
return showAssistantCopyPartID() ?? null
})
const isRetryFreeUsageLimitError = createMemo(() => {
const r = retry()
if (!r) return false
return r.message.includes("Free usage exceeded")
})
const response = createMemo(() => lastTextPart()?.text)
const responsePartId = createMemo(() => lastTextPart()?.id)
const hasDiffs = createMemo(() => (message()?.summary?.diffs?.length ?? 0) > 0)
const hideResponsePart = createMemo(() => !working() && !!responsePartId())
const [copied, setCopied] = createSignal(false)
const handleCopy = async () => {
const content = response() ?? ""
if (!content) return
await navigator.clipboard.writeText(content)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
const [rootRef, setRootRef] = createSignal<HTMLDivElement | undefined>()
const [stickyRef, setStickyRef] = createSignal<HTMLDivElement | undefined>()
const updateStickyHeight = (height: number) => {
const root = rootRef()
if (!root) return
const next = Math.ceil(height)
root.style.setProperty("--session-turn-sticky-height", `${next}px`)
}
function duration() {
const msg = message()
if (!msg) return ""
const completed = lastAssistantMessage()?.time.completed
const from = DateTime.fromMillis(msg.time.created)
const to = completed ? DateTime.fromMillis(completed) : DateTime.now()
const interval = Interval.fromDateTimes(from, to)
const unit: DurationUnit[] = interval.length("seconds") > 60 ? ["minutes", "seconds"] : ["seconds"]
const locale = i18n.locale()
const human = interval.toDuration(unit).normalize().reconfigure({ locale }).toHuman({
notation: "compact",
unitDisplay: "narrow",
compactDisplay: "short",
showZeros: false,
})
return locale.startsWith("zh") ? human.replaceAll("、", "") : human
}
const assistantVisible = createMemo(() =>
assistantMessages().reduce((count, message) => {
const parts = list(data.store.part?.[message.id], emptyParts)
return count + parts.filter(visible).length
}, 0),
)
const autoScroll = createAutoScroll({
working,
onUserInteracted: props.onUserInteracted,
overflowAnchor: "auto",
})
createResizeObserver(
() => stickyRef(),
({ height }) => {
updateStickyHeight(height)
},
)
createEffect(() => {
const root = rootRef()
if (!root) return
const sticky = stickyRef()
if (!sticky) {
root.style.setProperty("--session-turn-sticky-height", "0px")
return
}
updateStickyHeight(sticky.getBoundingClientRect().height)
})
const [store, setStore] = createStore({
retrySeconds: 0,
status: rawStatus(),
duration: duration(),
})
createEffect(() => {
const r = retry()
if (!r) {
setStore("retrySeconds", 0)
return
}
const updateSeconds = () => {
const next = r.next
if (next) setStore("retrySeconds", Math.max(0, Math.round((next - Date.now()) / 1000)))
}
updateSeconds()
const timer = setInterval(updateSeconds, 1000)
onCleanup(() => clearInterval(timer))
})
let retryLog = ""
createEffect(() => {
const r = retry()
if (!r) return
const key = `${r.attempt}:${r.next}:${r.message}`
if (key === retryLog) return
retryLog = key
console.warn("[session-turn] retry", {
sessionID: props.sessionID,
messageID: props.messageID,
attempt: r.attempt,
next: r.next,
raw: r.message,
parsed: unwrap(r.message),
})
})
let errorLog = ""
createEffect(() => {
const value = error()?.data?.message
if (value === undefined || value === null) return
const raw = typeof value === "string" ? value : String(value)
if (!raw) return
if (raw === errorLog) return
errorLog = raw
console.warn("[session-turn] assistant-error", {
sessionID: props.sessionID,
messageID: props.messageID,
raw,
parsed: unwrap(raw),
})
})
createEffect(() => {
const update = () => {
setStore("duration", duration())
}
update()
// Only keep ticking while the active (in-progress) turn is running.
if (!working()) return
const timer = setInterval(update, 1000)
onCleanup(() => clearInterval(timer))
})
let lastStatusChange = Date.now()
let statusTimeout: number | undefined
createEffect(() => {
const newStatus = rawStatus()
if (newStatus === store.status || !newStatus) return
const timeSinceLastChange = Date.now() - lastStatusChange
if (timeSinceLastChange >= 2500) {
setStore("status", newStatus)
lastStatusChange = Date.now()
if (statusTimeout) {
clearTimeout(statusTimeout)
statusTimeout = undefined
}
} else {
if (statusTimeout) clearTimeout(statusTimeout)
statusTimeout = setTimeout(() => {
setStore("status", rawStatus())
lastStatusChange = Date.now()
statusTimeout = undefined
}, 2500 - timeSinceLastChange) as unknown as number
}
})
onCleanup(() => {
if (!statusTimeout) return
clearTimeout(statusTimeout)
overflowAnchor: "dynamic",
})
return (
<div data-component="session-turn" class={props.classes?.root} ref={setRootRef}>
<div data-component="session-turn" class={props.classes?.root}>
<div
ref={autoScroll.scrollRef}
onScroll={autoScroll.handleScroll}
@@ -624,197 +271,83 @@ export function SessionTurn(
data-slot="session-turn-message-container"
class={props.classes?.container}
>
<Switch>
<Match when={isShellMode()}>
<Part part={shellModePart()!} message={msg()} defaultOpen />
</Match>
<Match when={true}>
<Show when={attachmentParts().length > 0}>
<div data-slot="session-turn-attachments" aria-live="off">
<Message message={msg()} parts={attachmentParts()} />
</div>
</Show>
<div data-slot="session-turn-sticky" ref={setStickyRef}>
{/* User Message */}
<div data-slot="session-turn-message-content" aria-live="off">
<Message message={msg()} parts={stickyParts()} />
</div>
{/* Trigger (sticky) */}
<Show when={working() || hasSteps()}>
<div data-slot="session-turn-response-trigger">
<Button
data-expandable={assistantMessages().length > 0}
data-slot="session-turn-collapsible-trigger-content"
variant="ghost"
size="small"
onClick={props.onStepsExpandedToggle ?? (() => {})}
aria-expanded={props.stepsExpanded}
>
<Switch>
<Match when={working()}>
<Spinner />
</Match>
<Match when={!props.stepsExpanded}>
<svg
width="10"
height="10"
viewBox="0 0 10 10"
fill="none"
xmlns="http://www.w3.org/2000/svg"
data-slot="session-turn-trigger-icon"
>
<path
d="M8.125 1.875H1.875L5 8.125L8.125 1.875Z"
fill="currentColor"
stroke="currentColor"
stroke-linejoin="round"
/>
</svg>
</Match>
<Match when={props.stepsExpanded}>
<svg
width="10"
height="10"
viewBox="0 0 10 10"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="text-icon-base"
>
<path
d="M8.125 8.125H1.875L5 1.875L8.125 8.125Z"
fill="currentColor"
stroke="currentColor"
stroke-linejoin="round"
/>
</svg>
</Match>
</Switch>
<Switch>
<Match when={retry()}>
<span data-slot="session-turn-retry-message">
{(() => {
const r = retry()
if (!r) return ""
const msg = isRetryFreeUsageLimitError()
? i18n.t("ui.sessionTurn.error.freeUsageExceeded")
: unwrap(r.message)
return msg.length > 60 ? msg.slice(0, 60) + "..." : msg
})()}
</span>
<Show when={isRetryFreeUsageLimitError()}>
<a
href="https://opencode.ai/zen"
target="_blank"
class="retry-error-link"
rel="noopener noreferrer"
>
{i18n.t("ui.sessionTurn.error.addCredits")}
</a>
</Show>
<span data-slot="session-turn-retry-seconds">
· {i18n.t("ui.sessionTurn.retry.retrying")}
{store.retrySeconds > 0
? " " + i18n.t("ui.sessionTurn.retry.inSeconds", { seconds: store.retrySeconds })
: ""}
</span>
<span data-slot="session-turn-retry-attempt">(#{retry()?.attempt})</span>
</Match>
<Match when={working()}>
<span data-slot="session-turn-status-text">
{store.status ?? i18n.t("ui.sessionTurn.status.consideringNextSteps")}
</span>
</Match>
<Match when={props.stepsExpanded}>
<span data-slot="session-turn-status-text">{i18n.t("ui.sessionTurn.steps.hide")}</span>
</Match>
<Match when={!props.stepsExpanded}>
<span data-slot="session-turn-status-text">{i18n.t("ui.sessionTurn.steps.show")}</span>
</Match>
</Switch>
<span aria-hidden="true">·</span>
<span aria-live="off">{store.duration}</span>
</Button>
<div data-slot="session-turn-message-content" aria-live="off">
<Message message={msg()} parts={parts()} interrupted={interrupted()} />
</div>
<Show when={working() && assistantVisible() === 0 && !error()}>
<div data-slot="session-turn-thinking">
<TextShimmer text={i18n.t("ui.sessionTurn.status.thinking")} />
</div>
</Show>
<Show when={assistantMessages().length > 0}>
<div data-slot="session-turn-assistant-content" aria-hidden={working()}>
<For each={assistantMessages()}>
{(assistantMessage) => (
<AssistantMessageItem
message={assistantMessage}
showAssistantCopyPartID={assistantCopyPartID()}
/>
)}
</For>
</div>
</Show>
<Show when={edited() > 0}>
<div data-slot="session-turn-diffs">
<Collapsible open={open()} onOpenChange={setOpen} variant="ghost">
<Collapsible.Trigger>
<div data-component="session-turn-diffs-trigger">
<div data-slot="session-turn-diffs-title">
<span data-slot="session-turn-diffs-label">
{i18n.t("ui.sessionReview.change.modified")}
</span>
<span data-slot="session-turn-diffs-count">
{edited()} {i18n.t(edited() === 1 ? "ui.common.file.one" : "ui.common.file.other")}
</span>
<div data-slot="session-turn-diffs-meta">
<DiffChanges changes={diffs()} variant="bars" />
<Collapsible.Arrow />
</div>
</div>
</div>
</Show>
</div>
{/* Response */}
<Show when={props.stepsExpanded && assistantMessages().length > 0}>
<div data-slot="session-turn-collapsible-content-inner" aria-hidden={working()}>
<For each={assistantMessages()}>
{(assistantMessage) => (
<AssistantMessageItem
message={assistantMessage}
responsePartId={responsePartId()}
hideResponsePart={hideResponsePart()}
hideReasoning={!working()}
hidden={hidden}
/>
)}
</For>
<Show when={error()}>
<Card variant="error" class="error-card">
{errorText()}
</Card>
</Collapsible.Trigger>
<Collapsible.Content>
<Show when={open()}>
<div data-component="session-turn-diffs-content">
<For each={diffs()}>
{(diff) => (
<div data-component="session-turn-diff">
<div data-slot="session-turn-diff-header">
<span data-slot="session-turn-diff-path">
<Show when={diff.file.includes("/")}>
<span data-slot="session-turn-diff-directory">{getDirectory(diff.file)}</span>
</Show>
<span data-slot="session-turn-diff-filename">{getFilename(diff.file)}</span>
</span>
<span data-slot="session-turn-diff-changes">
<DiffChanges changes={diff} />
</span>
</div>
<div data-slot="session-turn-diff-view">
<Dynamic
component={diffComponent}
before={{ name: diff.file, contents: diff.before }}
after={{ name: diff.file, contents: diff.after }}
/>
</div>
</div>
)}
</For>
</div>
</Show>
</div>
</Show>
<Show when={!props.stepsExpanded && answeredQuestionParts().length > 0}>
<div data-slot="session-turn-answered-question-parts">
<For each={answeredQuestionParts()}>
{({ part, message }) => <Part part={part} message={message} />}
</For>
</div>
</Show>
{/* Response */}
<div class="sr-only" aria-live="polite">
{!working() && response() ? response() : ""}
</div>
<Show when={!working() && response()}>
<div data-slot="session-turn-summary-section">
<div data-slot="session-turn-summary-header">
<div data-slot="session-turn-summary-title-row">
<h2 data-slot="session-turn-summary-title">{i18n.t("ui.sessionTurn.summary.response")}</h2>
<Show when={response()}>
<div data-slot="session-turn-response-copy-wrapper">
<Tooltip
value={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")}
placement="top"
gutter={8}
>
<IconButton
icon={copied() ? "check" : "copy"}
size="small"
variant="secondary"
onMouseDown={(e) => e.preventDefault()}
onClick={(event) => {
event.stopPropagation()
handleCopy()
}}
aria-label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")}
/>
</Tooltip>
</div>
</Show>
</div>
<div data-slot="session-turn-response">
<Markdown
data-slot="session-turn-markdown"
data-diffs={hasDiffs()}
text={response() ?? ""}
cacheKey={responsePartId()}
/>
</div>
</div>
</div>
</Show>
<Show when={error() && !props.stepsExpanded}>
<Card variant="error" class="error-card">
{errorText()}
</Card>
</Show>
</Match>
</Switch>
</Collapsible.Content>
</Collapsible>
</div>
</Show>
<Show when={error()}>
<Card variant="error" class="error-card">
{errorText()}
</Card>
</Show>
</div>
)}
</Show>

View File

@@ -0,0 +1,43 @@
[data-component="text-shimmer"] {
--text-shimmer-step: 45ms;
--text-shimmer-duration: 1200ms;
}
[data-component="text-shimmer"] [data-slot="text-shimmer-char"] {
white-space: pre;
color: inherit;
}
[data-component="text-shimmer"][data-active="true"] [data-slot="text-shimmer-char"] {
animation-name: text-shimmer-char;
animation-duration: var(--text-shimmer-duration);
animation-iteration-count: infinite;
animation-timing-function: ease-in-out;
animation-delay: calc(var(--text-shimmer-step) * var(--text-shimmer-index));
}
@keyframes text-shimmer-char {
0%,
100% {
color: var(--text-weaker);
}
30% {
color: var(--text-weak);
}
55% {
color: var(--text-base);
}
75% {
color: var(--text-strong);
}
}
@media (prefers-reduced-motion: reduce) {
[data-component="text-shimmer"] [data-slot="text-shimmer-char"] {
animation: none !important;
color: inherit;
}
}

View File

@@ -0,0 +1,36 @@
import { For, createMemo, type ValidComponent } from "solid-js"
import { Dynamic } from "solid-js/web"
export const TextShimmer = <T extends ValidComponent = "span">(props: {
text: string
class?: string
as?: T
active?: boolean
stepMs?: number
durationMs?: number
}) => {
const chars = createMemo(() => Array.from(props.text))
const active = () => props.active ?? true
return (
<Dynamic
component={props.as || "span"}
data-component="text-shimmer"
data-active={active()}
class={props.class}
aria-label={props.text}
style={{
"--text-shimmer-step": `${props.stepMs ?? 45}ms`,
"--text-shimmer-duration": `${props.durationMs ?? 1200}ms`,
}}
>
<For each={chars()}>
{(char, index) => (
<span data-slot="text-shimmer-char" aria-hidden="true" style={{ "--text-shimmer-index": `${index()}` }}>
{char}
</span>
)}
</For>
</Dynamic>
)
}