mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-15 07:48:24 -04:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0cb7fcc49a | |||
| c49f71185d | |||
| f75244d795 | |||
| 1d44d56d9c |
@@ -19,6 +19,13 @@
|
||||
- Expose the meaningful state dimensions through story keybindings and list them in `StoryFooter`; include a reset command when combinations can leave the fixture in a confusing state.
|
||||
- Run a specific story with `OPENCODE_STORY=<story-id> bun run dev:live` from the development worktree, and exercise narrow and wide terminal sizes when layout is relevant.
|
||||
|
||||
## TUI Theme Tokens
|
||||
|
||||
- Choose theme tokens by semantic role, not by their current color. Do not use raw `theme.hue` values or borrow an unrelated semantic token to achieve a preferred appearance.
|
||||
- Use `text.feedback` and `background.feedback` only for outcome or status feedback such as errors, warnings, success messages, and informational messages. Use `formfield` states for form-control text, ordinals, and selection markers, and `action` states for actions.
|
||||
- If the theme does not expose a token for the required semantic role, extend the theme schema, defaults, resolution, and types with that role before using it in a component. Do not repurpose the nearest-looking existing token.
|
||||
- When changing the public theme token surface, verify the built-in light and dark defaults and the custom-theme fallback path in addition to the affected TUI component.
|
||||
|
||||
## Branch Names
|
||||
|
||||
Use a short branch name of at most three words, separated by hyphens. Do not use slashes or type prefixes such as `feat/` or `fix/`.
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# TUI UI Experiments
|
||||
|
||||
- Before implementing a visual behavior as an experiment, add a fixture-driven story under `src/feature-plugins/system/storybook` that renders the real production component.
|
||||
- Put the current treatment and meaningfully different variants in the story. Expose replay and tuning controls in `StoryFooter`, including reset when values are adjustable.
|
||||
- Let the user choose or tune a variant in the story before selecting production defaults.
|
||||
- After selection, register the behavior in `src/component/dialog-experiments.tsx` and gate it with `config.data.experimental?.<id> === true`; experiments must not change default behavior.
|
||||
- Treat tuning-only stories as local scaffolding and remove them and their registration before committing. Commit a story only when the user explicitly wants it retained as a reusable regression fixture.
|
||||
- Use OpenCode Drive with a simulated LLM for deterministic turn/session behavior. Do not invoke a real model only to verify TUI behavior.
|
||||
- Run the story with `OPENCODE_STORY=<story-id> bun run dev:live` and exercise relevant wide and narrow terminal sizes.
|
||||
@@ -0,0 +1,55 @@
|
||||
import { RGBA } from "@opentui/core"
|
||||
import { useTerminalDimensions } from "@opentui/solid"
|
||||
import { createEffect, on, onMount, Show } from "solid-js"
|
||||
import { tint } from "../theme/color"
|
||||
import { createAnimatable, tween } from "../ui/animation"
|
||||
|
||||
export type AssistantSummaryFlash = {
|
||||
trigger: number
|
||||
duration: number
|
||||
intensity: number
|
||||
}
|
||||
|
||||
export function AssistantSummary(props: {
|
||||
agent: string
|
||||
model: string
|
||||
duration?: string
|
||||
interrupted?: boolean
|
||||
agentColor: RGBA
|
||||
subduedColor: RGBA
|
||||
flashColor: RGBA
|
||||
animations: boolean
|
||||
flash?: AssistantSummaryFlash
|
||||
}) {
|
||||
const dimensions = useTerminalDimensions()
|
||||
const flash = createAnimatable(
|
||||
{ level: 0 },
|
||||
{
|
||||
enabled: () => props.animations,
|
||||
transition: tween({ duration: props.flash?.duration ?? 0.32 }),
|
||||
},
|
||||
)
|
||||
const run = () => {
|
||||
if (!props.flash || !props.animations || props.flash.trigger === 0) return
|
||||
flash.jump({ level: props.flash.intensity })
|
||||
flash.animate({ level: 0 })
|
||||
}
|
||||
onMount(run)
|
||||
createEffect(on(() => props.flash?.trigger, run, { defer: true }))
|
||||
const color = (resting: RGBA) => tint(resting, props.flashColor, flash.value().level)
|
||||
|
||||
return (
|
||||
<text>
|
||||
<span style={{ fg: color(props.agentColor) }}>{props.agent}</span>
|
||||
<Show when={dimensions().width >= 28}>
|
||||
<span style={{ fg: color(props.subduedColor) }}> · {props.model}</span>
|
||||
</Show>
|
||||
<Show when={props.duration && (dimensions().width < 28 || dimensions().width >= 36)}>
|
||||
<span style={{ fg: color(props.subduedColor) }}> · {props.duration}</span>
|
||||
</Show>
|
||||
<Show when={props.interrupted}>
|
||||
<span style={{ fg: color(props.subduedColor) }}> · interrupted</span>
|
||||
</Show>
|
||||
</text>
|
||||
)
|
||||
}
|
||||
@@ -19,6 +19,11 @@ export const experiments: Experiment[] = [
|
||||
title: "Remember tab scroll",
|
||||
description: "Keep each open tab's reading position and show a shortcut back to the bottom.",
|
||||
},
|
||||
{
|
||||
id: "turn_summary_flash",
|
||||
title: "Turn summary flash",
|
||||
description: "Brighten the agent, model, and duration when a turn completes, then fade to their resting colors.",
|
||||
},
|
||||
]
|
||||
|
||||
export function DialogExperiments() {
|
||||
|
||||
@@ -307,6 +307,7 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
||||
const [addHovered, setAddHovered] = createSignal(false)
|
||||
const marquee = createTabMarquee(animations)
|
||||
const hovered = marquee.hovered
|
||||
// OpenTUI captures the first drag target, which may differ from the tab pressed on a fast move.
|
||||
const [dragging, setDragging] = createSignal<string>()
|
||||
const [preview, setPreview] = createSignal<{ sessionID: string; index: number }>()
|
||||
const [contextMenu, setContextMenu] = createSignal<TabContextMenuState>()
|
||||
@@ -343,6 +344,9 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
||||
const itemStatus = (tab: SessionTab) => statuses().get(tab.sessionID)!
|
||||
let rail: { screenX: number; screenY: number } | undefined
|
||||
let scroll: ScrollBoxRenderable | undefined
|
||||
let didDrag = false
|
||||
// A captured drag ends with a synthetic up on its drop target; do not turn that into a click.
|
||||
let suppressClick = false
|
||||
|
||||
createEffect(() => {
|
||||
const pending = preview()
|
||||
@@ -364,6 +368,29 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
||||
}
|
||||
})
|
||||
|
||||
const release = () => {
|
||||
const source = dragging()
|
||||
if (!source) return
|
||||
if (didDrag) suppressClick = true
|
||||
setDragging(undefined)
|
||||
const pending = preview()
|
||||
if (pending?.sessionID === source) tabs.move(pending.sessionID, pending.index)
|
||||
tabs.select(source)
|
||||
}
|
||||
|
||||
const drag = (event: MouseEvent) => {
|
||||
if (!rail) return
|
||||
const source = dragging()
|
||||
if (!source) return
|
||||
didDrag = true
|
||||
const target = Math.max(
|
||||
0,
|
||||
Math.min(tabs.tabs().length - 1, Math.floor((event.y - rail.screenY - 1 + (scroll?.scrollTop ?? 0)) / 3)),
|
||||
)
|
||||
const sourceIndex = items().findIndex((item) => item.sessionID === source)
|
||||
if (target !== sourceIndex && preview()?.index !== target) setPreview({ sessionID: source, index: target })
|
||||
}
|
||||
|
||||
return (
|
||||
<box
|
||||
ref={(element) => (rail = element)}
|
||||
@@ -375,6 +402,15 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
||||
paddingTop={1}
|
||||
backgroundColor={theme.background.default}
|
||||
onMouseOut={marquee.leaveHovered}
|
||||
onMouseUp={(event) => {
|
||||
if (event.button === RIGHT_MOUSE_BUTTON) return
|
||||
release()
|
||||
if (!didDrag) return
|
||||
didDrag = false
|
||||
queueMicrotask(() => (suppressClick = false))
|
||||
}}
|
||||
onMouseDrag={drag}
|
||||
onMouseDragEnd={release}
|
||||
>
|
||||
<scrollbox ref={(element) => (scroll = element)} flexGrow={1} scrollbarOptions={{ visible: false }}>
|
||||
<box flexShrink={0} flexDirection="column" gap={1}>
|
||||
@@ -522,12 +558,6 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
||||
: color
|
||||
return separator ? tint(faded, pulseBackground(), 0.55) : faded
|
||||
}
|
||||
const release = () => {
|
||||
setDragging(undefined)
|
||||
const pending = preview()
|
||||
if (pending?.sessionID === tab.sessionID) tabs.move(pending.sessionID, pending.index)
|
||||
tabs.select(tab.sessionID)
|
||||
}
|
||||
return (
|
||||
<box
|
||||
height={2}
|
||||
@@ -539,6 +569,7 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
||||
onMouseOut={() => marquee.leave(tab.sessionID)}
|
||||
onMouseDown={(event) => {
|
||||
if (event.button === RIGHT_MOUSE_BUTTON) {
|
||||
didDrag = false
|
||||
setDragging(undefined)
|
||||
if (!rail) return
|
||||
setContextMenu({
|
||||
@@ -551,26 +582,10 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
||||
event.stopPropagation()
|
||||
return
|
||||
}
|
||||
didDrag = false
|
||||
marquee.enter(tab.sessionID, title(), hoveredTitleWidth())
|
||||
setDragging(tab.sessionID)
|
||||
}}
|
||||
onMouseUp={(event) => {
|
||||
if (event.button === RIGHT_MOUSE_BUTTON) return
|
||||
release()
|
||||
}}
|
||||
onMouseDrag={(event) => {
|
||||
if (!rail) return
|
||||
const target = Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
tabs.tabs().length - 1,
|
||||
Math.floor((event.y - rail.screenY - 1 + (scroll?.scrollTop ?? 0)) / 3),
|
||||
),
|
||||
)
|
||||
if (target !== index() && preview()?.index !== target)
|
||||
setPreview({ sessionID: tab.sessionID, index: target })
|
||||
}}
|
||||
onMouseDragEnd={release}
|
||||
>
|
||||
<TabPulse
|
||||
top={-1}
|
||||
@@ -677,8 +692,14 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
||||
selectable={false}
|
||||
onMouseOver={() => setCloseHovered(true)}
|
||||
onMouseOut={() => setCloseHovered(false)}
|
||||
onMouseDown={(event) => {
|
||||
if (event.button === RIGHT_MOUSE_BUTTON || hovered() !== tab.sessionID) return
|
||||
didDrag = false
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onMouseUp={(event) => {
|
||||
if (event.button === RIGHT_MOUSE_BUTTON) return
|
||||
if (suppressClick) return
|
||||
if (hovered() !== tab.sessionID) return
|
||||
event.stopPropagation()
|
||||
tabs.close(tab.sessionID)
|
||||
@@ -737,6 +758,8 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
||||
onMouseOver={() => setAddHovered(true)}
|
||||
onMouseOut={() => setAddHovered(false)}
|
||||
onMouseDown={(event: MouseEvent) => {
|
||||
didDrag = false
|
||||
setDragging(undefined)
|
||||
if (event.button !== RIGHT_MOUSE_BUTTON) return
|
||||
if (!rail) return
|
||||
setContextMenu({ x: event.x, y: event.y })
|
||||
@@ -745,6 +768,7 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
||||
}}
|
||||
onMouseUp={(event: MouseEvent) => {
|
||||
if (event.button === RIGHT_MOUSE_BUTTON) return
|
||||
if (suppressClick) return
|
||||
if (!newTab()) tabs.add?.()
|
||||
}}
|
||||
>
|
||||
@@ -774,6 +798,7 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
||||
selectable={false}
|
||||
onMouseUp={(event) => {
|
||||
if (event.button === RIGHT_MOUSE_BUTTON) return
|
||||
if (suppressClick) return
|
||||
if (!addHovered()) return
|
||||
event.stopPropagation()
|
||||
tabs.close()
|
||||
@@ -803,6 +828,7 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
|
||||
const [addHovered, setAddHovered] = createSignal(false)
|
||||
const marquee = createTabMarquee(animations)
|
||||
const hovered = marquee.hovered
|
||||
// OpenTUI captures the first drag target, which may differ from the tab pressed on a fast move.
|
||||
const [dragging, setDragging] = createSignal<string>()
|
||||
// A drag reorders a local preview and persists one move on release instead of writing
|
||||
// per slot crossing; the preview holds after release until the store reflects the move,
|
||||
@@ -810,6 +836,9 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
|
||||
const [preview, setPreview] = createSignal<{ sessionID: string; index: number }>()
|
||||
const [contextMenu, setContextMenu] = createSignal<TabContextMenuState>()
|
||||
let strip: { screenX: number; screenY: number } | undefined
|
||||
let didDrag = false
|
||||
// A captured drag ends with a synthetic up on its drop target; do not turn that into a click.
|
||||
let suppressClick = false
|
||||
const hueStep = () => (mode() === "light" ? 800 : 200)
|
||||
const accent = () => theme.hue.accent[hueStep()]
|
||||
const activeNumber = () => theme.hue.interactive[hueStep()]
|
||||
@@ -931,6 +960,29 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
|
||||
return layout().before + layout().widths.length - 1
|
||||
}
|
||||
|
||||
const release = () => {
|
||||
const source = dragging()
|
||||
if (!source) return
|
||||
if (didDrag) suppressClick = true
|
||||
setDragging(undefined)
|
||||
const pending = preview()
|
||||
if (pending?.sessionID === source) tabs.move(pending.sessionID, pending.index)
|
||||
if (source === NEW_SESSION_TAB.sessionID) return
|
||||
tabs.select(source)
|
||||
}
|
||||
|
||||
const drag = (event: MouseEvent) => {
|
||||
const source = dragging()
|
||||
if (!source || source === NEW_SESSION_TAB.sessionID) return
|
||||
didDrag = true
|
||||
const slot = slotAt(event.x)
|
||||
const target = slot === undefined ? undefined : Math.min(slot, tabs.tabs().length - 1)
|
||||
const sourceIndex = items().findIndex((item) => item.sessionID === source)
|
||||
if (target !== undefined && target !== sourceIndex && preview()?.index !== target) {
|
||||
setPreview({ sessionID: source, index: target })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<box
|
||||
ref={(element) => (strip = element)}
|
||||
@@ -940,6 +992,15 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
|
||||
flexDirection="row"
|
||||
zIndex={1}
|
||||
onMouseOut={marquee.leaveHovered}
|
||||
onMouseUp={(event) => {
|
||||
if (event.button === RIGHT_MOUSE_BUTTON) return
|
||||
release()
|
||||
if (!didDrag) return
|
||||
didDrag = false
|
||||
queueMicrotask(() => (suppressClick = false))
|
||||
}}
|
||||
onMouseDrag={drag}
|
||||
onMouseDragEnd={release}
|
||||
renderAfter={function (buffer) {
|
||||
const x = Math.max(0, this.screenX)
|
||||
const y = this.screenY + this.height
|
||||
@@ -1051,15 +1112,6 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
|
||||
}
|
||||
const bold = () => (selected() || dragged() ? TextAttributes.BOLD : undefined)
|
||||
const closeColor = () => tint(theme.text.subdued, theme.text.default, 0.6)
|
||||
// Releasing a drag (or a plain click) selects the tab, matching browser tab strips and
|
||||
// keeping sloppy clicks indistinguishable from clean ones.
|
||||
const release = () => {
|
||||
setDragging(undefined)
|
||||
const pending = preview()
|
||||
if (pending?.sessionID === tab.sessionID) tabs.move(pending.sessionID, pending.index)
|
||||
if (tab === NEW_SESSION_TAB) return
|
||||
tabs.select(tab.sessionID)
|
||||
}
|
||||
return (
|
||||
<box
|
||||
width={width()}
|
||||
@@ -1070,6 +1122,7 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
|
||||
onMouseOut={() => marquee.leave(tab.sessionID)}
|
||||
onMouseDown={(event) => {
|
||||
if (event.button === RIGHT_MOUSE_BUTTON) {
|
||||
didDrag = false
|
||||
setDragging(undefined)
|
||||
setContextMenu({
|
||||
x: event.x,
|
||||
@@ -1081,20 +1134,10 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
|
||||
event.stopPropagation()
|
||||
return
|
||||
}
|
||||
didDrag = false
|
||||
marquee.enter(tab.sessionID, title(), hoveredTitleWidth())
|
||||
setDragging(tab.sessionID)
|
||||
}}
|
||||
onMouseUp={(event) => {
|
||||
if (event.button === RIGHT_MOUSE_BUTTON) return
|
||||
release()
|
||||
}}
|
||||
onMouseDrag={(event) => {
|
||||
if (tab === NEW_SESSION_TAB) return
|
||||
const slot = slotAt(event.x)
|
||||
if (slot !== undefined && slot !== tabNumber() - 1)
|
||||
setPreview({ sessionID: tab.sessionID, index: slot })
|
||||
}}
|
||||
onMouseDragEnd={release}
|
||||
>
|
||||
<TabPulse
|
||||
enabled={animations()}
|
||||
@@ -1140,8 +1183,14 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
|
||||
selectable={false}
|
||||
onMouseOver={() => setCloseHovered(true)}
|
||||
onMouseOut={() => setCloseHovered(false)}
|
||||
onMouseDown={(event) => {
|
||||
if (event.button === RIGHT_MOUSE_BUTTON || hovered() !== tab.sessionID) return
|
||||
didDrag = false
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onMouseUp={(event) => {
|
||||
if (event.button === RIGHT_MOUSE_BUTTON) return
|
||||
if (suppressClick) return
|
||||
// The close mark only renders while hovered; without motion events a click can
|
||||
// land here first, and must select the tab instead of closing it invisibly.
|
||||
if (hovered() !== tab.sessionID) return
|
||||
@@ -1170,6 +1219,8 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
|
||||
onMouseOver={() => setAddHovered(true)}
|
||||
onMouseOut={() => setAddHovered(false)}
|
||||
onMouseDown={(event) => {
|
||||
didDrag = false
|
||||
setDragging(undefined)
|
||||
if (event.button !== RIGHT_MOUSE_BUTTON) return
|
||||
setContextMenu({ x: event.x, y: event.y })
|
||||
event.preventDefault()
|
||||
@@ -1177,6 +1228,7 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
|
||||
}}
|
||||
onMouseUp={(event) => {
|
||||
if (event.button === RIGHT_MOUSE_BUTTON) return
|
||||
if (suppressClick) return
|
||||
tabs.add?.()
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -904,7 +904,13 @@ export function FormPrompt(props: {
|
||||
<text
|
||||
width={4}
|
||||
flexShrink={0}
|
||||
fg={picked() ? theme.text.feedback.success.default : theme.text.subdued}
|
||||
fg={
|
||||
active()
|
||||
? theme.text.formfield.focused
|
||||
: picked()
|
||||
? theme.text.formfield.selected
|
||||
: theme.text.subdued
|
||||
}
|
||||
>
|
||||
[{picked() ? "✓" : " "}]
|
||||
</text>
|
||||
@@ -914,7 +920,7 @@ export function FormPrompt(props: {
|
||||
</text>
|
||||
</box>
|
||||
<Show when={!multi()}>
|
||||
<text fg={theme.text.feedback.success.default}>{picked() ? " ✓" : ""}</text>
|
||||
<text fg={theme.text.formfield.selected}>{picked() ? " ✓" : ""}</text>
|
||||
</Show>
|
||||
</box>
|
||||
<Show when={row.description}>
|
||||
@@ -953,7 +959,13 @@ export function FormPrompt(props: {
|
||||
<text
|
||||
width={4}
|
||||
flexShrink={0}
|
||||
fg={customChecked() ? theme.text.feedback.success.default : theme.text.subdued}
|
||||
fg={
|
||||
other()
|
||||
? theme.text.formfield.focused
|
||||
: customChecked()
|
||||
? theme.text.formfield.selected
|
||||
: theme.text.subdued
|
||||
}
|
||||
>
|
||||
[{customChecked() ? "✓" : " "}]
|
||||
</text>
|
||||
@@ -966,7 +978,7 @@ export function FormPrompt(props: {
|
||||
{input() || "Type your own answer"}
|
||||
</text>
|
||||
<Show when={!multi() && customPicked()}>
|
||||
<text fg={theme.text.feedback.success.default}>✓</text>
|
||||
<text fg={theme.text.formfield.selected}>✓</text>
|
||||
</Show>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@ import {
|
||||
type SessionRow,
|
||||
} from "./rows"
|
||||
import { switchLabel } from "../../util/model"
|
||||
import { AssistantSummary } from "../../component/assistant-summary"
|
||||
import { findMessageBoundary, messageNavigationSlack } from "./message-navigation"
|
||||
import { stringWidth } from "../../util/string-width"
|
||||
import { useArgs } from "../../context/args"
|
||||
@@ -1334,7 +1335,7 @@ function SessionRowView(props: SessionRowViewProps) {
|
||||
<Show when={props.message(row().messageID)}>
|
||||
{(message) => (
|
||||
<Show when={message().type === "assistant"}>
|
||||
<AssistantFooter message={message() as SessionMessageAssistant} />
|
||||
<AssistantFooter message={message() as SessionMessageAssistant} flash={row().flash} />
|
||||
</Show>
|
||||
)}
|
||||
</Show>
|
||||
@@ -1794,11 +1795,10 @@ function SessionGroupView(props: {
|
||||
)
|
||||
}
|
||||
|
||||
function AssistantFooter(props: { message: SessionMessageAssistant }) {
|
||||
function AssistantFooter(props: { message: SessionMessageAssistant; flash?: true }) {
|
||||
const ctx = use()
|
||||
const data = useData()
|
||||
const local = useLocal()
|
||||
const dimensions = useTerminalDimensions()
|
||||
const theme = useTheme("elevated")
|
||||
const model = createMemo(
|
||||
() =>
|
||||
@@ -1818,20 +1818,21 @@ function AssistantFooter(props: { message: SessionMessageAssistant }) {
|
||||
</Show>
|
||||
<AssistantRetry retry={props.message.retry} />
|
||||
<box paddingLeft={3} marginTop={props.message.retry || (props.message.error && !interrupted()) ? 1 : 0}>
|
||||
<text>
|
||||
<span style={{ fg: props.message.error ? theme.text.subdued : local.agent.color(props.message.agent) }}>
|
||||
{Locale.titlecase(props.message.agent)}
|
||||
</span>
|
||||
<Show when={dimensions().width >= 28}>
|
||||
<span style={{ fg: theme.text.subdued }}> · {model()}</span>
|
||||
</Show>
|
||||
<Show when={duration() && (dimensions().width < 28 || dimensions().width >= 36)}>
|
||||
<span style={{ fg: theme.text.subdued }}> · {Locale.duration(duration())}</span>
|
||||
</Show>
|
||||
<Show when={interrupted()}>
|
||||
<span style={{ fg: theme.text.subdued }}> · interrupted</span>
|
||||
</Show>
|
||||
</text>
|
||||
<AssistantSummary
|
||||
agent={Locale.titlecase(props.message.agent)}
|
||||
model={model()}
|
||||
duration={duration() ? Locale.duration(duration()) : undefined}
|
||||
interrupted={interrupted()}
|
||||
agentColor={props.message.error ? theme.text.subdued : local.agent.color(props.message.agent)}
|
||||
subduedColor={theme.text.subdued}
|
||||
flashColor={theme.text.default}
|
||||
animations={ctx.config.animations ?? true}
|
||||
flash={
|
||||
props.flash && ctx.config.experimental?.turn_summary_flash === true
|
||||
? { trigger: 1, duration: 0.8, intensity: 0.7 }
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</box>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -32,7 +32,7 @@ export type SessionRow =
|
||||
pending: PartRef[]
|
||||
completed: boolean
|
||||
}
|
||||
| { type: "assistant-footer"; messageID: string }
|
||||
| { type: "assistant-footer"; messageID: string; flash?: true }
|
||||
| { type: "turn-usage"; messageIDs: string[]; previousCache?: CacheUsage }
|
||||
|
||||
export function createSessionRows(sessionID: Accessor<string>, onSynced?: (sessionID: string) => void) {
|
||||
@@ -176,13 +176,13 @@ export function createSessionRows(sessionID: Accessor<string>, onSynced?: (sessi
|
||||
}),
|
||||
)
|
||||
|
||||
const appendFooter = (messageID: string) =>
|
||||
const appendFooter = (messageID: string, flash?: true) =>
|
||||
setRows(
|
||||
produce((draft) => {
|
||||
if (draft.some((row) => row.type === "assistant-footer" && row.messageID === messageID)) return
|
||||
const index = queuedStart(draft)
|
||||
completePrevious(draft, index)
|
||||
draft.splice(index, 0, { type: "assistant-footer", messageID })
|
||||
draft.splice(index, 0, { type: "assistant-footer", messageID, ...(flash ? { flash } : {}) })
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -268,12 +268,12 @@ export function createSessionRows(sessionID: Accessor<string>, onSynced?: (sessi
|
||||
}),
|
||||
data.on("session.step.ended", (event) => {
|
||||
if (event.data.sessionID !== sessionID() || ["tool-calls", "unknown"].includes(event.data.finish)) return
|
||||
appendFooter(event.data.assistantMessageID)
|
||||
appendFooter(event.data.assistantMessageID, true)
|
||||
if (turnTokens()) setRows(reconcile(reduce()))
|
||||
}),
|
||||
data.on("session.step.failed", (event) => {
|
||||
if (event.data.sessionID !== sessionID()) return
|
||||
appendFooter(event.data.assistantMessageID)
|
||||
appendFooter(event.data.assistantMessageID, true)
|
||||
if (turnTokens()) setRows(reconcile(reduce()))
|
||||
}),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user