tui: improve command dialog, shimmer animation, and message handling
Some checks are pending
format / format (push) Waiting to run
snapshot / publish (push) Waiting to run

This commit is contained in:
Dax Raad 2025-09-24 01:15:58 -04:00
parent 0b5730d4dd
commit f9bba494b5
5 changed files with 17 additions and 26 deletions

View file

@ -56,7 +56,7 @@ export function CommandProvider(props: ParentProps) {
const dialog = useDialog()
useKeyboard((evt) => {
if (evt.name === "k" && evt.ctrl) {
if (evt.name === "p" && evt.ctrl) {
dialog.replace(() => <DialogCommand options={value.options} />)
return
}

View file

@ -207,7 +207,7 @@ export function Prompt(props: PromptProps) {
</Match>
<Match when={true}>
<text live>
ctrl+k <span style={{ fg: Theme.textMuted }}>commands</span>
ctrl+p <span style={{ fg: Theme.textMuted }}>commands</span>
</text>
</Match>
</Switch>
@ -421,7 +421,7 @@ function Autocomplete(props: {
return store.visible
},
onInput(value: string) {
if (value.length <= store.index) hide()
if (store.visible && value.length <= store.index) hide()
},
onKeyDown(e: ParsedKey) {
if (store.visible) {

View file

@ -138,13 +138,17 @@ export function Session() {
flexGrow={1}
>
<For each={messages()}>
{(message) => (
{(message, index) => (
<Switch>
<Match when={message.role === "user"}>
<UserMessage message={message as UserMessage} parts={sync.data.part[message.id] ?? []} />
</Match>
<Match when={message.role === "assistant"}>
<AssistantMessage message={message as AssistantMessage} parts={sync.data.part[message.id] ?? []} />
<AssistantMessage
last={index() === messages().length - 1}
message={message as AssistantMessage}
parts={sync.data.part[message.id] ?? []}
/>
</Match>
</Switch>
)}
@ -193,7 +197,7 @@ function UserMessage(props: { message: UserMessage; parts: Part[] }) {
)
}
function AssistantMessage(props: { message: AssistantMessage; parts: Part[] }) {
function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; last: boolean }) {
const local = useLocal()
return (
<>
@ -221,7 +225,7 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[] }) {
<text fg={Theme.textMuted}>{props.message.error?.data.message}</text>
</box>
</Show>
<Show when={!props.message.time.completed}>
<Show when={!props.message.time.completed || (props.last && props.message.finish === "tool-calls")}>
<box
paddingLeft={2}
marginTop={1}
@ -262,7 +266,7 @@ function resize(el: BoxRenderable) {
const index = children.indexOf(el)
const previous = children[index - 1]
if (!previous) return
if (previous.height > 1) {
if (previous.height > 1 || previous.marginTop === 1) {
el.marginTop = 1
return
}

View file

@ -7,11 +7,11 @@ export type ShimmerProps = {
color: string
}
const DURATION = 200
const DURATION = 2_500
export function Shimmer(props: ShimmerProps) {
const timeline = createComponentTimeline({
duration: (props.text.length + 1) * DURATION,
duration: DURATION,
loop: true,
})
const characters = props.text.split("")
@ -23,12 +23,12 @@ export function Shimmer(props: ShimmerProps) {
{ shimmer: 0.4 },
{ shimmer: 1 },
{
duration: DURATION,
duration: DURATION / (props.text.length + 1),
ease: "linear",
alternate: true,
loop: 2,
},
(i * DURATION) / 2,
(i * (DURATION / (props.text.length + 1))) / 2,
),
)

View file

@ -1008,12 +1008,6 @@ export namespace SessionPrompt {
throw value.error
case "start-step":
await Session.updatePart({
id: Identifier.ascending("part"),
messageID: assistantMsg.id,
sessionID: assistantMsg.sessionID,
type: "step-start",
})
snapshot = await Snapshot.track()
break
@ -1021,14 +1015,6 @@ export namespace SessionPrompt {
const usage = Session.getUsage(input.model, value.usage, value.providerMetadata)
assistantMsg.cost += usage.cost
assistantMsg.tokens = usage.tokens
await Session.updatePart({
id: Identifier.ascending("part"),
messageID: assistantMsg.id,
sessionID: assistantMsg.sessionID,
type: "step-finish",
tokens: usage.tokens,
cost: usage.cost,
})
await Session.updateMessage(assistantMsg)
if (snapshot) {
const patch = await Snapshot.patch(snapshot)
@ -1095,6 +1081,7 @@ export namespace SessionPrompt {
log.error("process", {
error: e,
})
assistantMsg.finish = "error"
switch (true) {
case e instanceof DOMException && e.name === "AbortError":
assistantMsg.error = new MessageV2.AbortedError(