feat(app): support current pty transport (#38463)
This commit is contained in:
parent
67a04787bf
commit
ad78ef5a4c
26 changed files with 640 additions and 156 deletions
|
|
@ -127,11 +127,13 @@ export const SettingsGeneral: Component = () => {
|
|||
const serverSdk = useServerSDK()
|
||||
|
||||
const [shells] = createResource(
|
||||
() =>
|
||||
serverSdk()
|
||||
.client.pty.shells()
|
||||
.then((res) => res.data ?? [])
|
||||
.catch(() => [] as ShellOption[]),
|
||||
async () => {
|
||||
const sdk = serverSdk()
|
||||
if ((await sdk.protocol) === "v1") {
|
||||
return (await sdk.client.pty.shells()).data ?? []
|
||||
}
|
||||
return (await sdk.api.pty.shells()).data
|
||||
},
|
||||
{ initialValue: [] as ShellOption[] },
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -122,11 +122,13 @@ export const SettingsGeneralV2: Component<{
|
|||
const themeOptions = createMemo<ThemeOption[]>(() => theme.ids().map((id) => ({ id, name: theme.name(id) })))
|
||||
|
||||
const [shells] = createResource(
|
||||
() =>
|
||||
serverSdk()
|
||||
.client.pty.shells()
|
||||
.then((res) => res.data ?? [])
|
||||
.catch(() => [] as ShellOption[]),
|
||||
async () => {
|
||||
const sdk = serverSdk()
|
||||
if ((await sdk.protocol) === "v1") {
|
||||
return (await sdk.client.pty.shells()).data ?? []
|
||||
}
|
||||
return (await sdk.api.pty.shells()).data
|
||||
},
|
||||
{ initialValue: [] as ShellOption[] },
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -178,7 +178,6 @@ export const Terminal = (props: TerminalProps) => {
|
|||
// Terminal captures its connection for the PTY lifetime, so callers must key it per server/session.
|
||||
const connection = useServerSDK()().server
|
||||
const directory = sdk().directory
|
||||
const client = sdk().client
|
||||
const url = sdk().url
|
||||
const auth = connection.http
|
||||
const username = auth?.username ?? "opencode"
|
||||
|
|
@ -241,10 +240,21 @@ export const Terminal = (props: TerminalProps) => {
|
|||
}
|
||||
}
|
||||
|
||||
const pushSize = (cols: number, rows: number) => {
|
||||
return client.pty
|
||||
const pushSize = async (cols: number, rows: number) => {
|
||||
if ((await sdk().protocol) === "v1") {
|
||||
return sdk().client.pty
|
||||
.update({
|
||||
ptyID: id,
|
||||
size: { cols, rows },
|
||||
})
|
||||
.catch((err) => {
|
||||
debugTerminal("failed to sync terminal size", err)
|
||||
})
|
||||
}
|
||||
return sdk().api.pty
|
||||
.update({
|
||||
ptyID: id,
|
||||
location: { directory },
|
||||
size: { cols, rows },
|
||||
})
|
||||
.catch((err) => {
|
||||
|
|
@ -522,34 +532,60 @@ export const Terminal = (props: TerminalProps) => {
|
|||
local.onConnectError?.(err)
|
||||
}
|
||||
|
||||
const gone = () =>
|
||||
client.pty
|
||||
.get({ ptyID: id }, { throwOnError: false })
|
||||
.then((result) => result.response.status === 404)
|
||||
const gone = async () => {
|
||||
if ((await sdk().protocol) === "v1") {
|
||||
return sdk().client.pty
|
||||
.get({ ptyID: id }, { throwOnError: false })
|
||||
.then((result) => result.response.status === 404)
|
||||
.catch((err) => {
|
||||
debugTerminal("failed to inspect terminal session", err)
|
||||
return false
|
||||
})
|
||||
}
|
||||
return sdk().api.pty
|
||||
.get({ ptyID: id, location: { directory } })
|
||||
.then((result) => result.data.status === "exited")
|
||||
.catch((err) => {
|
||||
if (err && typeof err === "object" && "_tag" in err && err._tag === "PtyNotFoundError") return true
|
||||
debugTerminal("failed to inspect terminal session", err)
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
const connectToken = async () => {
|
||||
const result = await client.pty
|
||||
.connectToken(
|
||||
{ ptyID: id, directory },
|
||||
{
|
||||
throwOnError: false,
|
||||
headers: { "x-opencode-ticket": "1" },
|
||||
},
|
||||
)
|
||||
if ((await sdk().protocol) === "v1") {
|
||||
const result = await sdk().client.pty
|
||||
.connectToken(
|
||||
{ ptyID: id, directory },
|
||||
{
|
||||
throwOnError: false,
|
||||
headers: { "x-opencode-ticket": "1" },
|
||||
},
|
||||
)
|
||||
.catch((err: unknown) => {
|
||||
if (err instanceof Error && err.message.includes("Request is not supported")) return
|
||||
throw err
|
||||
})
|
||||
if (!result) return
|
||||
if (result.response.status === 200 && result.data?.ticket) return result.data.ticket
|
||||
if (result.response.status === 404 || result.response.status === 405) return
|
||||
if (result.response.status === 403)
|
||||
throw new Error("PTY connect ticket rejected by origin or CSRF checks. Check the server CORS config.")
|
||||
throw new Error(`PTY connect ticket failed with ${result.response.status}`)
|
||||
}
|
||||
return sdk().api.pty
|
||||
.connectToken({
|
||||
ptyID: id,
|
||||
location: { directory },
|
||||
"x-opencode-ticket": "1",
|
||||
})
|
||||
.then((result) => result.data.ticket)
|
||||
.catch((err: unknown) => {
|
||||
if (err instanceof Error && err.message.includes("Request is not supported")) return
|
||||
if (err && typeof err === "object" && "_tag" in err && err._tag === "ForbiddenError") {
|
||||
throw new Error("PTY connect ticket rejected by origin or CSRF checks. Check the server CORS config.")
|
||||
}
|
||||
throw err
|
||||
})
|
||||
if (!result) return
|
||||
if (result.response.status === 200 && result.data?.ticket) return result.data.ticket
|
||||
if (result.response.status === 404 || result.response.status === 405) return
|
||||
if (result.response.status === 403)
|
||||
throw new Error("PTY connect ticket rejected by origin or CSRF checks. Check the server CORS config.")
|
||||
throw new Error(`PTY connect ticket failed with ${result.response.status}`)
|
||||
}
|
||||
|
||||
const retry = (err: unknown) => {
|
||||
|
|
@ -579,11 +615,14 @@ export const Terminal = (props: TerminalProps) => {
|
|||
fail(err)
|
||||
return undefined
|
||||
})
|
||||
const protocol = await sdk().protocol
|
||||
if (protocol === "v2" && !ticket) return
|
||||
if (once.value) return
|
||||
if (disposed) return
|
||||
|
||||
const socket = new WebSocket(
|
||||
terminalWebSocketURL({
|
||||
protocol,
|
||||
url,
|
||||
id,
|
||||
directory,
|
||||
|
|
|
|||
|
|
@ -424,6 +424,7 @@ function createDirSdkContext(directory: string, serverSDK: ServerSDKBase) {
|
|||
|
||||
return {
|
||||
scope: serverSDK.scope,
|
||||
protocol: serverSDK.protocol,
|
||||
directory,
|
||||
client,
|
||||
api: createCompatibleApi({
|
||||
|
|
|
|||
|
|
@ -149,6 +149,7 @@ function createWorkspaceTerminalSession(
|
|||
scope: ServerScopeValue,
|
||||
legacySessionID?: string,
|
||||
) {
|
||||
const location = { directory: sdk.directory }
|
||||
const legacy = scope === ServerScope.local ? getLegacyTerminalStorageKeys(dir, legacySessionID) : []
|
||||
|
||||
const [store, setStore, _, ready] = persisted(
|
||||
|
|
@ -240,47 +241,61 @@ function createWorkspaceTerminalSession(
|
|||
})
|
||||
onCleanup(unsub)
|
||||
|
||||
const update = (client: DirectorySDK["client"], pty: Partial<LocalPTY> & { id: string }) => {
|
||||
const update = (pty: Partial<LocalPTY> & { id: string }) => {
|
||||
const index = store.all.findIndex((x) => x.id === pty.id)
|
||||
const previous = index >= 0 ? store.all[index] : undefined
|
||||
if (index >= 0) {
|
||||
setStore("all", index, (item) => ({ ...item, ...pty }))
|
||||
}
|
||||
client.pty
|
||||
.update({
|
||||
ptyID: pty.id,
|
||||
title: pty.title,
|
||||
size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined,
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
if (previous) {
|
||||
const currentIndex = store.all.findIndex((item) => item.id === pty.id)
|
||||
if (currentIndex >= 0) setStore("all", currentIndex, previous)
|
||||
}
|
||||
console.error("Failed to update terminal", error)
|
||||
})
|
||||
const doUpdate = async () => {
|
||||
if ((await sdk.protocol) === "v1") {
|
||||
await sdk.client.pty.update({
|
||||
ptyID: pty.id,
|
||||
title: pty.title,
|
||||
size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined,
|
||||
})
|
||||
} else {
|
||||
await sdk.api.pty.update({
|
||||
ptyID: pty.id,
|
||||
location,
|
||||
title: pty.title,
|
||||
size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
doUpdate().catch((error: unknown) => {
|
||||
if (previous) {
|
||||
const currentIndex = store.all.findIndex((item) => item.id === pty.id)
|
||||
if (currentIndex >= 0) setStore("all", currentIndex, previous)
|
||||
}
|
||||
console.error("Failed to update terminal", error)
|
||||
})
|
||||
}
|
||||
|
||||
const clone = async (client: DirectorySDK["client"], id: string) => {
|
||||
const clone = async (id: string) => {
|
||||
const index = store.all.findIndex((x) => x.id === id)
|
||||
const pty = store.all[index]
|
||||
if (!pty) return
|
||||
const next = await client.pty
|
||||
.create({
|
||||
const data = await (async () => {
|
||||
if ((await sdk.protocol) === "v1") {
|
||||
return (await sdk.client.pty.create({ title: pty.title })).data
|
||||
}
|
||||
return (await sdk.api.pty.create({
|
||||
location,
|
||||
title: pty.title,
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
console.error("Failed to clone terminal", error)
|
||||
return undefined
|
||||
})
|
||||
if (!next?.data) return
|
||||
})).data
|
||||
})().catch((error: unknown) => {
|
||||
console.error("Failed to clone terminal", error)
|
||||
return undefined
|
||||
})
|
||||
if (!data?.id) return
|
||||
|
||||
const active = store.active === pty.id
|
||||
|
||||
batch(() => {
|
||||
setStore("all", index, {
|
||||
id: next.data.id,
|
||||
title: next.data.title ?? pty.title,
|
||||
id: data.id,
|
||||
title: data.title ?? pty.title,
|
||||
titleNumber: pty.titleNumber,
|
||||
buffer: undefined,
|
||||
cursor: undefined,
|
||||
|
|
@ -289,7 +304,7 @@ function createWorkspaceTerminalSession(
|
|||
cols: undefined,
|
||||
})
|
||||
if (active) {
|
||||
setStore("active", next.data.id)
|
||||
setStore("active", data.id)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -308,17 +323,22 @@ function createWorkspaceTerminalSession(
|
|||
const nextNumber = pickNextTerminalNumber()
|
||||
const focusRequest = options?.focus ? requestFocus(undefined, true) : undefined
|
||||
|
||||
sdk.client.pty
|
||||
.create({ title: defaultTitle(nextNumber) })
|
||||
.then((pty: { data?: { id?: string; title?: string } }) => {
|
||||
const id = pty.data?.id
|
||||
const doCreate = async () => {
|
||||
if ((await sdk.protocol) === "v1") {
|
||||
return (await sdk.client.pty.create({ title: defaultTitle(nextNumber) })).data
|
||||
}
|
||||
return (await sdk.api.pty.create({ location, title: defaultTitle(nextNumber) })).data
|
||||
}
|
||||
doCreate()
|
||||
.then((data) => {
|
||||
const id = data?.id
|
||||
if (!id) {
|
||||
if (focusRequest !== undefined) cancelFocus(focusRequest)
|
||||
return
|
||||
}
|
||||
const newTerminal = {
|
||||
id,
|
||||
title: pty.data?.title ?? defaultTitle(nextNumber),
|
||||
title: data?.title ?? defaultTitle(nextNumber),
|
||||
titleNumber: nextNumber,
|
||||
}
|
||||
batch(() => {
|
||||
|
|
@ -335,7 +355,7 @@ function createWorkspaceTerminalSession(
|
|||
})
|
||||
},
|
||||
update(pty: Partial<LocalPTY> & { id: string }) {
|
||||
update(sdk.client, pty)
|
||||
update(pty)
|
||||
},
|
||||
trim(id: string) {
|
||||
const index = store.all.findIndex((x) => x.id === id)
|
||||
|
|
@ -350,10 +370,9 @@ function createWorkspaceTerminalSession(
|
|||
})
|
||||
},
|
||||
async clone(id: string) {
|
||||
await clone(sdk.client, id)
|
||||
await clone(id)
|
||||
},
|
||||
bind() {
|
||||
const client = sdk.client
|
||||
return {
|
||||
trim(id: string) {
|
||||
const index = store.all.findIndex((x) => x.id === id)
|
||||
|
|
@ -361,10 +380,10 @@ function createWorkspaceTerminalSession(
|
|||
setStore("all", index, (pty) => trimTerminal(pty))
|
||||
},
|
||||
update(pty: Partial<LocalPTY> & { id: string }) {
|
||||
update(client, pty)
|
||||
update(pty)
|
||||
},
|
||||
async clone(id: string) {
|
||||
await clone(client, id)
|
||||
await clone(id)
|
||||
},
|
||||
}
|
||||
},
|
||||
|
|
@ -412,7 +431,9 @@ function createWorkspaceTerminalSession(
|
|||
})
|
||||
}
|
||||
|
||||
await sdk.client.pty.remove({ ptyID: id }).catch((error: unknown) => {
|
||||
const removePromise =
|
||||
(await sdk.protocol) === "v1" ? sdk.client.pty.remove({ ptyID: id }) : sdk.api.pty.remove({ ptyID: id, location })
|
||||
await removePromise.catch((error: unknown) => {
|
||||
console.error("Failed to close terminal", error)
|
||||
})
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { createEffect, onCleanup, type JSX } from "solid-js"
|
||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
|
||||
import type { FileDiffInfo } from "@opencode-ai/client/promise"
|
||||
import { SessionReview } from "@opencode-ai/session-ui/session-review"
|
||||
import type {
|
||||
SessionReviewCommentActions,
|
||||
|
|
@ -14,7 +15,7 @@ import type { LineComment } from "@/context/comments"
|
|||
|
||||
export type DiffStyle = "unified" | "split"
|
||||
|
||||
type ReviewDiff = SnapshotFileDiff | VcsFileDiff
|
||||
type ReviewDiff = FileDiffInfo | SnapshotFileDiff | VcsFileDiff
|
||||
|
||||
export interface SessionReviewTabProps {
|
||||
title?: JSX.Element
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
|||
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
|
||||
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
|
||||
import type { FileDiffInfo } from "@opencode-ai/client/promise"
|
||||
import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
|
||||
|
|
@ -56,15 +57,16 @@ import { setSessionHandoff } from "@/pages/session/handoff"
|
|||
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||
import { SessionFileBrowserTab, type SessionFileBrowserState } from "@/pages/session/v2/session-file-browser-tab"
|
||||
|
||||
type RenderDiff = (SnapshotFileDiff & { file: string }) | VcsFileDiff
|
||||
type ReviewDiff = FileDiffInfo | SnapshotFileDiff | VcsFileDiff
|
||||
type RenderDiff = FileDiffInfo | (SnapshotFileDiff & { file: string }) | VcsFileDiff
|
||||
|
||||
function renderDiff(value: SnapshotFileDiff | VcsFileDiff): value is RenderDiff {
|
||||
function renderDiff(value: ReviewDiff): value is RenderDiff {
|
||||
return typeof value.file === "string"
|
||||
}
|
||||
|
||||
export function SessionSidePanel(props: {
|
||||
canReview: () => boolean
|
||||
diffs: () => (SnapshotFileDiff | VcsFileDiff)[]
|
||||
diffs: () => ReviewDiff[]
|
||||
diffsReady: () => boolean
|
||||
empty: () => string
|
||||
hasReview: () => boolean
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
|
||||
import type { FileDiffInfo } from "@opencode-ai/client/promise"
|
||||
import type { Kind } from "@/components/file-tree-v2"
|
||||
import { normalizeFileTreeV2Path } from "@/components/file-tree-v2-model"
|
||||
|
||||
export type RenderDiff = (SnapshotFileDiff & { file: string }) | VcsFileDiff
|
||||
export type RenderDiff = FileDiffInfo | (SnapshotFileDiff & { file: string }) | VcsFileDiff
|
||||
|
||||
export function normalizePath(p: string) {
|
||||
return normalizeFileTreeV2Path(p)
|
||||
}
|
||||
|
||||
export function filterRenderableDiff(value: SnapshotFileDiff | VcsFileDiff): value is RenderDiff {
|
||||
export function filterRenderableDiff(value: FileDiffInfo | SnapshotFileDiff | VcsFileDiff): value is RenderDiff {
|
||||
return typeof value.file === "string"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { createMemo, createResource, createSignal, Show, type JSX } from "solid-js"
|
||||
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
|
||||
import type { FileDiffInfo } from "@opencode-ai/client/promise"
|
||||
import {
|
||||
SESSION_REVIEW_V2_SIDEBAR_WIDTH_MAX,
|
||||
SESSION_REVIEW_V2_SIDEBAR_WIDTH_MIN,
|
||||
|
|
@ -30,7 +31,7 @@ import {
|
|||
import type { ReviewPanelV2State } from "@/pages/session/v2/review-panel-v2-state"
|
||||
import { applyFileListKeyDown, SessionFileListV2 } from "@/pages/session/v2/session-file-list-v2"
|
||||
|
||||
type ReviewDiff = SnapshotFileDiff | VcsFileDiff
|
||||
type ReviewDiff = FileDiffInfo | SnapshotFileDiff | VcsFileDiff
|
||||
|
||||
export type ReviewPanelV2Props = {
|
||||
title?: JSX.Element
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { describe, expect, test } from "bun:test"
|
||||
import type { SnapshotFileDiff } from "@opencode-ai/sdk/v2"
|
||||
import type { FileDiffInfo } from "@opencode-ai/client/promise"
|
||||
import type { Message } from "@opencode-ai/sdk/v2/client"
|
||||
import { diffs, message } from "./diffs"
|
||||
|
||||
|
|
@ -9,7 +10,7 @@ const item = {
|
|||
additions: 1,
|
||||
deletions: 1,
|
||||
status: "modified",
|
||||
} satisfies SnapshotFileDiff
|
||||
} satisfies FileDiffInfo & SnapshotFileDiff
|
||||
|
||||
describe("diffs", () => {
|
||||
test("keeps valid arrays", () => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
|
||||
import type { FileDiffInfo } from "@opencode-ai/client/promise"
|
||||
import type { Message } from "@opencode-ai/sdk/v2/client"
|
||||
|
||||
type Diff = SnapshotFileDiff | VcsFileDiff
|
||||
type Diff = FileDiffInfo | SnapshotFileDiff | VcsFileDiff
|
||||
|
||||
function diff(value: unknown): value is Diff {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return false
|
||||
|
|
|
|||
|
|
@ -2,8 +2,28 @@ import { describe, expect, test } from "bun:test"
|
|||
import { terminalWebSocketURL } from "./terminal-websocket-url"
|
||||
|
||||
describe("terminalWebSocketURL", () => {
|
||||
test("uses query auth without embedding credentials in websocket URL", () => {
|
||||
test("uses the current ticketed PTY route", () => {
|
||||
const url = terminalWebSocketURL({
|
||||
url: "http://127.0.0.1:49365",
|
||||
id: "pty_test",
|
||||
directory: "/tmp/project",
|
||||
cursor: 0,
|
||||
ticket: "connect-ticket",
|
||||
})
|
||||
|
||||
expect(url.protocol).toBe("ws:")
|
||||
expect(url.username).toBe("")
|
||||
expect(url.password).toBe("")
|
||||
expect(url.pathname).toBe("/api/pty/pty_test/connect")
|
||||
expect(url.searchParams.get("location[directory]")).toBe("/tmp/project")
|
||||
expect(url.searchParams.get("cursor")).toBe("0")
|
||||
expect(url.searchParams.get("ticket")).toBe("connect-ticket")
|
||||
expect(url.searchParams.has("auth_token")).toBe(false)
|
||||
})
|
||||
|
||||
test("uses query auth without embedding credentials in websocket URL for v1", () => {
|
||||
const url = terminalWebSocketURL({
|
||||
protocol: "v1",
|
||||
url: "http://127.0.0.1:49365",
|
||||
id: "pty_test",
|
||||
directory: "/tmp/project",
|
||||
|
|
@ -16,11 +36,14 @@ describe("terminalWebSocketURL", () => {
|
|||
expect(url.protocol).toBe("ws:")
|
||||
expect(url.username).toBe("")
|
||||
expect(url.password).toBe("")
|
||||
expect(url.pathname).toBe("/pty/pty_test/connect")
|
||||
expect(url.searchParams.get("directory")).toBe("/tmp/project")
|
||||
expect(url.searchParams.get("auth_token")).toBe(btoa("opencode:secret"))
|
||||
})
|
||||
|
||||
test("omits query auth for same-origin saved credentials", () => {
|
||||
test("omits query auth for same-origin saved credentials for v1", () => {
|
||||
const url = terminalWebSocketURL({
|
||||
protocol: "v1",
|
||||
url: "https://app.example.test",
|
||||
id: "pty_test",
|
||||
directory: "/tmp/project",
|
||||
|
|
@ -31,11 +54,14 @@ describe("terminalWebSocketURL", () => {
|
|||
})
|
||||
|
||||
expect(url.protocol).toBe("wss:")
|
||||
expect(url.pathname).toBe("/pty/pty_test/connect")
|
||||
expect(url.searchParams.get("directory")).toBe("/tmp/project")
|
||||
expect(url.searchParams.has("auth_token")).toBe(false)
|
||||
})
|
||||
|
||||
test("uses query auth for same-origin credentials from auth_token", () => {
|
||||
test("uses query auth for same-origin credentials from auth_token for v1", () => {
|
||||
const url = terminalWebSocketURL({
|
||||
protocol: "v1",
|
||||
url: "https://app.example.test",
|
||||
id: "pty_test",
|
||||
directory: "/tmp/project",
|
||||
|
|
@ -47,6 +73,8 @@ describe("terminalWebSocketURL", () => {
|
|||
})
|
||||
|
||||
expect(url.protocol).toBe("wss:")
|
||||
expect(url.pathname).toBe("/pty/pty_test/connect")
|
||||
expect(url.searchParams.get("directory")).toBe("/tmp/project")
|
||||
expect(url.searchParams.get("auth_token")).toBe(btoa("opencode:secret"))
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { authTokenFromCredentials } from "@/utils/server"
|
||||
|
||||
export function terminalWebSocketURL(input: {
|
||||
protocol?: "v1" | "v2"
|
||||
url: string
|
||||
id: string
|
||||
directory: string
|
||||
|
|
@ -11,18 +12,24 @@ export function terminalWebSocketURL(input: {
|
|||
password?: string
|
||||
authToken?: boolean
|
||||
}) {
|
||||
const next = new URL(`${input.url}/pty/${input.id}/connect`)
|
||||
next.searchParams.set("directory", input.directory)
|
||||
const isV1 = input.protocol === "v1"
|
||||
const next = new URL(`${input.url}${isV1 ? `/pty/${input.id}/connect` : `/api/pty/${input.id}/connect`}`)
|
||||
if (isV1) {
|
||||
next.searchParams.set("directory", input.directory)
|
||||
} else {
|
||||
next.searchParams.set("location[directory]", input.directory)
|
||||
}
|
||||
next.searchParams.set("cursor", String(input.cursor))
|
||||
next.protocol = next.protocol === "https:" ? "wss:" : "ws:"
|
||||
if (input.ticket) {
|
||||
next.searchParams.set("ticket", input.ticket)
|
||||
return next
|
||||
}
|
||||
if (input.password && (!input.sameOrigin || input.authToken))
|
||||
if (isV1 && input.password && (!input.sameOrigin || input.authToken)) {
|
||||
next.searchParams.set(
|
||||
"auth_token",
|
||||
authTokenFromCredentials({ username: input.username, password: input.password }),
|
||||
)
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue