mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-04 17:26:22 -04:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 84787ec55f | |||
| 5deafd93a1 | |||
| e0285d3f83 | |||
| 251d26adf3 | |||
| ca00e39d4a |
@@ -14,8 +14,8 @@ const projectID = "proj_context_resize_regression"
|
||||
const sessionID = "ses_context_resize_regression"
|
||||
const title = "Context resize regression"
|
||||
const model = { providerID: "opencode", modelID: "claude-opus-4-6", variant: "max" }
|
||||
const contextIDs = ["prt_0100_read", "prt_0101_glob", "prt_0102_grep", "prt_0103_list"]
|
||||
const followingTextID = "prt_0104_text"
|
||||
const contextIDs = ["ctx_0100_read", "ctx_0101_glob", "ctx_0102_grep", "ctx_0103_list"]
|
||||
const followingTextID = `${id("msg_assistant", 10)}:text:0`
|
||||
|
||||
type Message = {
|
||||
info: Record<string, unknown> & { id: string; role: "user" | "assistant" }
|
||||
@@ -263,7 +263,7 @@ function turn(index: number, target: boolean, status: "running" | "completed" =
|
||||
),
|
||||
contextTool(contextIDs[3]!, assistantID, "list", { path: "src" }, status),
|
||||
{
|
||||
id: followingTextID,
|
||||
id: "prt_0104_text",
|
||||
sessionID,
|
||||
messageID: assistantID,
|
||||
type: "text",
|
||||
@@ -295,7 +295,7 @@ function contextTool(
|
||||
sessionID,
|
||||
messageID,
|
||||
type: "tool",
|
||||
callID: `call_${partID}`,
|
||||
callID: partID,
|
||||
tool,
|
||||
state: {
|
||||
status,
|
||||
|
||||
@@ -10,7 +10,6 @@ const title = "Hidden terminal regression"
|
||||
test("unmounts the terminal panel while it is hidden", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1400, height: 900 })
|
||||
await mockOpenCodeServer(page, {
|
||||
protocol: "v2",
|
||||
directory,
|
||||
project: {
|
||||
id: projectID,
|
||||
|
||||
@@ -32,7 +32,7 @@ test("keeps the terminal session alive when switching session tabs in a workspac
|
||||
const connection = new URL(connections[0]!)
|
||||
expect(connection.pathname).toBe(`/api/pty/${ptyID}/connect`)
|
||||
expect(connection.searchParams.get("location[directory]")).toBe(directory)
|
||||
expect(connection.searchParams.get("ticket")).toBeNull()
|
||||
expect(connection.searchParams.get("ticket")).toBe("e2e-ticket")
|
||||
await writeProbe(page)
|
||||
|
||||
await switchTab(page, titleB)
|
||||
@@ -66,7 +66,6 @@ async function readProbe(page: Page) {
|
||||
|
||||
async function setup(page: Page) {
|
||||
await mockOpenCodeServer(page, {
|
||||
protocol: "v2",
|
||||
directory,
|
||||
project: {
|
||||
id: projectID,
|
||||
|
||||
@@ -21,7 +21,7 @@ const words = [
|
||||
"vector",
|
||||
]
|
||||
|
||||
const serverKey = "http://127.0.0.1:4096"
|
||||
const serverKey = `http://127.0.0.1:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
|
||||
const sourceID = "ses_smoke_source"
|
||||
const targetID = "ses_smoke_target"
|
||||
const directory = "C:/OpenCode/SmokeProject"
|
||||
@@ -134,7 +134,7 @@ function toolPart(
|
||||
return {
|
||||
id: id(`prt_tool_${tool}_${partIndex}`, index),
|
||||
type: "tool",
|
||||
callID: id("call", index * 10 + partIndex),
|
||||
callID: id("call", index * 100 + partIndex),
|
||||
tool,
|
||||
state: {
|
||||
status: "completed",
|
||||
@@ -235,8 +235,17 @@ function renderable(part: MessagePart) {
|
||||
return part.type !== "step-start" && part.type !== "step-finish" && part.type !== "patch"
|
||||
}
|
||||
|
||||
function orderedParts(message: Message) {
|
||||
return message.parts.slice().sort((a, b) => a.id.localeCompare(b.id))
|
||||
function currentPartIDs(message: Message) {
|
||||
const ordinals = { text: 0, reasoning: 0 }
|
||||
return message.parts
|
||||
.flatMap((part) => {
|
||||
if (!renderable(part)) return []
|
||||
if (part.type === "text") return [`${message.info.id}:text:${ordinals.text++}`]
|
||||
if (part.type === "reasoning") return [`${message.info.id}:reasoning:${ordinals.reasoning++}`]
|
||||
if (part.type === "tool") return [typeof part.callID === "string" ? part.callID : part.id]
|
||||
return []
|
||||
})
|
||||
.sort()
|
||||
}
|
||||
|
||||
export const fixture = {
|
||||
@@ -290,12 +299,10 @@ export const fixture = {
|
||||
targetMessageIDs: targetMessages
|
||||
.filter((message) => message.info.role === "user")
|
||||
.map((message) => message.info.id),
|
||||
targetPartIDs: targetMessages.flatMap((message) =>
|
||||
orderedParts(message)
|
||||
.filter(renderable)
|
||||
.map((part) => part.id),
|
||||
),
|
||||
expandedShellPartID: targetMessages.flatMap((message) => message.parts).find((part) => part.tool === "bash")!.id,
|
||||
targetPartIDs: targetMessages.flatMap(currentPartIDs),
|
||||
expandedShellPartID: targetMessages
|
||||
.flatMap((message) => message.parts)
|
||||
.find((part) => part.tool === "bash")!.callID,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ test.describe("smoke: session timeline", () => {
|
||||
test("keeps the visible message fixed while prepending history", async ({ page }) => {
|
||||
const requests: { before?: string; phase: "start" | "end"; at: number }[] = []
|
||||
await mockOpenCodeServer(page, {
|
||||
protocol: "v2",
|
||||
sessions: fixture.sessions,
|
||||
provider: fixture.provider,
|
||||
directory: fixture.directory,
|
||||
@@ -91,6 +92,7 @@ test.describe("smoke: session timeline", () => {
|
||||
|
||||
test("preserves the timeline gap above the composer", async ({ page }) => {
|
||||
await mockOpenCodeServer(page, {
|
||||
protocol: "v2",
|
||||
sessions: fixture.sessions,
|
||||
provider: fixture.provider,
|
||||
directory: fixture.directory,
|
||||
@@ -117,6 +119,7 @@ test.describe("smoke: session timeline", () => {
|
||||
|
||||
test("paints cached session tabs at the latest message", async ({ page }) => {
|
||||
await mockOpenCodeServer(page, {
|
||||
protocol: "v2",
|
||||
sessions: fixture.sessions,
|
||||
provider: fixture.provider,
|
||||
directory: fixture.directory,
|
||||
@@ -125,20 +128,19 @@ test.describe("smoke: session timeline", () => {
|
||||
})
|
||||
await configureSmokePage(page, fixture.directory)
|
||||
await page.addInitScript(
|
||||
({ dirBase64, sourceID, targetID }) => {
|
||||
({ server, sourceID, targetID }) => {
|
||||
localStorage.setItem(
|
||||
"opencode.window.browser.dat:tabs",
|
||||
JSON.stringify(
|
||||
[sourceID, targetID].map((sessionId) => ({
|
||||
type: "session",
|
||||
server: "http://127.0.0.1:4096",
|
||||
dirBase64,
|
||||
server,
|
||||
sessionId,
|
||||
})),
|
||||
),
|
||||
)
|
||||
},
|
||||
{ dirBase64: base64Encode(fixture.directory), sourceID: fixture.sourceID, targetID: fixture.targetID },
|
||||
{ server: fixture.serverKey, sourceID: fixture.sourceID, targetID: fixture.targetID },
|
||||
)
|
||||
|
||||
await page.goto(`/${base64Encode(fixture.directory)}/session/${fixture.targetID}`)
|
||||
@@ -243,6 +245,7 @@ test.describe("smoke: session timeline", () => {
|
||||
|
||||
test("paints a cold session tab at the latest message", async ({ page }) => {
|
||||
await mockOpenCodeServer(page, {
|
||||
protocol: "v2",
|
||||
sessions: fixture.sessions,
|
||||
provider: fixture.provider,
|
||||
directory: fixture.directory,
|
||||
@@ -251,20 +254,19 @@ test.describe("smoke: session timeline", () => {
|
||||
})
|
||||
await configureSmokePage(page, fixture.directory)
|
||||
await page.addInitScript(
|
||||
({ dirBase64, sourceID, targetID }) => {
|
||||
({ server, sourceID, targetID }) => {
|
||||
localStorage.setItem(
|
||||
"opencode.window.browser.dat:tabs",
|
||||
JSON.stringify(
|
||||
[sourceID, targetID].map((sessionId) => ({
|
||||
type: "session",
|
||||
server: "http://127.0.0.1:4096",
|
||||
dirBase64,
|
||||
server,
|
||||
sessionId,
|
||||
})),
|
||||
),
|
||||
)
|
||||
},
|
||||
{ dirBase64: base64Encode(fixture.directory), sourceID: fixture.sourceID, targetID: fixture.targetID },
|
||||
{ server: fixture.serverKey, sourceID: fixture.sourceID, targetID: fixture.targetID },
|
||||
)
|
||||
await page.goto(`/${base64Encode(fixture.directory)}/session/${fixture.sourceID}`)
|
||||
await expectSessionTitle(page, fixture.expected.sourceTitle)
|
||||
@@ -322,6 +324,7 @@ test.describe("smoke: session timeline", () => {
|
||||
test("renders seeded timeline in order while paging through history", async ({ page }) => {
|
||||
const errors = trackPageErrors(page)
|
||||
await mockOpenCodeServer(page, {
|
||||
protocol: "v2",
|
||||
sessions: fixture.sessions,
|
||||
provider: fixture.provider,
|
||||
directory: fixture.directory,
|
||||
|
||||
@@ -6,7 +6,7 @@ const directory = "C:/OpenCode/NewProject"
|
||||
|
||||
test("creates a session in a new project and selects its model", async ({ page }) => {
|
||||
await mockOpenCodeServer(page, {
|
||||
protocol: "v1",
|
||||
protocol: "v2",
|
||||
directory,
|
||||
project: {
|
||||
id: "proj_model_selection_flow",
|
||||
|
||||
@@ -80,7 +80,7 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
|
||||
)
|
||||
}
|
||||
if (path === "/global/health")
|
||||
return config.protocol === "v2" ? json(route, {}, undefined, 404) : json(route, { healthy: true })
|
||||
return config.protocol === "v2" ? json(route, {}) : json(route, { healthy: true })
|
||||
if (path === "/api/health" && config.protocol === "v2")
|
||||
return json(route, { healthy: true, version: "2.0.0", pid: 1 })
|
||||
if (path === "/experimental/capabilities") return json(route, { backgroundSubagents: true })
|
||||
|
||||
@@ -146,7 +146,7 @@ export function createCommandPaletteModel(props: { filesOnly?: () => boolean; on
|
||||
server: ServerConnection.key(serverSDK.server),
|
||||
opened: serverCtx.projects.list,
|
||||
stored: () => serverCtx.sync.data.project,
|
||||
load: (search, signal) => serverSDK.api.session.list({ parentID: null, search, limit: 50 }, { signal }),
|
||||
load: (search, signal) => serverSDK.currentApi.session.list({ parentID: null, search, limit: 50 }, { signal }),
|
||||
untitled: () => language.t("command.session.new"),
|
||||
category: () => language.t("command.category.session"),
|
||||
})
|
||||
|
||||
@@ -79,7 +79,7 @@ export function DialogHomeCommandPaletteV2(props: {
|
||||
server: ServerConnection.key(props.server),
|
||||
opened: serverCtx.projects.list,
|
||||
stored: () => serverCtx.sync.data.project,
|
||||
load: (search, signal) => serverCtx.sdk.api.session.list({ parentID: null, search, limit: 50 }, { signal }),
|
||||
load: (search, signal) => serverCtx.sdk.currentApi.session.list({ parentID: null, search, limit: 50 }, { signal }),
|
||||
untitled: () => language.t("command.session.new"),
|
||||
category: () => language.t("command.category.session"),
|
||||
})
|
||||
|
||||
@@ -418,7 +418,7 @@ function ProviderConnection(props: {
|
||||
() => ({ provider: props.provider, directory: directory() }),
|
||||
(input) =>
|
||||
serverSDK()
|
||||
.api.integration.get({
|
||||
.currentApi.integration.get({
|
||||
integrationID: input.provider,
|
||||
location: input.directory ? { directory: input.directory } : undefined,
|
||||
})
|
||||
@@ -547,7 +547,7 @@ function ProviderConnection(props: {
|
||||
}
|
||||
dispatch({ type: "auth.pending" })
|
||||
await serverSDK()
|
||||
.api.integration.oauth.connect({
|
||||
.currentApi.integration.oauth.connect({
|
||||
integrationID: props.provider,
|
||||
methodID: method.id,
|
||||
inputs: inputs ?? {},
|
||||
@@ -816,7 +816,7 @@ function ProviderConnection(props: {
|
||||
}
|
||||
|
||||
setFormStore("error", undefined)
|
||||
await serverSDK().api.integration.connect.key({
|
||||
await serverSDK().currentApi.integration.connect.key({
|
||||
integrationID: props.provider,
|
||||
location: location(),
|
||||
key: apiKey,
|
||||
@@ -947,7 +947,7 @@ function ProviderConnection(props: {
|
||||
|
||||
setFormStore("error", undefined)
|
||||
const result = await serverSDK()
|
||||
.api.integration.oauth.complete({
|
||||
.currentApi.integration.oauth.complete({
|
||||
integrationID: props.provider,
|
||||
attemptID: store.authorization!.attemptID,
|
||||
location: location(),
|
||||
@@ -1044,7 +1044,7 @@ function ProviderConnection(props: {
|
||||
const authorization = store.authorization
|
||||
if (!authorization || !alive.value) return
|
||||
const result = await serverSDK()
|
||||
.api.integration.oauth.status({
|
||||
.currentApi.integration.oauth.status({
|
||||
integrationID: props.provider,
|
||||
attemptID: authorization.attemptID,
|
||||
location: location(),
|
||||
|
||||
@@ -69,7 +69,7 @@ export const DialogFork: Component = () => {
|
||||
const dir = base64Encode(sdk().directory)
|
||||
|
||||
sdk()
|
||||
.api.session.fork({ sessionID, boundary: { type: "before", messageID: item.id } })
|
||||
.currentApi.session.fork({ sessionID, boundary: { type: "before", messageID: item.id } })
|
||||
.then((forked) => {
|
||||
dialog.close()
|
||||
prompt.set(restored, undefined, { dir, id: forked.id })
|
||||
|
||||
@@ -70,10 +70,20 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) {
|
||||
const [fallbackPath] = createResource(
|
||||
() => (missingBase() ? true : undefined),
|
||||
async (): Promise<Path | undefined> => {
|
||||
if ((await sdk.protocol) !== "v1") return
|
||||
return sdk.client.path
|
||||
if ((await sdk.protocol) === "v1")
|
||||
return sdk.client.path
|
||||
.get()
|
||||
.then((result) => result.data)
|
||||
.catch(() => undefined)
|
||||
return sdk.currentApi.location
|
||||
.get()
|
||||
.then((result) => result.data)
|
||||
.then((location) => ({
|
||||
state: "",
|
||||
config: "",
|
||||
worktree: location.project.directory,
|
||||
directory: location.directory,
|
||||
home: "",
|
||||
}))
|
||||
.catch(() => undefined)
|
||||
},
|
||||
{ initialValue: undefined },
|
||||
@@ -97,7 +107,7 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) {
|
||||
if (!policy.includeFiles) return { query: value, items: directories.slice(0, 5) }
|
||||
const base = pickerRoot(cleaned) || root() || start()
|
||||
if (!base) return { query: value, items: directories.slice(0, 5) }
|
||||
const files = await sdk.api.file
|
||||
const files = await sdk.currentApi.file
|
||||
.find({
|
||||
location: { directory: base },
|
||||
query: pickerFileSearchQuery(base, value, home()),
|
||||
@@ -127,7 +137,7 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) {
|
||||
existing ??
|
||||
loads.schedule(`${generation}:${key}`, eager ? "background" : "user", () => {
|
||||
if (!activeTreeNavigation(generation, navigation)) return Promise.resolve(undefined)
|
||||
return sdk.api.file
|
||||
return sdk.currentApi.file
|
||||
.list({ location: { directory: absolute } })
|
||||
.then((result) =>
|
||||
result.data.map((entry) => ({
|
||||
|
||||
@@ -61,10 +61,20 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
||||
const [fallbackPath] = createResource(
|
||||
() => (missingBase() ? true : undefined),
|
||||
async (): Promise<Path | undefined> => {
|
||||
if ((await sdk.protocol) !== "v1") return
|
||||
return sdk.client.path
|
||||
if ((await sdk.protocol) === "v1")
|
||||
return sdk.client.path
|
||||
.get()
|
||||
.then((result) => result.data)
|
||||
.catch(() => undefined)
|
||||
return sdk.currentApi.location
|
||||
.get()
|
||||
.then((result) => result.data)
|
||||
.then((location) => ({
|
||||
state: "",
|
||||
config: "",
|
||||
worktree: location.project.directory,
|
||||
directory: location.directory,
|
||||
home: "",
|
||||
}))
|
||||
.catch(() => undefined)
|
||||
},
|
||||
{ initialValue: undefined },
|
||||
|
||||
@@ -133,7 +133,7 @@ test("scopes file autocomplete to the current browser root", () => {
|
||||
test("resolves directory autocomplete from the current browser root", async () => {
|
||||
const directories: string[] = []
|
||||
const sdk = {
|
||||
api: {
|
||||
currentApi: {
|
||||
file: {
|
||||
find: (input: { location?: { directory?: string } }) => {
|
||||
directories.push(input.location?.directory ?? "")
|
||||
@@ -155,7 +155,7 @@ test("resolves directory autocomplete from the current browser root", async () =
|
||||
test("searches from an absolute root without a default base", async () => {
|
||||
const directories: string[] = []
|
||||
const sdk = {
|
||||
api: {
|
||||
currentApi: {
|
||||
file: {
|
||||
list: (input: { location?: { directory?: string } }) => {
|
||||
directories.push(input.location?.directory ?? "")
|
||||
|
||||
@@ -342,7 +342,7 @@ export function createDirectorySearch(args: { sdk: ServerSDK; base: () => string
|
||||
const key = trimPickerPath(directory)
|
||||
const existing = cache.get(key)
|
||||
if (existing) return existing
|
||||
const request = args.sdk.api.file
|
||||
const request = args.sdk.currentApi.file
|
||||
.list({ location: { directory: key } })
|
||||
.then((result) => result.data)
|
||||
.catch(() => [])
|
||||
@@ -374,7 +374,7 @@ export function createDirectorySearch(args: { sdk: ServerSDK; base: () => string
|
||||
const pathInput = raw.startsWith("~") || !!pickerRoot(raw) || raw.includes("/")
|
||||
const query = normalizePickerDrive(input.path)
|
||||
if (!pathInput) {
|
||||
const results = await args.sdk.api.file
|
||||
const results = await args.sdk.currentApi.file
|
||||
.find({ location: { directory: input.directory }, query, type: "directory", limit: 50 })
|
||||
.then((result) => result.data.map((entry) => entry.path))
|
||||
.catch(() => [])
|
||||
|
||||
@@ -199,6 +199,7 @@ beforeAll(async () => {
|
||||
directory: "/repo/main",
|
||||
client: rootClient,
|
||||
api: rootClient.api,
|
||||
currentApi: rootClient.api,
|
||||
url: "http://localhost:4096",
|
||||
createClient(opts: any) {
|
||||
return clientFor(opts.directory)
|
||||
@@ -332,7 +333,7 @@ describe("prompt submit worktree selection", () => {
|
||||
selected = "/repo/worktree-b"
|
||||
await submit.handleSubmit(event)
|
||||
|
||||
expect(createdClients).toEqual(["/repo/worktree-a", "/repo/worktree-b"])
|
||||
expect(createdClients).toEqual([])
|
||||
expect(createdSessions).toEqual(["/repo/worktree-a", "/repo/worktree-b"])
|
||||
expect(sessionCreateInputs).toEqual([
|
||||
{
|
||||
@@ -489,9 +490,6 @@ describe("prompt submit worktree selection", () => {
|
||||
agents: [],
|
||||
})
|
||||
expect((promptInputs[0] as { id?: string }).id).toStartWith("msg_")
|
||||
expect((promptInputs[0] as { legacyParts?: { id: string; type: string; text?: string }[] }).legacyParts).toEqual([
|
||||
{ id: expect.stringMatching(/^prt_/), type: "text", text: "ls" },
|
||||
])
|
||||
})
|
||||
|
||||
test("submits slash commands through the current session API", async () => {
|
||||
|
||||
@@ -22,6 +22,7 @@ import { ScopedKey } from "@/utils/server-scope"
|
||||
import { createPromptSubmissionState } from "./submission-state"
|
||||
import { normalizeSessionInfo } from "@/utils/session"
|
||||
import { Event } from "@opencode-ai/schema/event"
|
||||
import { getDirectory } from "@opencode-ai/core/util/path"
|
||||
|
||||
type PendingPrompt = {
|
||||
abort: AbortController
|
||||
@@ -41,7 +42,7 @@ export type FollowupDraft = {
|
||||
}
|
||||
|
||||
type FollowupSendInput = {
|
||||
api: DirectorySDK["api"]["session"]
|
||||
api: DirectorySDK["currentApi"]["session"]
|
||||
serverSync: ServerSync
|
||||
sync: DirectorySync
|
||||
draft: FollowupDraft
|
||||
@@ -159,10 +160,6 @@ export async function sendFollowupDraft(input: FollowupSendInput) {
|
||||
await input.api.prompt({
|
||||
sessionID: input.draft.sessionID,
|
||||
id: messageID,
|
||||
agent: input.draft.agent,
|
||||
model: input.draft.model,
|
||||
variant: input.draft.variant,
|
||||
legacyParts: requestParts,
|
||||
text: requestParts.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("\n"),
|
||||
files: requestParts.flatMap((part) => {
|
||||
if (part.type !== "file") return []
|
||||
@@ -264,7 +261,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
return sdk()
|
||||
.api.session.interrupt({ sessionID })
|
||||
.currentApi.session.interrupt({ sessionID })
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
@@ -348,13 +345,16 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
const worktreeSelection = input.newSessionWorktree?.() || "main"
|
||||
|
||||
let sessionDirectory = projectDirectory
|
||||
let client = sdk().client
|
||||
|
||||
if (isNewSession) {
|
||||
if (worktreeSelection === "create") {
|
||||
const createdWorktree = await client.worktree
|
||||
.create({ directory: projectDirectory })
|
||||
.then((x) => x.data)
|
||||
const createdWorktree = await sdk()
|
||||
.currentApi.projectCopy.create({
|
||||
projectID: sync().data.project,
|
||||
strategy: "git_worktree",
|
||||
directory: getDirectory(projectDirectory),
|
||||
location: { directory: projectDirectory },
|
||||
})
|
||||
.catch((err) => {
|
||||
showToast({
|
||||
title: language.t("prompt.toast.worktreeCreateFailed.title"),
|
||||
@@ -363,13 +363,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
return undefined
|
||||
})
|
||||
|
||||
if (!createdWorktree?.directory) {
|
||||
showToast({
|
||||
title: language.t("prompt.toast.worktreeCreateFailed.title"),
|
||||
description: language.t("common.requestFailed"),
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!createdWorktree) return
|
||||
WorktreeState.pending(sdk().scope, createdWorktree.directory)
|
||||
sessionDirectory = createdWorktree.directory
|
||||
}
|
||||
@@ -379,10 +373,6 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
}
|
||||
|
||||
if (sessionDirectory !== projectDirectory) {
|
||||
client = sdk().createClient({
|
||||
directory: sessionDirectory,
|
||||
throwOnError: true,
|
||||
})
|
||||
serverSync().child(sessionDirectory)
|
||||
}
|
||||
|
||||
@@ -392,7 +382,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
let session = input.info()
|
||||
if (!session && isNewSession) {
|
||||
const created = await sdk()
|
||||
.api.session.create({
|
||||
.currentApi.session.create({
|
||||
agent: currentAgent.name,
|
||||
model: { id: currentModel.id, providerID: currentModel.provider.id, variant },
|
||||
location: { directory: sessionDirectory },
|
||||
@@ -483,12 +473,10 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
clearInput()
|
||||
const eventID = Event.ID.create()
|
||||
sdk()
|
||||
.api.session.shell({
|
||||
.currentApi.session.shell({
|
||||
sessionID: session.id,
|
||||
id: eventID,
|
||||
command: text,
|
||||
agent,
|
||||
model,
|
||||
})
|
||||
.catch((err) => {
|
||||
showToast({
|
||||
@@ -509,7 +497,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
const messageID = Identifier.ascending("message")
|
||||
serverSync().session.set("session_status", session.id, { type: "busy" })
|
||||
sdk()
|
||||
.api.session.command({
|
||||
.currentApi.session.command({
|
||||
sessionID: session.id,
|
||||
id: messageID,
|
||||
command: commandName,
|
||||
@@ -606,7 +594,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
}
|
||||
|
||||
void sendFollowupDraft({
|
||||
api: sdk().api.session,
|
||||
api: sdk().currentApi.session,
|
||||
sync: sync(),
|
||||
serverSync: serverSync(),
|
||||
draft,
|
||||
|
||||
@@ -129,9 +129,13 @@ const SettingsProvidersContent: Component<{ onBack?: () => void }> = (props) =>
|
||||
return
|
||||
}
|
||||
await serverSDK()
|
||||
.client.auth.remove({ providerID })
|
||||
.then(async () => {
|
||||
await serverSDK().client.global.dispose()
|
||||
.currentApi.integration.get({ integrationID: providerID })
|
||||
.then(async (integration) => {
|
||||
const credentials = integration.data?.connections.filter((item) => item.type === "credential") ?? []
|
||||
if (credentials.length === 0) throw new Error(`No removable credentials found for ${name}`)
|
||||
await Promise.all(
|
||||
credentials.map((credential) => serverSDK().currentApi.credential.remove({ credentialID: credential.id })),
|
||||
)
|
||||
showToast({
|
||||
variant: "success",
|
||||
icon: "circle-check",
|
||||
|
||||
@@ -125,10 +125,17 @@ export const SettingsProvidersV2: Component<{
|
||||
await disableProvider(providerID, name)
|
||||
return
|
||||
}
|
||||
const location = props.directory() ? { directory: props.directory() } : undefined
|
||||
await serverSdk()
|
||||
.client.auth.remove({ providerID })
|
||||
.then(async () => {
|
||||
await serverSdk().client.global.dispose()
|
||||
.currentApi.integration.get({ integrationID: providerID, location })
|
||||
.then(async (integration) => {
|
||||
const credentials = integration.data?.connections.filter((item) => item.type === "credential") ?? []
|
||||
if (credentials.length === 0) throw new Error(`No removable credentials found for ${name}`)
|
||||
await Promise.all(
|
||||
credentials.map((credential) =>
|
||||
serverSdk().currentApi.credential.remove({ credentialID: credential.id, location }),
|
||||
),
|
||||
)
|
||||
showToast({
|
||||
variant: "success",
|
||||
icon: "circle-check",
|
||||
|
||||
@@ -17,6 +17,7 @@ import type { LocalPTY } from "@/context/terminal"
|
||||
import { disposeIfDisposable, getHoveredLinkText, setOptionIfSupported } from "@/utils/runtime-adapters"
|
||||
import { terminalWriter } from "@/utils/terminal-writer"
|
||||
import { terminalWebSocketURL } from "@/utils/terminal-websocket-url"
|
||||
import { authTokenFromCredentials } from "@/utils/server"
|
||||
|
||||
const TOGGLE_TERMINAL_ID = "terminal.toggle"
|
||||
const DEFAULT_TOGGLE_TERMINAL_KEYBIND = "ctrl+`"
|
||||
@@ -182,8 +183,6 @@ export const Terminal = (props: TerminalProps) => {
|
||||
const auth = connection.http
|
||||
const username = auth?.username ?? "opencode"
|
||||
const password = auth?.password ?? ""
|
||||
const authToken = connection.type === "http" ? connection.authToken : false
|
||||
const sameOrigin = new URL(url, location.href).origin === location.origin
|
||||
let container!: HTMLDivElement
|
||||
const [local, others] = splitProps(props, [
|
||||
"pty",
|
||||
@@ -241,18 +240,8 @@ export const Terminal = (props: TerminalProps) => {
|
||||
}
|
||||
|
||||
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({
|
||||
.currentApi.pty.update({
|
||||
ptyID: id,
|
||||
location: { directory },
|
||||
size: { cols, rows },
|
||||
@@ -533,17 +522,8 @@ export const Terminal = (props: TerminalProps) => {
|
||||
}
|
||||
|
||||
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 } })
|
||||
.currentApi.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
|
||||
@@ -553,33 +533,23 @@ export const Terminal = (props: TerminalProps) => {
|
||||
}
|
||||
|
||||
const connectToken = async () => {
|
||||
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)
|
||||
const endpoint = new URL(`/api/pty/${encodeURIComponent(id)}/connect-token`, url)
|
||||
endpoint.searchParams.set("location[directory]", directory)
|
||||
const response = await (platform.fetch ?? globalThis.fetch)(endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-opencode-ticket": "1",
|
||||
...(password
|
||||
? { Authorization: `Basic ${authTokenFromCredentials({ username, password })}` }
|
||||
: undefined),
|
||||
},
|
||||
})
|
||||
if (response.status === 403)
|
||||
throw new Error("PTY connect ticket rejected by origin or CSRF checks. Check the server CORS config.")
|
||||
if (!response.ok) throw new Error(`PTY connect ticket failed with ${response.status}`)
|
||||
const result = (await response.json()) as { data?: { ticket?: string } }
|
||||
if (!result.data?.ticket) throw new Error("PTY connect ticket response did not include a ticket")
|
||||
return result.data.ticket
|
||||
}
|
||||
|
||||
const retry = (err: unknown) => {
|
||||
@@ -609,23 +579,16 @@ 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,
|
||||
cursor: seek,
|
||||
ticket,
|
||||
sameOrigin,
|
||||
username,
|
||||
password,
|
||||
authToken,
|
||||
}),
|
||||
)
|
||||
socket.binaryType = "arraybuffer"
|
||||
|
||||
@@ -105,7 +105,7 @@ function SessionTabEntry(props: {
|
||||
|
||||
ctx.sync.session.remember({ ...value, title })
|
||||
try {
|
||||
await ctx.sdk.api.session.rename({ sessionID: value.id, title })
|
||||
await ctx.sdk.currentApi.session.rename({ sessionID: value.id, title })
|
||||
} catch (err) {
|
||||
const current = session()
|
||||
const currentCtx = props.serverCtx()
|
||||
|
||||
@@ -192,7 +192,7 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
|
||||
return conn ? { route, sdk: global.ensureServerCtx(conn).sdk } : undefined
|
||||
},
|
||||
({ route, sdk }) =>
|
||||
sdk.api.session
|
||||
sdk.currentApi.session
|
||||
.get({ sessionID: route.sessionId })
|
||||
.then(normalizeSessionInfo)
|
||||
.catch(() => {}),
|
||||
|
||||
@@ -124,7 +124,7 @@ export const createDirSyncContext = (
|
||||
fetch: async (count = 10) => {
|
||||
const [store, setStore] = current()
|
||||
setStore("limit", (value) => value + count)
|
||||
const response = await serverSDK.api.session.list({ directory, limit: store.limit, order: "desc" })
|
||||
const response = await serverSDK.currentApi.session.list({ directory, limit: store.limit, order: "desc" })
|
||||
const sessions = response.data
|
||||
.map(normalizeSessionInfo)
|
||||
.sort((a, b) => cmp(a.id, b.id))
|
||||
|
||||
@@ -81,8 +81,15 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
||||
normalizeDir: path.normalizeDir,
|
||||
list: (dir) =>
|
||||
sdk()
|
||||
.client.file.list({ path: dir })
|
||||
.then((x) => x.data ?? []),
|
||||
.currentApi.file.list({ path: dir, location: { directory: scope() } })
|
||||
.then((x) =>
|
||||
x.data.map((entry) => ({
|
||||
...entry,
|
||||
name: entry.path.split("/").at(-1) ?? entry.path,
|
||||
absolute: `${scope()}/${entry.path}`,
|
||||
ignored: false,
|
||||
})),
|
||||
),
|
||||
onError: (message) => {
|
||||
showToast({
|
||||
variant: "error",
|
||||
@@ -181,10 +188,10 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
||||
setLoading(file)
|
||||
|
||||
const promise = sdk()
|
||||
.client.file.read({ path: file })
|
||||
.then((x) => {
|
||||
.currentApi.file.read({ path: file, location: { directory } })
|
||||
.then((data) => {
|
||||
if (scope() !== directory) return
|
||||
const content = x.data
|
||||
const content = { type: "text" as const, content: new TextDecoder().decode(data) }
|
||||
setLoaded(file, content)
|
||||
|
||||
if (!content) return
|
||||
@@ -205,7 +212,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
||||
|
||||
const search = (query: string, dirs: "true" | "false", options?: { limit?: number; signal?: AbortSignal }) =>
|
||||
serverSDK()
|
||||
.api.file.find(
|
||||
.currentApi.file.find(
|
||||
{
|
||||
location: { directory: sdk().directory },
|
||||
query,
|
||||
|
||||
@@ -76,9 +76,30 @@ function directoryState() {
|
||||
}
|
||||
|
||||
describe("bootstrapDirectory", () => {
|
||||
test("uses legacy MCP endpoints while refreshing a v1 directory", async () => {
|
||||
test("uses current MCP endpoints while retaining unsupported v1 directory reads", async () => {
|
||||
const mcpReads: string[] = []
|
||||
const [store, setStore] = directoryState()
|
||||
const currentApi = {
|
||||
...api,
|
||||
command: {
|
||||
list: async () => {
|
||||
mcpReads.push("command")
|
||||
return { location: {}, data: [] }
|
||||
},
|
||||
},
|
||||
mcp: {
|
||||
list: async () => {
|
||||
mcpReads.push("status")
|
||||
return { location: {}, data: [] }
|
||||
},
|
||||
resource: {
|
||||
catalog: async () => {
|
||||
mcpReads.push("resource")
|
||||
return { location: {}, data: { resources: [], templates: [] } }
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as ServerApi
|
||||
|
||||
await bootstrapDirectory({
|
||||
directory: "/project",
|
||||
@@ -120,7 +141,7 @@ describe("bootstrapDirectory", () => {
|
||||
},
|
||||
provider: { list: async () => ({ data: { all: [], connected: [], default: {} } }) },
|
||||
} as unknown as OpencodeClient,
|
||||
api,
|
||||
api: currentApi,
|
||||
store,
|
||||
setStore,
|
||||
vcsCache: { setStore() {} } as unknown as VcsCache,
|
||||
@@ -141,12 +162,21 @@ describe("bootstrapDirectory", () => {
|
||||
|
||||
describe("query keys", () => {
|
||||
test("partitions identical directories by server scope", () => {
|
||||
const client = {} as Parameters<typeof loadPathQuery>[2]
|
||||
const location = {} as Parameters<typeof loadPathQuery>[2]
|
||||
const client = {} as Parameters<typeof loadPathQuery>[3]
|
||||
const api = {} as CatalogApi
|
||||
const remote = "https://debian.example" as typeof ServerScope.local
|
||||
|
||||
expect([...loadPathQuery(ServerScope.local, "/repo", client).queryKey]).toEqual(["local", "/repo", "path"])
|
||||
expect([...loadPathQuery(remote, "/repo", client).queryKey]).toEqual(["https://debian.example", "/repo", "path"])
|
||||
expect([...loadPathQuery(ServerScope.local, "/repo", location, client).queryKey]).toEqual([
|
||||
"local",
|
||||
"/repo",
|
||||
"path",
|
||||
])
|
||||
expect([...loadPathQuery(remote, "/repo", location, client).queryKey]).toEqual([
|
||||
"https://debian.example",
|
||||
"/repo",
|
||||
"path",
|
||||
])
|
||||
expect([...loadProvidersQuery(remote, null, api).queryKey]).toEqual(["https://debian.example", null, "providers"])
|
||||
})
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ import type {
|
||||
CommandInfo,
|
||||
CommandListInput,
|
||||
CommandListOutput,
|
||||
LocationGetInput,
|
||||
LocationGetOutput,
|
||||
ProjectCurrentInput,
|
||||
ProjectCurrentOutput,
|
||||
ProjectListOutput,
|
||||
@@ -115,6 +117,7 @@ type ProjectApi = {
|
||||
readonly list: () => Promise<ProjectListOutput>
|
||||
readonly current: (input?: ProjectCurrentInput) => Promise<ProjectCurrentOutput>
|
||||
}
|
||||
type LocationApi = { readonly get: (input?: LocationGetInput) => Promise<LocationGetOutput> }
|
||||
|
||||
type McpApi = ServerApi["mcp"]
|
||||
type PermissionApi = ServerApi["permission"]
|
||||
@@ -139,7 +142,7 @@ export const loadProjectsQuery = (scope: ServerScope, api: ProjectApi) =>
|
||||
|
||||
export async function bootstrapGlobal(input: {
|
||||
serverSDK: OpencodeClient
|
||||
serverAPI: CatalogApi & { readonly project: ProjectApi }
|
||||
serverAPI: CatalogApi & { readonly location: LocationApi; readonly project: ProjectApi }
|
||||
protocol?: Promise<ServerProtocol>
|
||||
scope: ServerScope
|
||||
requestFailedTitle: string
|
||||
@@ -152,9 +155,12 @@ export async function bootstrapGlobal(input: {
|
||||
() => input.queryClient.fetchQuery(loadGlobalConfigQuery(input.scope, input.serverSDK)),
|
||||
() =>
|
||||
input.queryClient.fetchQuery(
|
||||
loadProvidersQuery(input.scope, null, input.serverAPI, input.serverSDK, input.protocol),
|
||||
loadProvidersQuery(input.scope, null, input.serverAPI),
|
||||
),
|
||||
() =>
|
||||
input.queryClient.fetchQuery(
|
||||
loadPathQuery(input.scope, null, input.serverAPI.location, input.serverSDK, input.protocol),
|
||||
),
|
||||
() => input.queryClient.fetchQuery(loadPathQuery(input.scope, null, input.serverSDK, input.protocol)),
|
||||
() =>
|
||||
input.queryClient
|
||||
.fetchQuery(loadProjectsQuery(input.scope, input.serverAPI.project))
|
||||
@@ -219,17 +225,11 @@ export const loadProvidersQuery = (
|
||||
scope: ServerScope,
|
||||
directory: string | null,
|
||||
sdk: CatalogApi,
|
||||
legacy?: OpencodeClient,
|
||||
protocol?: Promise<ServerProtocol>,
|
||||
) =>
|
||||
queryOptions({
|
||||
queryKey: [scope, directory, "providers"],
|
||||
queryFn: () =>
|
||||
retry(async () => {
|
||||
if ((await protocol) === "v1" && legacy) {
|
||||
const result = await legacy.provider.list()
|
||||
return normalizeProviderList(result.data!)
|
||||
}
|
||||
const location = directory ? { location: { directory } } : undefined
|
||||
const [providers, models, defaultModel] = await Promise.all([
|
||||
sdk.provider.list(location),
|
||||
@@ -256,54 +256,38 @@ export const loadAgentsQuery = (
|
||||
scope: ServerScope,
|
||||
directory: string,
|
||||
sdk: AgentListApi,
|
||||
legacy?: OpencodeClient,
|
||||
protocol?: Promise<ServerProtocol>,
|
||||
) =>
|
||||
queryOptions({
|
||||
queryKey: [scope, directory, "agents"],
|
||||
queryFn: () =>
|
||||
retry(async () => {
|
||||
if ((await protocol) === "v1" && legacy) return normalizeAgentList((await legacy.app.agents()).data ?? [])
|
||||
return sdk.list({ location: { directory } }).then((result) => normalizeAgentList(result.data))
|
||||
}),
|
||||
retry(() => sdk.list({ location: { directory } }).then((result) => normalizeAgentList(result.data))),
|
||||
})
|
||||
|
||||
export const loadCommands = (
|
||||
directory: string,
|
||||
api: CommandListApi,
|
||||
legacy?: OpencodeClient,
|
||||
protocol?: Promise<ServerProtocol>,
|
||||
): Promise<CommandInfo[]> =>
|
||||
retry(async () => {
|
||||
if ((await protocol) === "v1" && legacy) {
|
||||
return ((await legacy.command.list()).data ?? []).map((command) => {
|
||||
const [providerID, id] = command.model?.split("/") ?? []
|
||||
return {
|
||||
name: command.name,
|
||||
template: command.template,
|
||||
description: command.description,
|
||||
agent: command.agent,
|
||||
model: providerID && id ? { providerID, id } : undefined,
|
||||
subtask: command.subtask,
|
||||
// source: command.source === "skill" ? undefined : command.source,
|
||||
}
|
||||
})
|
||||
}
|
||||
return api.list({ location: { directory } }).then((result) => result.data)
|
||||
})
|
||||
retry(() => api.list({ location: { directory } }).then((result) => result.data))
|
||||
|
||||
export const loadPathQuery = (
|
||||
scope: ServerScope,
|
||||
directory: string | null,
|
||||
api: LocationApi,
|
||||
sdk: OpencodeClient,
|
||||
protocol?: Promise<ServerProtocol>,
|
||||
) =>
|
||||
queryOptions<Path>({
|
||||
queryKey: [scope, directory, "path"],
|
||||
queryFn: async () => {
|
||||
if ((await protocol) !== "v1")
|
||||
return { state: "", config: "", worktree: "", directory: directory ?? "", home: "" }
|
||||
return retry(() => sdk.path.get({ directory: directory ?? undefined }).then((result) => result.data!))
|
||||
if ((await protocol) === "v1")
|
||||
return retry(() => sdk.path.get({ directory: directory ?? undefined }).then((result) => result.data!))
|
||||
return retry(() => api.get(directory ? { location: { directory } } : undefined)).then((location) => ({
|
||||
state: "",
|
||||
config: "",
|
||||
worktree: location.project.directory,
|
||||
directory: location.directory,
|
||||
home: "",
|
||||
}))
|
||||
},
|
||||
})
|
||||
|
||||
@@ -311,16 +295,11 @@ export const loadReferencesQuery = (
|
||||
scope: ServerScope,
|
||||
directory: string,
|
||||
api: ReferenceListApi,
|
||||
legacy?: OpencodeClient,
|
||||
protocol?: Promise<ServerProtocol>,
|
||||
) =>
|
||||
queryOptions<ReferenceInfo[]>({
|
||||
queryKey: [scope, directory, "references"] as const,
|
||||
queryFn: () =>
|
||||
retry(async () => {
|
||||
if ((await protocol) === "v1" && legacy) return (await legacy.v2.reference.list()).data?.data ?? []
|
||||
return api.list({ location: { directory } }).then((result) => result.data)
|
||||
}).catch(() => []),
|
||||
retry(() => api.list({ location: { directory } }).then((result) => result.data)).catch(() => []),
|
||||
placeholderData: [],
|
||||
})
|
||||
|
||||
@@ -339,6 +318,7 @@ export async function bootstrapDirectory(input: {
|
||||
readonly reference: ReferenceListApi
|
||||
readonly session: SessionApi
|
||||
readonly vcs: VcsApi
|
||||
readonly location: LocationApi
|
||||
}
|
||||
store: Store<State>
|
||||
setStore: SetStoreFunction<State>
|
||||
@@ -373,7 +353,7 @@ export async function bootstrapDirectory(input: {
|
||||
() => Promise.resolve(input.loadSessions(input.directory)),
|
||||
() =>
|
||||
input.queryClient
|
||||
.ensureQueryData(loadAgentsQuery(input.scope, input.directory, input.api.agent, input.sdk, input.protocol))
|
||||
.ensureQueryData(loadAgentsQuery(input.scope, input.directory, input.api.agent))
|
||||
.then((data) => input.setStore("agent", data)),
|
||||
() =>
|
||||
retry(() => input.sdk.config.get().then((x) => input.setStore("config", reconcile(x.data!, { merge: false })))),
|
||||
@@ -412,7 +392,9 @@ export async function bootstrapDirectory(input: {
|
||||
!seededPath &&
|
||||
(() =>
|
||||
input.queryClient
|
||||
.ensureQueryData(loadPathQuery(input.scope, input.directory, input.sdk, input.protocol))
|
||||
.ensureQueryData(
|
||||
loadPathQuery(input.scope, input.directory, input.api.location, input.sdk, input.protocol),
|
||||
)
|
||||
.then((data) => {
|
||||
const next = projectID(data.directory ?? input.directory, input.global.project)
|
||||
if (next) input.setStore("project", next)
|
||||
@@ -428,12 +410,12 @@ export async function bootstrapDirectory(input: {
|
||||
}),
|
||||
input.mcp &&
|
||||
(() =>
|
||||
loadCommands(input.directory, input.api.command, input.sdk, input.protocol).then((commands) =>
|
||||
loadCommands(input.directory, input.api.command).then((commands) =>
|
||||
input.setStore("command", commands),
|
||||
)),
|
||||
() =>
|
||||
input.queryClient.fetchQuery(
|
||||
loadReferencesQuery(input.scope, input.directory, input.api.reference, input.sdk, input.protocol),
|
||||
loadReferencesQuery(input.scope, input.directory, input.api.reference),
|
||||
),
|
||||
() =>
|
||||
retry(() =>
|
||||
@@ -511,16 +493,16 @@ export async function bootstrapDirectory(input: {
|
||||
input.mcp &&
|
||||
(() =>
|
||||
input.queryClient.fetchQuery(
|
||||
loadMcpQuery(input.scope, input.directory, input.api.mcp, input.sdk, input.protocol),
|
||||
loadMcpQuery(input.scope, input.directory, input.api.mcp),
|
||||
)),
|
||||
input.mcp &&
|
||||
(() =>
|
||||
input.queryClient.fetchQuery(
|
||||
loadMcpResourcesQuery(input.scope, input.directory, input.api.mcp, input.sdk, input.protocol),
|
||||
loadMcpResourcesQuery(input.scope, input.directory, input.api.mcp),
|
||||
)),
|
||||
() =>
|
||||
input.queryClient
|
||||
.fetchQuery(loadProvidersQuery(input.scope, input.directory, input.api, input.sdk, input.protocol))
|
||||
.fetchQuery(loadProvidersQuery(input.scope, input.directory, input.api))
|
||||
.catch((err) => {
|
||||
const project = getFilename(input.directory)
|
||||
showToast({
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { type Accessor, createMemo } from "solid-js"
|
||||
import { type ServerSDK, useServerSDK } from "./server-sdk"
|
||||
import { type DirectorySDK, useServerSDK } from "./server-sdk"
|
||||
|
||||
export type DirectorySDK = ReturnType<ServerSDK["ensureDirSdkContext"]>
|
||||
export type { DirectorySDK }
|
||||
|
||||
export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
||||
name: "SDK",
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useGlobal } from "./global"
|
||||
import { ServerScope } from "@/utils/server-scope"
|
||||
import { detectServerProtocol, type ServerProtocol } from "@/utils/server-protocol"
|
||||
import { createCompatibleApi, type CompatibleApi } from "@/utils/server-compat"
|
||||
import type { OpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||
|
||||
const isAbortError = (error: unknown) =>
|
||||
error !== null && typeof error === "object" && "name" in error && error.name === "AbortError"
|
||||
@@ -192,11 +193,6 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS
|
||||
})()
|
||||
|
||||
const eventApi = createApiForServer({ server: server.http, fetch: eventFetch })
|
||||
const eventSdk = createSdkForServer({
|
||||
signal: abort.signal,
|
||||
fetch: eventFetch,
|
||||
server: server.http,
|
||||
})
|
||||
const protocol = detectServerProtocol(server.http, platform.fetch ?? globalThis.fetch)
|
||||
const [protocolKind] = createResource(
|
||||
() => protocol,
|
||||
@@ -264,18 +260,12 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS
|
||||
}
|
||||
abort.signal.addEventListener("abort", onAbort)
|
||||
try {
|
||||
const kind = await protocol
|
||||
const events =
|
||||
kind === "v1"
|
||||
? (await eventSdk.global.event({ signal: attempt.signal })).stream
|
||||
: eventApi.event.subscribe({ signal: attempt.signal })
|
||||
const events = eventApi.event.subscribe({ signal: attempt.signal })
|
||||
let yielded = Date.now()
|
||||
for await (const event of events) {
|
||||
streamErrorLogged = false
|
||||
const legacy = "payload" in event
|
||||
if (legacy && event.payload.type === "sync") continue
|
||||
const directory = legacy ? (event.directory ?? "global") : (event.location?.directory ?? "global")
|
||||
const payload = legacy ? (event.payload as Event) : adaptServerEvent(event)
|
||||
const directory = event.location?.directory ?? "global"
|
||||
const payload = adaptServerEvent(event)
|
||||
if (enqueueServerEvent(queue, { directory, payload })) schedule()
|
||||
|
||||
if (Date.now() - yielded < STREAM_YIELD_MS) continue
|
||||
@@ -364,8 +354,24 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS
|
||||
}
|
||||
}
|
||||
|
||||
type SDKEventMap = {
|
||||
[key in Event["type"]]: Extract<ServerEvent, { type: key }>
|
||||
}
|
||||
|
||||
export type DirectorySDK = {
|
||||
scope: ServerScope
|
||||
protocol: Promise<ServerProtocol>
|
||||
directory: string
|
||||
client: OpencodeClient
|
||||
currentApi: ServerApi
|
||||
api: CompatibleApi
|
||||
event: ReturnType<typeof createGlobalEmitter<SDKEventMap>>
|
||||
readonly url: string
|
||||
createClient: ServerSDKBase["createClient"]
|
||||
}
|
||||
|
||||
export type ServerSDK = ServerSDKBase & {
|
||||
ensureDirSdkContext: (directory: string) => ReturnType<typeof createDirSdkContext>
|
||||
ensureDirSdkContext: (directory: string) => DirectorySDK
|
||||
}
|
||||
|
||||
export function createServerSdkContext(server: ServerConnection.Any, scope: ServerScope): ServerSDK {
|
||||
@@ -397,11 +403,7 @@ export function useServerProtocol() {
|
||||
return createMemo(() => serverSDK().protocolKind())
|
||||
}
|
||||
|
||||
type SDKEventMap = {
|
||||
[key in Event["type"]]: Extract<ServerEvent, { type: key }>
|
||||
}
|
||||
|
||||
function createDirSdkContext(directory: string, serverSDK: ServerSDKBase) {
|
||||
function createDirSdkContext(directory: string, serverSDK: ServerSDKBase): DirectorySDK {
|
||||
const client = serverSDK.createClient({
|
||||
directory,
|
||||
throwOnError: true,
|
||||
@@ -419,6 +421,7 @@ function createDirSdkContext(directory: string, serverSDK: ServerSDKBase) {
|
||||
protocol: serverSDK.protocol,
|
||||
directory,
|
||||
client,
|
||||
currentApi: serverSDK.currentApi,
|
||||
api: createCompatibleApi({
|
||||
protocol: serverSDK.protocol,
|
||||
current: serverSDK.currentApi,
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
} from "./global-sync/bootstrap"
|
||||
import { createChildStoreManager } from "./global-sync/child-store"
|
||||
import { applyDirectoryEvent, applyGlobalEvent } from "./global-sync/event-reducer"
|
||||
import { estimateRootSessionTotal, loadRootSessions, loadRootSessionsV1 } from "./global-sync/session-load"
|
||||
import { estimateRootSessionTotal, loadRootSessions } from "./global-sync/session-load"
|
||||
import { trimSessions } from "./global-sync/session-trim"
|
||||
import type { ProjectMeta } from "./global-sync/types"
|
||||
import { SESSION_RECENT_LIMIT } from "./global-sync/types"
|
||||
@@ -59,6 +59,7 @@ import type {
|
||||
} from "@opencode-ai/client/promise"
|
||||
import { toggleMcp } from "./global-sync/mcp"
|
||||
import { createServerSession, type ServerSession } from "./server-session"
|
||||
import { usePlatform } from "./platform"
|
||||
|
||||
type GlobalStore = {
|
||||
ready: boolean
|
||||
@@ -94,8 +95,6 @@ export const loadMcpQuery = (
|
||||
scope: ServerScope,
|
||||
directory: string,
|
||||
api: McpListApi,
|
||||
legacy?: OpencodeClient,
|
||||
protocol?: Promise<"v1" | "v2">,
|
||||
): ApiQueryOptions<Record<string, McpServer["status"]>, readonly [ServerScope, string, "mcp"]> =>
|
||||
queryOptions<
|
||||
Record<string, McpServer["status"]>,
|
||||
@@ -105,7 +104,6 @@ export const loadMcpQuery = (
|
||||
>({
|
||||
queryKey: [scope, directory, "mcp"] as const,
|
||||
queryFn: async () => {
|
||||
if ((await protocol) === "v1" && legacy) return (await legacy.mcp.status()).data ?? {}
|
||||
return api
|
||||
.list({ location: { directory } })
|
||||
.then((result) => Object.fromEntries(result.data.map((server) => [server.name, server.status])))
|
||||
@@ -116,8 +114,6 @@ export const loadMcpResourcesQuery = (
|
||||
scope: ServerScope,
|
||||
directory: string,
|
||||
api: McpResourceApi,
|
||||
legacy?: OpencodeClient,
|
||||
protocol?: Promise<"v1" | "v2">,
|
||||
): ApiQueryOptions<Record<string, McpResource>, readonly [ServerScope, string, "mcpResources"]> =>
|
||||
queryOptions<
|
||||
Record<string, McpResource>,
|
||||
@@ -127,14 +123,6 @@ export const loadMcpResourcesQuery = (
|
||||
>({
|
||||
queryKey: [scope, directory, "mcpResources"] as const,
|
||||
queryFn: async () => {
|
||||
if ((await protocol) === "v1" && legacy) {
|
||||
return Object.fromEntries(
|
||||
Object.entries((await legacy.experimental.resource.list()).data ?? {}).map(([key, resource]) => [
|
||||
key,
|
||||
{ ...resource, server: resource.client },
|
||||
]),
|
||||
)
|
||||
}
|
||||
return api.resource
|
||||
.catalog({ location: { directory } })
|
||||
.then((result) =>
|
||||
@@ -186,16 +174,13 @@ function makeQueryOptionsApi(
|
||||
return {
|
||||
globalConfig: () => loadGlobalConfigQuery(scope, serverSDK()),
|
||||
projects: () => loadProjectsQuery(scope, serverAPI.project),
|
||||
providers: (directory: PathKey | null) =>
|
||||
loadProvidersQuery(scope, directory, serverAPI, directory ? sdkFor(directory) : serverSDK(), protocol),
|
||||
providers: (directory: PathKey | null) => loadProvidersQuery(scope, directory, serverAPI),
|
||||
path: (directory: PathKey | null) =>
|
||||
loadPathQuery(scope, directory, directory ? sdkFor(directory) : serverSDK(), protocol),
|
||||
agents: (directory: PathKey) => loadAgentsQuery(scope, directory, serverAPI.agent, sdkFor(directory), protocol),
|
||||
references: (directory: PathKey) =>
|
||||
loadReferencesQuery(scope, directory, serverAPI.reference, sdkFor(directory), protocol),
|
||||
mcp: (directory: PathKey) => loadMcpQuery(scope, directory, serverAPI.mcp, sdkFor(directory), protocol),
|
||||
mcpResources: (directory: PathKey) =>
|
||||
loadMcpResourcesQuery(scope, directory, serverAPI.mcp, sdkFor(directory), protocol),
|
||||
loadPathQuery(scope, directory, serverAPI.location, directory ? sdkFor(directory) : serverSDK(), protocol),
|
||||
agents: (directory: PathKey) => loadAgentsQuery(scope, directory, serverAPI.agent),
|
||||
references: (directory: PathKey) => loadReferencesQuery(scope, directory, serverAPI.reference),
|
||||
mcp: (directory: PathKey) => loadMcpQuery(scope, directory, serverAPI.mcp),
|
||||
mcpResources: (directory: PathKey) => loadMcpResourcesQuery(scope, directory, serverAPI.mcp),
|
||||
lsp: (directory: PathKey) => loadLspQuery(scope, directory, sdkFor(directory)),
|
||||
sessions: (directory: PathKey) => ({ queryKey: [scope, directory, "loadSessions"] as const }),
|
||||
}
|
||||
@@ -204,6 +189,7 @@ export type QueryOptionsApi = ReturnType<typeof makeQueryOptionsApi>
|
||||
|
||||
export function createServerSyncContextInner(serverSDK: ServerSDK) {
|
||||
const language = useLanguage()
|
||||
const platform = usePlatform()
|
||||
const owner = getOwner()
|
||||
if (!owner) throw new Error("ServerSync must be created within owner")
|
||||
|
||||
@@ -224,13 +210,11 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
|
||||
return sdk
|
||||
}
|
||||
|
||||
const session = createServerSession(serverSDK.client, serverSDK.api.session, serverSDK.api.message, {
|
||||
protocol: serverSDK.protocol,
|
||||
})
|
||||
const session = createServerSession(serverSDK.client, serverSDK.currentApi.session, serverSDK.currentApi.message)
|
||||
const queryOptionsApi = makeQueryOptionsApi(
|
||||
serverSDK.scope,
|
||||
() => serverSDK.client,
|
||||
serverSDK.api,
|
||||
serverSDK.currentApi,
|
||||
sdkFor,
|
||||
serverSDK.protocol,
|
||||
)
|
||||
@@ -241,19 +225,7 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
|
||||
const activeSessionsQuery = useQuery(() =>
|
||||
loadActiveSessionsQuery(serverSDK.scope, {
|
||||
active: async () => {
|
||||
if ((await serverSDK.protocol) === "v1") {
|
||||
const statuses = (await serverSDK.client.session.status()).data ?? {}
|
||||
seedActiveSessionStatuses(session, statuses)
|
||||
for (const sessionID of Object.keys(statuses)) {
|
||||
void session.resolve(sessionID).catch(() => undefined)
|
||||
}
|
||||
return Object.fromEntries(
|
||||
Object.entries(statuses).flatMap(([sessionID, status]) =>
|
||||
status.type === "idle" ? [] : [[sessionID, { type: "running" as const }]],
|
||||
),
|
||||
)
|
||||
}
|
||||
const active = await serverSDK.api.session.active()
|
||||
const active = await serverSDK.currentApi.session.active()
|
||||
seedActiveSessionStatuses(session, active)
|
||||
for (const sessionID of Object.keys(active)) {
|
||||
void session.resolve(sessionID).catch(() => undefined)
|
||||
@@ -322,7 +294,7 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
|
||||
queryFn: async () => {
|
||||
await bootstrapGlobal({
|
||||
serverSDK: serverSDK.client,
|
||||
serverAPI: serverSDK.api,
|
||||
serverAPI: serverSDK.currentApi,
|
||||
protocol: serverSDK.protocol,
|
||||
scope: serverSDK.scope,
|
||||
requestFailedTitle: language.t("common.requestFailed"),
|
||||
@@ -363,7 +335,7 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
|
||||
void bootstrapInstance(directory)
|
||||
},
|
||||
onMcp: (directory, setStore) => {
|
||||
void loadCommands(directory, serverSDK.api.command, sdkFor(directory), serverSDK.protocol)
|
||||
void loadCommands(directory, serverSDK.currentApi.command)
|
||||
.then((commands) => setStore("command", commands))
|
||||
.catch((err) => {
|
||||
showToast({
|
||||
@@ -416,12 +388,7 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
|
||||
.fetchQuery({
|
||||
...queryOptionsApi.sessions(key),
|
||||
queryFn: () =>
|
||||
serverSDK.protocol
|
||||
.then((protocol) =>
|
||||
protocol === "v1"
|
||||
? loadRootSessionsV1({ client: sdkFor(directory), directory, limit })
|
||||
: loadRootSessions({ api: serverSDK.api.session, directory, limit }),
|
||||
)
|
||||
loadRootSessions({ api: serverSDK.currentApi.session, directory, limit })
|
||||
.then((x) => {
|
||||
const nonArchived = (x.data ?? [])
|
||||
.filter((s) => !!s?.id)
|
||||
@@ -491,7 +458,7 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
|
||||
provider: globalStore.provider,
|
||||
},
|
||||
sdk,
|
||||
api: serverSDK.api,
|
||||
api: serverSDK.currentApi,
|
||||
store: child[0],
|
||||
setStore: child[1],
|
||||
vcsCache: cache,
|
||||
@@ -692,27 +659,35 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
|
||||
mcp: {
|
||||
toggle: async (directory: string, name: string) => {
|
||||
const key = directoryKey(directory)
|
||||
const sdk = sdkFor(key)
|
||||
const status = children.child(key, { bootstrap: false })[0].mcp[name]?.status
|
||||
if (!status) return
|
||||
await toggleMcp({
|
||||
status,
|
||||
connect: async () => {
|
||||
if ((await serverSDK.protocol) === "v1") {
|
||||
await sdk.mcp.connect({ name })
|
||||
return
|
||||
}
|
||||
await serverSDK.api.mcp.connect({ server: name, location: { directory: key } })
|
||||
await serverSDK.currentApi.mcp.connect({ server: name, location: { directory: key } })
|
||||
},
|
||||
disconnect: async () => {
|
||||
if ((await serverSDK.protocol) === "v1") {
|
||||
await sdk.mcp.disconnect({ name })
|
||||
return
|
||||
}
|
||||
await serverSDK.api.mcp.disconnect({ server: name, location: { directory: key } })
|
||||
await serverSDK.currentApi.mcp.disconnect({ server: name, location: { directory: key } })
|
||||
},
|
||||
authenticate: async () => {
|
||||
await sdk.mcp.auth.authenticate({ name })
|
||||
const server = (await serverSDK.currentApi.mcp.list({ location: { directory: key } })).data.find(
|
||||
(item) => item.name === name,
|
||||
)
|
||||
if (!server?.integrationID) throw new Error(`MCP server ${name} has no authentication integration`)
|
||||
const integration = await serverSDK.currentApi.integration.get({
|
||||
integrationID: server.integrationID,
|
||||
location: { directory: key },
|
||||
})
|
||||
const method = integration.data?.methods.find((item) => item.type === "oauth" && !item.prompts?.length)
|
||||
if (!method || method.type !== "oauth")
|
||||
throw new Error(`MCP server ${name} requires an interactive authentication form`)
|
||||
const attempt = await serverSDK.currentApi.integration.oauth.connect({
|
||||
integrationID: server.integrationID,
|
||||
methodID: method.id,
|
||||
inputs: {},
|
||||
location: { directory: key },
|
||||
})
|
||||
platform.openLink(attempt.data.url)
|
||||
},
|
||||
refresh: async () => {
|
||||
await queryClient.refetchQueries(queryOptionsApi.mcp(key))
|
||||
|
||||
@@ -248,20 +248,12 @@ function createWorkspaceTerminalSession(
|
||||
setStore("all", index, (item) => ({ ...item, ...pty }))
|
||||
}
|
||||
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,
|
||||
})
|
||||
}
|
||||
await sdk.currentApi.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) {
|
||||
@@ -276,20 +268,13 @@ function createWorkspaceTerminalSession(
|
||||
const index = store.all.findIndex((x) => x.id === id)
|
||||
const pty = store.all[index]
|
||||
if (!pty) return
|
||||
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,
|
||||
})
|
||||
).data
|
||||
})().catch((error: unknown) => {
|
||||
console.error("Failed to clone terminal", error)
|
||||
return undefined
|
||||
})
|
||||
const data = await sdk.currentApi.pty
|
||||
.create({ location, title: pty.title })
|
||||
.then((result) => result.data)
|
||||
.catch((error: unknown) => {
|
||||
console.error("Failed to clone terminal", error)
|
||||
return undefined
|
||||
})
|
||||
if (!data?.id) return
|
||||
|
||||
const active = store.active === pty.id
|
||||
@@ -326,10 +311,9 @@ function createWorkspaceTerminalSession(
|
||||
const focusRequest = options?.focus ? requestFocus(undefined, true) : undefined
|
||||
|
||||
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
|
||||
return sdk.currentApi.pty
|
||||
.create({ location, title: defaultTitle(nextNumber) })
|
||||
.then((result) => result.data)
|
||||
}
|
||||
doCreate()
|
||||
.then((data) => {
|
||||
@@ -433,11 +417,7 @@ function createWorkspaceTerminalSession(
|
||||
})
|
||||
}
|
||||
|
||||
const removePromise =
|
||||
(await sdk.protocol) === "v1"
|
||||
? sdk.client.pty.remove({ ptyID: id })
|
||||
: sdk.api.pty.remove({ ptyID: id, location })
|
||||
await removePromise.catch((error: unknown) => {
|
||||
await sdk.currentApi.pty.remove({ ptyID: id, location }).catch((error: unknown) => {
|
||||
console.error("Failed to close terminal", error)
|
||||
})
|
||||
},
|
||||
|
||||
@@ -25,7 +25,7 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { getFilename } from "@opencode-ai/core/util/path"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
||||
import type { Session } from "@/types"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useSettings } from "@/context/settings"
|
||||
@@ -1189,10 +1189,13 @@ export default function LegacyLayout(props: ParentProps) {
|
||||
const refreshDirs = async (target?: string) => {
|
||||
if (!target || target === root || canOpen(target)) return canOpen(target)
|
||||
const listed = await Promise.resolve(
|
||||
project?.id ?? serverSDK().api.project.current({ location: { directory: root } }),
|
||||
project?.id ?? serverSDK().currentApi.project.current({ location: { directory: root } }),
|
||||
)
|
||||
.then((value) => (typeof value === "string" ? value : value.id))
|
||||
.then((projectID) => serverSDK().api.project.directories({ projectID, location: { directory: root } }))
|
||||
.then(async (projectID) => {
|
||||
await serverSDK().currentApi.projectCopy.refresh({ projectID, location: { directory: root } })
|
||||
return serverSDK().currentApi.project.directories({ projectID, location: { directory: root } })
|
||||
})
|
||||
.then((items) => items.map((item) => item.directory).filter((item) => pathKey(item) !== pathKey(root)))
|
||||
.catch(() => [] as string[])
|
||||
dirs = effectiveWorkspaceOrder(root, [root, ...listed], store.workspaceOrder[root])
|
||||
@@ -1237,7 +1240,7 @@ export default function LegacyLayout(props: ParentProps) {
|
||||
await Promise.all(
|
||||
dirs.map(async (item) => ({
|
||||
path: { directory: item },
|
||||
session: await listAllSessions(serverSDK().api.session, {
|
||||
session: await listAllSessions(serverSDK().currentApi.session, {
|
||||
directory: item,
|
||||
parentID: null,
|
||||
order: "desc",
|
||||
@@ -1403,16 +1406,19 @@ export default function LegacyLayout(props: ParentProps) {
|
||||
|
||||
setBusy(directory, true)
|
||||
|
||||
const result = await serverSDK()
|
||||
.client.worktree.remove({ directory: root, worktreeRemoveInput: { directory } })
|
||||
.then((x) => x.data)
|
||||
.catch((err) => {
|
||||
showToast({
|
||||
title: language.t("workspace.delete.failed.title"),
|
||||
description: errorMessage(err, language.t("common.requestFailed")),
|
||||
})
|
||||
return false
|
||||
})
|
||||
const projectID = serverSync().data.project.find((project) => project.worktree === root)?.id
|
||||
const result = projectID
|
||||
? await serverSDK()
|
||||
.currentApi.projectCopy.remove({ projectID, directory, force: false, location: { directory: root } })
|
||||
.then(() => true)
|
||||
.catch((err) => {
|
||||
showToast({
|
||||
title: language.t("workspace.delete.failed.title"),
|
||||
description: errorMessage(err, language.t("common.requestFailed")),
|
||||
})
|
||||
return false
|
||||
})
|
||||
: false
|
||||
|
||||
setBusy(directory, false)
|
||||
|
||||
@@ -1461,7 +1467,9 @@ export default function LegacyLayout(props: ParentProps) {
|
||||
})
|
||||
const dismiss = () => toaster.dismiss(progress)
|
||||
|
||||
const sessions = await listAllSessions(serverSDK().api.session, { directory, order: "desc" }).catch(() => [])
|
||||
const sessions = await listAllSessions(serverSDK().currentApi.session, { directory, order: "desc" }).catch(
|
||||
() => [],
|
||||
)
|
||||
|
||||
clearWorkspaceTerminals(
|
||||
directory,
|
||||
@@ -1595,7 +1603,7 @@ export default function LegacyLayout(props: ParentProps) {
|
||||
})
|
||||
|
||||
const refresh = async () => {
|
||||
const sessions = await listAllSessions(serverSDK().api.session, {
|
||||
const sessions = await listAllSessions(serverSDK().currentApi.session, {
|
||||
directory: props.directory,
|
||||
order: "desc",
|
||||
}).catch(() => [])
|
||||
@@ -1835,20 +1843,26 @@ export default function LegacyLayout(props: ParentProps) {
|
||||
|
||||
const createWorkspace = async (project: LocalProject) => {
|
||||
clearSidebarHoverState()
|
||||
const created = await serverSDK()
|
||||
.client.worktree.create({ directory: project.worktree })
|
||||
.then((x) => x.data)
|
||||
.catch((err) => {
|
||||
showToast({
|
||||
title: language.t("workspace.create.failed.title"),
|
||||
description: errorMessage(err, language.t("common.requestFailed")),
|
||||
})
|
||||
return undefined
|
||||
})
|
||||
const created = project.id
|
||||
? await serverSDK()
|
||||
.currentApi.projectCopy.create({
|
||||
projectID: project.id,
|
||||
strategy: "git_worktree",
|
||||
directory: getDirectory(project.worktree),
|
||||
location: { directory: project.worktree },
|
||||
})
|
||||
.catch((err) => {
|
||||
showToast({
|
||||
title: language.t("workspace.create.failed.title"),
|
||||
description: errorMessage(err, language.t("common.requestFailed")),
|
||||
})
|
||||
return undefined
|
||||
})
|
||||
: undefined
|
||||
|
||||
if (!created?.directory) return
|
||||
|
||||
setWorkspaceName(created.directory, created.branch ?? getFilename(created.directory), project.id, created.branch)
|
||||
setWorkspaceName(created.directory, getFilename(created.directory), project.id)
|
||||
|
||||
const local = project.worktree
|
||||
const key = pathKey(created.directory)
|
||||
|
||||
@@ -1724,7 +1724,7 @@ export default function Page() {
|
||||
setFollowup("failed", input.sessionID, undefined)
|
||||
|
||||
const ok = await sendFollowupDraft({
|
||||
api: sdk().api.session,
|
||||
api: sdk().currentApi.session,
|
||||
sync: sync(),
|
||||
serverSync: serverSync(),
|
||||
draft: item,
|
||||
@@ -1820,13 +1820,13 @@ export default function Page() {
|
||||
const halt = (sessionID: string) =>
|
||||
busy(sessionID)
|
||||
? sdk()
|
||||
.api.session.interrupt({ sessionID })
|
||||
.currentApi.session.interrupt({ sessionID })
|
||||
.catch(() => {})
|
||||
: Promise.resolve()
|
||||
|
||||
const revertMutation = useMutation(() => ({
|
||||
mutationFn: async (input: { sessionID: string; messageID: string }) => {
|
||||
const session = sdk().api.session
|
||||
const session = sdk().currentApi.session
|
||||
const target = sync()
|
||||
const last = target.session.get(input.sessionID)?.revert
|
||||
const value = draft(input.messageID)
|
||||
@@ -1849,7 +1849,7 @@ export default function Page() {
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return
|
||||
|
||||
const session = sdk().api.session
|
||||
const session = sdk().currentApi.session
|
||||
const target = sync()
|
||||
const next = userMessages().find((item) => item.id > id)
|
||||
const last = target.session.get(sessionID)?.revert
|
||||
|
||||
@@ -55,8 +55,8 @@ export function SessionReviewTab(props: SessionReviewTabProps) {
|
||||
|
||||
const readFile = async (path: string) => {
|
||||
return sdk()
|
||||
.client.file.read({ path })
|
||||
.then((x) => x.data)
|
||||
.currentApi.file.read({ path, location: { directory: sdk().directory } })
|
||||
.then((content) => ({ type: "text" as const, content: new TextDecoder().decode(content) }))
|
||||
.catch((error) => {
|
||||
console.debug("[session-review] failed to read file", { path, error })
|
||||
return undefined
|
||||
|
||||
@@ -680,7 +680,7 @@ export function MessageTimeline(props: {
|
||||
|
||||
const titleMutation = useMutation(() => ({
|
||||
mutationFn: (input: { id: string; title: string }) =>
|
||||
sdk().api.session.rename({ sessionID: input.id, title: input.title }),
|
||||
sdk().currentApi.session.rename({ sessionID: input.id, title: input.title }),
|
||||
onSuccess: (_, input) => {
|
||||
sync().set(
|
||||
produce((draft) => {
|
||||
@@ -854,7 +854,7 @@ export function MessageTimeline(props: {
|
||||
const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1])
|
||||
|
||||
const result = await sdk()
|
||||
.api.session.remove({ sessionID })
|
||||
.currentApi.session.remove({ sessionID })
|
||||
.then(() => true)
|
||||
.catch((err) => {
|
||||
showToast({
|
||||
|
||||
@@ -306,7 +306,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return
|
||||
const owner = sessionOwnership.capture()
|
||||
const session = sdk().api.session
|
||||
const session = sdk().currentApi.session
|
||||
const directory = sdk().directory
|
||||
const promptSession = prompt.capture()
|
||||
const revert = info()?.revert?.messageID
|
||||
@@ -334,7 +334,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return
|
||||
const owner = sessionOwnership.capture()
|
||||
const session = sdk().api.session
|
||||
const session = sdk().currentApi.session
|
||||
const messages = userMessages()
|
||||
const promptSession = prompt.capture()
|
||||
|
||||
@@ -366,19 +366,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return
|
||||
|
||||
const model = local.model.current()
|
||||
if (!model) {
|
||||
showToast({
|
||||
title: language.t("toast.model.none.title"),
|
||||
description: language.t("toast.model.none.description"),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await sdk().api.session.compact({
|
||||
sessionID,
|
||||
model: { providerID: model.provider.id, modelID: model.id },
|
||||
})
|
||||
await sdk().currentApi.session.compact({ sessionID })
|
||||
}
|
||||
|
||||
const fork = () => {
|
||||
|
||||
@@ -102,8 +102,8 @@ export function ReviewPanelV2(props: ReviewPanelV2Props) {
|
||||
|
||||
const readFile = async (path: string) =>
|
||||
sdk()
|
||||
.client.file.read({ path })
|
||||
.then((x) => x.data)
|
||||
.currentApi.file.read({ path, location: { directory: sdk().directory } })
|
||||
.then((content) => ({ type: "text" as const, content: new TextDecoder().decode(content) }))
|
||||
.catch((error) => {
|
||||
console.debug("[session-review-v2] failed to read file", { path, error })
|
||||
return undefined
|
||||
|
||||
Reference in New Issue
Block a user