refactor(tui): wire generated client reads (#34381)
This commit is contained in:
parent
e8ac44430b
commit
381d67572e
28 changed files with 509 additions and 427 deletions
3
bun.lock
3
bun.lock
|
|
@ -98,6 +98,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
|
"@opencode-ai/client": "workspace:*",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opencode-ai/server": "workspace:*",
|
"@opencode-ai/server": "workspace:*",
|
||||||
|
|
@ -581,6 +582,7 @@
|
||||||
"@octokit/graphql": "9.0.2",
|
"@octokit/graphql": "9.0.2",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
"@openauthjs/openauth": "catalog:",
|
"@openauthjs/openauth": "catalog:",
|
||||||
|
"@opencode-ai/client": "workspace:*",
|
||||||
"@opencode-ai/llm": "workspace:*",
|
"@opencode-ai/llm": "workspace:*",
|
||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
"@opencode-ai/protocol": "workspace:*",
|
"@opencode-ai/protocol": "workspace:*",
|
||||||
|
|
@ -932,6 +934,7 @@
|
||||||
"name": "@opencode-ai/tui",
|
"name": "@opencode-ai/tui",
|
||||||
"version": "1.17.11",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@opencode-ai/client": "workspace:*",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
|
"@opencode-ai/client": "workspace:*",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opencode-ai/server": "workspace:*",
|
"@opencode-ai/server": "workspace:*",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { TuiConfig } from "@opencode-ai/tui/config"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { loadBuiltinPlugins } from "@opencode-ai/tui/builtins"
|
import { loadBuiltinPlugins } from "@opencode-ai/tui/builtins"
|
||||||
|
import { OpenCode } from "@opencode-ai/client"
|
||||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||||
|
|
||||||
type Transport = { url: string; headers: RequestInit["headers"] }
|
type Transport = { url: string; headers: RequestInit["headers"] }
|
||||||
|
|
@ -12,23 +13,23 @@ export function runTui(transport: Transport, reload?: () => Promise<Transport>)
|
||||||
let disposeSlots: (() => void) | undefined
|
let disposeSlots: (() => void) | undefined
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const options = { baseUrl: transport.url, headers: transport.headers }
|
const options = { baseUrl: transport.url, headers: transport.headers }
|
||||||
const client = createOpencodeClient(options)
|
const api = OpenCode.make(options)
|
||||||
const directory = yield* Effect.tryPromise(() =>
|
const directory = yield* Effect.tryPromise(() => api.files.list({ location: { directory: process.cwd() } })).pipe(
|
||||||
client.v2.fs.list({ location: { directory: process.cwd() } }, { throwOnError: true }),
|
Effect.map((response) => response.location.directory),
|
||||||
).pipe(
|
|
||||||
Effect.map((response) => response.data.location.directory),
|
|
||||||
Effect.catch(() =>
|
Effect.catch(() =>
|
||||||
Effect.tryPromise(() => client.v2.location.get(undefined, { throwOnError: true })).pipe(
|
Effect.tryPromise(() => api.location.get()).pipe(Effect.map((response) => response.directory)),
|
||||||
Effect.map((response) => response.data.directory),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return yield* run({
|
return yield* run({
|
||||||
client: createOpencodeClient({ ...options, directory }),
|
client: createOpencodeClient({ ...options, directory }),
|
||||||
|
api,
|
||||||
reload: reload
|
reload: reload
|
||||||
? async () => {
|
? async () => {
|
||||||
const next = await reload()
|
const next = await reload()
|
||||||
return createOpencodeClient({ baseUrl: next.url, headers: next.headers, directory })
|
return {
|
||||||
|
client: createOpencodeClient({ baseUrl: next.url, headers: next.headers, directory }),
|
||||||
|
api: OpenCode.make({ baseUrl: next.url, headers: next.headers }),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
args: {},
|
args: {},
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
export * from "./generated/index"
|
export * from "./generated/index"
|
||||||
export type { EventsSubscribeOutput as OpenCodeEvent } from "./generated/types"
|
export type { EventsSubscribeOutput as OpenCodeEvent } from "./generated/types"
|
||||||
|
export type OpenCodeClient = ReturnType<typeof import("./generated/client").make>
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,7 @@
|
||||||
"@octokit/graphql": "9.0.2",
|
"@octokit/graphql": "9.0.2",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
"@openauthjs/openauth": "catalog:",
|
"@openauthjs/openauth": "catalog:",
|
||||||
|
"@opencode-ai/client": "workspace:*",
|
||||||
"@opencode-ai/llm": "workspace:*",
|
"@opencode-ai/llm": "workspace:*",
|
||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
"@opencode-ai/protocol": "workspace:*",
|
"@opencode-ai/protocol": "workspace:*",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { UI } from "@/cli/ui"
|
||||||
import { errorMessage } from "@opencode-ai/tui/util/error"
|
import { errorMessage } from "@opencode-ai/tui/util/error"
|
||||||
import { validateSession } from "../tui/validate-session"
|
import { validateSession } from "../tui/validate-session"
|
||||||
import { ServerAuth } from "@/server/auth"
|
import { ServerAuth } from "@/server/auth"
|
||||||
|
import { OpenCode } from "@opencode-ai/client"
|
||||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
||||||
|
|
||||||
export const AttachCommand = cmd({
|
export const AttachCommand = cmd({
|
||||||
|
|
@ -134,6 +135,7 @@ export const AttachCommand = cmd({
|
||||||
await Effect.runPromise(
|
await Effect.runPromise(
|
||||||
run({
|
run({
|
||||||
client: createOpencodeClient({ baseUrl: args.url, headers, directory }),
|
client: createOpencodeClient({ baseUrl: args.url, headers, directory }),
|
||||||
|
api: OpenCode.make({ baseUrl: args.url, headers }),
|
||||||
config,
|
config,
|
||||||
pluginHost: createLegacyTuiPluginHost(),
|
pluginHost: createLegacyTuiPluginHost(),
|
||||||
args: {
|
args: {
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { errorMessage } from "@opencode-ai/tui/util/error"
|
||||||
import { withTimeout } from "@/util/timeout"
|
import { withTimeout } from "@/util/timeout"
|
||||||
import { withNetworkOptions, resolveNetworkOptionsNoConfig } from "@/cli/network"
|
import { withNetworkOptions, resolveNetworkOptionsNoConfig } from "@/cli/network"
|
||||||
import { Filesystem } from "@/util/filesystem"
|
import { Filesystem } from "@/util/filesystem"
|
||||||
|
import { OpenCode } from "@opencode-ai/client"
|
||||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
||||||
import { writeHeapSnapshot } from "v8"
|
import { writeHeapSnapshot } from "v8"
|
||||||
import { validateSession } from "../tui/validate-session"
|
import { validateSession } from "../tui/validate-session"
|
||||||
|
|
@ -205,6 +206,7 @@ export const TuiThreadCommand = cmd({
|
||||||
await Effect.runPromise(
|
await Effect.runPromise(
|
||||||
run({
|
run({
|
||||||
client: createOpencodeClient({ baseUrl: url, directory: cwd }),
|
client: createOpencodeClient({ baseUrl: url, directory: cwd }),
|
||||||
|
api: OpenCode.make({ baseUrl: url }),
|
||||||
async onSnapshot() {
|
async onSnapshot() {
|
||||||
const tui = writeHeapSnapshot("tui.heapsnapshot")
|
const tui = writeHeapSnapshot("tui.heapsnapshot")
|
||||||
const server = await client.call("snapshot", undefined)
|
const server = await client.call("snapshot", undefined)
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@
|
||||||
"./component/spinner": "./src/component/spinner.tsx"
|
"./component/spinner": "./src/component/spinner.tsx"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@opencode-ai/client": "workspace:*",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ import {
|
||||||
useOpencodeKeymap,
|
useOpencodeKeymap,
|
||||||
} from "./keymap"
|
} from "./keymap"
|
||||||
|
|
||||||
|
import type { OpenCodeClient } from "@opencode-ai/client"
|
||||||
import type { OpencodeClient } from "@opencode-ai/sdk/v2"
|
import type { OpencodeClient } from "@opencode-ai/sdk/v2"
|
||||||
import { DialogVariant } from "./component/dialog-variant"
|
import { DialogVariant } from "./component/dialog-variant"
|
||||||
import { createTuiAttention } from "./attention"
|
import { createTuiAttention } from "./attention"
|
||||||
|
|
@ -135,7 +136,8 @@ const appBindingCommands = [
|
||||||
|
|
||||||
export type TuiInput = {
|
export type TuiInput = {
|
||||||
client: OpencodeClient
|
client: OpencodeClient
|
||||||
reload?: () => Promise<OpencodeClient>
|
api: OpenCodeClient
|
||||||
|
reload?: () => Promise<{ client: OpencodeClient; api: OpenCodeClient }>
|
||||||
args: Args
|
args: Args
|
||||||
config: TuiConfig.Resolved
|
config: TuiConfig.Resolved
|
||||||
onSnapshot?: () => Promise<string[]>
|
onSnapshot?: () => Promise<string[]>
|
||||||
|
|
@ -290,7 +292,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
|
||||||
>
|
>
|
||||||
<TuiConfigProvider config={input.config}>
|
<TuiConfigProvider config={input.config}>
|
||||||
<PluginRuntimeProvider value={pluginRuntime}>
|
<PluginRuntimeProvider value={pluginRuntime}>
|
||||||
<SDKProvider client={input.client} reload={input.reload}>
|
<SDKProvider client={input.client} api={input.api} reload={input.reload}>
|
||||||
<ProjectProvider>
|
<ProjectProvider>
|
||||||
<SyncProvider>
|
<SyncProvider>
|
||||||
<DataProvider>
|
<DataProvider>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { TextAttributes } from "@opentui/core"
|
import { TextAttributes } from "@opentui/core"
|
||||||
import type { ConnectionInfo, IntegrationInfo, IntegrationOAuthMethod, IntegrationAttempt } from "@opencode-ai/sdk/v2"
|
import type { IntegrationsConnectOauthOutput } from "@opencode-ai/client"
|
||||||
|
import type { ConnectionInfo, IntegrationInfo, IntegrationOAuthMethod } from "@opencode-ai/sdk/v2"
|
||||||
import { createMemo, createSignal, onCleanup, onMount, Show } from "solid-js"
|
import { createMemo, createSignal, onCleanup, onMount, Show } from "solid-js"
|
||||||
import { useClipboard } from "../context/clipboard"
|
import { useClipboard } from "../context/clipboard"
|
||||||
import { useData } from "../context/data"
|
import { useData } from "../context/data"
|
||||||
|
|
@ -22,6 +23,7 @@ const INTEGRATION_PRIORITY: Record<string, number> = {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConnectMethod = Exclude<IntegrationInfo["methods"][number], { type: "env" }>
|
type ConnectMethod = Exclude<IntegrationInfo["methods"][number], { type: "env" }>
|
||||||
|
type IntegrationAttempt = IntegrationsConnectOauthOutput["data"]
|
||||||
|
|
||||||
export function integrationOptions(list: IntegrationInfo[]) {
|
export function integrationOptions(list: IntegrationInfo[]) {
|
||||||
return list.toSorted(
|
return list.toSorted(
|
||||||
|
|
@ -109,11 +111,8 @@ function manageConnections(
|
||||||
title: `Disconnect ${connection.label}`,
|
title: `Disconnect ${connection.label}`,
|
||||||
value: connection.id,
|
value: connection.id,
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
void sdk.client.v2.credential
|
void sdk.api.credentials
|
||||||
.remove(
|
.remove({ credentialID: connection.id, location: location(data) })
|
||||||
{ credentialID: connection.id, location: location(data) },
|
|
||||||
{ throwOnError: true },
|
|
||||||
)
|
|
||||||
.then(() => disconnected(integration.name, data, dialog, toast))
|
.then(() => disconnected(integration.name, data, dialog, toast))
|
||||||
.catch(toast.error)
|
.catch(toast.error)
|
||||||
},
|
},
|
||||||
|
|
@ -124,11 +123,7 @@ function manageConnections(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectMethod(
|
function selectMethod(integration: IntegrationInfo, methods: ConnectMethod[], dialog: ReturnType<typeof useDialog>) {
|
||||||
integration: IntegrationInfo,
|
|
||||||
methods: ConnectMethod[],
|
|
||||||
dialog: ReturnType<typeof useDialog>,
|
|
||||||
) {
|
|
||||||
if (methods.length === 1) return openMethod(integration, methods[0], dialog)
|
if (methods.length === 1) return openMethod(integration, methods[0], dialog)
|
||||||
dialog.replace(() => (
|
dialog.replace(() => (
|
||||||
<DialogSelect
|
<DialogSelect
|
||||||
|
|
@ -142,11 +137,7 @@ function selectMethod(
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
function openMethod(
|
function openMethod(integration: IntegrationInfo, method: ConnectMethod, dialog: ReturnType<typeof useDialog>) {
|
||||||
integration: IntegrationInfo,
|
|
||||||
method: ConnectMethod,
|
|
||||||
dialog: ReturnType<typeof useDialog>,
|
|
||||||
) {
|
|
||||||
if (method.type === "key") {
|
if (method.type === "key") {
|
||||||
dialog.replace(() => <KeyMethod integration={integration} method={method} />)
|
dialog.replace(() => <KeyMethod integration={integration} method={method} />)
|
||||||
return
|
return
|
||||||
|
|
@ -168,21 +159,16 @@ function KeyMethod(props: { integration: IntegrationInfo; method: Extract<Connec
|
||||||
placeholder="API key"
|
placeholder="API key"
|
||||||
onConfirm={(key) => {
|
onConfirm={(key) => {
|
||||||
if (!key) return
|
if (!key) return
|
||||||
void sdk.client.v2.integration.connect
|
void sdk.api.integrations
|
||||||
.key(
|
.connectKey({
|
||||||
{
|
integrationID: props.integration.id,
|
||||||
integrationID: props.integration.id,
|
location: location(data),
|
||||||
location: location(data),
|
key,
|
||||||
key,
|
})
|
||||||
},
|
|
||||||
{ throwOnError: true },
|
|
||||||
)
|
|
||||||
.then(() => connected(props.integration.name, data, dialog, toast))
|
.then(() => connected(props.integration.name, data, dialog, toast))
|
||||||
.catch((cause) => setError(message(cause)))
|
.catch((cause) => setError(message(cause)))
|
||||||
}}
|
}}
|
||||||
description={() => (
|
description={() => <Show when={error()}>{(value) => <text fg={theme.error}>{value()}</text>}</Show>}
|
||||||
<Show when={error()}>{(value) => <text fg={theme.error}>{value()}</text>}</Show>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -208,25 +194,22 @@ function OAuthStarting(props: {
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
void sdk.client.v2.integration.connect
|
void sdk.api.integrations
|
||||||
.oauth(
|
.connectOauth({
|
||||||
{
|
integrationID: props.integration.id,
|
||||||
integrationID: props.integration.id,
|
location: location(data),
|
||||||
location: location(data),
|
methodID: props.method.id,
|
||||||
methodID: props.method.id,
|
inputs: props.inputs,
|
||||||
inputs: props.inputs,
|
})
|
||||||
},
|
|
||||||
{ throwOnError: true },
|
|
||||||
)
|
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (result.data.data.mode === "code") {
|
if (result.data.mode === "code") {
|
||||||
dialog.replace(() => (
|
dialog.replace(() => (
|
||||||
<OAuthCode integration={props.integration} title={props.method.label} attempt={result.data.data} />
|
<OAuthCode integration={props.integration} title={props.method.label} attempt={result.data} />
|
||||||
))
|
))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
dialog.replace(() => (
|
dialog.replace(() => (
|
||||||
<OAuthAuto integration={props.integration} title={props.method.label} attempt={result.data.data} />
|
<OAuthAuto integration={props.integration} title={props.method.label} attempt={result.data} />
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
.catch((cause) => {
|
.catch((cause) => {
|
||||||
|
|
@ -265,10 +248,10 @@ function OAuthAuto(props: { integration: IntegrationInfo; title: string; attempt
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const poll = () => {
|
const poll = () => {
|
||||||
void sdk.client.v2.integration.attempt
|
void sdk.api.integrations
|
||||||
.status({ attemptID: props.attempt.attemptID, location: location(data) }, { throwOnError: true })
|
.attemptStatus({ attemptID: props.attempt.attemptID, location: location(data) })
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
const status = result.data.data
|
const status = result.data
|
||||||
if (status.status === "pending") {
|
if (status.status === "pending") {
|
||||||
timer = setTimeout(poll, 500)
|
timer = setTimeout(poll, 500)
|
||||||
return
|
return
|
||||||
|
|
@ -292,7 +275,7 @@ function OAuthAuto(props: { integration: IntegrationInfo; title: string; attempt
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
if (timer) clearTimeout(timer)
|
if (timer) clearTimeout(timer)
|
||||||
if (settled) return
|
if (settled) return
|
||||||
void sdk.client.v2.integration.attempt.cancel({ attemptID: props.attempt.attemptID, location: location(data) })
|
void sdk.api.integrations.attemptCancel({ attemptID: props.attempt.attemptID, location: location(data) })
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -317,7 +300,7 @@ function OAuthCode(props: { integration: IntegrationInfo; title: string; attempt
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
if (settled) return
|
if (settled) return
|
||||||
void sdk.client.v2.integration.attempt.cancel({ attemptID: props.attempt.attemptID, location: location(data) })
|
void sdk.api.integrations.attemptCancel({ attemptID: props.attempt.attemptID, location: location(data) })
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -326,11 +309,8 @@ function OAuthCode(props: { integration: IntegrationInfo; title: string; attempt
|
||||||
placeholder="Authorization code"
|
placeholder="Authorization code"
|
||||||
onConfirm={(code) => {
|
onConfirm={(code) => {
|
||||||
if (!code) return
|
if (!code) return
|
||||||
void sdk.client.v2.integration.attempt
|
void sdk.api.integrations
|
||||||
.complete(
|
.attemptComplete({ attemptID: props.attempt.attemptID, location: location(data), code })
|
||||||
{ attemptID: props.attempt.attemptID, location: location(data), code },
|
|
||||||
{ throwOnError: true },
|
|
||||||
)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
settled = true
|
settled = true
|
||||||
return connected(props.integration.name, data, dialog, toast)
|
return connected(props.integration.name, data, dialog, toast)
|
||||||
|
|
@ -348,13 +328,7 @@ function OAuthCode(props: { integration: IntegrationInfo; title: string; attempt
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function OAuthView(props: {
|
function OAuthView(props: { title: string; url?: string; instructions?: string; message: string; copy?: boolean }) {
|
||||||
title: string
|
|
||||||
url?: string
|
|
||||||
instructions?: string
|
|
||||||
message: string
|
|
||||||
copy?: boolean
|
|
||||||
}) {
|
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
return (
|
return (
|
||||||
|
|
@ -371,7 +345,9 @@ function OAuthView(props: {
|
||||||
{(url) => (
|
{(url) => (
|
||||||
<box gap={1}>
|
<box gap={1}>
|
||||||
<Link href={url()} fg={theme.primary} />
|
<Link href={url()} fg={theme.primary} />
|
||||||
<Show when={props.instructions}>{(instructions) => <text fg={theme.textMuted}>{instructions()}</text>}</Show>
|
<Show when={props.instructions}>
|
||||||
|
{(instructions) => <text fg={theme.textMuted}>{instructions()}</text>}
|
||||||
|
</Show>
|
||||||
</box>
|
</box>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
|
|
@ -436,7 +412,11 @@ async function connected(
|
||||||
dialog: ReturnType<typeof useDialog>,
|
dialog: ReturnType<typeof useDialog>,
|
||||||
toast: ReturnType<typeof useToast>,
|
toast: ReturnType<typeof useToast>,
|
||||||
) {
|
) {
|
||||||
await Promise.all([data.location.integration.refresh(), data.location.model.refresh(), data.location.provider.refresh()])
|
await Promise.all([
|
||||||
|
data.location.integration.refresh(),
|
||||||
|
data.location.model.refresh(),
|
||||||
|
data.location.provider.refresh(),
|
||||||
|
])
|
||||||
toast.show({ variant: "success", message: `Connected ${name}` })
|
toast.show({ variant: "success", message: `Connected ${name}` })
|
||||||
dialog.clear()
|
dialog.clear()
|
||||||
}
|
}
|
||||||
|
|
@ -447,7 +427,11 @@ async function disconnected(
|
||||||
dialog: ReturnType<typeof useDialog>,
|
dialog: ReturnType<typeof useDialog>,
|
||||||
toast: ReturnType<typeof useToast>,
|
toast: ReturnType<typeof useToast>,
|
||||||
) {
|
) {
|
||||||
await Promise.all([data.location.integration.refresh(), data.location.model.refresh(), data.location.provider.refresh()])
|
await Promise.all([
|
||||||
|
data.location.integration.refresh(),
|
||||||
|
data.location.model.refresh(),
|
||||||
|
data.location.provider.refresh(),
|
||||||
|
])
|
||||||
toast.show({ variant: "success", message: `Disconnected ${name}` })
|
toast.show({ variant: "success", message: `Disconnected ${name}` })
|
||||||
dialog.clear()
|
dialog.clear()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { abbreviateHome } from "../runtime"
|
||||||
import { useTuiPaths } from "../context/runtime"
|
import { useTuiPaths } from "../context/runtime"
|
||||||
import { Locale } from "../util/locale"
|
import { Locale } from "../util/locale"
|
||||||
import { errorMessage } from "../util/error"
|
import { errorMessage } from "../util/error"
|
||||||
|
import { isRecord } from "../util/record"
|
||||||
import { useToast } from "../ui/toast"
|
import { useToast } from "../ui/toast"
|
||||||
import { useCommandShortcut } from "../keymap"
|
import { useCommandShortcut } from "../keymap"
|
||||||
import { useProject } from "../context/project"
|
import { useProject } from "../context/project"
|
||||||
|
|
@ -74,10 +75,10 @@ export function DialogMoveSession(props: DialogMoveSessionProps) {
|
||||||
() => (props.initialRemoving ? undefined : props.projectID),
|
() => (props.initialRemoving ? undefined : props.projectID),
|
||||||
async (projectID, info): Promise<ProjectDirectory[] | undefined> => {
|
async (projectID, info): Promise<ProjectDirectory[] | undefined> => {
|
||||||
try {
|
try {
|
||||||
await sdk.client.v2.projectCopy.refresh(
|
await sdk.api.projectCopies.refresh({
|
||||||
{ projectID, location: { directory: projectContext.instance.directory() || paths.cwd } },
|
projectID,
|
||||||
{ throwOnError: true },
|
location: { directory: projectContext.instance.directory() || paths.cwd },
|
||||||
)
|
})
|
||||||
const directories = await sdk.client.project.directories({ projectID }, { throwOnError: true })
|
const directories = await sdk.client.project.directories({ projectID }, { throwOnError: true })
|
||||||
setLoadError(undefined)
|
setLoadError(undefined)
|
||||||
return directories.data ?? []
|
return directories.data ?? []
|
||||||
|
|
@ -221,18 +222,21 @@ export function DialogMoveSession(props: DialogMoveSessionProps) {
|
||||||
setToDelete(undefined)
|
setToDelete(undefined)
|
||||||
setRemoving(selected.directory)
|
setRemoving(selected.directory)
|
||||||
setWorking(true)
|
setWorking(true)
|
||||||
const result = await sdk.client.v2.projectCopy
|
const error = await sdk.api.projectCopies
|
||||||
.remove({
|
.remove({
|
||||||
projectID: props.projectID,
|
projectID: props.projectID,
|
||||||
location: { directory: projectContext.instance.directory() || paths.cwd },
|
location: { directory: projectContext.instance.directory() || paths.cwd },
|
||||||
directory: selected.directory,
|
directory: selected.directory,
|
||||||
force: false,
|
force: false,
|
||||||
})
|
})
|
||||||
.catch((error) => ({ error }))
|
.then(
|
||||||
if (result.error) {
|
() => undefined,
|
||||||
|
(error) => error,
|
||||||
|
)
|
||||||
|
if (error) {
|
||||||
setRemoving(undefined)
|
setRemoving(undefined)
|
||||||
setWorking(false)
|
setWorking(false)
|
||||||
if ("data" in result.error && result.error.data.forceRequired) {
|
if (isRecord(error) && isRecord(error.data) && error.data.forceRequired === true) {
|
||||||
const status = await sdk.client.vcs.status({ directory: selected.directory }).catch(() => undefined)
|
const status = await sdk.client.vcs.status({ directory: selected.directory }).catch(() => undefined)
|
||||||
const choice = await DialogWorkspaceFileChanges.show(dialog, status?.data ?? [], {
|
const choice = await DialogWorkspaceFileChanges.show(dialog, status?.data ?? [], {
|
||||||
title: "Delete working copy?",
|
title: "Delete working copy?",
|
||||||
|
|
@ -243,19 +247,22 @@ export function DialogMoveSession(props: DialogMoveSessionProps) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
reopen(selected.directory)
|
reopen(selected.directory)
|
||||||
const forced = await sdk.client.v2.projectCopy
|
const forcedError = await sdk.api.projectCopies
|
||||||
.remove({
|
.remove({
|
||||||
projectID: props.projectID,
|
projectID: props.projectID,
|
||||||
location: { directory: projectContext.instance.directory() || paths.cwd },
|
location: { directory: projectContext.instance.directory() || paths.cwd },
|
||||||
directory: selected.directory,
|
directory: selected.directory,
|
||||||
force: true,
|
force: true,
|
||||||
})
|
})
|
||||||
.catch((error) => ({ error }))
|
.then(
|
||||||
if (forced.error) {
|
() => undefined,
|
||||||
|
(error) => error,
|
||||||
|
)
|
||||||
|
if (forcedError) {
|
||||||
toast.show({
|
toast.show({
|
||||||
variant: "error",
|
variant: "error",
|
||||||
title: "Failed to delete project copy",
|
title: "Failed to delete project copy",
|
||||||
message: errorMessage(forced.error),
|
message: errorMessage(forcedError),
|
||||||
})
|
})
|
||||||
reopen()
|
reopen()
|
||||||
return
|
return
|
||||||
|
|
@ -269,7 +276,7 @@ export function DialogMoveSession(props: DialogMoveSessionProps) {
|
||||||
toast.show({
|
toast.show({
|
||||||
variant: "error",
|
variant: "error",
|
||||||
title: "Failed to delete project copy",
|
title: "Failed to delete project copy",
|
||||||
message: errorMessage(result.error),
|
message: errorMessage(error),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,11 +31,16 @@ export function DialogSessionList() {
|
||||||
|
|
||||||
const [searchResults] = createResource(search, async (query) => {
|
const [searchResults] = createResource(search, async (query) => {
|
||||||
if (!query) return
|
if (!query) return
|
||||||
const response = await sdk.client.v2.session.list(
|
const location = data.location.default()
|
||||||
{ search: query, limit: 50, order: "desc" },
|
const response = await sdk.api.sessions.list({
|
||||||
{ throwOnError: true },
|
search: query,
|
||||||
)
|
limit: 50,
|
||||||
return { query, sessions: response.data.data }
|
order: "desc",
|
||||||
|
directory: location.directory,
|
||||||
|
workspace: location.workspaceID,
|
||||||
|
})
|
||||||
|
// oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- generated client output is readonly; session list UI reuses legacy mutable session types.
|
||||||
|
return { query, sessions: structuredClone(response.data) as SessionV2Info[] }
|
||||||
})
|
})
|
||||||
|
|
||||||
const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined))
|
const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined))
|
||||||
|
|
@ -59,7 +64,11 @@ export function DialogSessionList() {
|
||||||
|
|
||||||
const options = createMemo(() => {
|
const options = createMemo(() => {
|
||||||
const today = new Date().toDateString()
|
const today = new Date().toDateString()
|
||||||
const sessionMap = new Map(sessions().filter((session) => !session.parentID).map((session) => [session.id, session]))
|
const sessionMap = new Map(
|
||||||
|
sessions()
|
||||||
|
.filter((session) => !session.parentID)
|
||||||
|
.map((session) => [session.id, session]),
|
||||||
|
)
|
||||||
const pinned = local.session.pinned().filter((sessionID) => sessionMap.has(sessionID))
|
const pinned = local.session.pinned().filter((sessionID) => sessionMap.has(sessionID))
|
||||||
const pinnedSet = new Set(pinned)
|
const pinnedSet = new Set(pinned)
|
||||||
const slotByID = new Map(local.session.slots().map((sessionID, index) => [sessionID, index + 1]))
|
const slotByID = new Map(local.session.slots().map((sessionID, index) => [sessionID, index + 1]))
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,15 @@ export function DialogSessionRename(props: { sessionID: string; currentTitle?: s
|
||||||
onConfirm={(value) => {
|
onConfirm={(value) => {
|
||||||
const title = value.trim()
|
const title = value.trim()
|
||||||
if (!title) return
|
if (!title) return
|
||||||
void sdk.client.v2.session
|
void sdk.api.sessions
|
||||||
.rename({ sessionID: props.sessionID, title }, { throwOnError: true })
|
.rename({ sessionID: props.sessionID, title })
|
||||||
.then(() => dialog.clear())
|
.then(() => dialog.clear())
|
||||||
.catch((error) =>
|
.catch((error) =>
|
||||||
toast.show({ message: `Failed to rename session: ${errorMessage(error)}`, variant: "error", duration: 5000 }),
|
toast.show({
|
||||||
|
message: `Failed to rename session: ${errorMessage(error)}`,
|
||||||
|
variant: "error",
|
||||||
|
duration: 5000,
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
onCancel={() => dialog.clear()}
|
onCancel={() => dialog.clear()}
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,15 @@ export function DialogTag(props: { onSelect?: (value: string) => void }) {
|
||||||
const [files] = createResource(
|
const [files] = createResource(
|
||||||
() => [store.filter],
|
() => [store.filter],
|
||||||
async () => {
|
async () => {
|
||||||
const result = await sdk.client.find.files({
|
const result = await sdk.api.files
|
||||||
query: store.filter,
|
.find({
|
||||||
workspace: project.workspace.current(),
|
query: store.filter,
|
||||||
})
|
type: "file",
|
||||||
if (result.error) return []
|
limit: 5,
|
||||||
const sliced = (result.data ?? []).slice(0, 5)
|
location: { workspace: project.workspace.current() },
|
||||||
return sliced
|
})
|
||||||
|
.catch(() => undefined)
|
||||||
|
return result?.data.map((item) => item.path) ?? []
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -320,29 +320,26 @@ export function Autocomplete(props: {
|
||||||
if (referenceMatch()) return []
|
if (referenceMatch()) return []
|
||||||
const { lineRange, baseQuery } = extractLineRange(input.query ?? "")
|
const { lineRange, baseQuery } = extractLineRange(input.query ?? "")
|
||||||
|
|
||||||
// Get files from SDK
|
const result = await sdk.api.files
|
||||||
const result = await sdk.client.v2.fs.find({
|
.find({
|
||||||
query: baseQuery,
|
query: baseQuery,
|
||||||
limit: "20",
|
limit: 20,
|
||||||
location: {
|
location: {
|
||||||
directory: input.location?.directory,
|
directory: input.location?.directory,
|
||||||
workspace: input.location?.workspaceID ?? project.workspace.current(),
|
workspace: input.location?.workspaceID ?? project.workspace.current(),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
.catch(() => undefined)
|
||||||
|
|
||||||
const options: AutocompleteOption[] = []
|
const options: AutocompleteOption[] = []
|
||||||
|
|
||||||
// Add file options. Trust the order returned by fff (frecency, fuzzy
|
// Add file options. Trust the order returned by fff (frecency, fuzzy
|
||||||
// score, filename bonus, etc. are already factored in).
|
// score, filename bonus, etc. are already factored in).
|
||||||
if (!result.error && result.data) {
|
if (result) {
|
||||||
const width = props.anchor().width - 4
|
const width = props.anchor().width - 4
|
||||||
options.push(
|
options.push(
|
||||||
...result.data.data.map((item): AutocompleteOption => {
|
...result.data.map((item): AutocompleteOption => {
|
||||||
const { filename, part } = createFilePart(
|
const { filename, part } = createFilePart(item, path.join(result.location.directory, item.path), lineRange)
|
||||||
item,
|
|
||||||
path.join(result.data.location.directory, item.path),
|
|
||||||
lineRange,
|
|
||||||
)
|
|
||||||
return {
|
return {
|
||||||
display: Locale.truncateMiddle(filename, width),
|
display: Locale.truncateMiddle(filename, width),
|
||||||
value: filename,
|
value: filename,
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ import { usePromptStash } from "../../prompt/stash"
|
||||||
import { DialogStash } from "../dialog-stash"
|
import { DialogStash } from "../dialog-stash"
|
||||||
import { type AutocompleteRef, Autocomplete } from "./autocomplete"
|
import { type AutocompleteRef, Autocomplete } from "./autocomplete"
|
||||||
import { useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid"
|
import { useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid"
|
||||||
import type { AssistantMessage, FilePart, UserMessage } from "@opencode-ai/sdk/v2"
|
import type { AssistantMessage, FilePart, SessionV2Info, UserMessage } from "@opencode-ai/sdk/v2"
|
||||||
import { Locale } from "../../util/locale"
|
import { Locale } from "../../util/locale"
|
||||||
import { errorMessage } from "../../util/error"
|
import { errorMessage } from "../../util/error"
|
||||||
import { createColors, createFrames } from "../../ui/spinner"
|
import { createColors, createFrames } from "../../ui/spinner"
|
||||||
|
|
@ -158,11 +158,12 @@ export function Prompt(props: PromptProps) {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const status = createMemo(() => data.session.status(props.sessionID ?? ""))
|
const status = createMemo(() => data.session.status(props.sessionID ?? ""))
|
||||||
const activeSubagents = createMemo(() =>
|
const activeSubagents = createMemo(
|
||||||
data.session
|
() =>
|
||||||
.list()
|
data.session
|
||||||
.filter((session) => session.parentID === props.sessionID && data.session.status(session.id) === "running")
|
.list()
|
||||||
.length,
|
.filter((session) => session.parentID === props.sessionID && data.session.status(session.id) === "running")
|
||||||
|
.length,
|
||||||
)
|
)
|
||||||
const runningShells = createMemo(
|
const runningShells = createMemo(
|
||||||
() => data.shell.list().filter((shell) => shell.metadata.sessionID === props.sessionID).length,
|
() => data.shell.list().filter((shell) => shell.metadata.sessionID === props.sessionID).length,
|
||||||
|
|
@ -424,7 +425,7 @@ export function Prompt(props: PromptProps) {
|
||||||
}, 5000)
|
}, 5000)
|
||||||
|
|
||||||
if (store.interrupt >= 2) {
|
if (store.interrupt >= 2) {
|
||||||
void sdk.client.v2.session.interrupt({
|
void sdk.api.sessions.interrupt({
|
||||||
sessionID: props.sessionID,
|
sessionID: props.sessionID,
|
||||||
})
|
})
|
||||||
setStore("interrupt", 0)
|
setStore("interrupt", 0)
|
||||||
|
|
@ -1009,18 +1010,23 @@ export function Prompt(props: PromptProps) {
|
||||||
const directory = await move.getDirectory(store.prompt.input)
|
const directory = await move.getDirectory(store.prompt.input)
|
||||||
if (move.pending() && !directory) return false
|
if (move.pending() && !directory) return false
|
||||||
finishMoveProgress = Boolean(move.progress())
|
finishMoveProgress = Boolean(move.progress())
|
||||||
|
const location = data.location.default()
|
||||||
|
|
||||||
const res = await sdk.client.v2.session.create({
|
const created = await sdk.api.sessions
|
||||||
location: directory ? { directory, workspaceID } : undefined,
|
.create({
|
||||||
agent: agent.id,
|
location: directory
|
||||||
model: {
|
? { directory, workspaceID }
|
||||||
providerID: selectedModel.providerID,
|
: { directory: location.directory, workspaceID: workspaceID ?? location.workspaceID },
|
||||||
id: selectedModel.modelID,
|
agent: agent.id,
|
||||||
variant,
|
model: {
|
||||||
},
|
providerID: selectedModel.providerID,
|
||||||
})
|
id: selectedModel.modelID,
|
||||||
|
variant,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch(() => undefined)
|
||||||
|
|
||||||
if (res.error) {
|
if (!created) {
|
||||||
if (finishMoveProgress) move.finishSubmit()
|
if (finishMoveProgress) move.finishSubmit()
|
||||||
toast.show({
|
toast.show({
|
||||||
message: "Creating a session failed. Open console for more details.",
|
message: "Creating a session failed. Open console for more details.",
|
||||||
|
|
@ -1030,8 +1036,9 @@ export function Prompt(props: PromptProps) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionID = res.data.data.id
|
sessionID = created.id
|
||||||
session = res.data.data
|
// oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- generated client output is readonly; prompt state still uses legacy mutable session types.
|
||||||
|
session = structuredClone(created) as SessionV2Info
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputText = expandTrackedPastedText(
|
const inputText = expandTrackedPastedText(
|
||||||
|
|
@ -1107,65 +1114,70 @@ export function Prompt(props: PromptProps) {
|
||||||
session = data.session.get(sessionID)
|
session = data.session.get(sessionID)
|
||||||
}
|
}
|
||||||
if (session?.agent !== agent.id) {
|
if (session?.agent !== agent.id) {
|
||||||
await sdk.client.v2.session.switchAgent({ sessionID, agent: agent.id }, { throwOnError: true })
|
await sdk.api.sessions.switchAgent({ sessionID, agent: agent.id })
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
session?.model?.providerID !== selectedModel.providerID ||
|
session?.model?.providerID !== selectedModel.providerID ||
|
||||||
session.model.id !== selectedModel.modelID ||
|
session.model.id !== selectedModel.modelID ||
|
||||||
session.model.variant !== variant
|
session.model.variant !== variant
|
||||||
) {
|
) {
|
||||||
await sdk.client.v2.session.switchModel(
|
await sdk.api.sessions.switchModel({
|
||||||
{
|
sessionID,
|
||||||
sessionID,
|
model: { providerID: selectedModel.providerID, id: selectedModel.modelID, variant },
|
||||||
model: { providerID: selectedModel.providerID, id: selectedModel.modelID, variant },
|
})
|
||||||
},
|
|
||||||
{ throwOnError: true },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if (session?.revert) {
|
if (session?.revert) {
|
||||||
const revertResult = await sdk.client.v2.session.revert.commit({ sessionID })
|
const error = await sdk.api.sessions.commit({ sessionID }).then(
|
||||||
if (revertResult.error) {
|
() => undefined,
|
||||||
toast.show({ title: "Failed to commit revert", message: errorMessage(revertResult.error), variant: "error" })
|
(error) => error,
|
||||||
|
)
|
||||||
|
if (error) {
|
||||||
|
toast.show({ title: "Failed to commit revert", message: errorMessage(error), variant: "error" })
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const result = await sdk.client.v2.session.prompt({
|
const error = await sdk.api.sessions
|
||||||
sessionID,
|
.prompt({
|
||||||
prompt: {
|
sessionID,
|
||||||
text: [...editorParts.map((part) => part.text), inputText].filter(Boolean).join("\n\n"),
|
prompt: {
|
||||||
files: nonTextParts.flatMap((part) =>
|
text: [...editorParts.map((part) => part.text), inputText].filter(Boolean).join("\n\n"),
|
||||||
part.type === "file"
|
files: nonTextParts.flatMap((part) =>
|
||||||
? [
|
part.type === "file"
|
||||||
{
|
? [
|
||||||
uri: part.url,
|
{
|
||||||
name: part.filename,
|
uri: part.url,
|
||||||
source: part.source
|
name: part.filename,
|
||||||
? {
|
source: part.source
|
||||||
start: part.source.text.start,
|
? {
|
||||||
end: part.source.text.end,
|
start: part.source.text.start,
|
||||||
text: part.source.text.value,
|
end: part.source.text.end,
|
||||||
}
|
text: part.source.text.value,
|
||||||
: undefined,
|
}
|
||||||
},
|
: undefined,
|
||||||
]
|
},
|
||||||
: [],
|
]
|
||||||
),
|
: [],
|
||||||
agents: nonTextParts.flatMap((part) =>
|
),
|
||||||
part.type === "agent"
|
agents: nonTextParts.flatMap((part) =>
|
||||||
? [
|
part.type === "agent"
|
||||||
{
|
? [
|
||||||
name: part.name,
|
{
|
||||||
source: part.source
|
name: part.name,
|
||||||
? { start: part.source.start, end: part.source.end, text: part.source.value }
|
source: part.source
|
||||||
: undefined,
|
? { start: part.source.start, end: part.source.end, text: part.source.value }
|
||||||
},
|
: undefined,
|
||||||
]
|
},
|
||||||
: [],
|
]
|
||||||
),
|
: [],
|
||||||
},
|
),
|
||||||
})
|
},
|
||||||
if (result.error) {
|
})
|
||||||
toast.show({ title: "Failed to send prompt", message: errorMessage(result.error), variant: "error" })
|
.then(
|
||||||
|
() => undefined,
|
||||||
|
(error) => error,
|
||||||
|
)
|
||||||
|
if (error) {
|
||||||
|
toast.show({ title: "Failed to send prompt", message: errorMessage(error), variant: "error" })
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (editorParts.length > 0) editor.markSelectionSent()
|
if (editorParts.length > 0) editor.markSelectionSent()
|
||||||
|
|
|
||||||
|
|
@ -37,17 +37,14 @@ export function usePromptMove(input: { projectID: () => string | undefined; sess
|
||||||
{ projectID, context },
|
{ projectID, context },
|
||||||
{ throwOnError: true },
|
{ throwOnError: true },
|
||||||
)
|
)
|
||||||
const result = await sdk.client.v2.projectCopy.create(
|
const result = await sdk.api.projectCopies.create({
|
||||||
{
|
projectID,
|
||||||
projectID,
|
location: { directory: project.instance.directory() || paths.cwd },
|
||||||
location: { directory: project.instance.directory() || paths.cwd },
|
strategy: "git_worktree",
|
||||||
strategy: "git_worktree",
|
directory: path.join(paths.worktree, projectID.slice(0, 6)),
|
||||||
directory: path.join(paths.worktree, projectID.slice(0, 6)),
|
name: generated.data.name,
|
||||||
name: generated.data.name,
|
})
|
||||||
},
|
const directory = result.directory
|
||||||
{ throwOnError: true },
|
|
||||||
)
|
|
||||||
const directory = result.data?.directory
|
|
||||||
if (!directory) throw new Error("No project copy directory returned")
|
if (!directory) throw new Error("No project copy directory returned")
|
||||||
|
|
||||||
// Call a location-based route to make sure it's bootstrapped
|
// Call a location-based route to make sure it's bootstrapped
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,14 @@ function locationQuery(ref?: LocationRef) {
|
||||||
return ref ? { directory: ref.directory, workspace: ref.workspaceID } : undefined
|
return ref ? { directory: ref.directory, workspace: ref.workspaceID } : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Mutable<T> =
|
||||||
|
T extends ReadonlyArray<infer U> ? Mutable<U>[] : T extends object ? { -readonly [K in keyof T]: Mutable<T[K]> } : T
|
||||||
|
|
||||||
|
function mutable<T>(value: T): Mutable<T> {
|
||||||
|
// oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- generated client data is readonly; the TUI store mutates cloned state.
|
||||||
|
return structuredClone(value) as Mutable<T>
|
||||||
|
}
|
||||||
|
|
||||||
export const { use: useData, provider: DataProvider } = createSimpleContext({
|
export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||||
name: "Data",
|
name: "Data",
|
||||||
init: () => {
|
init: () => {
|
||||||
|
|
@ -288,13 +296,19 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||||
break
|
break
|
||||||
case "session.next.text.delta":
|
case "session.next.text.delta":
|
||||||
message.update(event.data.sessionID, (draft, index) => {
|
message.update(event.data.sessionID, (draft, index) => {
|
||||||
const match = message.latestText(message.assistant(draft, index, event.data.assistantMessageID), event.data.textID)
|
const match = message.latestText(
|
||||||
|
message.assistant(draft, index, event.data.assistantMessageID),
|
||||||
|
event.data.textID,
|
||||||
|
)
|
||||||
if (match) match.text += event.data.delta
|
if (match) match.text += event.data.delta
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case "session.next.text.ended":
|
case "session.next.text.ended":
|
||||||
message.update(event.data.sessionID, (draft, index) => {
|
message.update(event.data.sessionID, (draft, index) => {
|
||||||
const match = message.latestText(message.assistant(draft, index, event.data.assistantMessageID), event.data.textID)
|
const match = message.latestText(
|
||||||
|
message.assistant(draft, index, event.data.assistantMessageID),
|
||||||
|
event.data.textID,
|
||||||
|
)
|
||||||
if (match) match.text = event.data.text
|
if (match) match.text = event.data.text
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
|
|
@ -311,19 +325,28 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||||
break
|
break
|
||||||
case "session.next.tool.input.delta":
|
case "session.next.tool.input.delta":
|
||||||
message.update(event.data.sessionID, (draft, index) => {
|
message.update(event.data.sessionID, (draft, index) => {
|
||||||
const match = message.latestTool(message.assistant(draft, index, event.data.assistantMessageID), event.data.callID)
|
const match = message.latestTool(
|
||||||
|
message.assistant(draft, index, event.data.assistantMessageID),
|
||||||
|
event.data.callID,
|
||||||
|
)
|
||||||
if (match?.state.status === "pending") match.state.input += event.data.delta
|
if (match?.state.status === "pending") match.state.input += event.data.delta
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case "session.next.tool.input.ended":
|
case "session.next.tool.input.ended":
|
||||||
message.update(event.data.sessionID, (draft, index) => {
|
message.update(event.data.sessionID, (draft, index) => {
|
||||||
const match = message.latestTool(message.assistant(draft, index, event.data.assistantMessageID), event.data.callID)
|
const match = message.latestTool(
|
||||||
|
message.assistant(draft, index, event.data.assistantMessageID),
|
||||||
|
event.data.callID,
|
||||||
|
)
|
||||||
if (match?.state.status === "pending") match.state.input = event.data.text
|
if (match?.state.status === "pending") match.state.input = event.data.text
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case "session.next.tool.called":
|
case "session.next.tool.called":
|
||||||
message.update(event.data.sessionID, (draft, index) => {
|
message.update(event.data.sessionID, (draft, index) => {
|
||||||
const match = message.latestTool(message.assistant(draft, index, event.data.assistantMessageID), event.data.callID)
|
const match = message.latestTool(
|
||||||
|
message.assistant(draft, index, event.data.assistantMessageID),
|
||||||
|
event.data.callID,
|
||||||
|
)
|
||||||
if (!match) return
|
if (!match) return
|
||||||
match.time.ran = event.data.timestamp
|
match.time.ran = event.data.timestamp
|
||||||
match.provider = event.data.provider
|
match.provider = event.data.provider
|
||||||
|
|
@ -332,7 +355,10 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||||
break
|
break
|
||||||
case "session.next.tool.progress":
|
case "session.next.tool.progress":
|
||||||
message.update(event.data.sessionID, (draft, index) => {
|
message.update(event.data.sessionID, (draft, index) => {
|
||||||
const match = message.latestTool(message.assistant(draft, index, event.data.assistantMessageID), event.data.callID)
|
const match = message.latestTool(
|
||||||
|
message.assistant(draft, index, event.data.assistantMessageID),
|
||||||
|
event.data.callID,
|
||||||
|
)
|
||||||
if (match?.state.status !== "running") return
|
if (match?.state.status !== "running") return
|
||||||
match.state.structured = event.data.structured
|
match.state.structured = event.data.structured
|
||||||
match.state.content = [...event.data.content]
|
match.state.content = [...event.data.content]
|
||||||
|
|
@ -340,7 +366,10 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||||
break
|
break
|
||||||
case "session.next.tool.success":
|
case "session.next.tool.success":
|
||||||
message.update(event.data.sessionID, (draft, index) => {
|
message.update(event.data.sessionID, (draft, index) => {
|
||||||
const match = message.latestTool(message.assistant(draft, index, event.data.assistantMessageID), event.data.callID)
|
const match = message.latestTool(
|
||||||
|
message.assistant(draft, index, event.data.assistantMessageID),
|
||||||
|
event.data.callID,
|
||||||
|
)
|
||||||
if (match?.state.status !== "running") return
|
if (match?.state.status !== "running") return
|
||||||
match.state = {
|
match.state = {
|
||||||
status: "completed",
|
status: "completed",
|
||||||
|
|
@ -359,7 +388,10 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||||
break
|
break
|
||||||
case "session.next.tool.failed":
|
case "session.next.tool.failed":
|
||||||
message.update(event.data.sessionID, (draft, index) => {
|
message.update(event.data.sessionID, (draft, index) => {
|
||||||
const match = message.latestTool(message.assistant(draft, index, event.data.assistantMessageID), event.data.callID)
|
const match = message.latestTool(
|
||||||
|
message.assistant(draft, index, event.data.assistantMessageID),
|
||||||
|
event.data.callID,
|
||||||
|
)
|
||||||
if (!match || (match.state.status !== "pending" && match.state.status !== "running")) return
|
if (!match || (match.state.status !== "pending" && match.state.status !== "running")) return
|
||||||
match.state = {
|
match.state = {
|
||||||
status: "error",
|
status: "error",
|
||||||
|
|
@ -522,8 +554,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||||
return store.session.status[sessionID] ?? "idle"
|
return store.session.status[sessionID] ?? "idle"
|
||||||
},
|
},
|
||||||
async refresh(sessionID: string) {
|
async refresh(sessionID: string) {
|
||||||
const result = await sdk.client.v2.session.get({ sessionID }, { throwOnError: true })
|
setStore("session", "info", sessionID, mutable(await sdk.api.sessions.get({ sessionID })))
|
||||||
setStore("session", "info", sessionID, result.data.data)
|
|
||||||
},
|
},
|
||||||
message: {
|
message: {
|
||||||
ids(sessionID: string) {
|
ids(sessionID: string) {
|
||||||
|
|
@ -540,11 +571,9 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||||
async refresh(sessionID: string) {
|
async refresh(sessionID: string) {
|
||||||
setStore("session", "message", sessionID, [])
|
setStore("session", "message", sessionID, [])
|
||||||
messageIndex.set(sessionID, new Map())
|
messageIndex.set(sessionID, new Map())
|
||||||
const response = await sdk.client.v2.session.messages(
|
const loaded = mutable(
|
||||||
{ sessionID, limit: 200, order: "desc" },
|
(await sdk.api.messages.list({ sessionID, limit: 200, order: "desc" })).data,
|
||||||
{ throwOnError: true },
|
).toReversed()
|
||||||
)
|
|
||||||
const loaded = response.data.data.toReversed()
|
|
||||||
const live = store.session.message[sessionID] ?? []
|
const live = store.session.message[sessionID] ?? []
|
||||||
const liveByID = new Map(live.map((message) => [message.id, message]))
|
const liveByID = new Map(live.map((message) => [message.id, message]))
|
||||||
const messages = [...loaded.map((message) => liveByID.get(message.id) ?? message), ...live]
|
const messages = [...loaded.map((message) => liveByID.get(message.id) ?? message), ...live]
|
||||||
|
|
@ -559,8 +588,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||||
return store.session.permission[sessionID]
|
return store.session.permission[sessionID]
|
||||||
},
|
},
|
||||||
async refresh(sessionID: string) {
|
async refresh(sessionID: string) {
|
||||||
const result = await sdk.client.v2.session.permission.list({ sessionID }, { throwOnError: true })
|
setStore("session", "permission", sessionID, mutable(await sdk.api.permissions.list({ sessionID })))
|
||||||
setStore("session", "permission", sessionID, result.data.data)
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
question: {
|
question: {
|
||||||
|
|
@ -568,8 +596,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||||
return store.session.question[sessionID]
|
return store.session.question[sessionID]
|
||||||
},
|
},
|
||||||
async refresh(sessionID: string) {
|
async refresh(sessionID: string) {
|
||||||
const result = await sdk.client.v2.session.question.list({ sessionID }, { throwOnError: true })
|
setStore("session", "question", sessionID, mutable(await sdk.api.questions.list({ sessionID })))
|
||||||
setStore("session", "question", sessionID, result.data.data)
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -579,8 +606,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||||
return store.project.permission[projectID]
|
return store.project.permission[projectID]
|
||||||
},
|
},
|
||||||
async refresh(projectID: string) {
|
async refresh(projectID: string) {
|
||||||
const result = await sdk.client.v2.permission.saved.list({ projectID }, { throwOnError: true })
|
setStore("project", "permission", projectID, mutable(await sdk.api.permissions.listSaved({ projectID })))
|
||||||
setStore("project", "permission", projectID, result.data.data)
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -606,8 +632,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||||
return defaultLocation()
|
return defaultLocation()
|
||||||
},
|
},
|
||||||
async refresh(ref?: LocationRef) {
|
async refresh(ref?: LocationRef) {
|
||||||
const response = await sdk.client.v2.location.get({ location: locationQuery(ref) }, { throwOnError: true })
|
const location = await sdk.api.location.get({ location: locationQuery(ref ?? defaultLocation()) })
|
||||||
const location = response.data
|
|
||||||
const key = locationKey(location)
|
const key = locationKey(location)
|
||||||
if (!store.location[key]) setStore("location", key, {})
|
if (!store.location[key]) setStore("location", key, {})
|
||||||
if (!ref) setDefaultLocation({ directory: location.directory, workspaceID: location.workspaceID })
|
if (!ref) setDefaultLocation({ directory: location.directory, workspaceID: location.workspaceID })
|
||||||
|
|
@ -617,9 +642,9 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||||
return store.location[locationKey(location ?? defaultLocation())]?.agent
|
return store.location[locationKey(location ?? defaultLocation())]?.agent
|
||||||
},
|
},
|
||||||
async refresh(ref?: LocationRef) {
|
async refresh(ref?: LocationRef) {
|
||||||
const result = await sdk.client.v2.agent.list({ location: locationQuery(ref) }, { throwOnError: true })
|
const result = await sdk.api.agents.list({ location: locationQuery(ref ?? defaultLocation()) })
|
||||||
const key = locationKey(result.data.location)
|
const key = locationKey(result.location)
|
||||||
setStore("location", key, { ...store.location[key], agent: result.data.data })
|
setStore("location", key, { ...store.location[key], agent: mutable(result.data) })
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
command: {
|
command: {
|
||||||
|
|
@ -627,9 +652,9 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||||
return store.location[locationKey(location ?? defaultLocation())]?.command
|
return store.location[locationKey(location ?? defaultLocation())]?.command
|
||||||
},
|
},
|
||||||
async refresh(ref?: LocationRef) {
|
async refresh(ref?: LocationRef) {
|
||||||
const result = await sdk.client.v2.command.list({ location: locationQuery(ref) }, { throwOnError: true })
|
const result = await sdk.api.commands.list({ location: locationQuery(ref ?? defaultLocation()) })
|
||||||
const key = locationKey(result.data.location)
|
const key = locationKey(result.location)
|
||||||
setStore("location", key, { ...store.location[key], command: result.data.data })
|
setStore("location", key, { ...store.location[key], command: mutable(result.data) })
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
integration: {
|
integration: {
|
||||||
|
|
@ -637,12 +662,9 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||||
return store.location[locationKey(location ?? defaultLocation())]?.integration
|
return store.location[locationKey(location ?? defaultLocation())]?.integration
|
||||||
},
|
},
|
||||||
async refresh(ref?: LocationRef) {
|
async refresh(ref?: LocationRef) {
|
||||||
const result = await sdk.client.v2.integration.list(
|
const result = await sdk.api.integrations.list({ location: locationQuery(ref ?? defaultLocation()) })
|
||||||
{ location: locationQuery(ref) },
|
const key = locationKey(result.location)
|
||||||
{ throwOnError: true },
|
setStore("location", key, { ...store.location[key], integration: mutable(result.data) })
|
||||||
)
|
|
||||||
const key = locationKey(result.data.location)
|
|
||||||
setStore("location", key, { ...store.location[key], integration: result.data.data })
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
model: {
|
model: {
|
||||||
|
|
@ -650,9 +672,9 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||||
return store.location[locationKey(location ?? defaultLocation())]?.model
|
return store.location[locationKey(location ?? defaultLocation())]?.model
|
||||||
},
|
},
|
||||||
async refresh(ref?: LocationRef) {
|
async refresh(ref?: LocationRef) {
|
||||||
const result = await sdk.client.v2.model.list({ location: locationQuery(ref) }, { throwOnError: true })
|
const result = await sdk.api.models.list({ location: locationQuery(ref ?? defaultLocation()) })
|
||||||
const key = locationKey(result.data.location)
|
const key = locationKey(result.location)
|
||||||
setStore("location", key, { ...store.location[key], model: result.data.data })
|
setStore("location", key, { ...store.location[key], model: mutable(result.data) })
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
provider: {
|
provider: {
|
||||||
|
|
@ -660,9 +682,9 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||||
return store.location[locationKey(location ?? defaultLocation())]?.provider
|
return store.location[locationKey(location ?? defaultLocation())]?.provider
|
||||||
},
|
},
|
||||||
async refresh(ref?: LocationRef) {
|
async refresh(ref?: LocationRef) {
|
||||||
const result = await sdk.client.v2.provider.list({ location: locationQuery(ref) }, { throwOnError: true })
|
const result = await sdk.api.providers.list({ location: locationQuery(ref ?? defaultLocation()) })
|
||||||
const key = locationKey(result.data.location)
|
const key = locationKey(result.location)
|
||||||
setStore("location", key, { ...store.location[key], provider: result.data.data })
|
setStore("location", key, { ...store.location[key], provider: mutable(result.data) })
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
reference: {
|
reference: {
|
||||||
|
|
@ -670,9 +692,9 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||||
return store.location[locationKey(location ?? defaultLocation())]?.reference
|
return store.location[locationKey(location ?? defaultLocation())]?.reference
|
||||||
},
|
},
|
||||||
async refresh(ref?: LocationRef) {
|
async refresh(ref?: LocationRef) {
|
||||||
const result = await sdk.client.v2.reference.list({ location: locationQuery(ref) }, { throwOnError: true })
|
const result = await sdk.api.references.list({ location: locationQuery(ref ?? defaultLocation()) })
|
||||||
const key = locationKey(result.data.location)
|
const key = locationKey(result.location)
|
||||||
setStore("location", key, { ...store.location[key], reference: result.data.data })
|
setStore("location", key, { ...store.location[key], reference: mutable(result.data) })
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
skill: {
|
skill: {
|
||||||
|
|
@ -680,9 +702,9 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||||
return store.location[locationKey(location ?? defaultLocation())]?.skill
|
return store.location[locationKey(location ?? defaultLocation())]?.skill
|
||||||
},
|
},
|
||||||
async refresh(ref?: LocationRef) {
|
async refresh(ref?: LocationRef) {
|
||||||
const result = await sdk.client.v2.skill.list({ location: locationQuery(ref) }, { throwOnError: true })
|
const result = await sdk.api.skills.list({ location: locationQuery(ref ?? defaultLocation()) })
|
||||||
const key = locationKey(result.data.location)
|
const key = locationKey(result.location)
|
||||||
setStore("location", key, { ...store.location[key], skill: result.data.data })
|
setStore("location", key, { ...store.location[key], skill: mutable(result.data) })
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -690,24 +712,31 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const settled = await Promise.allSettled([
|
const settled = await Promise.allSettled([
|
||||||
sdk.client.v2.session
|
sdk.api.sessions
|
||||||
.list({ limit: 50, order: "desc" }, { throwOnError: true })
|
.list({
|
||||||
|
limit: 50,
|
||||||
|
order: "desc",
|
||||||
|
directory: defaultLocation().directory,
|
||||||
|
workspace: defaultLocation().workspaceID,
|
||||||
|
})
|
||||||
.then((response) =>
|
.then((response) =>
|
||||||
setStore(
|
setStore(
|
||||||
"session",
|
"session",
|
||||||
"info",
|
"info",
|
||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
for (const session of response.data.data) draft[session.id] = session
|
for (const session of response.data) draft[session.id] = mutable(session)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
sdk.client.v2.session.active({ throwOnError: true }).then((response) =>
|
sdk.api.sessions
|
||||||
setStore(
|
.active()
|
||||||
"session",
|
.then((active) =>
|
||||||
"status",
|
setStore(
|
||||||
Object.fromEntries(Object.keys(response.data.data).map((sessionID) => [sessionID, "running" as const])),
|
"session",
|
||||||
|
"status",
|
||||||
|
Object.fromEntries(Object.keys(active).map((sessionID) => [sessionID, "running" as const])),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
result.location.refresh(),
|
result.location.refresh(),
|
||||||
result.location.agent.refresh(),
|
result.location.agent.refresh(),
|
||||||
result.location.integration.refresh(),
|
result.location.integration.refresh(),
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import type { OpenCodeClient } from "@opencode-ai/client"
|
||||||
import type { OpencodeClient, V2Event } from "@opencode-ai/sdk/v2"
|
import type { OpencodeClient, V2Event } from "@opencode-ai/sdk/v2"
|
||||||
import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
||||||
import { onCleanup, onMount } from "solid-js"
|
import { onCleanup, onMount } from "solid-js"
|
||||||
|
|
@ -11,9 +12,14 @@ const connectTimeout = 2_000
|
||||||
|
|
||||||
export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
||||||
name: "SDK",
|
name: "SDK",
|
||||||
init: (props: { client: OpencodeClient; reload?: () => Promise<OpencodeClient> }) => {
|
init: (props: {
|
||||||
|
client: OpencodeClient
|
||||||
|
api: OpenCodeClient
|
||||||
|
reload?: () => Promise<{ client: OpencodeClient; api: OpenCodeClient }>
|
||||||
|
}) => {
|
||||||
const abort = new AbortController()
|
const abort = new AbortController()
|
||||||
let client = props.client
|
let client = props.client
|
||||||
|
let api = props.api
|
||||||
const events = createGlobalEmitter<SDKEventMap>()
|
const events = createGlobalEmitter<SDKEventMap>()
|
||||||
const [connection, setConnection] = createStore<{
|
const [connection, setConnection] = createStore<{
|
||||||
status: SDKConnectionStatus
|
status: SDKConnectionStatus
|
||||||
|
|
@ -40,7 +46,10 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
||||||
while (!abort.signal.aborted && !controller.signal.aborted) {
|
while (!abort.signal.aborted && !controller.signal.aborted) {
|
||||||
const connection = new AbortController()
|
const connection = new AbortController()
|
||||||
const cancel = () => connection.abort(controller.signal.reason)
|
const cancel = () => connection.abort(controller.signal.reason)
|
||||||
const timeout = setTimeout(() => connection.abort(new Error("Timed out connecting to server")), connectTimeout)
|
const timeout = setTimeout(
|
||||||
|
() => connection.abort(new Error("Timed out connecting to server")),
|
||||||
|
connectTimeout,
|
||||||
|
)
|
||||||
controller.signal.addEventListener("abort", cancel, { once: true })
|
controller.signal.addEventListener("abort", cancel, { once: true })
|
||||||
const error = await (async () => {
|
const error = await (async () => {
|
||||||
const response = await current.v2.event.subscribe({
|
const response = await current.v2.event.subscribe({
|
||||||
|
|
@ -91,7 +100,8 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
||||||
pending = Promise.resolve()
|
pending = Promise.resolve()
|
||||||
.then(props.reload)
|
.then(props.reload)
|
||||||
.then(async (next) => {
|
.then(async (next) => {
|
||||||
client = next
|
client = next.client
|
||||||
|
api = next.api
|
||||||
if (!abort.signal.aborted) await start()
|
if (!abort.signal.aborted) await start()
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
|
@ -112,6 +122,9 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
||||||
get client() {
|
get client() {
|
||||||
return client
|
return client
|
||||||
},
|
},
|
||||||
|
get api() {
|
||||||
|
return api
|
||||||
|
},
|
||||||
event: {
|
event: {
|
||||||
on: events.on,
|
on: events.on,
|
||||||
listen: events.listen,
|
listen: events.listen,
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,7 @@ export function DialogMessage(props: { messageID: string; sessionID: string; set
|
||||||
const clipboard = useClipboard()
|
const clipboard = useClipboard()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const message = createMemo(() =>
|
const message = createMemo(() => data.session.message.get(props.sessionID, props.messageID))
|
||||||
data.session.message.get(props.sessionID, props.messageID),
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogSelect
|
<DialogSelect
|
||||||
|
|
@ -24,11 +22,9 @@ export function DialogMessage(props: { messageID: string; sessionID: string; set
|
||||||
value: "session.revert",
|
value: "session.revert",
|
||||||
description: "undo messages and file changes",
|
description: "undo messages and file changes",
|
||||||
onSelect: async (dialog) => {
|
onSelect: async (dialog) => {
|
||||||
const result = await sdk.client.v2.session.revert.stage({
|
await sdk.api.sessions
|
||||||
sessionID: props.sessionID,
|
.stage({ sessionID: props.sessionID, messageID: props.messageID })
|
||||||
messageID: props.messageID,
|
.catch((error) => toast.show({ message: errorMessage(error), variant: "error", duration: 5000 }))
|
||||||
})
|
|
||||||
if (result.error) toast.show({ message: errorMessage(result.error), variant: "error", duration: 5000 })
|
|
||||||
dialog.clear()
|
dialog.clear()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -156,10 +156,11 @@ export function Session() {
|
||||||
const promptRef = usePromptRef()
|
const promptRef = usePromptRef()
|
||||||
const session = createMemo(() => data.session.get(route.sessionID))
|
const session = createMemo(() => data.session.get(route.sessionID))
|
||||||
const messageIDs = createMemo(() => data.session.message.ids(route.sessionID))
|
const messageIDs = createMemo(() => data.session.message.ids(route.sessionID))
|
||||||
const sessionMessages = () => messageIDs().flatMap((id) => {
|
const sessionMessages = () =>
|
||||||
const message = data.session.message.get(route.sessionID, id)
|
messageIDs().flatMap((id) => {
|
||||||
return message ? [message] : []
|
const message = data.session.message.get(route.sessionID, id)
|
||||||
})
|
return message ? [message] : []
|
||||||
|
})
|
||||||
const location = createMemo(() => session()?.location)
|
const location = createMemo(() => session()?.location)
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
|
|
@ -286,7 +287,10 @@ export function Session() {
|
||||||
if (!message) return false
|
if (!message) return false
|
||||||
|
|
||||||
if (message.type === "user") return Boolean(message.text.trim())
|
if (message.type === "user") return Boolean(message.text.trim())
|
||||||
return message.type === "assistant" && message.content.some((content) => content.type === "text" && content.text.trim())
|
return (
|
||||||
|
message.type === "assistant" &&
|
||||||
|
message.content.some((content) => content.type === "text" && content.text.trim())
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.sort((a, b) => a.y - b.y)
|
.sort((a, b) => a.y - b.y)
|
||||||
|
|
||||||
|
|
@ -361,7 +365,7 @@ export function Session() {
|
||||||
aliases: ["summarize"],
|
aliases: ["summarize"],
|
||||||
},
|
},
|
||||||
run: () => {
|
run: () => {
|
||||||
void sdk.client.v2.session.compact({ sessionID: route.sessionID })
|
void sdk.api.sessions.compact({ sessionID: route.sessionID })
|
||||||
dialog.clear()
|
dialog.clear()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -395,8 +399,11 @@ export function Session() {
|
||||||
dialog.clear()
|
dialog.clear()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const result = await sdk.client.v2.session.revert.stage({ sessionID: route.sessionID, messageID: target })
|
const error = await sdk.api.sessions.stage({ sessionID: route.sessionID, messageID: target }).then(
|
||||||
if (result.error) toast.show({ message: errorMessage(result.error), variant: "error", duration: 5000 })
|
() => undefined,
|
||||||
|
(error) => error,
|
||||||
|
)
|
||||||
|
if (error) toast.show({ message: errorMessage(error), variant: "error", duration: 5000 })
|
||||||
dialog.clear()
|
dialog.clear()
|
||||||
})()
|
})()
|
||||||
},
|
},
|
||||||
|
|
@ -409,8 +416,11 @@ export function Session() {
|
||||||
slash: { name: "redo" },
|
slash: { name: "redo" },
|
||||||
run: () => {
|
run: () => {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const result = await sdk.client.v2.session.revert.clear({ sessionID: route.sessionID })
|
const error = await sdk.api.sessions.clear({ sessionID: route.sessionID }).then(
|
||||||
if (result.error) toast.show({ message: errorMessage(result.error), variant: "error", duration: 5000 })
|
() => undefined,
|
||||||
|
(error) => error,
|
||||||
|
)
|
||||||
|
if (error) toast.show({ message: errorMessage(error), variant: "error", duration: 5000 })
|
||||||
dialog.clear()
|
dialog.clear()
|
||||||
})()
|
})()
|
||||||
},
|
},
|
||||||
|
|
@ -878,23 +888,21 @@ export function Session() {
|
||||||
</For>
|
</For>
|
||||||
<Show when={session()?.revert?.messageID}>
|
<Show when={session()?.revert?.messageID}>
|
||||||
<RevertMessage
|
<RevertMessage
|
||||||
count={messages().filter((message) => message.id >= session()!.revert!.messageID && message.type === "user").length}
|
count={
|
||||||
|
messages().filter(
|
||||||
|
(message) => message.id >= session()!.revert!.messageID && message.type === "user",
|
||||||
|
).length
|
||||||
|
}
|
||||||
files={session()!.revert!.files ?? []}
|
files={session()!.revert!.files ?? []}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</scrollbox>
|
</scrollbox>
|
||||||
<box flexShrink={0}>
|
<box flexShrink={0}>
|
||||||
<Show when={permissions().length > 0}>
|
<Show when={permissions().length > 0}>
|
||||||
<PermissionPrompt
|
<PermissionPrompt request={permissions()[0]} directory={session()?.location.directory} />
|
||||||
request={permissions()[0]}
|
|
||||||
directory={session()?.location.directory}
|
|
||||||
/>
|
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={permissions().length === 0 && questions().length > 0}>
|
<Show when={permissions().length === 0 && questions().length > 0}>
|
||||||
<QuestionPrompt
|
<QuestionPrompt request={questions()[0]} directory={session()?.location.directory} />
|
||||||
request={questions()[0]}
|
|
||||||
directory={session()?.location.directory}
|
|
||||||
/>
|
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={session()?.parentID}>
|
<Show when={session()?.parentID}>
|
||||||
<SubagentFooter />
|
<SubagentFooter />
|
||||||
|
|
@ -957,9 +965,7 @@ function SessionRowView(props: { row: SessionRow; message: (messageID: string) =
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={props.row.type === "message" ? props.row : undefined}>
|
<Match when={props.row.type === "message" ? props.row : undefined}>
|
||||||
{(row) => (
|
{(row) => (
|
||||||
<Show when={props.message(row().messageID)}>
|
<Show when={props.message(row().messageID)}>{(message) => <SessionMessageView message={message()} />}</Show>
|
||||||
{(message) => <SessionMessageView message={message()} />}
|
|
||||||
</Show>
|
|
||||||
)}
|
)}
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={props.row.type === "part" ? props.row : undefined}>
|
<Match when={props.row.type === "part" ? props.row : undefined}>
|
||||||
|
|
@ -1073,8 +1079,8 @@ function SessionGroupView(props: {
|
||||||
result[name] = (result[name] ?? 0) + 1
|
result[name] = (result[name] ?? 0) + 1
|
||||||
return result
|
return result
|
||||||
}, {})
|
}, {})
|
||||||
const tools = Object.entries(counts).map(([name, count]) =>
|
const tools = Object.entries(counts).map(
|
||||||
`${count} ${count === 1 ? name : name === "search" ? "searches" : `${name}s`}`,
|
([name, count]) => `${count} ${count === 1 ? name : name === "search" ? "searches" : `${name}s`}`,
|
||||||
)
|
)
|
||||||
return `${props.completed ? "Explored" : "Exploring"} — ${tools.join(", ")}`
|
return `${props.completed ? "Explored" : "Exploring"} — ${tools.join(", ")}`
|
||||||
})
|
})
|
||||||
|
|
@ -1116,9 +1122,10 @@ function AssistantFooter(props: { message: SessionMessageAssistant }) {
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const model = createMemo(
|
const model = createMemo(
|
||||||
() =>
|
() =>
|
||||||
ctx.models().find(
|
ctx
|
||||||
(model) => model.providerID === props.message.model.providerID && model.id === props.message.model.id,
|
.models()
|
||||||
)?.name ?? `${props.message.model.providerID}/${props.message.model.id}`,
|
.find((model) => model.providerID === props.message.model.providerID && model.id === props.message.model.id)
|
||||||
|
?.name ?? `${props.message.model.providerID}/${props.message.model.id}`,
|
||||||
)
|
)
|
||||||
const duration = createMemo(() =>
|
const duration = createMemo(() =>
|
||||||
props.message.time.completed ? props.message.time.completed - props.message.time.created : 0,
|
props.message.time.completed ? props.message.time.completed - props.message.time.created : 0,
|
||||||
|
|
@ -1158,14 +1165,7 @@ function SessionNoticeMessageV2(props: { message: SessionMessage }) {
|
||||||
|
|
||||||
function CompactionMessage() {
|
function CompactionMessage() {
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
return (
|
return <box border={["top"]} title=" Compaction " titleAlignment="center" borderColor={theme.borderActive} />
|
||||||
<box
|
|
||||||
border={["top"]}
|
|
||||||
title=" Compaction "
|
|
||||||
titleAlignment="center"
|
|
||||||
borderColor={theme.borderActive}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function statusLabel(status: "added" | "modified" | "deleted") {
|
function statusLabel(status: "added" | "modified" | "deleted") {
|
||||||
|
|
@ -1197,8 +1197,11 @@ function RevertMessage(props: {
|
||||||
onMouseUp={() => {
|
onMouseUp={() => {
|
||||||
if (renderer.getSelection()?.getSelectedText()) return
|
if (renderer.getSelection()?.getSelectedText()) return
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const result = await sdk.client.v2.session.revert.clear({ sessionID: route.sessionID })
|
const error = await sdk.api.sessions.clear({ sessionID: route.sessionID }).then(
|
||||||
if (result.error) toast.show({ message: errorMessage(result.error), variant: "error", duration: 5000 })
|
() => undefined,
|
||||||
|
(error) => error,
|
||||||
|
)
|
||||||
|
if (error) toast.show({ message: errorMessage(error), variant: "error", duration: 5000 })
|
||||||
})()
|
})()
|
||||||
}}
|
}}
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
|
|
@ -1207,7 +1210,12 @@ function RevertMessage(props: {
|
||||||
customBorderChars={SplitBorder.customBorderChars}
|
customBorderChars={SplitBorder.customBorderChars}
|
||||||
borderColor={theme.backgroundPanel}
|
borderColor={theme.backgroundPanel}
|
||||||
>
|
>
|
||||||
<box paddingTop={1} paddingBottom={1} paddingLeft={2} backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel}>
|
<box
|
||||||
|
paddingTop={1}
|
||||||
|
paddingBottom={1}
|
||||||
|
paddingLeft={2}
|
||||||
|
backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel}
|
||||||
|
>
|
||||||
<text fg={theme.textMuted}>
|
<text fg={theme.textMuted}>
|
||||||
{props.count} message{props.count === 1 ? "" : "s"} reverted
|
{props.count} message{props.count === 1 ? "" : "s"} reverted
|
||||||
</text>
|
</text>
|
||||||
|
|
@ -1251,54 +1259,63 @@ function UserMessage(props: { message: SessionMessageUser }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={props.message.text.trim() || files().length}>
|
<Show when={props.message.text.trim() || files().length}>
|
||||||
|
<box
|
||||||
|
id={props.message.id}
|
||||||
|
border={["left"]}
|
||||||
|
borderColor={color()}
|
||||||
|
customBorderChars={SplitBorder.customBorderChars}
|
||||||
|
>
|
||||||
<box
|
<box
|
||||||
id={props.message.id}
|
onMouseOver={() => {
|
||||||
border={["left"]}
|
setHover(true)
|
||||||
borderColor={color()}
|
}}
|
||||||
customBorderChars={SplitBorder.customBorderChars}
|
onMouseOut={() => {
|
||||||
|
setHover(false)
|
||||||
|
}}
|
||||||
|
onMouseUp={() => {
|
||||||
|
if (renderer.getSelection()?.getSelectedText()) return
|
||||||
|
dialog.replace(() => <DialogMessage messageID={props.message.id} sessionID={ctx.sessionID} />)
|
||||||
|
}}
|
||||||
|
paddingTop={1}
|
||||||
|
paddingBottom={1}
|
||||||
|
paddingLeft={2}
|
||||||
|
backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel}
|
||||||
|
flexShrink={0}
|
||||||
>
|
>
|
||||||
<box
|
<text fg={theme.text}>{props.message.text}</text>
|
||||||
onMouseOver={() => {
|
<Show when={files().length}>
|
||||||
setHover(true)
|
<box
|
||||||
}}
|
flexDirection="row"
|
||||||
onMouseOut={() => {
|
paddingBottom={ctx.showTimestamps() ? 1 : 0}
|
||||||
setHover(false)
|
paddingTop={1}
|
||||||
}}
|
gap={1}
|
||||||
onMouseUp={() => {
|
flexWrap="wrap"
|
||||||
if (renderer.getSelection()?.getSelectedText()) return
|
>
|
||||||
dialog.replace(() => <DialogMessage messageID={props.message.id} sessionID={ctx.sessionID} />)
|
<For each={files()}>
|
||||||
}}
|
{(file) => {
|
||||||
paddingTop={1}
|
const directory = file.mime === "application/x-directory"
|
||||||
paddingBottom={1}
|
return (
|
||||||
paddingLeft={2}
|
<text fg={theme.text}>
|
||||||
backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel}
|
<span style={{ bg: theme.secondary, fg: theme.background }}>
|
||||||
flexShrink={0}
|
{directory ? " Directory " : " File "}
|
||||||
>
|
</span>
|
||||||
<text fg={theme.text}>{props.message.text}</text>
|
<span style={{ bg: theme.backgroundElement, fg: theme.textMuted }}>
|
||||||
<Show when={files().length}>
|
{" "}
|
||||||
<box flexDirection="row" paddingBottom={ctx.showTimestamps() ? 1 : 0} paddingTop={1} gap={1} flexWrap="wrap">
|
{file.name ?? file.uri}{" "}
|
||||||
<For each={files()}>
|
</span>
|
||||||
{(file) => {
|
</text>
|
||||||
const directory = file.mime === "application/x-directory"
|
)
|
||||||
return (
|
}}
|
||||||
<text fg={theme.text}>
|
</For>
|
||||||
<span style={{ bg: theme.secondary, fg: theme.background }}>
|
</box>
|
||||||
{directory ? " Directory " : " File "}
|
</Show>
|
||||||
</span>
|
<Show when={ctx.showTimestamps()}>
|
||||||
<span style={{ bg: theme.backgroundElement, fg: theme.textMuted }}> {file.name ?? file.uri} </span>
|
<text fg={theme.textMuted}>
|
||||||
</text>
|
<span style={{ fg: theme.textMuted }}>{Locale.todayTimeOrDateTime(props.message.time.created)}</span>
|
||||||
)
|
</text>
|
||||||
}}
|
</Show>
|
||||||
</For>
|
|
||||||
</box>
|
|
||||||
</Show>
|
|
||||||
<Show when={ctx.showTimestamps()}>
|
|
||||||
<text fg={theme.textMuted}>
|
|
||||||
<span style={{ fg: theme.textMuted }}>{Locale.todayTimeOrDateTime(props.message.time.created)}</span>
|
|
||||||
</text>
|
|
||||||
</Show>
|
|
||||||
</box>
|
|
||||||
</box>
|
</box>
|
||||||
|
</box>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1309,9 +1326,10 @@ function AssistantMessage(props: { message: SessionMessageAssistant; last: boole
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const model = createMemo(
|
const model = createMemo(
|
||||||
() =>
|
() =>
|
||||||
ctx.models().find(
|
ctx
|
||||||
(model) => model.providerID === props.message.model.providerID && model.id === props.message.model.id,
|
.models()
|
||||||
)?.name ?? `${props.message.model.providerID}/${props.message.model.id}`,
|
.find((model) => model.providerID === props.message.model.providerID && model.id === props.message.model.id)
|
||||||
|
?.name ?? `${props.message.model.providerID}/${props.message.model.id}`,
|
||||||
)
|
)
|
||||||
|
|
||||||
const final = createMemo(() => {
|
const final = createMemo(() => {
|
||||||
|
|
@ -1325,10 +1343,7 @@ function AssistantMessage(props: { message: SessionMessageAssistant; last: boole
|
||||||
})
|
})
|
||||||
|
|
||||||
const exploration = createMemo(() => {
|
const exploration = createMemo(() => {
|
||||||
const grouped = new Map<
|
const grouped = new Map<string, { first: boolean; parts: SessionMessageAssistantTool[]; active: boolean }>()
|
||||||
string,
|
|
||||||
{ first: boolean; parts: SessionMessageAssistantTool[]; active: boolean }
|
|
||||||
>()
|
|
||||||
if (!ctx.groupExploration()) return grouped
|
if (!ctx.groupExploration()) return grouped
|
||||||
const runs = props.message.content
|
const runs = props.message.content
|
||||||
.map((part) =>
|
.map((part) =>
|
||||||
|
|
@ -1471,7 +1486,9 @@ function ReasoningPart(props: {
|
||||||
// OpenRouter encrypts some reasoning blocks; drop the placeholder.
|
// OpenRouter encrypts some reasoning blocks; drop the placeholder.
|
||||||
return props.part.text.replace("[REDACTED]", "").trim()
|
return props.part.text.replace("[REDACTED]", "").trim()
|
||||||
})
|
})
|
||||||
const isDone = createMemo(() => props.part.time?.completed !== undefined || props.message.time.completed !== undefined)
|
const isDone = createMemo(
|
||||||
|
() => props.part.time?.completed !== undefined || props.message.time.completed !== undefined,
|
||||||
|
)
|
||||||
const inMinimal = createMemo(() => ctx.thinkingMode() === "hide")
|
const inMinimal = createMemo(() => ctx.thinkingMode() === "hide")
|
||||||
const duration = createMemo(() => {
|
const duration = createMemo(() => {
|
||||||
const end = props.part.time?.completed ?? props.message.time.completed
|
const end = props.part.time?.completed ?? props.message.time.completed
|
||||||
|
|
@ -1488,11 +1505,7 @@ function ReasoningPart(props: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={content()}>
|
<Show when={content()}>
|
||||||
<box
|
<box paddingLeft={3} flexDirection="column" flexShrink={0}>
|
||||||
paddingLeft={3}
|
|
||||||
flexDirection="column"
|
|
||||||
flexShrink={0}
|
|
||||||
>
|
|
||||||
<box onMouseUp={toggle}>
|
<box onMouseUp={toggle}>
|
||||||
<ReasoningHeader
|
<ReasoningHeader
|
||||||
toggleable={inMinimal()}
|
toggleable={inMinimal()}
|
||||||
|
|
@ -1808,12 +1821,7 @@ export function InlineToolRow(props: {
|
||||||
onMouseUp?: () => void
|
onMouseUp?: () => void
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<box
|
<box paddingLeft={3} onMouseOver={props.onMouseOver} onMouseOut={props.onMouseOut} onMouseUp={props.onMouseUp}>
|
||||||
paddingLeft={3}
|
|
||||||
onMouseOver={props.onMouseOver}
|
|
||||||
onMouseOut={props.onMouseOut}
|
|
||||||
onMouseUp={props.onMouseUp}
|
|
||||||
>
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={props.spinner}>
|
<Match when={props.spinner}>
|
||||||
<Spinner color={props.color} children={props.children} />
|
<Spinner color={props.color} children={props.children} />
|
||||||
|
|
@ -2003,12 +2011,7 @@ function Write(props: ToolProps) {
|
||||||
</BlockTool>
|
</BlockTool>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<InlineTool
|
<InlineTool icon="←" pending="Preparing write..." complete={stringValue(props.input.path)} part={props.part}>
|
||||||
icon="←"
|
|
||||||
pending="Preparing write..."
|
|
||||||
complete={stringValue(props.input.path)}
|
|
||||||
part={props.part}
|
|
||||||
>
|
|
||||||
Write {pathFormatter.format(stringValue(props.input.path))}
|
Write {pathFormatter.format(stringValue(props.input.path))}
|
||||||
</InlineTool>
|
</InlineTool>
|
||||||
</Match>
|
</Match>
|
||||||
|
|
@ -2473,7 +2476,14 @@ export function parseApplyPatchFiles(value: unknown) {
|
||||||
const patch = stringValue(file.patch)
|
const patch = stringValue(file.patch)
|
||||||
const additions = numberValue(file.additions)
|
const additions = numberValue(file.additions)
|
||||||
const deletions = numberValue(file.deletions)
|
const deletions = numberValue(file.deletions)
|
||||||
if (!type || !relativePath || !filePath || patch === undefined || additions === undefined || deletions === undefined)
|
if (
|
||||||
|
!type ||
|
||||||
|
!relativePath ||
|
||||||
|
!filePath ||
|
||||||
|
patch === undefined ||
|
||||||
|
additions === undefined ||
|
||||||
|
deletions === undefined
|
||||||
|
)
|
||||||
return []
|
return []
|
||||||
return [{ type, relativePath, filePath, patch, additions, deletions, movePath: stringValue(file.movePath) }]
|
return [{ type, relativePath, filePath, patch, additions, deletions, movePath: stringValue(file.movePath) }]
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -186,7 +186,7 @@ export function PermissionPrompt(props: { request: PermissionV2Request; director
|
||||||
onSelect={(option) => {
|
onSelect={(option) => {
|
||||||
setStore("stage", "permission")
|
setStore("stage", "permission")
|
||||||
if (option === "cancel") return
|
if (option === "cancel") return
|
||||||
void sdk.client.v2.session.permission.reply({
|
void sdk.api.permissions.reply({
|
||||||
sessionID: props.request.sessionID,
|
sessionID: props.request.sessionID,
|
||||||
reply: "always",
|
reply: "always",
|
||||||
requestID: props.request.id,
|
requestID: props.request.id,
|
||||||
|
|
@ -197,7 +197,7 @@ export function PermissionPrompt(props: { request: PermissionV2Request; director
|
||||||
<Match when={store.stage === "reject"}>
|
<Match when={store.stage === "reject"}>
|
||||||
<RejectPrompt
|
<RejectPrompt
|
||||||
onConfirm={(message) => {
|
onConfirm={(message) => {
|
||||||
void sdk.client.v2.session.permission.reply({
|
void sdk.api.permissions.reply({
|
||||||
sessionID: props.request.sessionID,
|
sessionID: props.request.sessionID,
|
||||||
reply: "reject",
|
reply: "reject",
|
||||||
requestID: props.request.id,
|
requestID: props.request.id,
|
||||||
|
|
@ -443,14 +443,14 @@ export function PermissionPrompt(props: { request: PermissionV2Request; director
|
||||||
setStore("stage", "reject")
|
setStore("stage", "reject")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
void sdk.client.v2.session.permission.reply({
|
void sdk.api.permissions.reply({
|
||||||
sessionID: props.request.sessionID,
|
sessionID: props.request.sessionID,
|
||||||
reply: "reject",
|
reply: "reject",
|
||||||
requestID: props.request.id,
|
requestID: props.request.id,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
void sdk.client.v2.session.permission.reply({
|
void sdk.api.permissions.reply({
|
||||||
sessionID: props.request.sessionID,
|
sessionID: props.request.sessionID,
|
||||||
reply: "once",
|
reply: "once",
|
||||||
requestID: props.request.id,
|
requestID: props.request.id,
|
||||||
|
|
|
||||||
|
|
@ -47,15 +47,15 @@ export function QuestionPrompt(props: { request: QuestionV2Request; directory?:
|
||||||
|
|
||||||
function submit() {
|
function submit() {
|
||||||
const answers = questions().map((_, i) => store.answers[i] ?? [])
|
const answers = questions().map((_, i) => store.answers[i] ?? [])
|
||||||
void sdk.client.v2.session.question.reply({
|
void sdk.api.questions.reply({
|
||||||
sessionID: props.request.sessionID,
|
sessionID: props.request.sessionID,
|
||||||
requestID: props.request.id,
|
requestID: props.request.id,
|
||||||
questionV2Reply: { answers },
|
answers,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function reject() {
|
function reject() {
|
||||||
void sdk.client.v2.session.question.reject({
|
void sdk.api.questions.reject({
|
||||||
sessionID: props.request.sessionID,
|
sessionID: props.request.sessionID,
|
||||||
requestID: props.request.id,
|
requestID: props.request.id,
|
||||||
})
|
})
|
||||||
|
|
@ -71,10 +71,10 @@ export function QuestionPrompt(props: { request: QuestionV2Request; directory?:
|
||||||
setStore("custom", inputs)
|
setStore("custom", inputs)
|
||||||
}
|
}
|
||||||
if (single()) {
|
if (single()) {
|
||||||
void sdk.client.v2.session.question.reply({
|
void sdk.api.questions.reply({
|
||||||
sessionID: props.request.sessionID,
|
sessionID: props.request.sessionID,
|
||||||
requestID: props.request.id,
|
requestID: props.request.id,
|
||||||
questionV2Reply: { answers: [[answer]] },
|
answers: [[answer]],
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { createTestRenderer } from "@opentui/core/testing"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { createTuiResolvedConfig } from "./fixture/tui-runtime"
|
import { createTuiResolvedConfig } from "./fixture/tui-runtime"
|
||||||
import { createClient, createEventStream, createFetch, directory, json } from "./fixture/tui-sdk"
|
import { createApi, createClient, createEventStream, createFetch, directory, json } from "./fixture/tui-sdk"
|
||||||
|
|
||||||
test("SIGHUP clears title and disposes scoped resources once", async () => {
|
test("SIGHUP clears title and disposes scoped resources once", async () => {
|
||||||
const setup = await createTestRenderer({ width: 80, height: 24, useThread: false })
|
const setup = await createTestRenderer({ width: 80, height: 24, useThread: false })
|
||||||
|
|
@ -30,6 +30,7 @@ test("SIGHUP clears title and disposes scoped resources once", async () => {
|
||||||
const task = Effect.runPromise(
|
const task = Effect.runPromise(
|
||||||
run({
|
run({
|
||||||
client: createClient(calls.fetch),
|
client: createClient(calls.fetch),
|
||||||
|
api: createApi(calls.fetch),
|
||||||
config: createTuiResolvedConfig({ plugin_enabled: {} }),
|
config: createTuiResolvedConfig({ plugin_enabled: {} }),
|
||||||
args: {},
|
args: {},
|
||||||
pluginHost: {
|
pluginHost: {
|
||||||
|
|
@ -61,26 +62,23 @@ test("app.exit prints the session epilogue after scoped cleanup", async () => {
|
||||||
const core = await import("@opentui/core")
|
const core = await import("@opentui/core")
|
||||||
mock.module("@opentui/core", () => ({ ...core, createCliRenderer: async () => setup.renderer }))
|
mock.module("@opentui/core", () => ({ ...core, createCliRenderer: async () => setup.renderer }))
|
||||||
const events = createEventStream()
|
const events = createEventStream()
|
||||||
const calls = createFetch(
|
const calls = createFetch((url) => {
|
||||||
(url) => {
|
if (url.pathname === "/api/session")
|
||||||
if (url.pathname === "/api/session")
|
return json({
|
||||||
return json({
|
data: [
|
||||||
data: [
|
{
|
||||||
{
|
id: "dummy",
|
||||||
id: "dummy",
|
title: "Demo session",
|
||||||
title: "Demo session",
|
projectID: "project",
|
||||||
projectID: "project",
|
location: { directory },
|
||||||
location: { directory },
|
cost: 0,
|
||||||
cost: 0,
|
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
time: { created: 0, updated: 0 },
|
||||||
time: { created: 0, updated: 0 },
|
},
|
||||||
},
|
],
|
||||||
],
|
cursor: {},
|
||||||
cursor: {},
|
})
|
||||||
})
|
}, events)
|
||||||
},
|
|
||||||
events,
|
|
||||||
)
|
|
||||||
const originalWrite = process.stdout.write.bind(process.stdout)
|
const originalWrite = process.stdout.write.bind(process.stdout)
|
||||||
let stdout = ""
|
let stdout = ""
|
||||||
let api: TuiPluginApi | undefined
|
let api: TuiPluginApi | undefined
|
||||||
|
|
@ -99,6 +97,7 @@ test("app.exit prints the session epilogue after scoped cleanup", async () => {
|
||||||
const task = Effect.runPromise(
|
const task = Effect.runPromise(
|
||||||
run({
|
run({
|
||||||
client: createClient(calls.fetch),
|
client: createClient(calls.fetch),
|
||||||
|
api: createApi(calls.fetch),
|
||||||
config: createTuiResolvedConfig({ plugin_enabled: {} }),
|
config: createTuiResolvedConfig({ plugin_enabled: {} }),
|
||||||
args: { continue: true },
|
args: { continue: true },
|
||||||
pluginHost: {
|
pluginHost: {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { ProjectProvider, useProject } from "../../../../src/context/project"
|
||||||
import { SDKProvider } from "../../../../src/context/sdk"
|
import { SDKProvider } from "../../../../src/context/sdk"
|
||||||
import { SyncProvider, useSync } from "../../../../src/context/sync"
|
import { SyncProvider, useSync } from "../../../../src/context/sync"
|
||||||
import { ExitProvider } from "../../../../src/context/exit"
|
import { ExitProvider } from "../../../../src/context/exit"
|
||||||
import { createClient, createEventStream, createFetch, type FetchHandler } from "../../../fixture/tui-sdk"
|
import { createApi, createClient, createEventStream, createFetch, type FetchHandler } from "../../../fixture/tui-sdk"
|
||||||
import { TestTuiContexts } from "../../../fixture/tui-environment"
|
import { TestTuiContexts } from "../../../fixture/tui-environment"
|
||||||
export { createEventStream, createFetch, directory, json, worktree } from "../../../fixture/tui-sdk"
|
export { createEventStream, createFetch, directory, json, worktree } from "../../../fixture/tui-sdk"
|
||||||
|
|
||||||
|
|
@ -47,7 +47,7 @@ export async function mount(override?: FetchHandler, state?: string) {
|
||||||
<TestTuiContexts paths={state ? { state } : undefined}>
|
<TestTuiContexts paths={state ? { state } : undefined}>
|
||||||
<ArgsProvider>
|
<ArgsProvider>
|
||||||
<KVProvider>
|
<KVProvider>
|
||||||
<SDKProvider client={createClient(calls.fetch)}>
|
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
|
||||||
<ProjectProvider>
|
<ProjectProvider>
|
||||||
<ExitProvider exit={() => {}}>
|
<ExitProvider exit={() => {}}>
|
||||||
<SyncProvider>
|
<SyncProvider>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { onMount } from "solid-js"
|
||||||
import { ProjectProvider } from "../../../src/context/project"
|
import { ProjectProvider } from "../../../src/context/project"
|
||||||
import { SDKProvider } from "../../../src/context/sdk"
|
import { SDKProvider } from "../../../src/context/sdk"
|
||||||
import { DataProvider, useData } from "../../../src/context/data"
|
import { DataProvider, useData } from "../../../src/context/data"
|
||||||
import { createClient, createEventStream, createFetch, directory, json } from "../../fixture/tui-sdk"
|
import { createApi, createClient, createEventStream, createFetch, directory, json } from "../../fixture/tui-sdk"
|
||||||
import { TestTuiContexts } from "../../fixture/tui-environment"
|
import { TestTuiContexts } from "../../fixture/tui-environment"
|
||||||
|
|
||||||
async function wait(fn: () => boolean, timeout = 2000) {
|
async function wait(fn: () => boolean, timeout = 2000) {
|
||||||
|
|
@ -69,7 +69,7 @@ test("refreshes resources into reactive getters", async () => {
|
||||||
|
|
||||||
const app = await testRender(() => (
|
const app = await testRender(() => (
|
||||||
<TestTuiContexts>
|
<TestTuiContexts>
|
||||||
<SDKProvider client={createClient(calls.fetch)}>
|
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
|
||||||
<ProjectProvider>
|
<ProjectProvider>
|
||||||
<DataProvider>
|
<DataProvider>
|
||||||
<Probe />
|
<Probe />
|
||||||
|
|
@ -139,7 +139,7 @@ test("reconnects the event stream and bootstraps fresh data", async () => {
|
||||||
|
|
||||||
const app = await testRender(() => (
|
const app = await testRender(() => (
|
||||||
<TestTuiContexts>
|
<TestTuiContexts>
|
||||||
<SDKProvider client={createClient(calls.fetch)}>
|
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
|
||||||
<ProjectProvider>
|
<ProjectProvider>
|
||||||
<DataProvider>
|
<DataProvider>
|
||||||
<Probe />
|
<Probe />
|
||||||
|
|
@ -172,8 +172,7 @@ test("reconnects the event stream and bootstraps fresh data", async () => {
|
||||||
test("tracks session status from active sessions and execution events", async () => {
|
test("tracks session status from active sessions and execution events", async () => {
|
||||||
const events = createEventStream()
|
const events = createEventStream()
|
||||||
const calls = createFetch((url) => {
|
const calls = createFetch((url) => {
|
||||||
if (url.pathname === "/api/session/active")
|
if (url.pathname === "/api/session/active") return json({ data: { "session-active": { type: "running" } } })
|
||||||
return json({ data: { "session-active": { type: "running" } } })
|
|
||||||
}, events)
|
}, events)
|
||||||
let data!: ReturnType<typeof useData>
|
let data!: ReturnType<typeof useData>
|
||||||
|
|
||||||
|
|
@ -184,7 +183,7 @@ test("tracks session status from active sessions and execution events", async ()
|
||||||
|
|
||||||
const app = await testRender(() => (
|
const app = await testRender(() => (
|
||||||
<TestTuiContexts>
|
<TestTuiContexts>
|
||||||
<SDKProvider client={createClient(calls.fetch)}>
|
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
|
||||||
<ProjectProvider>
|
<ProjectProvider>
|
||||||
<DataProvider>
|
<DataProvider>
|
||||||
<Probe />
|
<Probe />
|
||||||
|
|
@ -296,7 +295,7 @@ test("refreshes integrations after integration updates", async () => {
|
||||||
|
|
||||||
const app = await testRender(() => (
|
const app = await testRender(() => (
|
||||||
<TestTuiContexts>
|
<TestTuiContexts>
|
||||||
<SDKProvider client={createClient(calls.fetch)}>
|
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
|
||||||
<ProjectProvider>
|
<ProjectProvider>
|
||||||
<DataProvider>
|
<DataProvider>
|
||||||
<Probe />
|
<Probe />
|
||||||
|
|
@ -337,7 +336,7 @@ test("refreshes effective catalog data after catalog updates", async () => {
|
||||||
|
|
||||||
const app = await testRender(() => (
|
const app = await testRender(() => (
|
||||||
<TestTuiContexts>
|
<TestTuiContexts>
|
||||||
<SDKProvider client={createClient(calls.fetch)}>
|
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
|
||||||
<ProjectProvider>
|
<ProjectProvider>
|
||||||
<DataProvider>
|
<DataProvider>
|
||||||
<box />
|
<box />
|
||||||
|
|
@ -382,7 +381,7 @@ test("refreshes references after updates", async () => {
|
||||||
|
|
||||||
const app = await testRender(() => (
|
const app = await testRender(() => (
|
||||||
<TestTuiContexts>
|
<TestTuiContexts>
|
||||||
<SDKProvider client={createClient(calls.fetch)}>
|
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
|
||||||
<ProjectProvider>
|
<ProjectProvider>
|
||||||
<DataProvider>
|
<DataProvider>
|
||||||
<Probe />
|
<Probe />
|
||||||
|
|
@ -415,7 +414,7 @@ test("adds and dismisses permission requests from live events", async () => {
|
||||||
|
|
||||||
const app = await testRender(() => (
|
const app = await testRender(() => (
|
||||||
<TestTuiContexts>
|
<TestTuiContexts>
|
||||||
<SDKProvider client={createClient(calls.fetch)}>
|
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
|
||||||
<ProjectProvider>
|
<ProjectProvider>
|
||||||
<DataProvider>
|
<DataProvider>
|
||||||
<Probe />
|
<Probe />
|
||||||
|
|
@ -480,7 +479,7 @@ test("adds and dismisses question requests from live events", async () => {
|
||||||
|
|
||||||
const app = await testRender(() => (
|
const app = await testRender(() => (
|
||||||
<TestTuiContexts>
|
<TestTuiContexts>
|
||||||
<SDKProvider client={createClient(calls.fetch)}>
|
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
|
||||||
<ProjectProvider>
|
<ProjectProvider>
|
||||||
<DataProvider>
|
<DataProvider>
|
||||||
<Probe />
|
<Probe />
|
||||||
|
|
@ -548,7 +547,7 @@ test("settles pending tools when a live failure arrives", async () => {
|
||||||
|
|
||||||
const app = await testRender(() => (
|
const app = await testRender(() => (
|
||||||
<TestTuiContexts>
|
<TestTuiContexts>
|
||||||
<SDKProvider client={createClient(calls.fetch)}>
|
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
|
||||||
<ProjectProvider>
|
<ProjectProvider>
|
||||||
<DataProvider>
|
<DataProvider>
|
||||||
<Probe />
|
<Probe />
|
||||||
|
|
@ -677,7 +676,7 @@ test("renders admitted prompts only after they become model-visible", async () =
|
||||||
|
|
||||||
const app = await testRender(() => (
|
const app = await testRender(() => (
|
||||||
<TestTuiContexts>
|
<TestTuiContexts>
|
||||||
<SDKProvider client={createClient(calls.fetch)}>
|
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
|
||||||
<ProjectProvider>
|
<ProjectProvider>
|
||||||
<DataProvider>
|
<DataProvider>
|
||||||
<Probe />
|
<Probe />
|
||||||
|
|
@ -750,7 +749,7 @@ test("projects live context updates with their message ID", async () => {
|
||||||
|
|
||||||
const app = await testRender(() => (
|
const app = await testRender(() => (
|
||||||
<TestTuiContexts>
|
<TestTuiContexts>
|
||||||
<SDKProvider client={createClient(calls.fetch)}>
|
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
|
||||||
<ProjectProvider>
|
<ProjectProvider>
|
||||||
<DataProvider>
|
<DataProvider>
|
||||||
<Probe />
|
<Probe />
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
/** @jsxImportSource @opentui/solid */
|
/** @jsxImportSource @opentui/solid */
|
||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import type { OpenCodeClient } from "@opencode-ai/client"
|
||||||
import { testRender } from "@opentui/solid"
|
import { testRender } from "@opentui/solid"
|
||||||
import type { OpencodeClient, V2Event } from "@opencode-ai/sdk/v2"
|
import type { OpencodeClient, V2Event } from "@opencode-ai/sdk/v2"
|
||||||
import { onMount } from "solid-js"
|
import { onMount } from "solid-js"
|
||||||
import { ProjectProvider, useProject } from "../../../src/context/project"
|
import { ProjectProvider, useProject } from "../../../src/context/project"
|
||||||
import { SDKProvider, useSDK } from "../../../src/context/sdk"
|
import { SDKProvider, useSDK } from "../../../src/context/sdk"
|
||||||
import { useEvent } from "../../../src/context/event"
|
import { useEvent } from "../../../src/context/event"
|
||||||
import { createClient, createEventStream, createFetch, directory } from "../../fixture/tui-sdk"
|
import { createApi, createClient, createEventStream, createFetch } from "../../fixture/tui-sdk"
|
||||||
import { TestTuiContexts } from "../../fixture/tui-environment"
|
import { TestTuiContexts } from "../../fixture/tui-environment"
|
||||||
|
|
||||||
const projectID = "proj_test"
|
const projectID = "proj_test"
|
||||||
|
|
@ -46,7 +47,7 @@ function update(version: string): V2Event {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function mount(reload?: () => Promise<OpencodeClient>) {
|
async function mount(reload?: () => Promise<{ client: OpencodeClient; api: OpenCodeClient }>) {
|
||||||
const events = createEventStream()
|
const events = createEventStream()
|
||||||
const calls = createFetch(undefined, events)
|
const calls = createFetch(undefined, events)
|
||||||
const seen: V2Event[] = []
|
const seen: V2Event[] = []
|
||||||
|
|
@ -60,7 +61,7 @@ async function mount(reload?: () => Promise<OpencodeClient>) {
|
||||||
|
|
||||||
const app = await testRender(() => (
|
const app = await testRender(() => (
|
||||||
<TestTuiContexts>
|
<TestTuiContexts>
|
||||||
<SDKProvider client={createClient(calls.fetch)} reload={reload}>
|
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)} reload={reload}>
|
||||||
<ProjectProvider>
|
<ProjectProvider>
|
||||||
<Probe
|
<Probe
|
||||||
onReady={async (ctx) => {
|
onReady={async (ctx) => {
|
||||||
|
|
@ -150,7 +151,8 @@ describe("useEvent", () => {
|
||||||
test("reloads the host and reconnects the event stream", async () => {
|
test("reloads the host and reconnects the event stream", async () => {
|
||||||
let calls = 0
|
let calls = 0
|
||||||
const events = createEventStream()
|
const events = createEventStream()
|
||||||
const replacement = createClient(createFetch(undefined, events).fetch)
|
const replacementCalls = createFetch(undefined, events)
|
||||||
|
const replacement = { client: createClient(replacementCalls.fetch), api: createApi(replacementCalls.fetch) }
|
||||||
const { app, sdk, seen } = await mount(async () => {
|
const { app, sdk, seen } = await mount(async () => {
|
||||||
calls += 1
|
calls += 1
|
||||||
return replacement
|
return replacement
|
||||||
|
|
@ -164,19 +166,21 @@ describe("useEvent", () => {
|
||||||
await wait(() => seen.some((item) => item.type === "vcs.branch.updated" && item.data.branch === "reloaded"))
|
await wait(() => seen.some((item) => item.type === "vcs.branch.updated" && item.data.branch === "reloaded"))
|
||||||
|
|
||||||
expect(calls).toBe(1)
|
expect(calls).toBe(1)
|
||||||
expect(sdk.client).toBe(replacement)
|
expect(sdk.client).toBe(replacement.client)
|
||||||
|
expect(sdk.api).toBe(replacement.api)
|
||||||
} finally {
|
} finally {
|
||||||
app.renderer.destroy()
|
app.renderer.destroy()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
test("keeps the current event stream alive while the host reload is pending", async () => {
|
test("keeps the current event stream alive while the host reload is pending", async () => {
|
||||||
let complete!: (client: OpencodeClient) => void
|
let complete!: (client: { client: OpencodeClient; api: OpenCodeClient }) => void
|
||||||
const pending = new Promise<OpencodeClient>((resolve) => {
|
const pending = new Promise<{ client: OpencodeClient; api: OpenCodeClient }>((resolve) => {
|
||||||
complete = resolve
|
complete = resolve
|
||||||
})
|
})
|
||||||
const replacementEvents = createEventStream()
|
const replacementEvents = createEventStream()
|
||||||
const replacement = createClient(createFetch(undefined, replacementEvents).fetch)
|
const replacementCalls = createFetch(undefined, replacementEvents)
|
||||||
|
const replacement = { client: createClient(replacementCalls.fetch), api: createApi(replacementCalls.fetch) }
|
||||||
const { app, emit, sdk, seen } = await mount(() => pending)
|
const { app, emit, sdk, seen } = await mount(() => pending)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -188,7 +192,8 @@ describe("useEvent", () => {
|
||||||
expect(sdk.connection.status()).toBe("connected")
|
expect(sdk.connection.status()).toBe("connected")
|
||||||
complete(replacement)
|
complete(replacement)
|
||||||
await reload
|
await reload
|
||||||
expect(sdk.client).toBe(replacement)
|
expect(sdk.client).toBe(replacement.client)
|
||||||
|
expect(sdk.api).toBe(replacement.api)
|
||||||
} finally {
|
} finally {
|
||||||
app.renderer.destroy()
|
app.renderer.destroy()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { OpenCode } from "@opencode-ai/client"
|
||||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
||||||
import type { V2Event } from "@opencode-ai/sdk/v2"
|
import type { V2Event } from "@opencode-ai/sdk/v2"
|
||||||
|
|
||||||
|
|
@ -117,3 +118,7 @@ export function createFetch(override?: FetchHandler, events?: ReturnType<typeof
|
||||||
export function createClient(fetch: typeof globalThis.fetch) {
|
export function createClient(fetch: typeof globalThis.fetch) {
|
||||||
return createOpencodeClient({ baseUrl: "http://test", fetch })
|
return createOpencodeClient({ baseUrl: "http://test", fetch })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createApi(fetch: typeof globalThis.fetch) {
|
||||||
|
return OpenCode.make({ baseUrl: "http://test", fetch })
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue