Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 18 additions & 23 deletions packages/opencode/src/cli/cmd/tui/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,16 +176,7 @@ export function tui(input: {
<LanguageProvider>
<UiI18nBridge>
<ToastProvider>
<RouteProvider
initialRoute={
input.args.continue
? {
type: "session",
sessionID: "dummy",
}
: undefined
}
>
<RouteProvider>
<TuiConfigProvider config={input.config}>
<SDKProvider
url={input.url}
Expand Down Expand Up @@ -405,7 +396,8 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
let continued = false
createEffect(() => {
// When using -c, session list is loaded in blocking phase, so we can navigate at "partial"
if (continued || sync.status === "loading" || !args.continue) return
// An explicit -s takes priority: skip continue resume so it can't override the chosen session.
if (continued || sync.status === "loading" || !args.continue || args.sessionID) return
// RACE GUARD: orchestratorDirPath() resolves asynchronously (onMount above).
// If sync reaches "partial" first, orchestratorDirPath() is still undefined
// and we'd wrongly skip the orchestrator branch, resume the persistent
Expand Down Expand Up @@ -436,19 +428,22 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
const match = sync.data.session
.toSorted((a, b) => b.time.updated - a.time.updated)
.find((x) => x.parentID === undefined && !isSystemSession(x))?.id
if (match) {
if (!match) {
continued = true
if (args.fork) {
void sdk.client.session.fork({ sessionID: match }).then((result) => {
if (result.data?.id) {
route.navigate({ type: "session", sessionID: result.data.id })
} else {
toast.show({ message: "Failed to fork session", variant: "error" })
}
})
} else {
route.navigate({ type: "session", sessionID: match })
}
toast.show({ message: "No previous session to continue", variant: "info" })
return
}
continued = true
if (args.fork) {
void sdk.client.session.fork({ sessionID: match }).then((result) => {
if (result.data?.id) {
route.navigate({ type: "session", sessionID: result.data.id })
} else {
toast.show({ message: "Failed to fork session", variant: "error" })
}
})
} else {
route.navigate({ type: "session", sessionID: match })
}
})

Expand Down
72 changes: 51 additions & 21 deletions packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import type {
ReasoningPart,
} from "@mimo-ai/sdk/v2"
import { useLocal } from "@tui/context/local"
import { Locale } from "@/util"
import { Locale, Log } from "@/util"
import type { Tool } from "@/tool"
import type { ReadTool } from "@/tool/read"
import type { WriteTool } from "@/tool/write"
Expand Down Expand Up @@ -247,31 +247,61 @@ export function Session() {
const toast = useToast()
const sdk = useSDK()

createEffect(async () => {
createEffect(() => {
const sessionID = route.sessionID
const previousWorkspace = project.workspace.current()
const result = await sdk.client.session.get({ sessionID: route.sessionID }, { throwOnError: true })
if (!result.data) {

const isCurrentRoute = () =>
fullRoute.data.type === "session" && fullRoute.data.sessionID === sessionID

void (async () => {
const result = await sdk.client.session.get({ sessionID })

if (!isCurrentRoute()) return

if (!result.data) {
toast.show({
message: result.response?.status === 404 ? "Session not found" : "Failed to load session",
variant: "error",
})
navigate({ type: "home" })
return
}

if (result.data.workspaceID !== previousWorkspace) {
project.workspace.set(result.data.workspaceID)

try {
await sync.bootstrap({ fatal: false })
} catch {
// Preserve existing non-fatal bootstrap behavior.
}

if (!isCurrentRoute()) return
}

if (!isCurrentRoute()) return

await sync.session.sync(sessionID)

if (!isCurrentRoute()) return

scroll?.scrollBy(100_000)
})().catch((error) => {
Log.Default.error("session route load failed", {
sessionID,
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
})

if (!isCurrentRoute()) return

toast.show({
message: `Session not found: ${route.sessionID}`,
message: "Failed to load session",
variant: "error",
})
navigate({ type: "home" })
return
}

if (result.data.workspaceID !== previousWorkspace) {
project.workspace.set(result.data.workspaceID)

// Sync all the data for this workspace. Note that this
// workspace may not exist anymore which is why this is not
// fatal. If it doesn't we still want to show the session
// (which will be non-interactive)
try {
await sync.bootstrap({ fatal: false })
} catch (e) {}
}
await sync.session.sync(route.sessionID)
if (scroll) scroll.scrollBy(100_000)
})
})

let lastSwitch: string | undefined = undefined
Expand Down