fix(tui): fail fast on invalid session startup (#23837)
This commit is contained in:
parent
d884ab73d5
commit
6196b81e0a
5 changed files with 97 additions and 21 deletions
|
|
@ -3,6 +3,8 @@ import { UI } from "@/cli/ui"
|
||||||
import { tui } from "./app"
|
import { tui } from "./app"
|
||||||
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
|
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
|
||||||
import { TuiConfig } from "@/cli/cmd/tui/config/tui"
|
import { TuiConfig } from "@/cli/cmd/tui/config/tui"
|
||||||
|
import { errorMessage } from "@/util/error"
|
||||||
|
import { validateSession } from "./validate-session"
|
||||||
|
|
||||||
export const AttachCommand = cmd({
|
export const AttachCommand = cmd({
|
||||||
command: "attach <url>",
|
command: "attach <url>",
|
||||||
|
|
@ -65,6 +67,20 @@ export const AttachCommand = cmd({
|
||||||
return { Authorization: auth }
|
return { Authorization: auth }
|
||||||
})()
|
})()
|
||||||
const config = await TuiConfig.get()
|
const config = await TuiConfig.get()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await validateSession({
|
||||||
|
url: args.url,
|
||||||
|
sessionID: args.session,
|
||||||
|
directory,
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
UI.error(errorMessage(error))
|
||||||
|
process.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
await tui({
|
await tui({
|
||||||
url: args.url,
|
url: args.url,
|
||||||
config,
|
config,
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ import { Flag } from "@/flag/flag"
|
||||||
import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
|
import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
|
||||||
import parsers from "../../../../../../parsers-config.ts"
|
import parsers from "../../../../../../parsers-config.ts"
|
||||||
import * as Clipboard from "../../util/clipboard"
|
import * as Clipboard from "../../util/clipboard"
|
||||||
|
import { errorMessage } from "@/util/error"
|
||||||
import { Toast, useToast } from "../../ui/toast"
|
import { Toast, useToast } from "../../ui/toast"
|
||||||
import { useKV } from "../../context/kv.tsx"
|
import { useKV } from "../../context/kv.tsx"
|
||||||
import * as Editor from "../../util/editor"
|
import * as Editor from "../../util/editor"
|
||||||
|
|
@ -180,31 +181,43 @@ export function Session() {
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
|
|
||||||
createEffect(async () => {
|
createEffect(() => {
|
||||||
const previousWorkspace = project.workspace.current()
|
const sessionID = route.sessionID
|
||||||
const result = await sdk.client.session.get({ sessionID: route.sessionID }, { throwOnError: true })
|
void (async () => {
|
||||||
if (!result.data) {
|
const previousWorkspace = project.workspace.current()
|
||||||
|
const result = await sdk.client.session.get({ sessionID }, { throwOnError: true })
|
||||||
|
if (!result.data) {
|
||||||
|
toast.show({
|
||||||
|
message: `Session not found: ${sessionID}`,
|
||||||
|
variant: "error",
|
||||||
|
duration: 5000,
|
||||||
|
})
|
||||||
|
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 {}
|
||||||
|
}
|
||||||
|
await sync.session.sync(sessionID)
|
||||||
|
if (route.sessionID === sessionID && scroll) scroll.scrollBy(100_000)
|
||||||
|
})().catch((error) => {
|
||||||
|
if (route.sessionID !== sessionID) return
|
||||||
toast.show({
|
toast.show({
|
||||||
message: `Session not found: ${route.sessionID}`,
|
message: errorMessage(error),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
|
duration: 5000,
|
||||||
})
|
})
|
||||||
navigate({ type: "home" })
|
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
|
let lastSwitch: string | undefined = undefined
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
|
||||||
import { writeHeapSnapshot } from "v8"
|
import { writeHeapSnapshot } from "v8"
|
||||||
import { TuiConfig } from "./config/tui"
|
import { TuiConfig } from "./config/tui"
|
||||||
import { OPENCODE_PROCESS_ROLE, OPENCODE_RUN_ID, ensureRunID, sanitizedProcessEnv } from "@/util/opencode-process"
|
import { OPENCODE_PROCESS_ROLE, OPENCODE_RUN_ID, ensureRunID, sanitizedProcessEnv } from "@/util/opencode-process"
|
||||||
|
import { validateSession } from "./validate-session"
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
const OPENCODE_WORKER_PATH: string
|
const OPENCODE_WORKER_PATH: string
|
||||||
|
|
@ -202,6 +203,19 @@ export const TuiThreadCommand = cmd({
|
||||||
events: createEventSource(client),
|
events: createEventSource(client),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await validateSession({
|
||||||
|
url: transport.url,
|
||||||
|
sessionID: args.session,
|
||||||
|
directory: cwd,
|
||||||
|
fetch: transport.fetch,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
UI.error(errorMessage(error))
|
||||||
|
process.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
client.call("checkUpgrade", { directory: cwd }).catch(() => {})
|
client.call("checkUpgrade", { directory: cwd }).catch(() => {})
|
||||||
}, 1000).unref?.()
|
}, 1000).unref?.()
|
||||||
|
|
|
||||||
24
packages/opencode/src/cli/cmd/tui/validate-session.ts
Normal file
24
packages/opencode/src/cli/cmd/tui/validate-session.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
||||||
|
import { SessionID } from "@/session/schema"
|
||||||
|
|
||||||
|
export async function validateSession(input: {
|
||||||
|
url: string
|
||||||
|
sessionID?: string
|
||||||
|
directory?: string
|
||||||
|
fetch?: typeof fetch
|
||||||
|
headers?: RequestInit["headers"]
|
||||||
|
}) {
|
||||||
|
if (!input.sessionID) return
|
||||||
|
|
||||||
|
const result = SessionID.zod.safeParse(input.sessionID)
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(`Invalid session ID: ${result.error.issues.at(0)?.message ?? "unknown error"}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
await createOpencodeClient({
|
||||||
|
baseUrl: input.url,
|
||||||
|
directory: input.directory,
|
||||||
|
fetch: input.fetch,
|
||||||
|
headers: input.headers,
|
||||||
|
}).session.get({ sessionID: result.data }, { throwOnError: true })
|
||||||
|
}
|
||||||
|
|
@ -26,6 +26,15 @@ export function errorMessage(error: unknown): string {
|
||||||
return error.message
|
return error.message
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isRecord(error) &&
|
||||||
|
isRecord(error.data) &&
|
||||||
|
typeof error.data.message === "string" &&
|
||||||
|
error.data.message
|
||||||
|
) {
|
||||||
|
return error.data.message
|
||||||
|
}
|
||||||
|
|
||||||
const text = String(error)
|
const text = String(error)
|
||||||
if (text && text !== "[object Object]") return text
|
if (text && text !== "[object Object]") return text
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue