fix(app): clear stale running session indicators
Use live session status for the running UI and finalize assistant messages when prompt setup fails so crashed runs do not leave sessions stuck as active.
This commit is contained in:
parent
2fc06c5a17
commit
060f482eb2
10 changed files with 214 additions and 114 deletions
|
|
@ -15,6 +15,7 @@ import { useLanguage } from "@/context/language"
|
||||||
import { getAvatarColors, type LocalProject, useLayout } from "@/context/layout"
|
import { getAvatarColors, type LocalProject, useLayout } from "@/context/layout"
|
||||||
import { useNotification } from "@/context/notification"
|
import { useNotification } from "@/context/notification"
|
||||||
import { usePermission } from "@/context/permission"
|
import { usePermission } from "@/context/permission"
|
||||||
|
import { working } from "@/pages/session/activity"
|
||||||
import { messageAgentColor } from "@/utils/agent"
|
import { messageAgentColor } from "@/utils/agent"
|
||||||
import { sessionPermissionRequest } from "../session/composer/session-request-tree"
|
import { sessionPermissionRequest } from "../session/composer/session-request-tree"
|
||||||
import { hasProjectPermissions } from "./helpers"
|
import { hasProjectPermissions } from "./helpers"
|
||||||
|
|
@ -204,18 +205,7 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
|
||||||
})
|
})
|
||||||
const isWorking = createMemo(() => {
|
const isWorking = createMemo(() => {
|
||||||
if (hasPermissions()) return false
|
if (hasPermissions()) return false
|
||||||
const pending = (sessionStore.message[props.session.id] ?? []).findLast(
|
return working(sessionStore.session_status[props.session.id])
|
||||||
(message) =>
|
|
||||||
message.role === "assistant" &&
|
|
||||||
typeof (message as { time?: { completed?: unknown } }).time?.completed !== "number",
|
|
||||||
)
|
|
||||||
const status = sessionStore.session_status[props.session.id]
|
|
||||||
return (
|
|
||||||
pending !== undefined ||
|
|
||||||
status?.type === "busy" ||
|
|
||||||
status?.type === "retry" ||
|
|
||||||
(status !== undefined && status.type !== "idle")
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const tint = createMemo(() => {
|
const tint = createMemo(() => {
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ import { useSettings } from "@/context/settings"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useTerminal } from "@/context/terminal"
|
import { useTerminal } from "@/context/terminal"
|
||||||
import { type FollowupDraft, sendFollowupDraft } from "@/components/prompt-input/submit"
|
import { type FollowupDraft, sendFollowupDraft } from "@/components/prompt-input/submit"
|
||||||
|
import { working } from "@/pages/session/activity"
|
||||||
import { createSessionComposerState, SessionComposerRegion } from "@/pages/session/composer"
|
import { createSessionComposerState, SessionComposerRegion } from "@/pages/session/composer"
|
||||||
import { createOpenReviewFile, createSessionTabs, createSizing, focusTerminalById } from "@/pages/session/helpers"
|
import { createOpenReviewFile, createSessionTabs, createSizing, focusTerminalById } from "@/pages/session/helpers"
|
||||||
import { MessageTimeline } from "@/pages/session/message-timeline"
|
import { MessageTimeline } from "@/pages/session/message-timeline"
|
||||||
|
|
@ -1361,10 +1362,7 @@ export default function Page() {
|
||||||
})
|
})
|
||||||
|
|
||||||
const busy = (sessionID: string) => {
|
const busy = (sessionID: string) => {
|
||||||
if ((sync.data.session_status[sessionID] ?? { type: "idle" as const }).type !== "idle") return true
|
return working(sync.data.session_status[sessionID])
|
||||||
return (sync.data.message[sessionID] ?? []).some(
|
|
||||||
(item) => item.role === "assistant" && typeof item.time.completed !== "number",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const queuedFollowups = createMemo(() => {
|
const queuedFollowups = createMemo(() => {
|
||||||
|
|
|
||||||
46
packages/app/src/pages/session/activity.test.ts
Normal file
46
packages/app/src/pages/session/activity.test.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import type { AssistantMessage, Message as MessageType, UserMessage } from "@opencode-ai/sdk/v2"
|
||||||
|
import { pending, working } from "./activity"
|
||||||
|
|
||||||
|
const user = (id: string) =>
|
||||||
|
({
|
||||||
|
id,
|
||||||
|
sessionID: "ses_1",
|
||||||
|
role: "user",
|
||||||
|
time: { created: 1 },
|
||||||
|
}) as UserMessage
|
||||||
|
|
||||||
|
const assistant = (id: string, parentID: string, completed?: number) =>
|
||||||
|
({
|
||||||
|
id,
|
||||||
|
sessionID: "ses_1",
|
||||||
|
parentID,
|
||||||
|
role: "assistant",
|
||||||
|
time: completed === undefined ? { created: 2 } : { created: 2, completed },
|
||||||
|
}) as AssistantMessage
|
||||||
|
|
||||||
|
describe("session activity", () => {
|
||||||
|
test("treats only non-idle status as running", () => {
|
||||||
|
expect(working(undefined)).toBe(false)
|
||||||
|
expect(working({ type: "idle" })).toBe(false)
|
||||||
|
expect(working({ type: "busy" })).toBe(true)
|
||||||
|
expect(working({ type: "retry", attempt: 1, message: "retry", next: 1 })).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns the trailing incomplete assistant", () => {
|
||||||
|
const messages: MessageType[] = [user("msg_1"), assistant("msg_2", "msg_1")]
|
||||||
|
|
||||||
|
expect(pending(messages)?.id).toBe("msg_2")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("ignores older incomplete assistants once a later assistant completed", () => {
|
||||||
|
const messages: MessageType[] = [
|
||||||
|
user("msg_1"),
|
||||||
|
assistant("msg_2", "msg_1"),
|
||||||
|
user("msg_3"),
|
||||||
|
assistant("msg_4", "msg_3", 4),
|
||||||
|
]
|
||||||
|
|
||||||
|
expect(pending(messages)).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
10
packages/app/src/pages/session/activity.ts
Normal file
10
packages/app/src/pages/session/activity.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import type { AssistantMessage, Message as MessageType } from "@opencode-ai/sdk/v2"
|
||||||
|
import type { SessionStatus } from "@opencode-ai/sdk/v2/client"
|
||||||
|
|
||||||
|
export const pending = (messages: readonly MessageType[]) => {
|
||||||
|
const item = messages.findLast((item): item is AssistantMessage => item.role === "assistant")
|
||||||
|
if (!item || typeof item.time.completed === "number") return
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
export const working = (status: SessionStatus | undefined) => status !== undefined && status.type !== "idle"
|
||||||
|
|
@ -12,7 +12,7 @@ import { Spinner } from "@opencode-ai/ui/spinner"
|
||||||
import { SessionTurn } from "@opencode-ai/ui/session-turn"
|
import { SessionTurn } from "@opencode-ai/ui/session-turn"
|
||||||
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
||||||
import { TextField } from "@opencode-ai/ui/text-field"
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
import type { AssistantMessage, Message as MessageType, Part, TextPart, UserMessage } from "@opencode-ai/sdk/v2"
|
import type { Message as MessageType, Part, TextPart, UserMessage } from "@opencode-ai/sdk/v2"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { Binary } from "@opencode-ai/util/binary"
|
import { Binary } from "@opencode-ai/util/binary"
|
||||||
import { getFilename } from "@opencode-ai/util/path"
|
import { getFilename } from "@opencode-ai/util/path"
|
||||||
|
|
@ -27,6 +27,7 @@ import { usePlatform } from "@/context/platform"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
|
import { pending, working } from "@/pages/session/activity"
|
||||||
import { messageAgentColor } from "@/utils/agent"
|
import { messageAgentColor } from "@/utils/agent"
|
||||||
import { parseCommentNote, readCommentMetadata } from "@/utils/comment-note"
|
import { parseCommentNote, readCommentMetadata } from "@/utils/comment-note"
|
||||||
|
|
||||||
|
|
@ -236,17 +237,13 @@ export function MessageTimeline(props: {
|
||||||
if (!id) return emptyMessages
|
if (!id) return emptyMessages
|
||||||
return sync.data.message[id] ?? emptyMessages
|
return sync.data.message[id] ?? emptyMessages
|
||||||
})
|
})
|
||||||
const pending = createMemo(() =>
|
const assistant = createMemo(() => pending(sessionMessages()))
|
||||||
sessionMessages().findLast(
|
|
||||||
(item): item is AssistantMessage => item.role === "assistant" && typeof item.time.completed !== "number",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
const sessionStatus = createMemo(() => {
|
const sessionStatus = createMemo(() => {
|
||||||
const id = sessionID()
|
const id = sessionID()
|
||||||
if (!id) return idle
|
if (!id) return idle
|
||||||
return sync.data.session_status[id] ?? idle
|
return sync.data.session_status[id] ?? idle
|
||||||
})
|
})
|
||||||
const working = createMemo(() => !!pending() || sessionStatus().type !== "idle")
|
const busy = createMemo(() => working(sessionStatus()))
|
||||||
const tint = createMemo(() => messageAgentColor(sessionMessages(), sync.data.agent))
|
const tint = createMemo(() => messageAgentColor(sessionMessages(), sync.data.agent))
|
||||||
|
|
||||||
const [slot, setSlot] = createStore({
|
const [slot, setSlot] = createStore({
|
||||||
|
|
@ -264,7 +261,7 @@ export function MessageTimeline(props: {
|
||||||
onCleanup(clear)
|
onCleanup(clear)
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
working,
|
busy,
|
||||||
(on, prev) => {
|
(on, prev) => {
|
||||||
clear()
|
clear()
|
||||||
if (on) {
|
if (on) {
|
||||||
|
|
@ -282,7 +279,9 @@ export function MessageTimeline(props: {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
const activeMessageID = createMemo(() => {
|
const activeMessageID = createMemo(() => {
|
||||||
const parentID = pending()?.parentID
|
if (!busy()) return undefined
|
||||||
|
|
||||||
|
const parentID = assistant()?.parentID
|
||||||
if (parentID) {
|
if (parentID) {
|
||||||
const messages = sessionMessages()
|
const messages = sessionMessages()
|
||||||
const result = Binary.search(messages, parentID, (message) => message.id)
|
const result = Binary.search(messages, parentID, (message) => message.id)
|
||||||
|
|
@ -290,15 +289,10 @@ export function MessageTimeline(props: {
|
||||||
if (message && message.role === "user") return message.id
|
if (message && message.role === "user") return message.id
|
||||||
}
|
}
|
||||||
|
|
||||||
const status = sessionStatus()
|
const messages = sessionMessages()
|
||||||
if (status.type !== "idle") {
|
for (let i = messages.length - 1; i >= 0; i--) {
|
||||||
const messages = sessionMessages()
|
if (messages[i].role === "user") return messages[i].id
|
||||||
for (let i = messages.length - 1; i >= 0; i--) {
|
|
||||||
if (messages[i].role === "user") return messages[i].id
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined
|
|
||||||
})
|
})
|
||||||
const info = createMemo(() => {
|
const info = createMemo(() => {
|
||||||
const id = sessionID()
|
const id = sessionID()
|
||||||
|
|
|
||||||
7
packages/opencode/src/session/assistant.ts
Normal file
7
packages/opencode/src/session/assistant.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { MessageV2 } from "./message-v2"
|
||||||
|
|
||||||
|
export const done = (msg: MessageV2.Assistant, time = Date.now()) => {
|
||||||
|
if (typeof msg.time.completed === "number") return msg
|
||||||
|
msg.time.completed = time
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,7 @@ import { Config } from "@/config/config"
|
||||||
import { SessionCompaction } from "./compaction"
|
import { SessionCompaction } from "./compaction"
|
||||||
import { PermissionNext } from "@/permission/next"
|
import { PermissionNext } from "@/permission/next"
|
||||||
import { Question } from "@/question"
|
import { Question } from "@/question"
|
||||||
|
import { done } from "./assistant"
|
||||||
import { PartID } from "./schema"
|
import { PartID } from "./schema"
|
||||||
import type { SessionID, MessageID } from "./schema"
|
import type { SessionID, MessageID } from "./schema"
|
||||||
|
|
||||||
|
|
@ -416,7 +417,7 @@ export namespace SessionProcessor {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
input.assistantMessage.time.completed = Date.now()
|
done(input.assistantMessage)
|
||||||
await Session.updateMessage(input.assistantMessage)
|
await Session.updateMessage(input.assistantMessage)
|
||||||
if (needsCompaction) return "compact"
|
if (needsCompaction) return "compact"
|
||||||
if (blocked) return "stop"
|
if (blocked) return "stop"
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import fs from "fs/promises"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Filesystem } from "../util/filesystem"
|
import { Filesystem } from "../util/filesystem"
|
||||||
import { SessionID, MessageID, PartID } from "./schema"
|
import { SessionID, MessageID, PartID } from "./schema"
|
||||||
|
import { done } from "./assistant"
|
||||||
import { MessageV2 } from "./message-v2"
|
import { MessageV2 } from "./message-v2"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { SessionRevert } from "./revert"
|
import { SessionRevert } from "./revert"
|
||||||
|
|
@ -465,7 +466,7 @@ export namespace SessionPrompt {
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
assistantMessage.finish = "tool-calls"
|
assistantMessage.finish = "tool-calls"
|
||||||
assistantMessage.time.completed = Date.now()
|
done(assistantMessage)
|
||||||
await Session.updateMessage(assistantMessage)
|
await Session.updateMessage(assistantMessage)
|
||||||
if (result && part.state.status === "running") {
|
if (result && part.state.status === "running") {
|
||||||
await Session.updatePart({
|
await Session.updatePart({
|
||||||
|
|
@ -599,90 +600,99 @@ export namespace SessionPrompt {
|
||||||
})
|
})
|
||||||
using _ = defer(() => InstructionPrompt.clear(processor.message.id))
|
using _ = defer(() => InstructionPrompt.clear(processor.message.id))
|
||||||
|
|
||||||
// Check if user explicitly invoked an agent via @ in this turn
|
const format = lastUser.format ?? { type: "text" }
|
||||||
const lastUserMsg = msgs.findLast((m) => m.info.role === "user")
|
const result = await (async () => {
|
||||||
const bypassAgentCheck = lastUserMsg?.parts.some((p) => p.type === "agent") ?? false
|
const lastUserMsg = msgs.findLast((m) => m.info.role === "user")
|
||||||
|
const bypassAgentCheck = lastUserMsg?.parts.some((p) => p.type === "agent") ?? false
|
||||||
|
|
||||||
const tools = await resolveTools({
|
const tools = await resolveTools({
|
||||||
agent,
|
agent,
|
||||||
session,
|
session,
|
||||||
model,
|
model,
|
||||||
tools: lastUser.tools,
|
tools: lastUser.tools,
|
||||||
processor,
|
processor,
|
||||||
bypassAgentCheck,
|
bypassAgentCheck,
|
||||||
messages: msgs,
|
messages: msgs,
|
||||||
})
|
|
||||||
|
|
||||||
// Inject StructuredOutput tool if JSON schema mode enabled
|
|
||||||
if (lastUser.format?.type === "json_schema") {
|
|
||||||
tools["StructuredOutput"] = createStructuredOutputTool({
|
|
||||||
schema: lastUser.format.schema,
|
|
||||||
onSuccess(output) {
|
|
||||||
structuredOutput = output
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
if (step === 1) {
|
if (format.type === "json_schema") {
|
||||||
SessionSummary.summarize({
|
tools["StructuredOutput"] = createStructuredOutputTool({
|
||||||
sessionID: sessionID,
|
schema: format.schema,
|
||||||
messageID: lastUser.id,
|
onSuccess(output) {
|
||||||
})
|
structuredOutput = output
|
||||||
}
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Ephemerally wrap queued user messages with a reminder to stay on track
|
if (step === 1) {
|
||||||
if (step > 1 && lastFinished) {
|
SessionSummary.summarize({
|
||||||
for (const msg of msgs) {
|
sessionID: sessionID,
|
||||||
if (msg.info.role !== "user" || msg.info.id <= lastFinished.id) continue
|
messageID: lastUser.id,
|
||||||
for (const part of msg.parts) {
|
})
|
||||||
if (part.type !== "text" || part.ignored || part.synthetic) continue
|
}
|
||||||
if (!part.text.trim()) continue
|
|
||||||
part.text = [
|
if (step > 1 && lastFinished) {
|
||||||
"<system-reminder>",
|
for (const msg of msgs) {
|
||||||
"The user sent the following message:",
|
if (msg.info.role !== "user" || msg.info.id <= lastFinished.id) continue
|
||||||
part.text,
|
for (const part of msg.parts) {
|
||||||
"",
|
if (part.type !== "text" || part.ignored || part.synthetic) continue
|
||||||
"Please address this message and continue with your tasks.",
|
if (!part.text.trim()) continue
|
||||||
"</system-reminder>",
|
part.text = [
|
||||||
].join("\n")
|
"<system-reminder>",
|
||||||
|
"The user sent the following message:",
|
||||||
|
part.text,
|
||||||
|
"",
|
||||||
|
"Please address this message and continue with your tasks.",
|
||||||
|
"</system-reminder>",
|
||||||
|
].join("\n")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
await Plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs })
|
await Plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs })
|
||||||
|
|
||||||
// Build system prompt, adding structured output instruction if needed
|
const skills = await SystemPrompt.skills(agent)
|
||||||
const skills = await SystemPrompt.skills(agent)
|
const system = [
|
||||||
const system = [
|
...(await SystemPrompt.environment(model)),
|
||||||
...(await SystemPrompt.environment(model)),
|
...(skills ? [skills] : []),
|
||||||
...(skills ? [skills] : []),
|
...(await InstructionPrompt.system()),
|
||||||
...(await InstructionPrompt.system()),
|
]
|
||||||
]
|
if (format.type === "json_schema") {
|
||||||
const format = lastUser.format ?? { type: "text" }
|
system.push(STRUCTURED_OUTPUT_SYSTEM_PROMPT)
|
||||||
if (format.type === "json_schema") {
|
}
|
||||||
system.push(STRUCTURED_OUTPUT_SYSTEM_PROMPT)
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await processor.process({
|
return processor.process({
|
||||||
user: lastUser,
|
user: lastUser,
|
||||||
agent,
|
agent,
|
||||||
abort,
|
abort,
|
||||||
sessionID,
|
sessionID,
|
||||||
system,
|
system,
|
||||||
messages: [
|
messages: [
|
||||||
...MessageV2.toModelMessages(msgs, model),
|
...MessageV2.toModelMessages(msgs, model),
|
||||||
...(isLastStep
|
...(isLastStep
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
role: "assistant" as const,
|
role: "assistant" as const,
|
||||||
content: MAX_STEPS,
|
content: MAX_STEPS,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
],
|
],
|
||||||
tools,
|
tools,
|
||||||
model,
|
model,
|
||||||
toolChoice: format.type === "json_schema" ? "required" : undefined,
|
toolChoice: format.type === "json_schema" ? "required" : undefined,
|
||||||
|
})
|
||||||
|
})().catch(async (err) => {
|
||||||
|
if (typeof processor.message.time.completed !== "number") {
|
||||||
|
await Session.updateMessage(done(processor.message)).catch((cause) => {
|
||||||
|
log.error("failed to finalize assistant after prompt error", {
|
||||||
|
cause,
|
||||||
|
messageID: processor.message.id,
|
||||||
|
sessionID,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
throw err
|
||||||
})
|
})
|
||||||
|
|
||||||
// If structured output was captured, save it and exit immediately
|
// If structured output was captured, save it and exit immediately
|
||||||
|
|
@ -1697,7 +1707,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||||
if (aborted) {
|
if (aborted) {
|
||||||
output += "\n\n" + ["<metadata>", "User aborted the command", "</metadata>"].join("\n")
|
output += "\n\n" + ["<metadata>", "User aborted the command", "</metadata>"].join("\n")
|
||||||
}
|
}
|
||||||
msg.time.completed = Date.now()
|
done(msg)
|
||||||
await Session.updateMessage(msg)
|
await Session.updateMessage(msg)
|
||||||
if (part.state.status === "running") {
|
if (part.state.status === "running") {
|
||||||
part.state = {
|
part.state = {
|
||||||
|
|
|
||||||
44
packages/opencode/test/session/assistant.test.ts
Normal file
44
packages/opencode/test/session/assistant.test.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { ModelID, ProviderID } from "../../src/provider/schema"
|
||||||
|
import { done } from "../../src/session/assistant"
|
||||||
|
import type { MessageV2 } from "../../src/session/message-v2"
|
||||||
|
import { SessionID } from "../../src/session/schema"
|
||||||
|
|
||||||
|
const sessionID = SessionID.make("session")
|
||||||
|
const providerID = ProviderID.make("test")
|
||||||
|
const modelID = ModelID.make("model")
|
||||||
|
|
||||||
|
const assistant = (completed?: number) =>
|
||||||
|
({
|
||||||
|
id: "msg_1",
|
||||||
|
sessionID,
|
||||||
|
parentID: "msg_0",
|
||||||
|
role: "assistant",
|
||||||
|
time: completed === undefined ? { created: 1 } : { created: 1, completed },
|
||||||
|
mode: "build",
|
||||||
|
agent: "build",
|
||||||
|
path: { cwd: "/", root: "/" },
|
||||||
|
cost: 0,
|
||||||
|
tokens: {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
reasoning: 0,
|
||||||
|
cache: { read: 0, write: 0 },
|
||||||
|
},
|
||||||
|
modelID,
|
||||||
|
providerID,
|
||||||
|
}) as MessageV2.Assistant
|
||||||
|
|
||||||
|
describe("session assistant", () => {
|
||||||
|
test("marks incomplete assistants as done", () => {
|
||||||
|
const msg = assistant()
|
||||||
|
|
||||||
|
expect(done(msg, 10).time.completed).toBe(10)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("preserves existing completion time", () => {
|
||||||
|
const msg = assistant(5)
|
||||||
|
|
||||||
|
expect(done(msg, 10).time.completed).toBe(5)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -195,9 +195,9 @@ export function SessionTurn(
|
||||||
const pending = createMemo(() => {
|
const pending = createMemo(() => {
|
||||||
if (typeof props.active === "boolean") return
|
if (typeof props.active === "boolean") return
|
||||||
const messages = allMessages() ?? emptyMessages
|
const messages = allMessages() ?? emptyMessages
|
||||||
return messages.findLast(
|
const item = messages.findLast((item): item is AssistantMessage => item.role === "assistant")
|
||||||
(item): item is AssistantMessage => item.role === "assistant" && typeof item.time.completed !== "number",
|
if (!item || typeof item.time.completed === "number") return
|
||||||
)
|
return item
|
||||||
})
|
})
|
||||||
|
|
||||||
const pendingUser = createMemo(() => {
|
const pendingUser = createMemo(() => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue