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 { useNotification } from "@/context/notification"
|
||||
import { usePermission } from "@/context/permission"
|
||||
import { working } from "@/pages/session/activity"
|
||||
import { messageAgentColor } from "@/utils/agent"
|
||||
import { sessionPermissionRequest } from "../session/composer/session-request-tree"
|
||||
import { hasProjectPermissions } from "./helpers"
|
||||
|
|
@ -204,18 +205,7 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
|
|||
})
|
||||
const isWorking = createMemo(() => {
|
||||
if (hasPermissions()) return false
|
||||
const pending = (sessionStore.message[props.session.id] ?? []).findLast(
|
||||
(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")
|
||||
)
|
||||
return working(sessionStore.session_status[props.session.id])
|
||||
})
|
||||
|
||||
const tint = createMemo(() => {
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import { useSettings } from "@/context/settings"
|
|||
import { useSync } from "@/context/sync"
|
||||
import { useTerminal } from "@/context/terminal"
|
||||
import { type FollowupDraft, sendFollowupDraft } from "@/components/prompt-input/submit"
|
||||
import { working } from "@/pages/session/activity"
|
||||
import { createSessionComposerState, SessionComposerRegion } from "@/pages/session/composer"
|
||||
import { createOpenReviewFile, createSessionTabs, createSizing, focusTerminalById } from "@/pages/session/helpers"
|
||||
import { MessageTimeline } from "@/pages/session/message-timeline"
|
||||
|
|
@ -1361,10 +1362,7 @@ export default function Page() {
|
|||
})
|
||||
|
||||
const busy = (sessionID: string) => {
|
||||
if ((sync.data.session_status[sessionID] ?? { type: "idle" as const }).type !== "idle") return true
|
||||
return (sync.data.message[sessionID] ?? []).some(
|
||||
(item) => item.role === "assistant" && typeof item.time.completed !== "number",
|
||||
)
|
||||
return working(sync.data.session_status[sessionID])
|
||||
}
|
||||
|
||||
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 { ScrollView } from "@opencode-ai/ui/scroll-view"
|
||||
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 { Binary } from "@opencode-ai/util/binary"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
|
|
@ -27,6 +27,7 @@ import { usePlatform } from "@/context/platform"
|
|||
import { useSettings } from "@/context/settings"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { pending, working } from "@/pages/session/activity"
|
||||
import { messageAgentColor } from "@/utils/agent"
|
||||
import { parseCommentNote, readCommentMetadata } from "@/utils/comment-note"
|
||||
|
||||
|
|
@ -236,17 +237,13 @@ export function MessageTimeline(props: {
|
|||
if (!id) return emptyMessages
|
||||
return sync.data.message[id] ?? emptyMessages
|
||||
})
|
||||
const pending = createMemo(() =>
|
||||
sessionMessages().findLast(
|
||||
(item): item is AssistantMessage => item.role === "assistant" && typeof item.time.completed !== "number",
|
||||
),
|
||||
)
|
||||
const assistant = createMemo(() => pending(sessionMessages()))
|
||||
const sessionStatus = createMemo(() => {
|
||||
const id = sessionID()
|
||||
if (!id) return 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 [slot, setSlot] = createStore({
|
||||
|
|
@ -264,7 +261,7 @@ export function MessageTimeline(props: {
|
|||
onCleanup(clear)
|
||||
createEffect(
|
||||
on(
|
||||
working,
|
||||
busy,
|
||||
(on, prev) => {
|
||||
clear()
|
||||
if (on) {
|
||||
|
|
@ -282,7 +279,9 @@ export function MessageTimeline(props: {
|
|||
),
|
||||
)
|
||||
const activeMessageID = createMemo(() => {
|
||||
const parentID = pending()?.parentID
|
||||
if (!busy()) return undefined
|
||||
|
||||
const parentID = assistant()?.parentID
|
||||
if (parentID) {
|
||||
const messages = sessionMessages()
|
||||
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
|
||||
}
|
||||
|
||||
const status = sessionStatus()
|
||||
if (status.type !== "idle") {
|
||||
const messages = sessionMessages()
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i].role === "user") return messages[i].id
|
||||
}
|
||||
const messages = sessionMessages()
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i].role === "user") return messages[i].id
|
||||
}
|
||||
|
||||
return undefined
|
||||
})
|
||||
const info = createMemo(() => {
|
||||
const id = sessionID()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue