Skip to content

Commit 2a480a9

Browse files
authored
fix(tui): fail fast on invalid session startup (#23837)
1 parent 88c5f6b commit 2a480a9

5 files changed

Lines changed: 97 additions & 21 deletions

File tree

packages/opencode/src/cli/cmd/tui/attach.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { UI } from "@/cli/ui"
33
import { tui } from "./app"
44
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
55
import { TuiConfig } from "@/cli/cmd/tui/config/tui"
6+
import { errorMessage } from "@/util/error"
7+
import { validateSession } from "./validate-session"
68

79
export const AttachCommand = cmd({
810
command: "attach <url>",
@@ -65,6 +67,20 @@ export const AttachCommand = cmd({
6567
return { Authorization: auth }
6668
})()
6769
const config = await TuiConfig.get()
70+
71+
try {
72+
await validateSession({
73+
url: args.url,
74+
sessionID: args.session,
75+
directory,
76+
headers,
77+
})
78+
} catch (error) {
79+
UI.error(errorMessage(error))
80+
process.exitCode = 1
81+
return
82+
}
83+
6884
await tui({
6985
url: args.url,
7086
config,

packages/opencode/src/cli/cmd/tui/routes/session/index.tsx

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import { Flag } from "@/flag/flag"
6868
import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
6969
import parsers from "../../../../../../parsers-config.ts"
7070
import * as Clipboard from "../../util/clipboard"
71+
import { errorMessage } from "@/util/error"
7172
import { Toast, useToast } from "../../ui/toast"
7273
import { useKV } from "../../context/kv.tsx"
7374
import * as Editor from "../../util/editor"
@@ -180,31 +181,43 @@ export function Session() {
180181
const toast = useToast()
181182
const sdk = useSDK()
182183

183-
createEffect(async () => {
184-
const previousWorkspace = project.workspace.current()
185-
const result = await sdk.client.session.get({ sessionID: route.sessionID }, { throwOnError: true })
186-
if (!result.data) {
184+
createEffect(() => {
185+
const sessionID = route.sessionID
186+
void (async () => {
187+
const previousWorkspace = project.workspace.current()
188+
const result = await sdk.client.session.get({ sessionID }, { throwOnError: true })
189+
if (!result.data) {
190+
toast.show({
191+
message: `Session not found: ${sessionID}`,
192+
variant: "error",
193+
duration: 5000,
194+
})
195+
navigate({ type: "home" })
196+
return
197+
}
198+
199+
if (result.data.workspaceID !== previousWorkspace) {
200+
project.workspace.set(result.data.workspaceID)
201+
202+
// Sync all the data for this workspace. Note that this
203+
// workspace may not exist anymore which is why this is not
204+
// fatal. If it doesn't we still want to show the session
205+
// (which will be non-interactive)
206+
try {
207+
await sync.bootstrap({ fatal: false })
208+
} catch {}
209+
}
210+
await sync.session.sync(sessionID)
211+
if (route.sessionID === sessionID && scroll) scroll.scrollBy(100_000)
212+
})().catch((error) => {
213+
if (route.sessionID !== sessionID) return
187214
toast.show({
188-
message: `Session not found: ${route.sessionID}`,
215+
message: errorMessage(error),
189216
variant: "error",
217+
duration: 5000,
190218
})
191219
navigate({ type: "home" })
192-
return
193-
}
194-
195-
if (result.data.workspaceID !== previousWorkspace) {
196-
project.workspace.set(result.data.workspaceID)
197-
198-
// Sync all the data for this workspace. Note that this
199-
// workspace may not exist anymore which is why this is not
200-
// fatal. If it doesn't we still want to show the session
201-
// (which will be non-interactive)
202-
try {
203-
await sync.bootstrap({ fatal: false })
204-
} catch (e) {}
205-
}
206-
await sync.session.sync(route.sessionID)
207-
if (scroll) scroll.scrollBy(100_000)
220+
})
208221
})
209222

210223
let lastSwitch: string | undefined = undefined

packages/opencode/src/cli/cmd/tui/thread.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
1616
import { writeHeapSnapshot } from "v8"
1717
import { TuiConfig } from "./config/tui"
1818
import { OPENCODE_PROCESS_ROLE, OPENCODE_RUN_ID, ensureRunID, sanitizedProcessEnv } from "@/util/opencode-process"
19+
import { validateSession } from "./validate-session"
1920

2021
declare global {
2122
const OPENCODE_WORKER_PATH: string
@@ -202,6 +203,19 @@ export const TuiThreadCommand = cmd({
202203
events: createEventSource(client),
203204
}
204205

206+
try {
207+
await validateSession({
208+
url: transport.url,
209+
sessionID: args.session,
210+
directory: cwd,
211+
fetch: transport.fetch,
212+
})
213+
} catch (error) {
214+
UI.error(errorMessage(error))
215+
process.exitCode = 1
216+
return
217+
}
218+
205219
setTimeout(() => {
206220
client.call("checkUpgrade", { directory: cwd }).catch(() => {})
207221
}, 1000).unref?.()
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
2+
import { SessionID } from "@/session/schema"
3+
4+
export async function validateSession(input: {
5+
url: string
6+
sessionID?: string
7+
directory?: string
8+
fetch?: typeof fetch
9+
headers?: RequestInit["headers"]
10+
}) {
11+
if (!input.sessionID) return
12+
13+
const result = SessionID.zod.safeParse(input.sessionID)
14+
if (!result.success) {
15+
throw new Error(`Invalid session ID: ${result.error.issues.at(0)?.message ?? "unknown error"}`)
16+
}
17+
18+
await createOpencodeClient({
19+
baseUrl: input.url,
20+
directory: input.directory,
21+
fetch: input.fetch,
22+
headers: input.headers,
23+
}).session.get({ sessionID: result.data }, { throwOnError: true })
24+
}

packages/opencode/src/util/error.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ export function errorMessage(error: unknown): string {
2626
return error.message
2727
}
2828

29+
if (
30+
isRecord(error) &&
31+
isRecord(error.data) &&
32+
typeof error.data.message === "string" &&
33+
error.data.message
34+
) {
35+
return error.data.message
36+
}
37+
2938
const text = String(error)
3039
if (text && text !== "[object Object]") return text
3140

0 commit comments

Comments
 (0)