mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-09 10:59:49 -04:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b41fbcf9bc | |||
| 95b488cc3e | |||
| 34c83bbf18 | |||
| 5e5888a24f |
@@ -178,7 +178,6 @@ export class LanguageModelCompatibility extends Schema.Class<LanguageModelCompat
|
|||||||
toolSchema: Schema.optional(LanguageModelToolSchemaCompatibility),
|
toolSchema: Schema.optional(LanguageModelToolSchemaCompatibility),
|
||||||
reasoningField: Schema.optional(Schema.String),
|
reasoningField: Schema.optional(Schema.String),
|
||||||
maxTokensField: Schema.optional(LanguageModelMaxTokensFieldCompatibility),
|
maxTokensField: Schema.optional(LanguageModelMaxTokensFieldCompatibility),
|
||||||
requireFinishReason: Schema.optional(Schema.Boolean),
|
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
export namespace LanguageModelCompatibility {
|
export namespace LanguageModelCompatibility {
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ describe("llm constructors", () => {
|
|||||||
const updated = LanguageModel.update(base, {
|
const updated = LanguageModel.update(base, {
|
||||||
route: responsesRoute,
|
route: responsesRoute,
|
||||||
defaults: { generation: { maxTokens: 20 } },
|
defaults: { generation: { maxTokens: 20 } },
|
||||||
compatibility: { toolSchema: "gemini", requireFinishReason: false },
|
compatibility: { toolSchema: "gemini" },
|
||||||
})
|
})
|
||||||
const updatedInput = LanguageModel.input(updated)
|
const updatedInput = LanguageModel.input(updated)
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ describe("llm constructors", () => {
|
|||||||
expect(String(updated.id)).toBe("fake-model")
|
expect(String(updated.id)).toBe("fake-model")
|
||||||
expect(updated.route).toBe(responsesRoute)
|
expect(updated.route).toBe(responsesRoute)
|
||||||
expect(updated.defaults?.generation).toEqual({ maxTokens: 20 })
|
expect(updated.defaults?.generation).toEqual({ maxTokens: 20 })
|
||||||
expect(updated.compatibility).toEqual({ toolSchema: "gemini", requireFinishReason: false })
|
expect(updated.compatibility).toEqual({ toolSchema: "gemini" })
|
||||||
expect(updatedInput.defaults).toBe(updated.defaults)
|
expect(updatedInput.defaults).toBe(updated.defaults)
|
||||||
expect(updatedInput.compatibility).toBe(updated.compatibility)
|
expect(updatedInput.compatibility).toBe(updated.compatibility)
|
||||||
expect(String(updatedInput.provider)).toBe("fake")
|
expect(String(updatedInput.provider)).toBe("fake")
|
||||||
|
|||||||
@@ -140,32 +140,6 @@ export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCO
|
|||||||
description: "List all available models",
|
description: "List all available models",
|
||||||
params: ServerParams,
|
params: ServerParams,
|
||||||
}),
|
}),
|
||||||
Spec.make("export", {
|
|
||||||
description: "Export session data as JSON",
|
|
||||||
params: {
|
|
||||||
...ServerParams,
|
|
||||||
session: Flag.string("session").pipe(
|
|
||||||
Flag.withAlias("s"),
|
|
||||||
Flag.withDescription("Session ID to export to stdout"),
|
|
||||||
Flag.optional,
|
|
||||||
),
|
|
||||||
sanitize: Flag.boolean("sanitize").pipe(
|
|
||||||
Flag.withDescription("Redact sensitive transcript and file data"),
|
|
||||||
Flag.withDefault(false),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
Spec.make("import", {
|
|
||||||
description: "Import session data from a JSON file or URL",
|
|
||||||
params: {
|
|
||||||
...ServerParams,
|
|
||||||
file: Argument.string("file").pipe(Argument.withDescription("JSON file or URL to import")),
|
|
||||||
directory: Flag.string("directory").pipe(
|
|
||||||
Flag.withDescription("Directory in which to import the session"),
|
|
||||||
Flag.optional,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
Spec.make("mini", {
|
Spec.make("mini", {
|
||||||
description: "Start the minimal interactive interface",
|
description: "Start the minimal interactive interface",
|
||||||
params: {
|
params: {
|
||||||
|
|||||||
@@ -1,140 +0,0 @@
|
|||||||
import { OpenCode, type SessionInfo } from "@opencode-ai/client"
|
|
||||||
import { Service } from "@opencode-ai/client/effect/service"
|
|
||||||
import { Effect, Option } from "effect"
|
|
||||||
import { EOL, tmpdir } from "node:os"
|
|
||||||
import path from "node:path"
|
|
||||||
import { emitKeypressEvents, type Key } from "node:readline"
|
|
||||||
import { Commands } from "../commands"
|
|
||||||
import { Runtime } from "../../framework/runtime"
|
|
||||||
import { ServerConnection } from "../../services/server-connection"
|
|
||||||
|
|
||||||
export default Runtime.handler(
|
|
||||||
Commands.commands.export,
|
|
||||||
Effect.fn("cli.export")(function* (input) {
|
|
||||||
const server = yield* ServerConnection.resolve({
|
|
||||||
server: Option.getOrUndefined(input.server),
|
|
||||||
standalone: input.standalone,
|
|
||||||
})
|
|
||||||
const client = OpenCode.make({
|
|
||||||
baseUrl: server.endpoint.url,
|
|
||||||
headers: Service.headers(server.endpoint),
|
|
||||||
})
|
|
||||||
const requested = Option.getOrUndefined(input.session)
|
|
||||||
const selected = requested
|
|
||||||
? undefined
|
|
||||||
: yield* Effect.promise(async () => {
|
|
||||||
const location = await client.location.get({ location: { directory: process.cwd() } })
|
|
||||||
const page = await client.session.list({
|
|
||||||
directory: location.directory,
|
|
||||||
workspace: location.workspaceID,
|
|
||||||
parentID: null,
|
|
||||||
order: "desc",
|
|
||||||
limit: 50,
|
|
||||||
})
|
|
||||||
if (page.data.length === 0) {
|
|
||||||
process.stderr.write(`No sessions found${EOL}`)
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
return selectSession(page.data, input.sanitize)
|
|
||||||
})
|
|
||||||
const sessionID = requested ?? selected?.session.id
|
|
||||||
if (!sessionID) return
|
|
||||||
const data = yield* Effect.promise(() =>
|
|
||||||
client.session.export({ sessionID, sanitize: selected?.sanitize ?? input.sanitize }),
|
|
||||||
)
|
|
||||||
process.stdout.write(yield* Effect.promise(() => writeExport(data, sessionID, requested !== undefined)))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
type Selection = { session: SessionInfo; sanitize: boolean }
|
|
||||||
|
|
||||||
function selectSession(sessions: SessionInfo[], initialSanitize: boolean) {
|
|
||||||
if (!process.stdin.isTTY) return Promise.reject(new Error("Session ID is required when stdin is not interactive"))
|
|
||||||
const input = process.stdin
|
|
||||||
const output = process.stderr
|
|
||||||
const wasRaw = input.isRaw
|
|
||||||
const wasPaused = input.isPaused()
|
|
||||||
const date = new Intl.DateTimeFormat(undefined, {
|
|
||||||
month: "short",
|
|
||||||
day: "numeric",
|
|
||||||
hour: "numeric",
|
|
||||||
minute: "2-digit",
|
|
||||||
})
|
|
||||||
const columns = output.columns ?? 100
|
|
||||||
const titleWidth = Math.max(8, Math.min(48, columns - 34))
|
|
||||||
let selected = 0
|
|
||||||
let offset = 0
|
|
||||||
let sanitize = initialSanitize
|
|
||||||
let height = 0
|
|
||||||
|
|
||||||
const render = () => {
|
|
||||||
const visible = sessions.slice(offset, offset + 10)
|
|
||||||
const lines = [" \x1b[36mExport session\x1b[0m", ""]
|
|
||||||
lines.push(
|
|
||||||
...visible.map((session) => {
|
|
||||||
const index = sessions.indexOf(session)
|
|
||||||
const title = (session.title ?? "Untitled session").slice(0, titleWidth).padEnd(titleWidth)
|
|
||||||
const updated = date.format(session.time.updated).slice(0, 18).padEnd(18)
|
|
||||||
const row = `${index === selected ? ">" : " "} ${title} ${updated} ${session.id.slice(-8)}`
|
|
||||||
return index === selected ? `\x1b[1m${row}\x1b[0m` : row
|
|
||||||
}),
|
|
||||||
"",
|
|
||||||
` [${sanitize ? "x" : " "}] sanitize sensitive data`,
|
|
||||||
"",
|
|
||||||
" navigate \x1b[2mup/down\x1b[0m sanitize \x1b[2mspace\x1b[0m export \x1b[2menter\x1b[0m cancel \x1b[2mesc\x1b[0m",
|
|
||||||
)
|
|
||||||
if (height > 0) output.write(`\x1b[${height}F\x1b[J`)
|
|
||||||
output.write(lines.join(EOL) + EOL)
|
|
||||||
height = lines.length
|
|
||||||
}
|
|
||||||
const clear = () => {
|
|
||||||
if (height > 0) output.write(`\x1b[${height}F\x1b[J`)
|
|
||||||
output.write("\x1b[?25h")
|
|
||||||
input.removeListener("keypress", onKeypress)
|
|
||||||
input.setRawMode(wasRaw ?? false)
|
|
||||||
if (wasPaused) input.pause()
|
|
||||||
}
|
|
||||||
const onKeypress = (value: string | undefined, key: Key) => {
|
|
||||||
if (key.name === "up") {
|
|
||||||
selected = (selected - 1 + sessions.length) % sessions.length
|
|
||||||
if (selected === sessions.length - 1) offset = Math.max(0, sessions.length - 10)
|
|
||||||
if (selected < offset) offset = selected
|
|
||||||
}
|
|
||||||
if (key.name === "down") {
|
|
||||||
selected = (selected + 1) % sessions.length
|
|
||||||
if (selected === 0) offset = 0
|
|
||||||
if (selected >= offset + 10) offset = selected - 9
|
|
||||||
}
|
|
||||||
if (key.name === "space" || value === " ") sanitize = !sanitize
|
|
||||||
if (key.name === "return") return finish(sessions[selected])
|
|
||||||
if (key.name === "escape" || (key.ctrl && key.name === "c")) return cancel()
|
|
||||||
render()
|
|
||||||
}
|
|
||||||
const finish = (session: SessionInfo) => {
|
|
||||||
clear()
|
|
||||||
resolveSelection?.({ session, sanitize })
|
|
||||||
}
|
|
||||||
const cancel = () => {
|
|
||||||
clear()
|
|
||||||
resolveSelection?.()
|
|
||||||
}
|
|
||||||
let resolveSelection: ((selection?: Selection) => void) | undefined
|
|
||||||
|
|
||||||
emitKeypressEvents(input)
|
|
||||||
input.setRawMode(true)
|
|
||||||
input.resume()
|
|
||||||
input.on("keypress", onKeypress)
|
|
||||||
output.write("\x1b[?25l")
|
|
||||||
render()
|
|
||||||
return new Promise<Selection | undefined>((resolve) => {
|
|
||||||
resolveSelection = resolve
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function writeExport(data: unknown, sessionID: string, stdout: boolean) {
|
|
||||||
const json = JSON.stringify(data, null, 2) + EOL
|
|
||||||
if (stdout) return json
|
|
||||||
const file = path.join(tmpdir(), `opencode-session-${sessionID}-${crypto.randomUUID().slice(0, 8)}.json`)
|
|
||||||
await Bun.write(file, json)
|
|
||||||
return file + EOL
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import { OpenCode } from "@opencode-ai/client"
|
|
||||||
import { Service } from "@opencode-ai/client/effect/service"
|
|
||||||
import { Session } from "@opencode-ai/schema/session"
|
|
||||||
import { SessionTransfer } from "@opencode-ai/schema/session-transfer"
|
|
||||||
import { Effect, Option, Schema } from "effect"
|
|
||||||
import { EOL } from "node:os"
|
|
||||||
import path from "node:path"
|
|
||||||
import { Commands } from "../commands"
|
|
||||||
import { Runtime } from "../../framework/runtime"
|
|
||||||
import { ServerConnection } from "../../services/server-connection"
|
|
||||||
|
|
||||||
export default Runtime.handler(
|
|
||||||
Commands.commands.import,
|
|
||||||
Effect.fn("cli.import")(function* (input) {
|
|
||||||
const text = yield* Effect.tryPromise({
|
|
||||||
try: () =>
|
|
||||||
input.file.startsWith("http://") || input.file.startsWith("https://")
|
|
||||||
? fetch(input.file).then((response) => {
|
|
||||||
if (!response.ok) throw new Error(`Failed to fetch session data: ${response.statusText}`)
|
|
||||||
return response.text()
|
|
||||||
})
|
|
||||||
: Bun.file(input.file).text(),
|
|
||||||
catch: (cause) => new Error(`Failed to read session data: ${cause instanceof Error ? cause.message : String(cause)}`),
|
|
||||||
})
|
|
||||||
const data = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(SessionTransfer.Data))(text)
|
|
||||||
const encoded = Schema.encodeSync(SessionTransfer.Data)(data)
|
|
||||||
const server = yield* ServerConnection.resolve({
|
|
||||||
server: Option.getOrUndefined(input.server),
|
|
||||||
standalone: input.standalone,
|
|
||||||
})
|
|
||||||
const client = OpenCode.make({
|
|
||||||
baseUrl: server.endpoint.url,
|
|
||||||
headers: Service.headers(server.endpoint),
|
|
||||||
})
|
|
||||||
const location = yield* Effect.promise(() =>
|
|
||||||
client.location.get({
|
|
||||||
location: { directory: path.resolve(Option.getOrElse(input.directory, () => process.cwd())) },
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
const response = yield* Effect.promise(() =>
|
|
||||||
fetch(new URL("/api/session/import", server.endpoint.url), {
|
|
||||||
method: "POST",
|
|
||||||
headers: { ...Service.headers(server.endpoint), "content-type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
...encoded,
|
|
||||||
location: { directory: location.directory, workspaceID: location.workspaceID },
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
if (response.status === 409) {
|
|
||||||
process.stderr.write(`Session already exists${EOL}`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!response.ok) yield* Effect.fail(new Error(`Failed to import session: ${response.statusText}`))
|
|
||||||
const imported = yield* Schema.decodeUnknownEffect(
|
|
||||||
Schema.fromJsonString(Schema.Struct({ data: Session.Info })),
|
|
||||||
)(yield* Effect.promise(() => response.text()))
|
|
||||||
process.stdout.write(`Imported session: ${imported.data.id}${EOL}`)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
@@ -37,8 +37,6 @@ const Handlers = Runtime.handlers(Commands, {
|
|||||||
list: () => import("./commands/handlers/plugin/list"),
|
list: () => import("./commands/handlers/plugin/list"),
|
||||||
},
|
},
|
||||||
models: () => import("./commands/handlers/models"),
|
models: () => import("./commands/handlers/models"),
|
||||||
export: () => import("./commands/handlers/export"),
|
|
||||||
import: () => import("./commands/handlers/import"),
|
|
||||||
mini: () => import("./commands/handlers/mini"),
|
mini: () => import("./commands/handlers/mini"),
|
||||||
run: () => import("./commands/handlers/run"),
|
run: () => import("./commands/handlers/run"),
|
||||||
pair: () => import("./commands/handlers/pair"),
|
pair: () => import("./commands/handlers/pair"),
|
||||||
|
|||||||
@@ -94,9 +94,11 @@ async function execute(input: RunCommandInput, prepared: Prepared, endpoint: End
|
|||||||
prepare: async (next) => {
|
prepare: async (next) => {
|
||||||
const selected =
|
const selected =
|
||||||
next.model ??
|
next.model ??
|
||||||
(await client.model
|
(options.variant
|
||||||
.default({ location: { directory: next.location.directory, workspace: next.location.workspaceID } })
|
? await client.model
|
||||||
.then((result) => result.data))
|
.default({ location: { directory: next.location.directory, workspace: next.location.workspaceID } })
|
||||||
|
.then((result) => result.data)
|
||||||
|
: undefined)
|
||||||
const model = selected
|
const model = selected
|
||||||
? {
|
? {
|
||||||
providerID: selected.providerID,
|
providerID: selected.providerID,
|
||||||
@@ -106,12 +108,7 @@ async function execute(input: RunCommandInput, prepared: Prepared, endpoint: End
|
|||||||
: undefined
|
: undefined
|
||||||
if ((options.variant ?? explicit?.variant) && !model)
|
if ((options.variant ?? explicit?.variant) && !model)
|
||||||
throw new RunTargetError("Cannot select a variant before selecting a model", next.session?.id)
|
throw new RunTargetError("Cannot select a variant before selecting a model", next.session?.id)
|
||||||
const agent =
|
return { model, agent: next.agent }
|
||||||
next.agent ??
|
|
||||||
(await client.agent
|
|
||||||
.list({ location: { directory: next.location.directory, workspace: next.location.workspaceID } })
|
|
||||||
.then((result) => result.data.find((item) => item.mode !== "subagent" && !item.hidden)?.id))
|
|
||||||
return { model, agent }
|
|
||||||
},
|
},
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
if (!(error instanceof RunTargetError)) throw error
|
if (!(error instanceof RunTargetError)) throw error
|
||||||
|
|||||||
@@ -56,16 +56,13 @@ export async function resolveSessionTarget(input: {
|
|||||||
agent: input.agent ?? selected?.agent,
|
agent: input.agent ?? selected?.agent,
|
||||||
signal: input.signal,
|
signal: input.signal,
|
||||||
})
|
})
|
||||||
if (!selected && (!prepared.agent || !prepared.model)) {
|
|
||||||
throw new SessionTargetMutationError(new Error("Creating a session requires an agent and model"))
|
|
||||||
}
|
|
||||||
const session =
|
const session =
|
||||||
selected ??
|
selected ??
|
||||||
(await input.client.session
|
(await input.client.session
|
||||||
.create(
|
.create(
|
||||||
{
|
{
|
||||||
agent: prepared.agent!,
|
agent: prepared.agent,
|
||||||
model: prepared.model!,
|
model: prepared.model,
|
||||||
location: { directory: location.directory, workspaceID: location.workspaceID },
|
location: { directory: location.directory, workspaceID: location.workspaceID },
|
||||||
},
|
},
|
||||||
...requestOptions(input.signal),
|
...requestOptions(input.signal),
|
||||||
|
|||||||
@@ -1,210 +0,0 @@
|
|||||||
import { expect, test } from "bun:test"
|
|
||||||
import fs from "node:fs/promises"
|
|
||||||
import os from "node:os"
|
|
||||||
import path from "node:path"
|
|
||||||
import { OPENCODE_VERSION } from "../src/version"
|
|
||||||
import { writeExport } from "../src/commands/handlers/export"
|
|
||||||
|
|
||||||
const info = {
|
|
||||||
id: "ses_export_test",
|
|
||||||
projectID: "global",
|
|
||||||
cost: 0,
|
|
||||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
||||||
time: { created: 1, updated: 2 },
|
|
||||||
title: "Exported session",
|
|
||||||
location: { directory: "/project" },
|
|
||||||
}
|
|
||||||
const transfer = {
|
|
||||||
info,
|
|
||||||
messages: [
|
|
||||||
{ id: "msg_first", type: "user", text: "First", time: { created: 1 } },
|
|
||||||
{ id: "msg_second", type: "user", text: "Second", time: { created: 2 } },
|
|
||||||
],
|
|
||||||
}
|
|
||||||
const sanitizedTransfer = {
|
|
||||||
info: {
|
|
||||||
...info,
|
|
||||||
title: "[redacted:session-title:ses_export_test]",
|
|
||||||
location: { directory: "/[redacted:session-directory:ses_export_test]" },
|
|
||||||
},
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
id: "msg_first",
|
|
||||||
type: "user",
|
|
||||||
text: "[redacted:text:msg_first]",
|
|
||||||
time: { created: 1 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "msg_second",
|
|
||||||
type: "user",
|
|
||||||
text: "[redacted:text:msg_second]",
|
|
||||||
time: { created: 2 },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
const health = () => Response.json({ healthy: true, version: OPENCODE_VERSION, pid: process.pid })
|
|
||||||
|
|
||||||
function run(args: string[], stdin?: string) {
|
|
||||||
const child = Bun.spawn([process.execPath, "run", "src/index.ts", ...args], {
|
|
||||||
cwd: path.join(import.meta.dir, ".."),
|
|
||||||
stdin: stdin === undefined ? undefined : new Blob([stdin]),
|
|
||||||
stdout: "pipe",
|
|
||||||
stderr: "pipe",
|
|
||||||
})
|
|
||||||
return Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited])
|
|
||||||
}
|
|
||||||
|
|
||||||
test("export is raw by default and supports explicit sanitization", async () => {
|
|
||||||
const sanitization: string[] = []
|
|
||||||
const server = Bun.serve({
|
|
||||||
port: 0,
|
|
||||||
fetch(request) {
|
|
||||||
const url = new URL(request.url)
|
|
||||||
if (url.pathname === "/api/health") return health()
|
|
||||||
if (url.pathname === `/api/session/${info.id}`) return Response.json({ data: info })
|
|
||||||
if (url.pathname === `/api/session/${info.id}/export`) {
|
|
||||||
sanitization.push(url.searchParams.get("sanitize") ?? "")
|
|
||||||
return Response.json({ data: url.searchParams.get("sanitize") === "true" ? sanitizedTransfer : transfer })
|
|
||||||
}
|
|
||||||
return new Response("Not found", { status: 404 })
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [stdout, , exitCode] = await run(["export", "-s", info.id, "--server", server.url.toString()])
|
|
||||||
const exported = JSON.parse(stdout)
|
|
||||||
|
|
||||||
expect(exitCode).toBe(0)
|
|
||||||
expect(exported).toEqual(transfer)
|
|
||||||
|
|
||||||
const [sanitized, , sanitizedExitCode] = await run([
|
|
||||||
"export",
|
|
||||||
"-s",
|
|
||||||
info.id,
|
|
||||||
"--sanitize",
|
|
||||||
"--server",
|
|
||||||
server.url.toString(),
|
|
||||||
])
|
|
||||||
expect(sanitizedExitCode).toBe(0)
|
|
||||||
expect(JSON.parse(sanitized)).toEqual(sanitizedTransfer)
|
|
||||||
expect(sanitization).toEqual(["false", "true"])
|
|
||||||
} finally {
|
|
||||||
await server.stop(true)
|
|
||||||
}
|
|
||||||
}, 15_000)
|
|
||||||
|
|
||||||
test("export reports an empty session list without a stack trace", async () => {
|
|
||||||
const server = Bun.serve({
|
|
||||||
port: 0,
|
|
||||||
fetch(request) {
|
|
||||||
const url = new URL(request.url)
|
|
||||||
if (url.pathname === "/api/health") return health()
|
|
||||||
if (url.pathname === "/api/location") {
|
|
||||||
return Response.json({
|
|
||||||
directory: "/project",
|
|
||||||
project: { id: "global", directory: "/project", canonical: "/project" },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (url.pathname === "/api/session") return Response.json({ data: [], cursor: {} })
|
|
||||||
return new Response("Not found", { status: 404 })
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [stdout, stderr, exitCode] = await run(["export", "--server", server.url.toString()])
|
|
||||||
|
|
||||||
expect(exitCode).toBe(0)
|
|
||||||
expect(stdout).toBe("")
|
|
||||||
expect(stderr).toBe(`No sessions found${os.EOL}`)
|
|
||||||
} finally {
|
|
||||||
await server.stop(true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("interactive export writes a temporary JSON file", async () => {
|
|
||||||
const output = await writeExport(transfer, info.id, false)
|
|
||||||
const file = output.trim()
|
|
||||||
|
|
||||||
try {
|
|
||||||
expect(path.dirname(file)).toBe(os.tmpdir())
|
|
||||||
expect(await Bun.file(file).json()).toEqual(transfer)
|
|
||||||
} finally {
|
|
||||||
await fs.rm(file, { force: true })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("import validates a file and sends it to the resolved location", async () => {
|
|
||||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-import-"))
|
|
||||||
const file = path.join(root, "session.json")
|
|
||||||
await fs.writeFile(file, JSON.stringify(transfer))
|
|
||||||
let imported: unknown
|
|
||||||
const server = Bun.serve({
|
|
||||||
port: 0,
|
|
||||||
async fetch(request) {
|
|
||||||
const url = new URL(request.url)
|
|
||||||
if (url.pathname === "/api/health") return health()
|
|
||||||
if (url.pathname === "/api/location") {
|
|
||||||
return Response.json({
|
|
||||||
directory: root,
|
|
||||||
project: { id: "global", directory: root, canonical: root },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (url.pathname === "/api/session/import") {
|
|
||||||
imported = await request.json()
|
|
||||||
return Response.json({ data: { ...info, location: { directory: root } } })
|
|
||||||
}
|
|
||||||
return new Response("Not found", { status: 404 })
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [stdout, , exitCode] = await run([
|
|
||||||
"import",
|
|
||||||
file,
|
|
||||||
"--directory",
|
|
||||||
root,
|
|
||||||
"--server",
|
|
||||||
server.url.toString(),
|
|
||||||
])
|
|
||||||
|
|
||||||
expect(exitCode).toBe(0)
|
|
||||||
expect(stdout).toBe(`Imported session: ${info.id}${os.EOL}`)
|
|
||||||
expect(imported).toEqual({ ...transfer, location: { directory: root } })
|
|
||||||
} finally {
|
|
||||||
await server.stop(true)
|
|
||||||
await fs.rm(root, { recursive: true, force: true })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("import reports an existing session without a stack trace", async () => {
|
|
||||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-import-conflict-"))
|
|
||||||
const file = path.join(root, "session.json")
|
|
||||||
await fs.writeFile(file, JSON.stringify(transfer))
|
|
||||||
const server = Bun.serve({
|
|
||||||
port: 0,
|
|
||||||
fetch(request) {
|
|
||||||
const url = new URL(request.url)
|
|
||||||
if (url.pathname === "/api/health") return health()
|
|
||||||
if (url.pathname === "/api/location") {
|
|
||||||
return Response.json({
|
|
||||||
directory: root,
|
|
||||||
project: { id: "global", directory: root, canonical: root },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (url.pathname === "/api/session/import") return new Response("Conflict", { status: 409 })
|
|
||||||
return new Response("Not found", { status: 404 })
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [stdout, stderr, exitCode] = await run(["import", file, "--server", server.url.toString()])
|
|
||||||
|
|
||||||
expect(exitCode).toBe(0)
|
|
||||||
expect(stdout).toBe("")
|
|
||||||
expect(stderr).toBe(`Session already exists${os.EOL}`)
|
|
||||||
} finally {
|
|
||||||
await server.stop(true)
|
|
||||||
await fs.rm(root, { recursive: true, force: true })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -61,11 +61,7 @@ describe("session target resolver", () => {
|
|||||||
spyOn(client.location, "get").mockResolvedValue(location("/server", "work_1"))
|
spyOn(client.location, "get").mockResolvedValue(location("/server", "work_1"))
|
||||||
const create = spyOn(client.session, "create").mockImplementation(async (input) => {
|
const create = spyOn(client.session, "create").mockImplementation(async (input) => {
|
||||||
order.push("create")
|
order.push("create")
|
||||||
expect(input).toMatchObject({
|
expect(input).toMatchObject({ agent: "prepared", location: { directory: "/server", workspaceID: "work_1" } })
|
||||||
agent: "prepared",
|
|
||||||
model: { providerID: "openai", id: "gpt-5" },
|
|
||||||
location: { directory: "/server", workspaceID: "work_1" },
|
|
||||||
})
|
|
||||||
return session("ses_fresh", "/server", "work_1")
|
return session("ses_fresh", "/server", "work_1")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -75,22 +71,20 @@ describe("session target resolver", () => {
|
|||||||
prepare: async (input) => {
|
prepare: async (input) => {
|
||||||
order.push("prepare")
|
order.push("prepare")
|
||||||
expect(input.location.workspaceID).toBe("work_1")
|
expect(input.location.workspaceID).toBe("work_1")
|
||||||
return { model: { providerID: "openai", id: "gpt-5" }, agent: "prepared" }
|
return { model: input.model, agent: "prepared" }
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
expect(create).toHaveBeenCalledTimes(1)
|
expect(create).toHaveBeenCalledTimes(1)
|
||||||
expect(order).toEqual(["prepare", "create"])
|
expect(order).toEqual(["prepare", "create"])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("requires an explicit agent and model for a fresh Session", async () => {
|
test("uses the agent resolved by the server for a fresh Session", async () => {
|
||||||
const client = OpenCode.make({ baseUrl: "https://opencode.test" })
|
const client = OpenCode.make({ baseUrl: "https://opencode.test" })
|
||||||
spyOn(client.location, "get").mockResolvedValue(location("/project"))
|
spyOn(client.location, "get").mockResolvedValue(location("/project"))
|
||||||
const create = spyOn(client.session, "create")
|
spyOn(client.session, "create").mockResolvedValue({ ...session("ses_fresh", "/project"), agent: "review" })
|
||||||
|
|
||||||
await expect(resolveSessionTarget({ client, prepare })).rejects.toThrow(
|
const target = await resolveSessionTarget({ client, prepare })
|
||||||
"Creating a session requires an agent and model",
|
expect(target.agent).toBe("review")
|
||||||
)
|
|
||||||
expect(create).not.toHaveBeenCalled()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("does not retry an ambiguous Session creation", async () => {
|
test("does not retry an ambiguous Session creation", async () => {
|
||||||
|
|||||||
@@ -120,61 +120,49 @@ export type SessionListOperation<E = never> = (input?: Endpoint5_0Input) => Effe
|
|||||||
export type Endpoint5_1Input = {
|
export type Endpoint5_1Input = {
|
||||||
readonly id?: Session.ID | undefined
|
readonly id?: Session.ID | undefined
|
||||||
readonly title?: string | undefined
|
readonly title?: string | undefined
|
||||||
readonly agent: Agent.ID
|
readonly agent?: Agent.ID | undefined
|
||||||
readonly model: Model.Ref
|
readonly model?: Model.Ref | undefined
|
||||||
readonly location?: Location.Ref | undefined
|
readonly location?: Location.Ref | undefined
|
||||||
}
|
}
|
||||||
export type Endpoint5_1Output = Session.Info
|
export type Endpoint5_1Output = Session.Info
|
||||||
export type SessionCreateOperation<E = never> = (input: Endpoint5_1Input) => Effect.Effect<Endpoint5_1Output, E>
|
export type SessionCreateOperation<E = never> = (input?: Endpoint5_1Input) => Effect.Effect<Endpoint5_1Output, E>
|
||||||
|
|
||||||
export type Endpoint5_2Input = {
|
export type Endpoint5_2Output = { readonly [x: Session.ID]: { readonly type: "running" } }
|
||||||
readonly info: Session.Info
|
export type SessionActiveOperation<E = never> = () => Effect.Effect<Endpoint5_2Output, E>
|
||||||
readonly messages: ReadonlyArray<SessionMessage.Info>
|
|
||||||
readonly location?: Location.Ref | undefined
|
|
||||||
}
|
|
||||||
export type Endpoint5_2Output = Session.Info
|
|
||||||
export type SessionImportOperation<E = never> = (input: Endpoint5_2Input) => Effect.Effect<Endpoint5_2Output, E>
|
|
||||||
|
|
||||||
export type Endpoint5_3Input = { readonly sessionID: Session.ID; readonly sanitize?: boolean | undefined }
|
export type Endpoint5_3Input = { readonly sessionID: Session.ID }
|
||||||
export type Endpoint5_3Output = { readonly info: Session.Info; readonly messages: ReadonlyArray<SessionMessage.Info> }
|
export type Endpoint5_3Output = Session.Info
|
||||||
export type SessionExportOperation<E = never> = (input: Endpoint5_3Input) => Effect.Effect<Endpoint5_3Output, E>
|
export type SessionGetOperation<E = never> = (input: Endpoint5_3Input) => Effect.Effect<Endpoint5_3Output, E>
|
||||||
|
|
||||||
export type Endpoint5_4Output = { readonly [x: Session.ID]: { readonly type: "running" } }
|
export type Endpoint5_4Input = { readonly sessionID: Session.ID }
|
||||||
export type SessionActiveOperation<E = never> = () => Effect.Effect<Endpoint5_4Output, E>
|
export type Endpoint5_4Output = void
|
||||||
|
export type SessionRemoveOperation<E = never> = (input: Endpoint5_4Input) => Effect.Effect<Endpoint5_4Output, E>
|
||||||
|
|
||||||
export type Endpoint5_5Input = { readonly sessionID: Session.ID }
|
export type Endpoint5_5Input = { readonly sessionID: Session.ID; readonly boundary: Session.ForkRequestBoundary }
|
||||||
export type Endpoint5_5Output = Session.Info
|
export type Endpoint5_5Output = Session.Info
|
||||||
export type SessionGetOperation<E = never> = (input: Endpoint5_5Input) => Effect.Effect<Endpoint5_5Output, E>
|
export type SessionForkOperation<E = never> = (input: Endpoint5_5Input) => Effect.Effect<Endpoint5_5Output, E>
|
||||||
|
|
||||||
export type Endpoint5_6Input = { readonly sessionID: Session.ID }
|
export type Endpoint5_6Input = { readonly sessionID: Session.ID; readonly agent: Agent.ID }
|
||||||
export type Endpoint5_6Output = void
|
export type Endpoint5_6Output = void
|
||||||
export type SessionRemoveOperation<E = never> = (input: Endpoint5_6Input) => Effect.Effect<Endpoint5_6Output, E>
|
export type SessionSwitchAgentOperation<E = never> = (input: Endpoint5_6Input) => Effect.Effect<Endpoint5_6Output, E>
|
||||||
|
|
||||||
export type Endpoint5_7Input = { readonly sessionID: Session.ID; readonly boundary: Session.ForkRequestBoundary }
|
export type Endpoint5_7Input = { readonly sessionID: Session.ID; readonly model: Model.Ref }
|
||||||
export type Endpoint5_7Output = Session.Info
|
export type Endpoint5_7Output = void
|
||||||
export type SessionForkOperation<E = never> = (input: Endpoint5_7Input) => Effect.Effect<Endpoint5_7Output, E>
|
export type SessionSwitchModelOperation<E = never> = (input: Endpoint5_7Input) => Effect.Effect<Endpoint5_7Output, E>
|
||||||
|
|
||||||
export type Endpoint5_8Input = { readonly sessionID: Session.ID; readonly agent: Agent.ID }
|
export type Endpoint5_8Input = { readonly sessionID: Session.ID; readonly title: string }
|
||||||
export type Endpoint5_8Output = void
|
export type Endpoint5_8Output = void
|
||||||
export type SessionSwitchAgentOperation<E = never> = (input: Endpoint5_8Input) => Effect.Effect<Endpoint5_8Output, E>
|
export type SessionRenameOperation<E = never> = (input: Endpoint5_8Input) => Effect.Effect<Endpoint5_8Output, E>
|
||||||
|
|
||||||
export type Endpoint5_9Input = { readonly sessionID: Session.ID; readonly model: Model.Ref }
|
export type Endpoint5_9Input = {
|
||||||
export type Endpoint5_9Output = void
|
|
||||||
export type SessionSwitchModelOperation<E = never> = (input: Endpoint5_9Input) => Effect.Effect<Endpoint5_9Output, E>
|
|
||||||
|
|
||||||
export type Endpoint5_10Input = { readonly sessionID: Session.ID; readonly title: string }
|
|
||||||
export type Endpoint5_10Output = void
|
|
||||||
export type SessionRenameOperation<E = never> = (input: Endpoint5_10Input) => Effect.Effect<Endpoint5_10Output, E>
|
|
||||||
|
|
||||||
export type Endpoint5_11Input = {
|
|
||||||
readonly sessionID: Session.ID
|
readonly sessionID: Session.ID
|
||||||
readonly directory: AbsolutePath
|
readonly directory: AbsolutePath
|
||||||
readonly workspaceID?: Workspace.ID | undefined
|
readonly workspaceID?: Workspace.ID | undefined
|
||||||
}
|
}
|
||||||
export type Endpoint5_11Output = void
|
export type Endpoint5_9Output = void
|
||||||
export type SessionMoveOperation<E = never> = (input: Endpoint5_11Input) => Effect.Effect<Endpoint5_11Output, E>
|
export type SessionMoveOperation<E = never> = (input: Endpoint5_9Input) => Effect.Effect<Endpoint5_9Output, E>
|
||||||
|
|
||||||
export type Endpoint5_12Input = {
|
export type Endpoint5_10Input = {
|
||||||
readonly sessionID: Session.ID
|
readonly sessionID: Session.ID
|
||||||
readonly id?: SessionMessage.ID | undefined
|
readonly id?: SessionMessage.ID | undefined
|
||||||
readonly text: string
|
readonly text: string
|
||||||
@@ -184,10 +172,10 @@ export type Endpoint5_12Input = {
|
|||||||
readonly delivery?: "steer" | "queue" | undefined
|
readonly delivery?: "steer" | "queue" | undefined
|
||||||
readonly resume?: boolean | undefined
|
readonly resume?: boolean | undefined
|
||||||
}
|
}
|
||||||
export type Endpoint5_12Output = SessionPending.User
|
export type Endpoint5_10Output = SessionPending.User
|
||||||
export type SessionPromptOperation<E = never> = (input: Endpoint5_12Input) => Effect.Effect<Endpoint5_12Output, E>
|
export type SessionPromptOperation<E = never> = (input: Endpoint5_10Input) => Effect.Effect<Endpoint5_10Output, E>
|
||||||
|
|
||||||
export type Endpoint5_13Input = {
|
export type Endpoint5_11Input = {
|
||||||
readonly sessionID: Session.ID
|
readonly sessionID: Session.ID
|
||||||
readonly id?: SessionMessage.ID | undefined
|
readonly id?: SessionMessage.ID | undefined
|
||||||
readonly command: string
|
readonly command: string
|
||||||
@@ -199,19 +187,19 @@ export type Endpoint5_13Input = {
|
|||||||
readonly delivery?: "steer" | "queue" | undefined
|
readonly delivery?: "steer" | "queue" | undefined
|
||||||
readonly resume?: boolean | undefined
|
readonly resume?: boolean | undefined
|
||||||
}
|
}
|
||||||
export type Endpoint5_13Output = SessionPending.User
|
export type Endpoint5_11Output = SessionPending.User
|
||||||
export type SessionCommandOperation<E = never> = (input: Endpoint5_13Input) => Effect.Effect<Endpoint5_13Output, E>
|
export type SessionCommandOperation<E = never> = (input: Endpoint5_11Input) => Effect.Effect<Endpoint5_11Output, E>
|
||||||
|
|
||||||
export type Endpoint5_14Input = {
|
export type Endpoint5_12Input = {
|
||||||
readonly sessionID: Session.ID
|
readonly sessionID: Session.ID
|
||||||
readonly id?: SessionMessage.ID | undefined
|
readonly id?: SessionMessage.ID | undefined
|
||||||
readonly skill: Skill.ID
|
readonly skill: Skill.ID
|
||||||
readonly resume?: boolean | undefined
|
readonly resume?: boolean | undefined
|
||||||
}
|
}
|
||||||
export type Endpoint5_14Output = void
|
export type Endpoint5_12Output = void
|
||||||
export type SessionSkillOperation<E = never> = (input: Endpoint5_14Input) => Effect.Effect<Endpoint5_14Output, E>
|
export type SessionSkillOperation<E = never> = (input: Endpoint5_12Input) => Effect.Effect<Endpoint5_12Output, E>
|
||||||
|
|
||||||
export type Endpoint5_15Input = {
|
export type Endpoint5_13Input = {
|
||||||
readonly sessionID: Session.ID
|
readonly sessionID: Session.ID
|
||||||
readonly id?: SessionMessage.ID | undefined
|
readonly id?: SessionMessage.ID | undefined
|
||||||
readonly text: string
|
readonly text: string
|
||||||
@@ -220,81 +208,81 @@ export type Endpoint5_15Input = {
|
|||||||
readonly delivery?: "steer" | "queue" | undefined
|
readonly delivery?: "steer" | "queue" | undefined
|
||||||
readonly resume?: boolean | undefined
|
readonly resume?: boolean | undefined
|
||||||
}
|
}
|
||||||
export type Endpoint5_15Output = SessionPending.Synthetic
|
export type Endpoint5_13Output = SessionPending.Synthetic
|
||||||
export type SessionSyntheticOperation<E = never> = (input: Endpoint5_15Input) => Effect.Effect<Endpoint5_15Output, E>
|
export type SessionSyntheticOperation<E = never> = (input: Endpoint5_13Input) => Effect.Effect<Endpoint5_13Output, E>
|
||||||
|
|
||||||
export type Endpoint5_16Input = {
|
export type Endpoint5_14Input = {
|
||||||
readonly sessionID: Session.ID
|
readonly sessionID: Session.ID
|
||||||
readonly id?: Event.ID | undefined
|
readonly id?: Event.ID | undefined
|
||||||
readonly command: string
|
readonly command: string
|
||||||
}
|
}
|
||||||
|
export type Endpoint5_14Output = void
|
||||||
|
export type SessionShellOperation<E = never> = (input: Endpoint5_14Input) => Effect.Effect<Endpoint5_14Output, E>
|
||||||
|
|
||||||
|
export type Endpoint5_15Input = { readonly sessionID: Session.ID; readonly id?: SessionMessage.ID | undefined }
|
||||||
|
export type Endpoint5_15Output = SessionPending.Compaction
|
||||||
|
export type SessionCompactOperation<E = never> = (input: Endpoint5_15Input) => Effect.Effect<Endpoint5_15Output, E>
|
||||||
|
|
||||||
|
export type Endpoint5_16Input = { readonly sessionID: Session.ID }
|
||||||
export type Endpoint5_16Output = void
|
export type Endpoint5_16Output = void
|
||||||
export type SessionShellOperation<E = never> = (input: Endpoint5_16Input) => Effect.Effect<Endpoint5_16Output, E>
|
export type SessionWaitOperation<E = never> = (input: Endpoint5_16Input) => Effect.Effect<Endpoint5_16Output, E>
|
||||||
|
|
||||||
export type Endpoint5_17Input = { readonly sessionID: Session.ID; readonly id?: SessionMessage.ID | undefined }
|
export type Endpoint5_17Input = {
|
||||||
export type Endpoint5_17Output = SessionPending.Compaction
|
|
||||||
export type SessionCompactOperation<E = never> = (input: Endpoint5_17Input) => Effect.Effect<Endpoint5_17Output, E>
|
|
||||||
|
|
||||||
export type Endpoint5_18Input = { readonly sessionID: Session.ID }
|
|
||||||
export type Endpoint5_18Output = void
|
|
||||||
export type SessionWaitOperation<E = never> = (input: Endpoint5_18Input) => Effect.Effect<Endpoint5_18Output, E>
|
|
||||||
|
|
||||||
export type Endpoint5_19Input = {
|
|
||||||
readonly sessionID: Session.ID
|
readonly sessionID: Session.ID
|
||||||
readonly messageID: SessionMessage.ID
|
readonly messageID: SessionMessage.ID
|
||||||
readonly files?: boolean | undefined
|
readonly files?: boolean | undefined
|
||||||
}
|
}
|
||||||
export type Endpoint5_19Output = Session.Revert
|
export type Endpoint5_17Output = Session.Revert
|
||||||
export type SessionRevertStageOperation<E = never> = (input: Endpoint5_19Input) => Effect.Effect<Endpoint5_19Output, E>
|
export type SessionRevertStageOperation<E = never> = (input: Endpoint5_17Input) => Effect.Effect<Endpoint5_17Output, E>
|
||||||
|
|
||||||
|
export type Endpoint5_18Input = { readonly sessionID: Session.ID }
|
||||||
|
export type Endpoint5_18Output = void
|
||||||
|
export type SessionRevertClearOperation<E = never> = (input: Endpoint5_18Input) => Effect.Effect<Endpoint5_18Output, E>
|
||||||
|
|
||||||
|
export type Endpoint5_19Input = { readonly sessionID: Session.ID }
|
||||||
|
export type Endpoint5_19Output = void
|
||||||
|
export type SessionRevertCommitOperation<E = never> = (input: Endpoint5_19Input) => Effect.Effect<Endpoint5_19Output, E>
|
||||||
|
|
||||||
export type Endpoint5_20Input = { readonly sessionID: Session.ID }
|
export type Endpoint5_20Input = { readonly sessionID: Session.ID }
|
||||||
export type Endpoint5_20Output = void
|
export type Endpoint5_20Output = ReadonlyArray<SessionMessage.Info>
|
||||||
export type SessionRevertClearOperation<E = never> = (input: Endpoint5_20Input) => Effect.Effect<Endpoint5_20Output, E>
|
export type SessionContextOperation<E = never> = (input: Endpoint5_20Input) => Effect.Effect<Endpoint5_20Output, E>
|
||||||
|
|
||||||
export type Endpoint5_21Input = { readonly sessionID: Session.ID }
|
export type Endpoint5_21Input = { readonly sessionID: Session.ID }
|
||||||
export type Endpoint5_21Output = void
|
export type Endpoint5_21Output = ReadonlyArray<SessionPending.Info>
|
||||||
export type SessionRevertCommitOperation<E = never> = (input: Endpoint5_21Input) => Effect.Effect<Endpoint5_21Output, E>
|
export type SessionPendingListOperation<E = never> = (input: Endpoint5_21Input) => Effect.Effect<Endpoint5_21Output, E>
|
||||||
|
|
||||||
export type Endpoint5_22Input = { readonly sessionID: Session.ID }
|
export type Endpoint5_22Input = { readonly sessionID: Session.ID }
|
||||||
export type Endpoint5_22Output = ReadonlyArray<SessionMessage.Info>
|
export type Endpoint5_22Output = ReadonlyArray<InstructionEntry.Info>
|
||||||
export type SessionContextOperation<E = never> = (input: Endpoint5_22Input) => Effect.Effect<Endpoint5_22Output, E>
|
|
||||||
|
|
||||||
export type Endpoint5_23Input = { readonly sessionID: Session.ID }
|
|
||||||
export type Endpoint5_23Output = ReadonlyArray<SessionPending.Info>
|
|
||||||
export type SessionPendingListOperation<E = never> = (input: Endpoint5_23Input) => Effect.Effect<Endpoint5_23Output, E>
|
|
||||||
|
|
||||||
export type Endpoint5_24Input = { readonly sessionID: Session.ID }
|
|
||||||
export type Endpoint5_24Output = ReadonlyArray<InstructionEntry.Info>
|
|
||||||
export type SessionInstructionsEntryListOperation<E = never> = (
|
export type SessionInstructionsEntryListOperation<E = never> = (
|
||||||
input: Endpoint5_24Input,
|
input: Endpoint5_22Input,
|
||||||
) => Effect.Effect<Endpoint5_24Output, E>
|
) => Effect.Effect<Endpoint5_22Output, E>
|
||||||
|
|
||||||
export type Endpoint5_25Input = {
|
export type Endpoint5_23Input = {
|
||||||
readonly sessionID: Session.ID
|
readonly sessionID: Session.ID
|
||||||
readonly key: InstructionEntry.Key
|
readonly key: InstructionEntry.Key
|
||||||
readonly value: Schema.Json
|
readonly value: Schema.Json
|
||||||
}
|
}
|
||||||
export type Endpoint5_25Output = void
|
export type Endpoint5_23Output = void
|
||||||
export type SessionInstructionsEntryPutOperation<E = never> = (
|
export type SessionInstructionsEntryPutOperation<E = never> = (
|
||||||
input: Endpoint5_25Input,
|
input: Endpoint5_23Input,
|
||||||
) => Effect.Effect<Endpoint5_25Output, E>
|
) => Effect.Effect<Endpoint5_23Output, E>
|
||||||
|
|
||||||
export type Endpoint5_26Input = { readonly sessionID: Session.ID; readonly key: InstructionEntry.Key }
|
export type Endpoint5_24Input = { readonly sessionID: Session.ID; readonly key: InstructionEntry.Key }
|
||||||
export type Endpoint5_26Output = void
|
export type Endpoint5_24Output = void
|
||||||
export type SessionInstructionsEntryRemoveOperation<E = never> = (
|
export type SessionInstructionsEntryRemoveOperation<E = never> = (
|
||||||
input: Endpoint5_26Input,
|
input: Endpoint5_24Input,
|
||||||
) => Effect.Effect<Endpoint5_26Output, E>
|
) => Effect.Effect<Endpoint5_24Output, E>
|
||||||
|
|
||||||
export type Endpoint5_27Input = { readonly sessionID: Session.ID; readonly prompt: string }
|
export type Endpoint5_25Input = { readonly sessionID: Session.ID; readonly prompt: string }
|
||||||
export type Endpoint5_27Output = { readonly text: string }
|
export type Endpoint5_25Output = { readonly text: string }
|
||||||
export type SessionGenerateOperation<E = never> = (input: Endpoint5_27Input) => Effect.Effect<Endpoint5_27Output, E>
|
export type SessionGenerateOperation<E = never> = (input: Endpoint5_25Input) => Effect.Effect<Endpoint5_25Output, E>
|
||||||
|
|
||||||
export type Endpoint5_28Input = {
|
export type Endpoint5_26Input = {
|
||||||
readonly sessionID: Session.ID
|
readonly sessionID: Session.ID
|
||||||
readonly after?: Event.Seq | undefined
|
readonly after?: Event.Seq | undefined
|
||||||
readonly follow?: boolean | undefined
|
readonly follow?: boolean | undefined
|
||||||
}
|
}
|
||||||
export type Endpoint5_28Output =
|
export type Endpoint5_26Output =
|
||||||
| (
|
| (
|
||||||
| {
|
| {
|
||||||
readonly id: Event.ID
|
readonly id: Event.ID
|
||||||
@@ -862,25 +850,23 @@ export type Endpoint5_28Output =
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
| EventLog.Synced
|
| EventLog.Synced
|
||||||
export type SessionLogOperation<E = never> = (input: Endpoint5_28Input) => Stream.Stream<Endpoint5_28Output, E>
|
export type SessionLogOperation<E = never> = (input: Endpoint5_26Input) => Stream.Stream<Endpoint5_26Output, E>
|
||||||
|
|
||||||
export type Endpoint5_29Input = { readonly sessionID: Session.ID }
|
export type Endpoint5_27Input = { readonly sessionID: Session.ID }
|
||||||
export type Endpoint5_29Output = void
|
export type Endpoint5_27Output = void
|
||||||
export type SessionInterruptOperation<E = never> = (input: Endpoint5_29Input) => Effect.Effect<Endpoint5_29Output, E>
|
export type SessionInterruptOperation<E = never> = (input: Endpoint5_27Input) => Effect.Effect<Endpoint5_27Output, E>
|
||||||
|
|
||||||
export type Endpoint5_30Input = { readonly sessionID: Session.ID }
|
export type Endpoint5_28Input = { readonly sessionID: Session.ID }
|
||||||
export type Endpoint5_30Output = void
|
export type Endpoint5_28Output = void
|
||||||
export type SessionBackgroundOperation<E = never> = (input: Endpoint5_30Input) => Effect.Effect<Endpoint5_30Output, E>
|
export type SessionBackgroundOperation<E = never> = (input: Endpoint5_28Input) => Effect.Effect<Endpoint5_28Output, E>
|
||||||
|
|
||||||
export type Endpoint5_31Input = { readonly sessionID: Session.ID; readonly messageID: SessionMessage.ID }
|
export type Endpoint5_29Input = { readonly sessionID: Session.ID; readonly messageID: SessionMessage.ID }
|
||||||
export type Endpoint5_31Output = SessionMessage.Info
|
export type Endpoint5_29Output = SessionMessage.Info
|
||||||
export type SessionMessageOperation<E = never> = (input: Endpoint5_31Input) => Effect.Effect<Endpoint5_31Output, E>
|
export type SessionMessageOperation<E = never> = (input: Endpoint5_29Input) => Effect.Effect<Endpoint5_29Output, E>
|
||||||
|
|
||||||
export interface SessionApi<E = never> {
|
export interface SessionApi<E = never> {
|
||||||
readonly list: SessionListOperation<E>
|
readonly list: SessionListOperation<E>
|
||||||
readonly create: SessionCreateOperation<E>
|
readonly create: SessionCreateOperation<E>
|
||||||
readonly import: SessionImportOperation<E>
|
|
||||||
readonly export: SessionExportOperation<E>
|
|
||||||
readonly active: SessionActiveOperation<E>
|
readonly active: SessionActiveOperation<E>
|
||||||
readonly get: SessionGetOperation<E>
|
readonly get: SessionGetOperation<E>
|
||||||
readonly remove: SessionRemoveOperation<E>
|
readonly remove: SessionRemoveOperation<E>
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ import type {
|
|||||||
Endpoint5_0Output,
|
Endpoint5_0Output,
|
||||||
Endpoint5_1Input,
|
Endpoint5_1Input,
|
||||||
Endpoint5_1Output,
|
Endpoint5_1Output,
|
||||||
Endpoint5_2Input,
|
|
||||||
Endpoint5_2Output,
|
Endpoint5_2Output,
|
||||||
Endpoint5_3Input,
|
Endpoint5_3Input,
|
||||||
Endpoint5_3Output,
|
Endpoint5_3Output,
|
||||||
|
Endpoint5_4Input,
|
||||||
Endpoint5_4Output,
|
Endpoint5_4Output,
|
||||||
Endpoint5_5Input,
|
Endpoint5_5Input,
|
||||||
Endpoint5_5Output,
|
Endpoint5_5Output,
|
||||||
@@ -76,10 +76,6 @@ import type {
|
|||||||
Endpoint5_28Output,
|
Endpoint5_28Output,
|
||||||
Endpoint5_29Input,
|
Endpoint5_29Input,
|
||||||
Endpoint5_29Output,
|
Endpoint5_29Output,
|
||||||
Endpoint5_30Input,
|
|
||||||
Endpoint5_30Output,
|
|
||||||
Endpoint5_31Input,
|
|
||||||
Endpoint5_31Output,
|
|
||||||
Endpoint6_0Input,
|
Endpoint6_0Input,
|
||||||
Endpoint6_0Output,
|
Endpoint6_0Output,
|
||||||
Endpoint7_0Input,
|
Endpoint7_0Input,
|
||||||
@@ -305,15 +301,15 @@ const Endpoint5_0 = (raw: RawClient["server.session"]) => (input?: Endpoint5_0In
|
|||||||
}).pipe(Effect.mapError(mapClientError)),
|
}).pipe(Effect.mapError(mapClientError)),
|
||||||
)
|
)
|
||||||
|
|
||||||
const Endpoint5_1 = (raw: RawClient["server.session"]) => (input: Endpoint5_1Input) =>
|
const Endpoint5_1 = (raw: RawClient["server.session"]) => (input?: Endpoint5_1Input) =>
|
||||||
preserveEffect<Endpoint5_1Output>()(
|
preserveEffect<Endpoint5_1Output>()(
|
||||||
raw["session.create"]({
|
raw["session.create"]({
|
||||||
payload: {
|
payload: {
|
||||||
id: input["id"],
|
id: input?.["id"],
|
||||||
title: input["title"],
|
title: input?.["title"],
|
||||||
agent: input["agent"],
|
agent: input?.["agent"],
|
||||||
model: input["model"],
|
model: input?.["model"],
|
||||||
location: input["location"],
|
location: input?.["location"],
|
||||||
},
|
},
|
||||||
}).pipe(
|
}).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
@@ -321,11 +317,9 @@ const Endpoint5_1 = (raw: RawClient["server.session"]) => (input: Endpoint5_1Inp
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const Endpoint5_2 = (raw: RawClient["server.session"]) => (input: Endpoint5_2Input) =>
|
const Endpoint5_2 = (raw: RawClient["server.session"]) => () =>
|
||||||
preserveEffect<Endpoint5_2Output>()(
|
preserveEffect<Endpoint5_2Output>()(
|
||||||
raw["session.import"]({
|
raw["session.active"]({}).pipe(
|
||||||
payload: { info: input["info"], messages: input["messages"], location: input["location"] },
|
|
||||||
}).pipe(
|
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
),
|
),
|
||||||
@@ -333,23 +327,20 @@ const Endpoint5_2 = (raw: RawClient["server.session"]) => (input: Endpoint5_2Inp
|
|||||||
|
|
||||||
const Endpoint5_3 = (raw: RawClient["server.session"]) => (input: Endpoint5_3Input) =>
|
const Endpoint5_3 = (raw: RawClient["server.session"]) => (input: Endpoint5_3Input) =>
|
||||||
preserveEffect<Endpoint5_3Output>()(
|
preserveEffect<Endpoint5_3Output>()(
|
||||||
raw["session.export"]({ params: { sessionID: input["sessionID"] }, query: { sanitize: input["sanitize"] } }).pipe(
|
raw["session.get"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const Endpoint5_4 = (raw: RawClient["server.session"]) => () =>
|
const Endpoint5_4 = (raw: RawClient["server.session"]) => (input: Endpoint5_4Input) =>
|
||||||
preserveEffect<Endpoint5_4Output>()(
|
preserveEffect<Endpoint5_4Output>()(
|
||||||
raw["session.active"]({}).pipe(
|
raw["session.remove"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)),
|
||||||
Effect.mapError(mapClientError),
|
|
||||||
Effect.map((value) => value.data),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const Endpoint5_5 = (raw: RawClient["server.session"]) => (input: Endpoint5_5Input) =>
|
const Endpoint5_5 = (raw: RawClient["server.session"]) => (input: Endpoint5_5Input) =>
|
||||||
preserveEffect<Endpoint5_5Output>()(
|
preserveEffect<Endpoint5_5Output>()(
|
||||||
raw["session.get"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
raw["session.fork"]({ params: { sessionID: input["sessionID"] }, payload: { boundary: input["boundary"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
),
|
),
|
||||||
@@ -357,48 +348,35 @@ const Endpoint5_5 = (raw: RawClient["server.session"]) => (input: Endpoint5_5Inp
|
|||||||
|
|
||||||
const Endpoint5_6 = (raw: RawClient["server.session"]) => (input: Endpoint5_6Input) =>
|
const Endpoint5_6 = (raw: RawClient["server.session"]) => (input: Endpoint5_6Input) =>
|
||||||
preserveEffect<Endpoint5_6Output>()(
|
preserveEffect<Endpoint5_6Output>()(
|
||||||
raw["session.remove"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)),
|
raw["session.switchAgent"]({ params: { sessionID: input["sessionID"] }, payload: { agent: input["agent"] } }).pipe(
|
||||||
|
Effect.mapError(mapClientError),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const Endpoint5_7 = (raw: RawClient["server.session"]) => (input: Endpoint5_7Input) =>
|
const Endpoint5_7 = (raw: RawClient["server.session"]) => (input: Endpoint5_7Input) =>
|
||||||
preserveEffect<Endpoint5_7Output>()(
|
preserveEffect<Endpoint5_7Output>()(
|
||||||
raw["session.fork"]({ params: { sessionID: input["sessionID"] }, payload: { boundary: input["boundary"] } }).pipe(
|
raw["session.switchModel"]({ params: { sessionID: input["sessionID"] }, payload: { model: input["model"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
Effect.map((value) => value.data),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const Endpoint5_8 = (raw: RawClient["server.session"]) => (input: Endpoint5_8Input) =>
|
const Endpoint5_8 = (raw: RawClient["server.session"]) => (input: Endpoint5_8Input) =>
|
||||||
preserveEffect<Endpoint5_8Output>()(
|
preserveEffect<Endpoint5_8Output>()(
|
||||||
raw["session.switchAgent"]({ params: { sessionID: input["sessionID"] }, payload: { agent: input["agent"] } }).pipe(
|
raw["session.rename"]({ params: { sessionID: input["sessionID"] }, payload: { title: input["title"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const Endpoint5_9 = (raw: RawClient["server.session"]) => (input: Endpoint5_9Input) =>
|
const Endpoint5_9 = (raw: RawClient["server.session"]) => (input: Endpoint5_9Input) =>
|
||||||
preserveEffect<Endpoint5_9Output>()(
|
preserveEffect<Endpoint5_9Output>()(
|
||||||
raw["session.switchModel"]({ params: { sessionID: input["sessionID"] }, payload: { model: input["model"] } }).pipe(
|
|
||||||
Effect.mapError(mapClientError),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const Endpoint5_10 = (raw: RawClient["server.session"]) => (input: Endpoint5_10Input) =>
|
|
||||||
preserveEffect<Endpoint5_10Output>()(
|
|
||||||
raw["session.rename"]({ params: { sessionID: input["sessionID"] }, payload: { title: input["title"] } }).pipe(
|
|
||||||
Effect.mapError(mapClientError),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const Endpoint5_11 = (raw: RawClient["server.session"]) => (input: Endpoint5_11Input) =>
|
|
||||||
preserveEffect<Endpoint5_11Output>()(
|
|
||||||
raw["session.move"]({
|
raw["session.move"]({
|
||||||
params: { sessionID: input["sessionID"] },
|
params: { sessionID: input["sessionID"] },
|
||||||
payload: { directory: input["directory"], workspaceID: input["workspaceID"] },
|
payload: { directory: input["directory"], workspaceID: input["workspaceID"] },
|
||||||
}).pipe(Effect.mapError(mapClientError)),
|
}).pipe(Effect.mapError(mapClientError)),
|
||||||
)
|
)
|
||||||
|
|
||||||
const Endpoint5_12 = (raw: RawClient["server.session"]) => (input: Endpoint5_12Input) =>
|
const Endpoint5_10 = (raw: RawClient["server.session"]) => (input: Endpoint5_10Input) =>
|
||||||
preserveEffect<Endpoint5_12Output>()(
|
preserveEffect<Endpoint5_10Output>()(
|
||||||
raw["session.prompt"]({
|
raw["session.prompt"]({
|
||||||
params: { sessionID: input["sessionID"] },
|
params: { sessionID: input["sessionID"] },
|
||||||
payload: {
|
payload: {
|
||||||
@@ -416,8 +394,8 @@ const Endpoint5_12 = (raw: RawClient["server.session"]) => (input: Endpoint5_12I
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const Endpoint5_13 = (raw: RawClient["server.session"]) => (input: Endpoint5_13Input) =>
|
const Endpoint5_11 = (raw: RawClient["server.session"]) => (input: Endpoint5_11Input) =>
|
||||||
preserveEffect<Endpoint5_13Output>()(
|
preserveEffect<Endpoint5_11Output>()(
|
||||||
raw["session.command"]({
|
raw["session.command"]({
|
||||||
params: { sessionID: input["sessionID"] },
|
params: { sessionID: input["sessionID"] },
|
||||||
payload: {
|
payload: {
|
||||||
@@ -437,16 +415,16 @@ const Endpoint5_13 = (raw: RawClient["server.session"]) => (input: Endpoint5_13I
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const Endpoint5_14 = (raw: RawClient["server.session"]) => (input: Endpoint5_14Input) =>
|
const Endpoint5_12 = (raw: RawClient["server.session"]) => (input: Endpoint5_12Input) =>
|
||||||
preserveEffect<Endpoint5_14Output>()(
|
preserveEffect<Endpoint5_12Output>()(
|
||||||
raw["session.skill"]({
|
raw["session.skill"]({
|
||||||
params: { sessionID: input["sessionID"] },
|
params: { sessionID: input["sessionID"] },
|
||||||
payload: { id: input["id"], skill: input["skill"], resume: input["resume"] },
|
payload: { id: input["id"], skill: input["skill"], resume: input["resume"] },
|
||||||
}).pipe(Effect.mapError(mapClientError)),
|
}).pipe(Effect.mapError(mapClientError)),
|
||||||
)
|
)
|
||||||
|
|
||||||
const Endpoint5_15 = (raw: RawClient["server.session"]) => (input: Endpoint5_15Input) =>
|
const Endpoint5_13 = (raw: RawClient["server.session"]) => (input: Endpoint5_13Input) =>
|
||||||
preserveEffect<Endpoint5_15Output>()(
|
preserveEffect<Endpoint5_13Output>()(
|
||||||
raw["session.synthetic"]({
|
raw["session.synthetic"]({
|
||||||
params: { sessionID: input["sessionID"] },
|
params: { sessionID: input["sessionID"] },
|
||||||
payload: {
|
payload: {
|
||||||
@@ -463,29 +441,29 @@ const Endpoint5_15 = (raw: RawClient["server.session"]) => (input: Endpoint5_15I
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const Endpoint5_16 = (raw: RawClient["server.session"]) => (input: Endpoint5_16Input) =>
|
const Endpoint5_14 = (raw: RawClient["server.session"]) => (input: Endpoint5_14Input) =>
|
||||||
preserveEffect<Endpoint5_16Output>()(
|
preserveEffect<Endpoint5_14Output>()(
|
||||||
raw["session.shell"]({
|
raw["session.shell"]({
|
||||||
params: { sessionID: input["sessionID"] },
|
params: { sessionID: input["sessionID"] },
|
||||||
payload: { id: input["id"], command: input["command"] },
|
payload: { id: input["id"], command: input["command"] },
|
||||||
}).pipe(Effect.mapError(mapClientError)),
|
}).pipe(Effect.mapError(mapClientError)),
|
||||||
)
|
)
|
||||||
|
|
||||||
const Endpoint5_17 = (raw: RawClient["server.session"]) => (input: Endpoint5_17Input) =>
|
const Endpoint5_15 = (raw: RawClient["server.session"]) => (input: Endpoint5_15Input) =>
|
||||||
preserveEffect<Endpoint5_17Output>()(
|
preserveEffect<Endpoint5_15Output>()(
|
||||||
raw["session.compact"]({ params: { sessionID: input["sessionID"] }, payload: { id: input["id"] } }).pipe(
|
raw["session.compact"]({ params: { sessionID: input["sessionID"] }, payload: { id: input["id"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const Endpoint5_18 = (raw: RawClient["server.session"]) => (input: Endpoint5_18Input) =>
|
const Endpoint5_16 = (raw: RawClient["server.session"]) => (input: Endpoint5_16Input) =>
|
||||||
preserveEffect<Endpoint5_18Output>()(
|
preserveEffect<Endpoint5_16Output>()(
|
||||||
raw["session.wait"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)),
|
raw["session.wait"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)),
|
||||||
)
|
)
|
||||||
|
|
||||||
const Endpoint5_19 = (raw: RawClient["server.session"]) => (input: Endpoint5_19Input) =>
|
const Endpoint5_17 = (raw: RawClient["server.session"]) => (input: Endpoint5_17Input) =>
|
||||||
preserveEffect<Endpoint5_19Output>()(
|
preserveEffect<Endpoint5_17Output>()(
|
||||||
raw["session.revert.stage"]({
|
raw["session.revert.stage"]({
|
||||||
params: { sessionID: input["sessionID"] },
|
params: { sessionID: input["sessionID"] },
|
||||||
payload: { messageID: input["messageID"], files: input["files"] },
|
payload: { messageID: input["messageID"], files: input["files"] },
|
||||||
@@ -495,19 +473,35 @@ const Endpoint5_19 = (raw: RawClient["server.session"]) => (input: Endpoint5_19I
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const Endpoint5_18 = (raw: RawClient["server.session"]) => (input: Endpoint5_18Input) =>
|
||||||
|
preserveEffect<Endpoint5_18Output>()(
|
||||||
|
raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)),
|
||||||
|
)
|
||||||
|
|
||||||
|
const Endpoint5_19 = (raw: RawClient["server.session"]) => (input: Endpoint5_19Input) =>
|
||||||
|
preserveEffect<Endpoint5_19Output>()(
|
||||||
|
raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)),
|
||||||
|
)
|
||||||
|
|
||||||
const Endpoint5_20 = (raw: RawClient["server.session"]) => (input: Endpoint5_20Input) =>
|
const Endpoint5_20 = (raw: RawClient["server.session"]) => (input: Endpoint5_20Input) =>
|
||||||
preserveEffect<Endpoint5_20Output>()(
|
preserveEffect<Endpoint5_20Output>()(
|
||||||
raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)),
|
raw["session.context"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
||||||
|
Effect.mapError(mapClientError),
|
||||||
|
Effect.map((value) => value.data),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const Endpoint5_21 = (raw: RawClient["server.session"]) => (input: Endpoint5_21Input) =>
|
const Endpoint5_21 = (raw: RawClient["server.session"]) => (input: Endpoint5_21Input) =>
|
||||||
preserveEffect<Endpoint5_21Output>()(
|
preserveEffect<Endpoint5_21Output>()(
|
||||||
raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)),
|
raw["session.pending.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
||||||
|
Effect.mapError(mapClientError),
|
||||||
|
Effect.map((value) => value.data),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const Endpoint5_22 = (raw: RawClient["server.session"]) => (input: Endpoint5_22Input) =>
|
const Endpoint5_22 = (raw: RawClient["server.session"]) => (input: Endpoint5_22Input) =>
|
||||||
preserveEffect<Endpoint5_22Output>()(
|
preserveEffect<Endpoint5_22Output>()(
|
||||||
raw["session.context"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
raw["session.instructions.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
),
|
),
|
||||||
@@ -515,45 +509,29 @@ const Endpoint5_22 = (raw: RawClient["server.session"]) => (input: Endpoint5_22I
|
|||||||
|
|
||||||
const Endpoint5_23 = (raw: RawClient["server.session"]) => (input: Endpoint5_23Input) =>
|
const Endpoint5_23 = (raw: RawClient["server.session"]) => (input: Endpoint5_23Input) =>
|
||||||
preserveEffect<Endpoint5_23Output>()(
|
preserveEffect<Endpoint5_23Output>()(
|
||||||
raw["session.pending.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
|
||||||
Effect.mapError(mapClientError),
|
|
||||||
Effect.map((value) => value.data),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const Endpoint5_24 = (raw: RawClient["server.session"]) => (input: Endpoint5_24Input) =>
|
|
||||||
preserveEffect<Endpoint5_24Output>()(
|
|
||||||
raw["session.instructions.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
|
||||||
Effect.mapError(mapClientError),
|
|
||||||
Effect.map((value) => value.data),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const Endpoint5_25 = (raw: RawClient["server.session"]) => (input: Endpoint5_25Input) =>
|
|
||||||
preserveEffect<Endpoint5_25Output>()(
|
|
||||||
raw["session.instructions.entry.put"]({
|
raw["session.instructions.entry.put"]({
|
||||||
params: { sessionID: input["sessionID"], key: input["key"] },
|
params: { sessionID: input["sessionID"], key: input["key"] },
|
||||||
payload: { value: input["value"] },
|
payload: { value: input["value"] },
|
||||||
}).pipe(Effect.mapError(mapClientError)),
|
}).pipe(Effect.mapError(mapClientError)),
|
||||||
)
|
)
|
||||||
|
|
||||||
const Endpoint5_26 = (raw: RawClient["server.session"]) => (input: Endpoint5_26Input) =>
|
const Endpoint5_24 = (raw: RawClient["server.session"]) => (input: Endpoint5_24Input) =>
|
||||||
preserveEffect<Endpoint5_26Output>()(
|
preserveEffect<Endpoint5_24Output>()(
|
||||||
raw["session.instructions.entry.remove"]({ params: { sessionID: input["sessionID"], key: input["key"] } }).pipe(
|
raw["session.instructions.entry.remove"]({ params: { sessionID: input["sessionID"], key: input["key"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const Endpoint5_27 = (raw: RawClient["server.session"]) => (input: Endpoint5_27Input) =>
|
const Endpoint5_25 = (raw: RawClient["server.session"]) => (input: Endpoint5_25Input) =>
|
||||||
preserveEffect<Endpoint5_27Output>()(
|
preserveEffect<Endpoint5_25Output>()(
|
||||||
raw["session.generate"]({ params: { sessionID: input["sessionID"] }, payload: { prompt: input["prompt"] } }).pipe(
|
raw["session.generate"]({ params: { sessionID: input["sessionID"] }, payload: { prompt: input["prompt"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const Endpoint5_28 = (raw: RawClient["server.session"]) => (input: Endpoint5_28Input) =>
|
const Endpoint5_26 = (raw: RawClient["server.session"]) => (input: Endpoint5_26Input) =>
|
||||||
preserveStream<Endpoint5_28Output>()(
|
preserveStream<Endpoint5_26Output>()(
|
||||||
Stream.unwrap(
|
Stream.unwrap(
|
||||||
raw["session.log"]({
|
raw["session.log"]({
|
||||||
params: { sessionID: input["sessionID"] },
|
params: { sessionID: input["sessionID"] },
|
||||||
@@ -565,18 +543,18 @@ const Endpoint5_28 = (raw: RawClient["server.session"]) => (input: Endpoint5_28I
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const Endpoint5_29 = (raw: RawClient["server.session"]) => (input: Endpoint5_29Input) =>
|
const Endpoint5_27 = (raw: RawClient["server.session"]) => (input: Endpoint5_27Input) =>
|
||||||
preserveEffect<Endpoint5_29Output>()(
|
preserveEffect<Endpoint5_27Output>()(
|
||||||
raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)),
|
raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)),
|
||||||
)
|
)
|
||||||
|
|
||||||
const Endpoint5_30 = (raw: RawClient["server.session"]) => (input: Endpoint5_30Input) =>
|
const Endpoint5_28 = (raw: RawClient["server.session"]) => (input: Endpoint5_28Input) =>
|
||||||
preserveEffect<Endpoint5_30Output>()(
|
preserveEffect<Endpoint5_28Output>()(
|
||||||
raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)),
|
raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)),
|
||||||
)
|
)
|
||||||
|
|
||||||
const Endpoint5_31 = (raw: RawClient["server.session"]) => (input: Endpoint5_31Input) =>
|
const Endpoint5_29 = (raw: RawClient["server.session"]) => (input: Endpoint5_29Input) =>
|
||||||
preserveEffect<Endpoint5_31Output>()(
|
preserveEffect<Endpoint5_29Output>()(
|
||||||
raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe(
|
raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
@@ -586,32 +564,30 @@ const Endpoint5_31 = (raw: RawClient["server.session"]) => (input: Endpoint5_31I
|
|||||||
const adaptGroup5 = (raw: RawClient["server.session"]) => ({
|
const adaptGroup5 = (raw: RawClient["server.session"]) => ({
|
||||||
list: Endpoint5_0(raw),
|
list: Endpoint5_0(raw),
|
||||||
create: Endpoint5_1(raw),
|
create: Endpoint5_1(raw),
|
||||||
import: Endpoint5_2(raw),
|
active: Endpoint5_2(raw),
|
||||||
export: Endpoint5_3(raw),
|
get: Endpoint5_3(raw),
|
||||||
active: Endpoint5_4(raw),
|
remove: Endpoint5_4(raw),
|
||||||
get: Endpoint5_5(raw),
|
fork: Endpoint5_5(raw),
|
||||||
remove: Endpoint5_6(raw),
|
switchAgent: Endpoint5_6(raw),
|
||||||
fork: Endpoint5_7(raw),
|
switchModel: Endpoint5_7(raw),
|
||||||
switchAgent: Endpoint5_8(raw),
|
rename: Endpoint5_8(raw),
|
||||||
switchModel: Endpoint5_9(raw),
|
move: Endpoint5_9(raw),
|
||||||
rename: Endpoint5_10(raw),
|
prompt: Endpoint5_10(raw),
|
||||||
move: Endpoint5_11(raw),
|
command: Endpoint5_11(raw),
|
||||||
prompt: Endpoint5_12(raw),
|
skill: Endpoint5_12(raw),
|
||||||
command: Endpoint5_13(raw),
|
synthetic: Endpoint5_13(raw),
|
||||||
skill: Endpoint5_14(raw),
|
shell: Endpoint5_14(raw),
|
||||||
synthetic: Endpoint5_15(raw),
|
compact: Endpoint5_15(raw),
|
||||||
shell: Endpoint5_16(raw),
|
wait: Endpoint5_16(raw),
|
||||||
compact: Endpoint5_17(raw),
|
revert: { stage: Endpoint5_17(raw), clear: Endpoint5_18(raw), commit: Endpoint5_19(raw) },
|
||||||
wait: Endpoint5_18(raw),
|
context: Endpoint5_20(raw),
|
||||||
revert: { stage: Endpoint5_19(raw), clear: Endpoint5_20(raw), commit: Endpoint5_21(raw) },
|
pending: { list: Endpoint5_21(raw) },
|
||||||
context: Endpoint5_22(raw),
|
instructions: { entry: { list: Endpoint5_22(raw), put: Endpoint5_23(raw), remove: Endpoint5_24(raw) } },
|
||||||
pending: { list: Endpoint5_23(raw) },
|
generate: Endpoint5_25(raw),
|
||||||
instructions: { entry: { list: Endpoint5_24(raw), put: Endpoint5_25(raw), remove: Endpoint5_26(raw) } },
|
log: Endpoint5_26(raw),
|
||||||
generate: Endpoint5_27(raw),
|
interrupt: Endpoint5_27(raw),
|
||||||
log: Endpoint5_28(raw),
|
background: Endpoint5_28(raw),
|
||||||
interrupt: Endpoint5_29(raw),
|
message: Endpoint5_29(raw),
|
||||||
background: Endpoint5_30(raw),
|
|
||||||
message: Endpoint5_31(raw),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const Endpoint6_0 = (raw: RawClient["server.message"]) => (input: Endpoint6_0Input) =>
|
const Endpoint6_0 = (raw: RawClient["server.message"]) => (input: Endpoint6_0Input) =>
|
||||||
|
|||||||
@@ -15,10 +15,6 @@ import type {
|
|||||||
SessionListOutput,
|
SessionListOutput,
|
||||||
SessionCreateInput,
|
SessionCreateInput,
|
||||||
SessionCreateOutput,
|
SessionCreateOutput,
|
||||||
SessionImportInput,
|
|
||||||
SessionImportOutput,
|
|
||||||
SessionExportInput,
|
|
||||||
SessionExportOutput,
|
|
||||||
SessionActiveOutput,
|
SessionActiveOutput,
|
||||||
SessionGetInput,
|
SessionGetInput,
|
||||||
SessionGetOutput,
|
SessionGetOutput,
|
||||||
@@ -464,17 +460,17 @@ export function make(options: ClientOptions) {
|
|||||||
},
|
},
|
||||||
requestOptions,
|
requestOptions,
|
||||||
),
|
),
|
||||||
create: (input: SessionCreateInput, requestOptions?: RequestOptions) =>
|
create: (input?: SessionCreateInput, requestOptions?: RequestOptions) =>
|
||||||
request<{ readonly data: SessionCreateOutput }>(
|
request<{ readonly data: SessionCreateOutput }>(
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
path: `/api/session`,
|
path: `/api/session`,
|
||||||
body: {
|
body: {
|
||||||
id: input["id"],
|
id: input?.["id"],
|
||||||
title: input["title"],
|
title: input?.["title"],
|
||||||
agent: input["agent"],
|
agent: input?.["agent"],
|
||||||
model: input["model"],
|
model: input?.["model"],
|
||||||
location: input["location"],
|
location: input?.["location"],
|
||||||
},
|
},
|
||||||
successStatus: 200,
|
successStatus: 200,
|
||||||
declaredStatuses: [401, 400],
|
declaredStatuses: [401, 400],
|
||||||
@@ -482,30 +478,6 @@ export function make(options: ClientOptions) {
|
|||||||
},
|
},
|
||||||
requestOptions,
|
requestOptions,
|
||||||
).then((value) => value.data),
|
).then((value) => value.data),
|
||||||
import: (input: SessionImportInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<{ readonly data: SessionImportOutput }>(
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
path: `/api/session/import`,
|
|
||||||
body: { info: input["info"], messages: input["messages"], location: input["location"] },
|
|
||||||
successStatus: 200,
|
|
||||||
declaredStatuses: [409, 401, 400],
|
|
||||||
empty: false,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
).then((value) => value.data),
|
|
||||||
export: (input: SessionExportInput, requestOptions?: RequestOptions) =>
|
|
||||||
request<{ readonly data: SessionExportOutput }>(
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/export`,
|
|
||||||
query: { sanitize: input["sanitize"] },
|
|
||||||
successStatus: 200,
|
|
||||||
declaredStatuses: [404, 500, 401, 400],
|
|
||||||
empty: false,
|
|
||||||
},
|
|
||||||
requestOptions,
|
|
||||||
).then((value) => value.data),
|
|
||||||
active: (requestOptions?: RequestOptions) =>
|
active: (requestOptions?: RequestOptions) =>
|
||||||
request<{ readonly data: SessionActiveOutput }>(
|
request<{ readonly data: SessionActiveOutput }>(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -35,6 +35,18 @@ export type FileDiffInfo = {
|
|||||||
status: "added" | "deleted" | "modified"
|
status: "added" | "deleted" | "modified"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SessionActive = { type: "running" }
|
||||||
|
|
||||||
|
export type PromptBase64 = string
|
||||||
|
|
||||||
|
export type PromptFileSource = { type: "inline" } | { type: "uri"; uri: string }
|
||||||
|
|
||||||
|
export type PromptMention = { start: number; end: number; text: string }
|
||||||
|
|
||||||
|
export type SessionPendingSyntheticData = { text: string; description?: string; metadata?: { [x: string]: JsonValue } }
|
||||||
|
|
||||||
|
export type SessionPendingCompaction = { id: string; sessionID: string; timeCreated: number; type: "compaction" }
|
||||||
|
|
||||||
export type SessionMessageAgentSelected = {
|
export type SessionMessageAgentSelected = {
|
||||||
id: string
|
id: string
|
||||||
metadata?: { [x: string]: JsonValue }
|
metadata?: { [x: string]: JsonValue }
|
||||||
@@ -43,12 +55,6 @@ export type SessionMessageAgentSelected = {
|
|||||||
agent: string
|
agent: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PromptBase64 = string
|
|
||||||
|
|
||||||
export type PromptFileSource = { type: "inline" } | { type: "uri"; uri: string }
|
|
||||||
|
|
||||||
export type PromptMention = { start: number; end: number; text: string }
|
|
||||||
|
|
||||||
export type SessionMessageSynthetic = {
|
export type SessionMessageSynthetic = {
|
||||||
id: string
|
id: string
|
||||||
metadata?: { [x: string]: JsonValue }
|
metadata?: { [x: string]: JsonValue }
|
||||||
@@ -126,12 +132,6 @@ export type SessionMessageCompactionCompleted = {
|
|||||||
recent: string
|
recent: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SessionActive = { type: "running" }
|
|
||||||
|
|
||||||
export type SessionPendingSyntheticData = { text: string; description?: string; metadata?: { [x: string]: JsonValue } }
|
|
||||||
|
|
||||||
export type SessionPendingCompaction = { id: string; sessionID: string; timeCreated: number; type: "compaction" }
|
|
||||||
|
|
||||||
export type InstructionEntryKey = string
|
export type InstructionEntryKey = string
|
||||||
|
|
||||||
export type SessionGenerateResponse = { data: { text: string } }
|
export type SessionGenerateResponse = { data: { text: string } }
|
||||||
@@ -169,8 +169,6 @@ export type EventLogSynced = { type: "log.synced"; aggregateID: string; seq?: nu
|
|||||||
|
|
||||||
export type ModelReasoningField = "reasoning" | "reasoning_content" | "reasoning_text" | (string & {})
|
export type ModelReasoningField = "reasoning" | "reasoning_content" | "reasoning_text" | (string & {})
|
||||||
|
|
||||||
export type ModelMaxTokensField = "max_completion_tokens" | "max_tokens"
|
|
||||||
|
|
||||||
export type ModelCapabilities = { tools: boolean; input: Array<string>; output: Array<string> }
|
export type ModelCapabilities = { tools: boolean; input: Array<string>; output: Array<string> }
|
||||||
|
|
||||||
export type ModelVariant = {
|
export type ModelVariant = {
|
||||||
@@ -1050,6 +1048,15 @@ export type PromptFileAttachment = {
|
|||||||
|
|
||||||
export type PromptAgentAttachment = { name: string; mention?: PromptMention }
|
export type PromptAgentAttachment = { name: string; mention?: PromptMention }
|
||||||
|
|
||||||
|
export type SessionPendingSynthetic = {
|
||||||
|
id: string
|
||||||
|
sessionID: string
|
||||||
|
timeCreated: number
|
||||||
|
type: "synthetic"
|
||||||
|
data: SessionPendingSyntheticData
|
||||||
|
delivery: "steer" | "queue"
|
||||||
|
}
|
||||||
|
|
||||||
export type SessionMessageAssistantText = { type: "text"; text: string; state?: SessionMessageProviderState }
|
export type SessionMessageAssistantText = { type: "text"; text: string; state?: SessionMessageProviderState }
|
||||||
|
|
||||||
export type SessionMessageAssistantReasoning = {
|
export type SessionMessageAssistantReasoning = {
|
||||||
@@ -1121,15 +1128,6 @@ export type SessionCompactionFailed = {
|
|||||||
data: { sessionID: string; reason: "auto" | "manual"; error: SessionStructuredError; inputID?: string }
|
data: { sessionID: string; reason: "auto" | "manual"; error: SessionStructuredError; inputID?: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SessionPendingSynthetic = {
|
|
||||||
id: string
|
|
||||||
sessionID: string
|
|
||||||
timeCreated: number
|
|
||||||
type: "synthetic"
|
|
||||||
data: SessionPendingSyntheticData
|
|
||||||
delivery: "steer" | "queue"
|
|
||||||
}
|
|
||||||
|
|
||||||
export type InstructionEntryInfo = { key: InstructionEntryKey; value: JsonValue }
|
export type InstructionEntryInfo = { key: InstructionEntryKey; value: JsonValue }
|
||||||
|
|
||||||
export type SessionPendingSyntheticMessage = {
|
export type SessionPendingSyntheticMessage = {
|
||||||
@@ -1232,11 +1230,7 @@ export type SessionToolCalled = {
|
|||||||
|
|
||||||
export type ToolContent1 = ToolTextContent | ToolFileContent1
|
export type ToolContent1 = ToolTextContent | ToolFileContent1
|
||||||
|
|
||||||
export type ModelCompatibility = {
|
export type ModelCompatibility = { reasoningField?: ModelReasoningField }
|
||||||
reasoningField?: ModelReasoningField
|
|
||||||
maxTokensField?: ModelMaxTokensField
|
|
||||||
requireFinishReason?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ModelCost = {
|
export type ModelCost = {
|
||||||
tier?: { type: "context"; size: number }
|
tier?: { type: "context"; size: number }
|
||||||
@@ -1528,6 +1522,13 @@ export type SessionRevertStaged = {
|
|||||||
data: { sessionID: string; revert: SessionRevert }
|
data: { sessionID: string; revert: SessionRevert }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SessionPendingUserData = {
|
||||||
|
text: string
|
||||||
|
files?: Array<PromptFileAttachment>
|
||||||
|
agents?: Array<PromptAgentAttachment>
|
||||||
|
metadata?: { [x: string]: JsonValue }
|
||||||
|
}
|
||||||
|
|
||||||
export type SessionMessageUser = {
|
export type SessionMessageUser = {
|
||||||
id: string
|
id: string
|
||||||
metadata?: { [x: string]: JsonValue }
|
metadata?: { [x: string]: JsonValue }
|
||||||
@@ -1538,13 +1539,6 @@ export type SessionMessageUser = {
|
|||||||
type: "user"
|
type: "user"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SessionPendingUserData = {
|
|
||||||
text: string
|
|
||||||
files?: Array<PromptFileAttachment>
|
|
||||||
agents?: Array<PromptAgentAttachment>
|
|
||||||
metadata?: { [x: string]: JsonValue }
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SessionPendingUserData1 = {
|
export type SessionPendingUserData1 = {
|
||||||
text: string
|
text: string
|
||||||
files?: Array<PromptFileAttachment>
|
files?: Array<PromptFileAttachment>
|
||||||
@@ -2001,8 +1995,6 @@ export type SessionEventDurable =
|
|||||||
| SessionRevertCommitted
|
| SessionRevertCommitted
|
||||||
| SessionUsageRecorded
|
| SessionUsageRecorded
|
||||||
|
|
||||||
export type SessionTransferData = { info: SessionInfo; messages: Array<SessionMessageInfo> }
|
|
||||||
|
|
||||||
export type SessionMessagesResponse = {
|
export type SessionMessagesResponse = {
|
||||||
data: Array<SessionMessageInfo>
|
data: Array<SessionMessageInfo>
|
||||||
cursor: { previous?: string | null; next?: string | null }
|
cursor: { previous?: string | null; next?: string | null }
|
||||||
@@ -2122,14 +2114,6 @@ export type InvalidCursorError = { readonly _tag: "InvalidCursorError"; readonly
|
|||||||
export const isInvalidCursorError = (value: unknown): value is InvalidCursorError =>
|
export const isInvalidCursorError = (value: unknown): value is InvalidCursorError =>
|
||||||
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "InvalidCursorError"
|
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "InvalidCursorError"
|
||||||
|
|
||||||
export type ConflictError = {
|
|
||||||
readonly _tag: "ConflictError"
|
|
||||||
readonly message: string
|
|
||||||
readonly resource?: string | undefined
|
|
||||||
}
|
|
||||||
export const isConflictError = (value: unknown): value is ConflictError =>
|
|
||||||
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ConflictError"
|
|
||||||
|
|
||||||
export type SessionNotFoundError = {
|
export type SessionNotFoundError = {
|
||||||
readonly _tag: "SessionNotFoundError"
|
readonly _tag: "SessionNotFoundError"
|
||||||
readonly sessionID: string
|
readonly sessionID: string
|
||||||
@@ -2138,14 +2122,6 @@ export type SessionNotFoundError = {
|
|||||||
export const isSessionNotFoundError = (value: unknown): value is SessionNotFoundError =>
|
export const isSessionNotFoundError = (value: unknown): value is SessionNotFoundError =>
|
||||||
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "SessionNotFoundError"
|
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "SessionNotFoundError"
|
||||||
|
|
||||||
export type UnknownError = {
|
|
||||||
readonly _tag: "UnknownError"
|
|
||||||
readonly message: string
|
|
||||||
readonly ref?: string | undefined
|
|
||||||
}
|
|
||||||
export const isUnknownError = (value: unknown): value is UnknownError =>
|
|
||||||
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "UnknownError"
|
|
||||||
|
|
||||||
export type MessageNotFoundError = {
|
export type MessageNotFoundError = {
|
||||||
readonly _tag: "MessageNotFoundError"
|
readonly _tag: "MessageNotFoundError"
|
||||||
readonly sessionID: string
|
readonly sessionID: string
|
||||||
@@ -2155,6 +2131,14 @@ export type MessageNotFoundError = {
|
|||||||
export const isMessageNotFoundError = (value: unknown): value is MessageNotFoundError =>
|
export const isMessageNotFoundError = (value: unknown): value is MessageNotFoundError =>
|
||||||
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "MessageNotFoundError"
|
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "MessageNotFoundError"
|
||||||
|
|
||||||
|
export type ConflictError = {
|
||||||
|
readonly _tag: "ConflictError"
|
||||||
|
readonly message: string
|
||||||
|
readonly resource?: string | undefined
|
||||||
|
}
|
||||||
|
export const isConflictError = (value: unknown): value is ConflictError =>
|
||||||
|
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ConflictError"
|
||||||
|
|
||||||
export type CommandNotFoundError = {
|
export type CommandNotFoundError = {
|
||||||
readonly _tag: "CommandNotFoundError"
|
readonly _tag: "CommandNotFoundError"
|
||||||
readonly command: string
|
readonly command: string
|
||||||
@@ -2195,6 +2179,14 @@ export type SessionBusyError = {
|
|||||||
export const isSessionBusyError = (value: unknown): value is SessionBusyError =>
|
export const isSessionBusyError = (value: unknown): value is SessionBusyError =>
|
||||||
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "SessionBusyError"
|
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "SessionBusyError"
|
||||||
|
|
||||||
|
export type UnknownError = {
|
||||||
|
readonly _tag: "UnknownError"
|
||||||
|
readonly message: string
|
||||||
|
readonly ref?: string | undefined
|
||||||
|
}
|
||||||
|
export const isUnknownError = (value: unknown): value is UnknownError =>
|
||||||
|
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "UnknownError"
|
||||||
|
|
||||||
export type InstructionEntryValueTooLargeError = {
|
export type InstructionEntryValueTooLargeError = {
|
||||||
readonly _tag: "InstructionEntryValueTooLargeError"
|
readonly _tag: "InstructionEntryValueTooLargeError"
|
||||||
readonly actualBytes: number
|
readonly actualBytes: number
|
||||||
@@ -2436,789 +2428,42 @@ export type SessionCreateInput = {
|
|||||||
readonly id?: {
|
readonly id?: {
|
||||||
readonly id?: string | null
|
readonly id?: string | null
|
||||||
readonly title?: string | null
|
readonly title?: string | null
|
||||||
readonly agent: string
|
readonly agent?: string | null
|
||||||
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
||||||
readonly location?: { readonly directory: string; readonly workspaceID?: string } | null
|
readonly location?: { readonly directory: string; readonly workspaceID?: string } | null
|
||||||
}["id"]
|
}["id"]
|
||||||
readonly title?: {
|
readonly title?: {
|
||||||
readonly id?: string | null
|
readonly id?: string | null
|
||||||
readonly title?: string | null
|
readonly title?: string | null
|
||||||
readonly agent: string
|
readonly agent?: string | null
|
||||||
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
||||||
readonly location?: { readonly directory: string; readonly workspaceID?: string } | null
|
readonly location?: { readonly directory: string; readonly workspaceID?: string } | null
|
||||||
}["title"]
|
}["title"]
|
||||||
readonly agent: {
|
readonly agent?: {
|
||||||
readonly id?: string | null
|
readonly id?: string | null
|
||||||
readonly title?: string | null
|
readonly title?: string | null
|
||||||
readonly agent: string
|
readonly agent?: string | null
|
||||||
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
||||||
readonly location?: { readonly directory: string; readonly workspaceID?: string } | null
|
readonly location?: { readonly directory: string; readonly workspaceID?: string } | null
|
||||||
}["agent"]
|
}["agent"]
|
||||||
readonly model: {
|
readonly model?: {
|
||||||
readonly id?: string | null
|
readonly id?: string | null
|
||||||
readonly title?: string | null
|
readonly title?: string | null
|
||||||
readonly agent: string
|
readonly agent?: string | null
|
||||||
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
||||||
readonly location?: { readonly directory: string; readonly workspaceID?: string } | null
|
readonly location?: { readonly directory: string; readonly workspaceID?: string } | null
|
||||||
}["model"]
|
}["model"]
|
||||||
readonly location?: {
|
readonly location?: {
|
||||||
readonly id?: string | null
|
readonly id?: string | null
|
||||||
readonly title?: string | null
|
readonly title?: string | null
|
||||||
readonly agent: string
|
readonly agent?: string | null
|
||||||
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
||||||
readonly location?: { readonly directory: string; readonly workspaceID?: string } | null
|
readonly location?: { readonly directory: string; readonly workspaceID?: string } | null
|
||||||
}["location"]
|
}["location"]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SessionCreateOutput = { data: SessionInfo }["data"]
|
export type SessionCreateOutput = { data: SessionInfo }["data"]
|
||||||
|
|
||||||
export type SessionImportInput = {
|
|
||||||
readonly info: {
|
|
||||||
readonly info: {
|
|
||||||
readonly id: string
|
|
||||||
readonly parentID?: string
|
|
||||||
readonly fork?: {
|
|
||||||
readonly sessionID: string
|
|
||||||
readonly boundary:
|
|
||||||
| { readonly type: "before"; readonly messageID: string }
|
|
||||||
| { readonly type: "through"; readonly messageID: string }
|
|
||||||
}
|
|
||||||
readonly projectID: string
|
|
||||||
readonly agent?: string
|
|
||||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
|
||||||
readonly cost: number
|
|
||||||
readonly tokens: {
|
|
||||||
readonly input: number
|
|
||||||
readonly output: number
|
|
||||||
readonly reasoning: number
|
|
||||||
readonly cache: { readonly read: number; readonly write: number }
|
|
||||||
}
|
|
||||||
readonly time: { readonly created: number; readonly updated: number; readonly archived?: number }
|
|
||||||
readonly title?: string
|
|
||||||
readonly location: { readonly directory: string; readonly workspaceID?: string }
|
|
||||||
readonly subpath?: string
|
|
||||||
readonly revert?: {
|
|
||||||
readonly messageID: string
|
|
||||||
readonly partID?: string
|
|
||||||
readonly snapshot?: string
|
|
||||||
readonly files?: ReadonlyArray<{
|
|
||||||
readonly file: string
|
|
||||||
readonly patch: string
|
|
||||||
readonly additions: number
|
|
||||||
readonly deletions: number
|
|
||||||
readonly status: "added" | "deleted" | "modified"
|
|
||||||
}>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
readonly messages: ReadonlyArray<
|
|
||||||
| {
|
|
||||||
readonly id: string
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly time: { readonly created: number }
|
|
||||||
readonly type: "agent-switched"
|
|
||||||
readonly agent: string
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly id: string
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly time: { readonly created: number }
|
|
||||||
readonly type: "model-switched"
|
|
||||||
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
|
||||||
readonly previous?: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly id: string
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly time: { readonly created: number }
|
|
||||||
readonly text: string
|
|
||||||
readonly files?: ReadonlyArray<{
|
|
||||||
readonly data: string
|
|
||||||
readonly mime: string
|
|
||||||
readonly source: { readonly type: "inline" } | { readonly type: "uri"; readonly uri: string }
|
|
||||||
readonly name?: string
|
|
||||||
readonly description?: string
|
|
||||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
|
||||||
}>
|
|
||||||
readonly agents?: ReadonlyArray<{
|
|
||||||
readonly name: string
|
|
||||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
|
||||||
}>
|
|
||||||
readonly type: "user"
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly id: string
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly time: { readonly created: number }
|
|
||||||
readonly text: string
|
|
||||||
readonly description?: string
|
|
||||||
readonly type: "synthetic"
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly id: string
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly time: { readonly created: number }
|
|
||||||
readonly type: "system"
|
|
||||||
readonly text: string
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly id: string
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly time: { readonly created: number }
|
|
||||||
readonly type: "skill"
|
|
||||||
readonly skill: string
|
|
||||||
readonly name: string
|
|
||||||
readonly text: string
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly id: string
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly time: { readonly created: number; readonly completed?: number }
|
|
||||||
readonly type: "shell"
|
|
||||||
readonly shellID: string
|
|
||||||
readonly command: string
|
|
||||||
readonly status: "running" | "exited" | "timeout" | "killed"
|
|
||||||
readonly exit?: number | "Infinity" | "-Infinity" | "NaN"
|
|
||||||
readonly output?: {
|
|
||||||
readonly output: string
|
|
||||||
readonly cursor: number
|
|
||||||
readonly size: number
|
|
||||||
readonly truncated: boolean
|
|
||||||
}
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly id: string
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly time: { readonly created: number; readonly completed?: number }
|
|
||||||
readonly type: "assistant"
|
|
||||||
readonly agent: string
|
|
||||||
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
|
||||||
readonly content: ReadonlyArray<
|
|
||||||
| { readonly type: "text"; readonly text: string; readonly state?: { readonly [x: string]: JsonValue } }
|
|
||||||
| {
|
|
||||||
readonly type: "reasoning"
|
|
||||||
readonly text: string
|
|
||||||
readonly state?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly time?: { readonly created: number; readonly completed?: number }
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly type: "tool"
|
|
||||||
readonly id: string
|
|
||||||
readonly name: string
|
|
||||||
readonly executed?: boolean
|
|
||||||
readonly providerState?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly providerResultState?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly state:
|
|
||||||
| { readonly status: "streaming"; readonly input: string }
|
|
||||||
| {
|
|
||||||
readonly status: "running"
|
|
||||||
readonly input: { readonly [x: string]: JsonValue }
|
|
||||||
readonly metadata: { readonly [x: string]: JsonValue }
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly status: "completed"
|
|
||||||
readonly input: { readonly [x: string]: JsonValue }
|
|
||||||
readonly content: readonly [
|
|
||||||
(
|
|
||||||
| { readonly type: "text"; readonly text: string }
|
|
||||||
| {
|
|
||||||
readonly type: "file"
|
|
||||||
readonly uri: string
|
|
||||||
readonly mime: string
|
|
||||||
readonly name?: string | null
|
|
||||||
}
|
|
||||||
),
|
|
||||||
...Array<
|
|
||||||
| { readonly type: "text"; readonly text: string }
|
|
||||||
| {
|
|
||||||
readonly type: "file"
|
|
||||||
readonly uri: string
|
|
||||||
readonly mime: string
|
|
||||||
readonly name?: string | null
|
|
||||||
}
|
|
||||||
>,
|
|
||||||
]
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly status: "error"
|
|
||||||
readonly input: { readonly [x: string]: JsonValue }
|
|
||||||
readonly error: { readonly type: string; readonly message: string; readonly status?: number }
|
|
||||||
readonly content?: readonly [
|
|
||||||
(
|
|
||||||
| { readonly type: "text"; readonly text: string }
|
|
||||||
| {
|
|
||||||
readonly type: "file"
|
|
||||||
readonly uri: string
|
|
||||||
readonly mime: string
|
|
||||||
readonly name?: string | null
|
|
||||||
}
|
|
||||||
),
|
|
||||||
...Array<
|
|
||||||
| { readonly type: "text"; readonly text: string }
|
|
||||||
| {
|
|
||||||
readonly type: "file"
|
|
||||||
readonly uri: string
|
|
||||||
readonly mime: string
|
|
||||||
readonly name?: string | null
|
|
||||||
}
|
|
||||||
>,
|
|
||||||
]
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
}
|
|
||||||
readonly time: { readonly created: number; readonly ran?: number; readonly completed?: number }
|
|
||||||
}
|
|
||||||
>
|
|
||||||
readonly snapshot?: { readonly start?: string; readonly end?: string; readonly files?: ReadonlyArray<string> }
|
|
||||||
readonly finish?: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown"
|
|
||||||
readonly cost?: number
|
|
||||||
readonly tokens?: {
|
|
||||||
readonly input: number
|
|
||||||
readonly output: number
|
|
||||||
readonly reasoning: number
|
|
||||||
readonly cache: { readonly read: number; readonly write: number }
|
|
||||||
}
|
|
||||||
readonly error?: { readonly type: string; readonly message: string; readonly status?: number }
|
|
||||||
readonly retry?: {
|
|
||||||
readonly attempt: number
|
|
||||||
readonly at: number
|
|
||||||
readonly error: { readonly type: string; readonly message: string; readonly status?: number }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
| (
|
|
||||||
| {
|
|
||||||
readonly type: "compaction"
|
|
||||||
readonly id: string
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly time: { readonly created: number }
|
|
||||||
readonly status: "running"
|
|
||||||
readonly reason: "auto" | "manual"
|
|
||||||
readonly summary: string
|
|
||||||
readonly recent: string
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly type: "compaction"
|
|
||||||
readonly id: string
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly time: { readonly created: number }
|
|
||||||
readonly status: "completed"
|
|
||||||
readonly reason: "auto" | "manual"
|
|
||||||
readonly summary: string
|
|
||||||
readonly recent: string
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly type: "compaction"
|
|
||||||
readonly id: string
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly time: { readonly created: number }
|
|
||||||
readonly status: "failed"
|
|
||||||
readonly reason: "auto" | "manual"
|
|
||||||
readonly error: { readonly type: string; readonly message: string; readonly status?: number }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
>
|
|
||||||
readonly location?: { readonly directory: string; readonly workspaceID?: string } | null
|
|
||||||
}["info"]
|
|
||||||
readonly messages: {
|
|
||||||
readonly info: {
|
|
||||||
readonly id: string
|
|
||||||
readonly parentID?: string
|
|
||||||
readonly fork?: {
|
|
||||||
readonly sessionID: string
|
|
||||||
readonly boundary:
|
|
||||||
| { readonly type: "before"; readonly messageID: string }
|
|
||||||
| { readonly type: "through"; readonly messageID: string }
|
|
||||||
}
|
|
||||||
readonly projectID: string
|
|
||||||
readonly agent?: string
|
|
||||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
|
||||||
readonly cost: number
|
|
||||||
readonly tokens: {
|
|
||||||
readonly input: number
|
|
||||||
readonly output: number
|
|
||||||
readonly reasoning: number
|
|
||||||
readonly cache: { readonly read: number; readonly write: number }
|
|
||||||
}
|
|
||||||
readonly time: { readonly created: number; readonly updated: number; readonly archived?: number }
|
|
||||||
readonly title?: string
|
|
||||||
readonly location: { readonly directory: string; readonly workspaceID?: string }
|
|
||||||
readonly subpath?: string
|
|
||||||
readonly revert?: {
|
|
||||||
readonly messageID: string
|
|
||||||
readonly partID?: string
|
|
||||||
readonly snapshot?: string
|
|
||||||
readonly files?: ReadonlyArray<{
|
|
||||||
readonly file: string
|
|
||||||
readonly patch: string
|
|
||||||
readonly additions: number
|
|
||||||
readonly deletions: number
|
|
||||||
readonly status: "added" | "deleted" | "modified"
|
|
||||||
}>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
readonly messages: ReadonlyArray<
|
|
||||||
| {
|
|
||||||
readonly id: string
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly time: { readonly created: number }
|
|
||||||
readonly type: "agent-switched"
|
|
||||||
readonly agent: string
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly id: string
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly time: { readonly created: number }
|
|
||||||
readonly type: "model-switched"
|
|
||||||
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
|
||||||
readonly previous?: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly id: string
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly time: { readonly created: number }
|
|
||||||
readonly text: string
|
|
||||||
readonly files?: ReadonlyArray<{
|
|
||||||
readonly data: string
|
|
||||||
readonly mime: string
|
|
||||||
readonly source: { readonly type: "inline" } | { readonly type: "uri"; readonly uri: string }
|
|
||||||
readonly name?: string
|
|
||||||
readonly description?: string
|
|
||||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
|
||||||
}>
|
|
||||||
readonly agents?: ReadonlyArray<{
|
|
||||||
readonly name: string
|
|
||||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
|
||||||
}>
|
|
||||||
readonly type: "user"
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly id: string
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly time: { readonly created: number }
|
|
||||||
readonly text: string
|
|
||||||
readonly description?: string
|
|
||||||
readonly type: "synthetic"
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly id: string
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly time: { readonly created: number }
|
|
||||||
readonly type: "system"
|
|
||||||
readonly text: string
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly id: string
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly time: { readonly created: number }
|
|
||||||
readonly type: "skill"
|
|
||||||
readonly skill: string
|
|
||||||
readonly name: string
|
|
||||||
readonly text: string
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly id: string
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly time: { readonly created: number; readonly completed?: number }
|
|
||||||
readonly type: "shell"
|
|
||||||
readonly shellID: string
|
|
||||||
readonly command: string
|
|
||||||
readonly status: "running" | "exited" | "timeout" | "killed"
|
|
||||||
readonly exit?: number | "Infinity" | "-Infinity" | "NaN"
|
|
||||||
readonly output?: {
|
|
||||||
readonly output: string
|
|
||||||
readonly cursor: number
|
|
||||||
readonly size: number
|
|
||||||
readonly truncated: boolean
|
|
||||||
}
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly id: string
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly time: { readonly created: number; readonly completed?: number }
|
|
||||||
readonly type: "assistant"
|
|
||||||
readonly agent: string
|
|
||||||
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
|
||||||
readonly content: ReadonlyArray<
|
|
||||||
| { readonly type: "text"; readonly text: string; readonly state?: { readonly [x: string]: JsonValue } }
|
|
||||||
| {
|
|
||||||
readonly type: "reasoning"
|
|
||||||
readonly text: string
|
|
||||||
readonly state?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly time?: { readonly created: number; readonly completed?: number }
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly type: "tool"
|
|
||||||
readonly id: string
|
|
||||||
readonly name: string
|
|
||||||
readonly executed?: boolean
|
|
||||||
readonly providerState?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly providerResultState?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly state:
|
|
||||||
| { readonly status: "streaming"; readonly input: string }
|
|
||||||
| {
|
|
||||||
readonly status: "running"
|
|
||||||
readonly input: { readonly [x: string]: JsonValue }
|
|
||||||
readonly metadata: { readonly [x: string]: JsonValue }
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly status: "completed"
|
|
||||||
readonly input: { readonly [x: string]: JsonValue }
|
|
||||||
readonly content: readonly [
|
|
||||||
(
|
|
||||||
| { readonly type: "text"; readonly text: string }
|
|
||||||
| {
|
|
||||||
readonly type: "file"
|
|
||||||
readonly uri: string
|
|
||||||
readonly mime: string
|
|
||||||
readonly name?: string | null
|
|
||||||
}
|
|
||||||
),
|
|
||||||
...Array<
|
|
||||||
| { readonly type: "text"; readonly text: string }
|
|
||||||
| {
|
|
||||||
readonly type: "file"
|
|
||||||
readonly uri: string
|
|
||||||
readonly mime: string
|
|
||||||
readonly name?: string | null
|
|
||||||
}
|
|
||||||
>,
|
|
||||||
]
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly status: "error"
|
|
||||||
readonly input: { readonly [x: string]: JsonValue }
|
|
||||||
readonly error: { readonly type: string; readonly message: string; readonly status?: number }
|
|
||||||
readonly content?: readonly [
|
|
||||||
(
|
|
||||||
| { readonly type: "text"; readonly text: string }
|
|
||||||
| {
|
|
||||||
readonly type: "file"
|
|
||||||
readonly uri: string
|
|
||||||
readonly mime: string
|
|
||||||
readonly name?: string | null
|
|
||||||
}
|
|
||||||
),
|
|
||||||
...Array<
|
|
||||||
| { readonly type: "text"; readonly text: string }
|
|
||||||
| {
|
|
||||||
readonly type: "file"
|
|
||||||
readonly uri: string
|
|
||||||
readonly mime: string
|
|
||||||
readonly name?: string | null
|
|
||||||
}
|
|
||||||
>,
|
|
||||||
]
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
}
|
|
||||||
readonly time: { readonly created: number; readonly ran?: number; readonly completed?: number }
|
|
||||||
}
|
|
||||||
>
|
|
||||||
readonly snapshot?: { readonly start?: string; readonly end?: string; readonly files?: ReadonlyArray<string> }
|
|
||||||
readonly finish?: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown"
|
|
||||||
readonly cost?: number
|
|
||||||
readonly tokens?: {
|
|
||||||
readonly input: number
|
|
||||||
readonly output: number
|
|
||||||
readonly reasoning: number
|
|
||||||
readonly cache: { readonly read: number; readonly write: number }
|
|
||||||
}
|
|
||||||
readonly error?: { readonly type: string; readonly message: string; readonly status?: number }
|
|
||||||
readonly retry?: {
|
|
||||||
readonly attempt: number
|
|
||||||
readonly at: number
|
|
||||||
readonly error: { readonly type: string; readonly message: string; readonly status?: number }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
| (
|
|
||||||
| {
|
|
||||||
readonly type: "compaction"
|
|
||||||
readonly id: string
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly time: { readonly created: number }
|
|
||||||
readonly status: "running"
|
|
||||||
readonly reason: "auto" | "manual"
|
|
||||||
readonly summary: string
|
|
||||||
readonly recent: string
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly type: "compaction"
|
|
||||||
readonly id: string
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly time: { readonly created: number }
|
|
||||||
readonly status: "completed"
|
|
||||||
readonly reason: "auto" | "manual"
|
|
||||||
readonly summary: string
|
|
||||||
readonly recent: string
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly type: "compaction"
|
|
||||||
readonly id: string
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly time: { readonly created: number }
|
|
||||||
readonly status: "failed"
|
|
||||||
readonly reason: "auto" | "manual"
|
|
||||||
readonly error: { readonly type: string; readonly message: string; readonly status?: number }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
>
|
|
||||||
readonly location?: { readonly directory: string; readonly workspaceID?: string } | null
|
|
||||||
}["messages"]
|
|
||||||
readonly location?: {
|
|
||||||
readonly info: {
|
|
||||||
readonly id: string
|
|
||||||
readonly parentID?: string
|
|
||||||
readonly fork?: {
|
|
||||||
readonly sessionID: string
|
|
||||||
readonly boundary:
|
|
||||||
| { readonly type: "before"; readonly messageID: string }
|
|
||||||
| { readonly type: "through"; readonly messageID: string }
|
|
||||||
}
|
|
||||||
readonly projectID: string
|
|
||||||
readonly agent?: string
|
|
||||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
|
||||||
readonly cost: number
|
|
||||||
readonly tokens: {
|
|
||||||
readonly input: number
|
|
||||||
readonly output: number
|
|
||||||
readonly reasoning: number
|
|
||||||
readonly cache: { readonly read: number; readonly write: number }
|
|
||||||
}
|
|
||||||
readonly time: { readonly created: number; readonly updated: number; readonly archived?: number }
|
|
||||||
readonly title?: string
|
|
||||||
readonly location: { readonly directory: string; readonly workspaceID?: string }
|
|
||||||
readonly subpath?: string
|
|
||||||
readonly revert?: {
|
|
||||||
readonly messageID: string
|
|
||||||
readonly partID?: string
|
|
||||||
readonly snapshot?: string
|
|
||||||
readonly files?: ReadonlyArray<{
|
|
||||||
readonly file: string
|
|
||||||
readonly patch: string
|
|
||||||
readonly additions: number
|
|
||||||
readonly deletions: number
|
|
||||||
readonly status: "added" | "deleted" | "modified"
|
|
||||||
}>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
readonly messages: ReadonlyArray<
|
|
||||||
| {
|
|
||||||
readonly id: string
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly time: { readonly created: number }
|
|
||||||
readonly type: "agent-switched"
|
|
||||||
readonly agent: string
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly id: string
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly time: { readonly created: number }
|
|
||||||
readonly type: "model-switched"
|
|
||||||
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
|
||||||
readonly previous?: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly id: string
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly time: { readonly created: number }
|
|
||||||
readonly text: string
|
|
||||||
readonly files?: ReadonlyArray<{
|
|
||||||
readonly data: string
|
|
||||||
readonly mime: string
|
|
||||||
readonly source: { readonly type: "inline" } | { readonly type: "uri"; readonly uri: string }
|
|
||||||
readonly name?: string
|
|
||||||
readonly description?: string
|
|
||||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
|
||||||
}>
|
|
||||||
readonly agents?: ReadonlyArray<{
|
|
||||||
readonly name: string
|
|
||||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
|
||||||
}>
|
|
||||||
readonly type: "user"
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly id: string
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly time: { readonly created: number }
|
|
||||||
readonly text: string
|
|
||||||
readonly description?: string
|
|
||||||
readonly type: "synthetic"
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly id: string
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly time: { readonly created: number }
|
|
||||||
readonly type: "system"
|
|
||||||
readonly text: string
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly id: string
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly time: { readonly created: number }
|
|
||||||
readonly type: "skill"
|
|
||||||
readonly skill: string
|
|
||||||
readonly name: string
|
|
||||||
readonly text: string
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly id: string
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly time: { readonly created: number; readonly completed?: number }
|
|
||||||
readonly type: "shell"
|
|
||||||
readonly shellID: string
|
|
||||||
readonly command: string
|
|
||||||
readonly status: "running" | "exited" | "timeout" | "killed"
|
|
||||||
readonly exit?: number | "Infinity" | "-Infinity" | "NaN"
|
|
||||||
readonly output?: {
|
|
||||||
readonly output: string
|
|
||||||
readonly cursor: number
|
|
||||||
readonly size: number
|
|
||||||
readonly truncated: boolean
|
|
||||||
}
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly id: string
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly time: { readonly created: number; readonly completed?: number }
|
|
||||||
readonly type: "assistant"
|
|
||||||
readonly agent: string
|
|
||||||
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
|
||||||
readonly content: ReadonlyArray<
|
|
||||||
| { readonly type: "text"; readonly text: string; readonly state?: { readonly [x: string]: JsonValue } }
|
|
||||||
| {
|
|
||||||
readonly type: "reasoning"
|
|
||||||
readonly text: string
|
|
||||||
readonly state?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly time?: { readonly created: number; readonly completed?: number }
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly type: "tool"
|
|
||||||
readonly id: string
|
|
||||||
readonly name: string
|
|
||||||
readonly executed?: boolean
|
|
||||||
readonly providerState?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly providerResultState?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly state:
|
|
||||||
| { readonly status: "streaming"; readonly input: string }
|
|
||||||
| {
|
|
||||||
readonly status: "running"
|
|
||||||
readonly input: { readonly [x: string]: JsonValue }
|
|
||||||
readonly metadata: { readonly [x: string]: JsonValue }
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly status: "completed"
|
|
||||||
readonly input: { readonly [x: string]: JsonValue }
|
|
||||||
readonly content: readonly [
|
|
||||||
(
|
|
||||||
| { readonly type: "text"; readonly text: string }
|
|
||||||
| {
|
|
||||||
readonly type: "file"
|
|
||||||
readonly uri: string
|
|
||||||
readonly mime: string
|
|
||||||
readonly name?: string | null
|
|
||||||
}
|
|
||||||
),
|
|
||||||
...Array<
|
|
||||||
| { readonly type: "text"; readonly text: string }
|
|
||||||
| {
|
|
||||||
readonly type: "file"
|
|
||||||
readonly uri: string
|
|
||||||
readonly mime: string
|
|
||||||
readonly name?: string | null
|
|
||||||
}
|
|
||||||
>,
|
|
||||||
]
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly status: "error"
|
|
||||||
readonly input: { readonly [x: string]: JsonValue }
|
|
||||||
readonly error: { readonly type: string; readonly message: string; readonly status?: number }
|
|
||||||
readonly content?: readonly [
|
|
||||||
(
|
|
||||||
| { readonly type: "text"; readonly text: string }
|
|
||||||
| {
|
|
||||||
readonly type: "file"
|
|
||||||
readonly uri: string
|
|
||||||
readonly mime: string
|
|
||||||
readonly name?: string | null
|
|
||||||
}
|
|
||||||
),
|
|
||||||
...Array<
|
|
||||||
| { readonly type: "text"; readonly text: string }
|
|
||||||
| {
|
|
||||||
readonly type: "file"
|
|
||||||
readonly uri: string
|
|
||||||
readonly mime: string
|
|
||||||
readonly name?: string | null
|
|
||||||
}
|
|
||||||
>,
|
|
||||||
]
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
}
|
|
||||||
readonly time: { readonly created: number; readonly ran?: number; readonly completed?: number }
|
|
||||||
}
|
|
||||||
>
|
|
||||||
readonly snapshot?: { readonly start?: string; readonly end?: string; readonly files?: ReadonlyArray<string> }
|
|
||||||
readonly finish?: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown"
|
|
||||||
readonly cost?: number
|
|
||||||
readonly tokens?: {
|
|
||||||
readonly input: number
|
|
||||||
readonly output: number
|
|
||||||
readonly reasoning: number
|
|
||||||
readonly cache: { readonly read: number; readonly write: number }
|
|
||||||
}
|
|
||||||
readonly error?: { readonly type: string; readonly message: string; readonly status?: number }
|
|
||||||
readonly retry?: {
|
|
||||||
readonly attempt: number
|
|
||||||
readonly at: number
|
|
||||||
readonly error: { readonly type: string; readonly message: string; readonly status?: number }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
| (
|
|
||||||
| {
|
|
||||||
readonly type: "compaction"
|
|
||||||
readonly id: string
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly time: { readonly created: number }
|
|
||||||
readonly status: "running"
|
|
||||||
readonly reason: "auto" | "manual"
|
|
||||||
readonly summary: string
|
|
||||||
readonly recent: string
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly type: "compaction"
|
|
||||||
readonly id: string
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly time: { readonly created: number }
|
|
||||||
readonly status: "completed"
|
|
||||||
readonly reason: "auto" | "manual"
|
|
||||||
readonly summary: string
|
|
||||||
readonly recent: string
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly type: "compaction"
|
|
||||||
readonly id: string
|
|
||||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
|
||||||
readonly time: { readonly created: number }
|
|
||||||
readonly status: "failed"
|
|
||||||
readonly reason: "auto" | "manual"
|
|
||||||
readonly error: { readonly type: string; readonly message: string; readonly status?: number }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
>
|
|
||||||
readonly location?: { readonly directory: string; readonly workspaceID?: string } | null
|
|
||||||
}["location"]
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SessionImportOutput = { data: SessionInfo }["data"]
|
|
||||||
|
|
||||||
export type SessionExportInput = {
|
|
||||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
|
||||||
readonly sanitize?: { readonly sanitize?: boolean | undefined }["sanitize"]
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SessionExportOutput = { data: SessionTransferData }["data"]
|
|
||||||
|
|
||||||
export type SessionActiveOutput = { data: { [x: string]: SessionActive } }["data"]
|
export type SessionActiveOutput = { data: { [x: string]: SessionActive } }["data"]
|
||||||
|
|
||||||
export type SessionGetInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
|
export type SessionGetInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
|
||||||
|
|||||||
@@ -181,8 +181,6 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
|
|||||||
const page = yield* client.session.list({ limit: 10 })
|
const page = yield* client.session.list({ limit: 10 })
|
||||||
const active = yield* client.session.active()
|
const active = yield* client.session.active()
|
||||||
const created = yield* client.session.create({
|
const created = yield* client.session.create({
|
||||||
agent: Agent.ID.make("build"),
|
|
||||||
model: Model.Ref.make({ id: "claude", providerID: "anthropic" }),
|
|
||||||
location: Location.Ref.make({ directory: AbsolutePath.make("/tmp/project") }),
|
location: Location.Ref.make({ directory: AbsolutePath.make("/tmp/project") }),
|
||||||
})
|
})
|
||||||
yield* client.session.switchAgent({ sessionID: Session.ID.make("ses_test"), agent: Agent.ID.make("build") })
|
yield* client.session.switchAgent({ sessionID: Session.ID.make("ses_test"), agent: Agent.ID.make("build") })
|
||||||
|
|||||||
@@ -454,11 +454,7 @@ test("session methods use the public HTTP contract", async () => {
|
|||||||
|
|
||||||
const page = await client.session.list({ limit: 10, order: "desc", parentID: null })
|
const page = await client.session.list({ limit: 10, order: "desc", parentID: null })
|
||||||
const active = await client.session.active()
|
const active = await client.session.active()
|
||||||
const created = await client.session.create({
|
const created = await client.session.create({ location: { directory: "/tmp/project" } })
|
||||||
agent: "build",
|
|
||||||
model: { id: "claude", providerID: "anthropic" },
|
|
||||||
location: { directory: "/tmp/project" },
|
|
||||||
})
|
|
||||||
await client.session.switchAgent({ sessionID: "ses_test", agent: "build" })
|
await client.session.switchAgent({ sessionID: "ses_test", agent: "build" })
|
||||||
await client.session.switchModel({
|
await client.session.switchModel({
|
||||||
sessionID: "ses_test",
|
sessionID: "ses_test",
|
||||||
@@ -532,7 +528,7 @@ test("middleware errors remain declared client errors", async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await client.session.create({ agent: "build", model: { id: "claude", providerID: "anthropic" } })
|
await client.session.create({})
|
||||||
throw new Error("Expected request to fail")
|
throw new Error("Expected request to fail")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(isUnauthorizedError(error)).toBe(true)
|
expect(isUnauthorizedError(error)).toBe(true)
|
||||||
|
|||||||
@@ -161,14 +161,11 @@ function isPathAction(action: string): action is PathAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function expandHome(resource: string, home: string) {
|
function expandHome(resource: string, home: string) {
|
||||||
|
if (resource.startsWith("~/")) return home + resource.slice(1)
|
||||||
if (resource === "~") return home
|
if (resource === "~") return home
|
||||||
if (resource === "$HOME") return home
|
if (resource === "$HOME") return home
|
||||||
const relative = resource.startsWith("~/")
|
if (resource.startsWith("$HOME/")) return home + resource.slice(5)
|
||||||
? resource.slice(2)
|
if (resource.startsWith("$HOME\\")) return home + resource.slice(5)
|
||||||
: resource.startsWith("$HOME/") || resource.startsWith("$HOME\\")
|
|
||||||
? resource.slice(6)
|
|
||||||
: undefined
|
|
||||||
if (relative !== undefined) return (path.posix.isAbsolute(home) ? path.posix : path.win32).join(home, relative)
|
|
||||||
return resource
|
return resource
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -340,12 +340,12 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: import("../p
|
|||||||
hook: (name, callback) => hooks.register("session", name, callback),
|
hook: (name, callback) => hooks.register("session", name, callback),
|
||||||
create: (input) =>
|
create: (input) =>
|
||||||
runtime.session.create({
|
runtime.session.create({
|
||||||
id: input.id,
|
id: input?.id,
|
||||||
title: input.title,
|
title: input?.title,
|
||||||
agent: input.agent,
|
agent: input?.agent,
|
||||||
model: input.model,
|
model: input?.model,
|
||||||
location:
|
location:
|
||||||
input.location ?? Location.Ref.make({ directory: location.directory, workspaceID: location.workspaceID }),
|
input?.location ?? Location.Ref.make({ directory: location.directory, workspaceID: location.workspaceID }),
|
||||||
}),
|
}),
|
||||||
get: (input) => runtime.session.get(input.sessionID),
|
get: (input) => runtime.session.get(input.sessionID),
|
||||||
prompt: runtime.session.prompt,
|
prompt: runtime.session.prompt,
|
||||||
|
|||||||
@@ -269,21 +269,25 @@ export function fromPromise(plugin: Plugin) {
|
|||||||
register(host.session.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))))),
|
register(host.session.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))))),
|
||||||
create: (input) =>
|
create: (input) =>
|
||||||
run(
|
run(
|
||||||
host.session.create({
|
host.session.create(
|
||||||
id: input.id == null ? undefined : Session.ID.make(input.id),
|
input === undefined
|
||||||
agent: Agent.ID.make(input.agent),
|
? undefined
|
||||||
model: model(input.model),
|
: {
|
||||||
location:
|
id: input.id == null ? undefined : Session.ID.make(input.id),
|
||||||
input.location == null
|
agent: input.agent == null ? undefined : Agent.ID.make(input.agent),
|
||||||
? undefined
|
model: input.model == null ? undefined : model(input.model),
|
||||||
: Location.Ref.make({
|
location:
|
||||||
directory: AbsolutePath.make(input.location.directory),
|
input.location == null
|
||||||
workspaceID:
|
? undefined
|
||||||
input.location.workspaceID === undefined
|
: Location.Ref.make({
|
||||||
? undefined
|
directory: AbsolutePath.make(input.location.directory),
|
||||||
: Workspace.ID.make(input.location.workspaceID),
|
workspaceID:
|
||||||
}),
|
input.location.workspaceID === undefined
|
||||||
}),
|
? undefined
|
||||||
|
: Workspace.ID.make(input.location.workspaceID),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
get: (input) => run(host.session.get({ sessionID: Session.ID.make(input.sessionID) })),
|
get: (input) => run(host.session.get({ sessionID: Session.ID.make(input.sessionID) })),
|
||||||
prompt: (input) =>
|
prompt: (input) =>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export interface Interface {
|
|||||||
| "synthetic"
|
| "synthetic"
|
||||||
| "wait"
|
| "wait"
|
||||||
>
|
>
|
||||||
readonly job: Pick<Job.Interface, "start" | "wait" | "block" | "background" | "cancel">
|
readonly job: Pick<Job.Interface, "get" | "start" | "wait" | "block" | "background" | "cancel">
|
||||||
readonly location: {
|
readonly location: {
|
||||||
readonly agent: {
|
readonly agent: {
|
||||||
readonly list: (
|
readonly list: (
|
||||||
@@ -69,6 +69,7 @@ export const layerWithCell = (cell: Cell) =>
|
|||||||
wait: (sessionID) => require(cell, (runtime) => runtime.session.wait(sessionID)),
|
wait: (sessionID) => require(cell, (runtime) => runtime.session.wait(sessionID)),
|
||||||
},
|
},
|
||||||
job: {
|
job: {
|
||||||
|
get: (id) => require(cell, (runtime) => runtime.job.get(id)),
|
||||||
start: (input) => require(cell, (runtime) => runtime.job.start(input)),
|
start: (input) => require(cell, (runtime) => runtime.job.start(input)),
|
||||||
wait: (input) => require(cell, (runtime) => runtime.job.wait(input)),
|
wait: (input) => require(cell, (runtime) => runtime.job.wait(input)),
|
||||||
block: (input) => require(cell, (runtime) => runtime.job.block(input)),
|
block: (input) => require(cell, (runtime) => runtime.job.block(input)),
|
||||||
|
|||||||
@@ -1,323 +0,0 @@
|
|||||||
export * as SessionTransfer from "./transfer"
|
|
||||||
|
|
||||||
import { SessionTransfer } from "@opencode-ai/schema/session-transfer"
|
|
||||||
import { Tool } from "@opencode-ai/schema/tool"
|
|
||||||
import { eq, isNotNull, isNull, ne, or } from "drizzle-orm"
|
|
||||||
import { Context, DateTime, Effect, Layer, Schema } from "effect"
|
|
||||||
import path from "path"
|
|
||||||
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
|
|
||||||
import { App } from "../app"
|
|
||||||
import { Bus } from "../bus"
|
|
||||||
import { Database } from "../database/database"
|
|
||||||
import { Location } from "../location"
|
|
||||||
import { Project } from "../project"
|
|
||||||
import { ProjectTable } from "../project/sql"
|
|
||||||
import { AbsolutePath, RelativePath } from "../schema"
|
|
||||||
import { Session } from "../session"
|
|
||||||
import { Slug } from "../util/slug"
|
|
||||||
import { SessionEvent } from "./event"
|
|
||||||
import { SessionMessage } from "./message"
|
|
||||||
import { SessionProjector } from "./projector"
|
|
||||||
import { SessionMessageTable, SessionTable } from "./sql"
|
|
||||||
|
|
||||||
export const Data = SessionTransfer.Data
|
|
||||||
export type Data = SessionTransfer.Data
|
|
||||||
|
|
||||||
export class ImportConflictError extends Schema.TaggedErrorClass<ImportConflictError>()(
|
|
||||||
"SessionTransfer.ImportConflictError",
|
|
||||||
{ sessionID: Session.ID },
|
|
||||||
) {}
|
|
||||||
|
|
||||||
export interface Interface {
|
|
||||||
readonly export: (input: {
|
|
||||||
sessionID: Session.ID
|
|
||||||
sanitize?: boolean
|
|
||||||
}) => Effect.Effect<Data, Session.NotFoundError | Session.MessageDecodeError>
|
|
||||||
readonly import: (input: {
|
|
||||||
data: Data
|
|
||||||
location: Location.Ref
|
|
||||||
}) => Effect.Effect<Session.Info, ImportConflictError>
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionTransfer") {}
|
|
||||||
|
|
||||||
const layer = Layer.effect(
|
|
||||||
Service,
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const app = yield* App.Metadata
|
|
||||||
const bus = yield* Bus.Service
|
|
||||||
const { db } = yield* Database.Service
|
|
||||||
const projects = yield* Project.Service
|
|
||||||
const sessions = yield* Session.Service
|
|
||||||
const encodeMessage = Schema.encodeSync(SessionMessage.Info)
|
|
||||||
|
|
||||||
const persistProject = (project: Project.Resolved) => {
|
|
||||||
const vcs = project.vcs?.type
|
|
||||||
return db
|
|
||||||
.insert(ProjectTable)
|
|
||||||
.values({ id: project.id, worktree: project.canonical, vcs, sandboxes: [] })
|
|
||||||
.onConflictDoUpdate({
|
|
||||||
target: ProjectTable.id,
|
|
||||||
set: { worktree: project.canonical, vcs: vcs ?? null },
|
|
||||||
setWhere: or(
|
|
||||||
ne(ProjectTable.worktree, project.canonical),
|
|
||||||
vcs ? or(isNull(ProjectTable.vcs), ne(ProjectTable.vcs, vcs)) : isNotNull(ProjectTable.vcs),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
.run()
|
|
||||||
.pipe(Effect.orDie)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Service.of({
|
|
||||||
export: Effect.fn("SessionTransfer.export")(function* (input) {
|
|
||||||
const data = {
|
|
||||||
info: yield* sessions.get(input.sessionID),
|
|
||||||
messages: yield* sessions.messages({ sessionID: input.sessionID, order: "asc" }),
|
|
||||||
}
|
|
||||||
return input.sanitize ? sanitize(data) : data
|
|
||||||
}),
|
|
||||||
import: Effect.fn("SessionTransfer.import")(function* (input) {
|
|
||||||
const sessionID = input.data.info.id
|
|
||||||
const recorded = yield* db
|
|
||||||
.select({ id: SessionTable.id })
|
|
||||||
.from(SessionTable)
|
|
||||||
.where(eq(SessionTable.id, sessionID))
|
|
||||||
.get()
|
|
||||||
.pipe(Effect.orDie)
|
|
||||||
if (recorded) return yield* new ImportConflictError({ sessionID })
|
|
||||||
const project = yield* projects.resolve(input.location.directory)
|
|
||||||
yield* persistProject(project)
|
|
||||||
const messages = input.data.messages.map((message, index) => {
|
|
||||||
const encoded = encodeMessage(message)
|
|
||||||
const { id: _, type, ...data } = encoded
|
|
||||||
return {
|
|
||||||
id: message.id,
|
|
||||||
session_id: sessionID,
|
|
||||||
type,
|
|
||||||
seq: index + 1,
|
|
||||||
time_created: DateTime.toEpochMillis(message.time.created),
|
|
||||||
data,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
yield* bus
|
|
||||||
.publish(
|
|
||||||
SessionEvent.Created,
|
|
||||||
{
|
|
||||||
sessionID,
|
|
||||||
slug: Slug.create(),
|
|
||||||
version: app.version,
|
|
||||||
projectID: project.id,
|
|
||||||
location: input.location,
|
|
||||||
subpath: RelativePath.make(path.relative(project.directory, input.location.directory).replaceAll("\\", "/")),
|
|
||||||
title: input.data.info.title,
|
|
||||||
agent: input.data.info.agent,
|
|
||||||
model: input.data.info.model,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
location: input.location,
|
|
||||||
commit: (seq) =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
if (messages.length > 0) {
|
|
||||||
yield* db.insert(SessionMessageTable).values(messages).run().pipe(Effect.orDie)
|
|
||||||
yield* Bus.reserveSequence(db, sessionID, seq + messages.length)
|
|
||||||
}
|
|
||||||
yield* db
|
|
||||||
.update(SessionTable)
|
|
||||||
.set({
|
|
||||||
cost: input.data.info.cost,
|
|
||||||
tokens_input: input.data.info.tokens.input,
|
|
||||||
tokens_output: input.data.info.tokens.output,
|
|
||||||
tokens_reasoning: input.data.info.tokens.reasoning,
|
|
||||||
tokens_cache_read: input.data.info.tokens.cache.read,
|
|
||||||
tokens_cache_write: input.data.info.tokens.cache.write,
|
|
||||||
time_created: DateTime.toEpochMillis(input.data.info.time.created),
|
|
||||||
time_updated: DateTime.toEpochMillis(input.data.info.time.updated),
|
|
||||||
time_archived: input.data.info.time.archived
|
|
||||||
? DateTime.toEpochMillis(input.data.info.time.archived)
|
|
||||||
: null,
|
|
||||||
})
|
|
||||||
.where(eq(SessionTable.id, sessionID))
|
|
||||||
.run()
|
|
||||||
.pipe(Effect.orDie)
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.pipe(
|
|
||||||
Effect.catchDefect((defect) =>
|
|
||||||
defect instanceof SessionProjector.SessionAlreadyProjected
|
|
||||||
? Effect.fail(new ImportConflictError({ sessionID }))
|
|
||||||
: Effect.die(defect),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return yield* sessions.get(sessionID).pipe(Effect.orDie)
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
export const node = makeGlobalNode({
|
|
||||||
service: Service,
|
|
||||||
layer,
|
|
||||||
deps: [App.node, Bus.node, Database.node, Project.node, Session.node],
|
|
||||||
})
|
|
||||||
|
|
||||||
function redact(kind: string, id: string, value: string) {
|
|
||||||
return value.trim() ? `[redacted:${kind}:${id}]` : value
|
|
||||||
}
|
|
||||||
|
|
||||||
function metadata(kind: string, id: string, value: Readonly<Record<string, unknown>> | undefined) {
|
|
||||||
if (!value) return value
|
|
||||||
return Object.keys(value).length > 0 ? { redacted: `${kind}:${id}` } : value
|
|
||||||
}
|
|
||||||
|
|
||||||
function sanitize(data: Data): Data {
|
|
||||||
return {
|
|
||||||
info: {
|
|
||||||
...data.info,
|
|
||||||
title: data.info.title === undefined ? undefined : redact("session-title", data.info.id, data.info.title),
|
|
||||||
location: {
|
|
||||||
...data.info.location,
|
|
||||||
directory: AbsolutePath.make(`/${redact("session-directory", data.info.id, data.info.location.directory)}`),
|
|
||||||
},
|
|
||||||
revert: data.info.revert
|
|
||||||
? {
|
|
||||||
...data.info.revert,
|
|
||||||
files: data.info.revert.files?.map((file, index) => ({
|
|
||||||
...file,
|
|
||||||
file: redact("revert-file", String(index), file.file),
|
|
||||||
patch: redact("revert-patch", String(index), file.patch),
|
|
||||||
})),
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
},
|
|
||||||
messages: data.messages.map(sanitizeMessage),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function sanitizeMessage(message: SessionMessage.Info): SessionMessage.Info {
|
|
||||||
const meta = metadata("message-metadata", message.id, message.metadata)
|
|
||||||
if (message.type === "user")
|
|
||||||
return {
|
|
||||||
...message,
|
|
||||||
metadata: meta,
|
|
||||||
text: redact("text", message.id, message.text),
|
|
||||||
files: message.files?.map((file, index) => ({
|
|
||||||
...file,
|
|
||||||
data: "",
|
|
||||||
source: { type: "inline" },
|
|
||||||
name: file.name === undefined ? undefined : redact("file-name", String(index), file.name),
|
|
||||||
description:
|
|
||||||
file.description === undefined ? undefined : redact("file-description", String(index), file.description),
|
|
||||||
mention: file.mention
|
|
||||||
? { ...file.mention, text: redact("file-mention", String(index), file.mention.text) }
|
|
||||||
: undefined,
|
|
||||||
})),
|
|
||||||
agents: message.agents?.map((agent, index) => ({
|
|
||||||
...agent,
|
|
||||||
name: redact("agent-name", String(index), agent.name),
|
|
||||||
mention: agent.mention
|
|
||||||
? { ...agent.mention, text: redact("agent-mention", String(index), agent.mention.text) }
|
|
||||||
: undefined,
|
|
||||||
})),
|
|
||||||
}
|
|
||||||
if (message.type === "synthetic")
|
|
||||||
return {
|
|
||||||
...message,
|
|
||||||
metadata: meta,
|
|
||||||
text: redact("synthetic", message.id, message.text),
|
|
||||||
description:
|
|
||||||
message.description === undefined
|
|
||||||
? undefined
|
|
||||||
: redact("synthetic-description", message.id, message.description),
|
|
||||||
}
|
|
||||||
if (message.type === "system")
|
|
||||||
return { ...message, metadata: meta, text: redact("system", message.id, message.text) }
|
|
||||||
if (message.type === "skill") return { ...message, metadata: meta, text: redact("skill", message.id, message.text) }
|
|
||||||
if (message.type === "shell")
|
|
||||||
return {
|
|
||||||
...message,
|
|
||||||
metadata: meta,
|
|
||||||
command: redact("shell-command", message.id, message.command),
|
|
||||||
output: message.output
|
|
||||||
? { ...message.output, output: redact("shell-output", message.id, message.output.output) }
|
|
||||||
: undefined,
|
|
||||||
}
|
|
||||||
if (message.type === "assistant")
|
|
||||||
return {
|
|
||||||
...message,
|
|
||||||
metadata: meta,
|
|
||||||
content: message.content.map((content) => {
|
|
||||||
if (content.type === "text")
|
|
||||||
return {
|
|
||||||
...content,
|
|
||||||
text: redact("text", message.id, content.text),
|
|
||||||
state: content.state ? { redacted: `text-state:${message.id}` } : undefined,
|
|
||||||
}
|
|
||||||
if (content.type === "reasoning")
|
|
||||||
return {
|
|
||||||
...content,
|
|
||||||
text: redact("reasoning", message.id, content.text),
|
|
||||||
state: content.state ? { redacted: `reasoning-state:${message.id}` } : undefined,
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...content,
|
|
||||||
providerState: content.providerState ? { redacted: `tool-provider-state:${message.id}` } : undefined,
|
|
||||||
providerResultState: content.providerResultState
|
|
||||||
? { redacted: `tool-provider-result-state:${message.id}` }
|
|
||||||
: undefined,
|
|
||||||
state: sanitizeToolState(message.id, content.state),
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
if (message.type === "compaction") {
|
|
||||||
if (message.status === "failed")
|
|
||||||
return {
|
|
||||||
...message,
|
|
||||||
metadata: meta,
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...message,
|
|
||||||
metadata: meta,
|
|
||||||
summary: redact("compaction-summary", message.id, message.summary),
|
|
||||||
recent: redact("compaction-recent", message.id, message.recent),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { ...message, metadata: meta }
|
|
||||||
}
|
|
||||||
|
|
||||||
function sanitizeToolState(id: string, state: SessionMessage.ToolState): SessionMessage.ToolState {
|
|
||||||
if (state.status === "streaming") return { ...state, input: redact("tool-input", id, state.input) }
|
|
||||||
if (state.status === "running")
|
|
||||||
return { ...state, input: { redacted: `tool-input:${id}` }, metadata: { redacted: `tool-metadata:${id}` } }
|
|
||||||
const meta = state.metadata === undefined ? undefined : { redacted: `tool-metadata:${id}` }
|
|
||||||
if (state.status === "completed")
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
input: { redacted: `tool-input:${id}` },
|
|
||||||
content: [
|
|
||||||
sanitizeToolContent(id, state.content[0]),
|
|
||||||
...state.content.slice(1).map((item) => sanitizeToolContent(id, item)),
|
|
||||||
],
|
|
||||||
metadata: meta,
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
input: { redacted: `tool-input:${id}` },
|
|
||||||
content: state.content
|
|
||||||
? [
|
|
||||||
sanitizeToolContent(id, state.content[0]),
|
|
||||||
...state.content.slice(1).map((item) => sanitizeToolContent(id, item)),
|
|
||||||
]
|
|
||||||
: undefined,
|
|
||||||
metadata: meta,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function sanitizeToolContent(id: string, content: Tool.Content): Tool.Content {
|
|
||||||
if (content.type === "text") return { ...content, text: redact("tool-output", id, content.text) }
|
|
||||||
return {
|
|
||||||
...content,
|
|
||||||
uri: redact("tool-file-uri", id, content.uri),
|
|
||||||
name: content.name === undefined ? undefined : redact("tool-file-name", id, content.name),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -23,6 +23,10 @@ export const Input = Schema.Struct({
|
|||||||
agent: Schema.String.annotate({ description: "The type of specialized agent to use for this task" }),
|
agent: Schema.String.annotate({ description: "The type of specialized agent to use for this task" }),
|
||||||
description: Schema.String.annotate({ description: "A short 3-5 word label for the task, displayed to the user" }),
|
description: Schema.String.annotate({ description: "A short 3-5 word label for the task, displayed to the user" }),
|
||||||
prompt: Schema.String.annotate({ description: "The task for the subagent to perform" }),
|
prompt: Schema.String.annotate({ description: "The task for the subagent to perform" }),
|
||||||
|
sessionID: Schema.optionalKey(SessionSchema.ID).annotate({
|
||||||
|
description:
|
||||||
|
"Continue a specific previous subagent conversation by passing its sessionID. Calls without a sessionID start a new conversation.",
|
||||||
|
}),
|
||||||
background: Schema.optionalKey(Schema.Boolean).annotate({
|
background: Schema.optionalKey(Schema.Boolean).annotate({
|
||||||
description:
|
description:
|
||||||
"Run the subagent in the background and return immediately. You will be notified when it completes. DO NOT sleep, poll, or proactively check on its progress.",
|
"Run the subagent in the background and return immediately. You will be notified when it completes. DO NOT sleep, poll, or proactively check on its progress.",
|
||||||
@@ -36,7 +40,8 @@ export const Output = Schema.Struct({
|
|||||||
})
|
})
|
||||||
export const description = [
|
export const description = [
|
||||||
"Spawns an agent in a child session to work on the specified task.",
|
"Spawns an agent in a child session to work on the specified task.",
|
||||||
"Include all relevant context and instructions in the prompt because the child starts with fresh context.",
|
"The output includes a sessionID you can pass back later to continue that specific conversation with the subagent.",
|
||||||
|
"New child sessions start with fresh context, so include all relevant context and instructions when you don't pass a sessionID.",
|
||||||
"Foreground (default) runs the subagent to completion and returns its final response.",
|
"Foreground (default) runs the subagent to completion and returns its final response.",
|
||||||
"Background mode (background=true) launches it asynchronously and returns immediately; you are notified when it finishes.",
|
"Background mode (background=true) launches it asynchronously and returns immediately; you are notified when it finishes.",
|
||||||
"Use background only for independent work that can run while you continue elsewhere.",
|
"Use background only for independent work that can run while you continue elsewhere.",
|
||||||
@@ -77,7 +82,7 @@ export const Plugin = {
|
|||||||
) {
|
) {
|
||||||
yield* runtime.session.synthetic({
|
yield* runtime.session.synthetic({
|
||||||
sessionID: parentID,
|
sessionID: parentID,
|
||||||
text: `<subagent id="${childID}" state="${state}" description="${description}">\n${text}\n</subagent>`,
|
text: `<subagent sessionID="${childID}" state="${state}" description="${description}">\n${text}\n</subagent>`,
|
||||||
description,
|
description,
|
||||||
metadata: { source: "subagent", childID, agent, state },
|
metadata: { source: "subagent", childID, agent, state },
|
||||||
})
|
})
|
||||||
@@ -164,22 +169,51 @@ export const Plugin = {
|
|||||||
})
|
})
|
||||||
.pipe(Effect.mapError((error) => new ToolFailure({ message: `Subagent denied: ${agent.id}`, error })))
|
.pipe(Effect.mapError((error) => new ToolFailure({ message: `Subagent denied: ${agent.id}`, error })))
|
||||||
|
|
||||||
|
if (input.sessionID !== undefined && input.background === true)
|
||||||
|
return yield* new ToolFailure({
|
||||||
|
message: "Continuing a subagent in the background is not implemented yet",
|
||||||
|
})
|
||||||
|
|
||||||
|
const existing =
|
||||||
|
input.sessionID === undefined
|
||||||
|
? undefined
|
||||||
|
: yield* runtime.session.get(input.sessionID).pipe(
|
||||||
|
Effect.mapError(
|
||||||
|
(error) =>
|
||||||
|
new ToolFailure({ message: `Subagent session not found: ${input.sessionID}`, error }),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if (existing !== undefined && existing.parentID !== context.sessionID)
|
||||||
|
return yield* new ToolFailure({
|
||||||
|
message: `Session ${existing.id} is not a child of the current session`,
|
||||||
|
})
|
||||||
|
if (existing !== undefined && existing.agent !== agent.id)
|
||||||
|
return yield* new ToolFailure({
|
||||||
|
message: `Session ${existing.id} belongs to agent ${existing.agent ?? "unknown"}, not ${agent.id}`,
|
||||||
|
})
|
||||||
|
if (existing !== undefined && (yield* runtime.job.get(existing.id))?.status === "running")
|
||||||
|
return yield* new ToolFailure({
|
||||||
|
message: "Continuing a running subagent is not implemented yet",
|
||||||
|
})
|
||||||
|
|
||||||
// Model selection is policy/config/session state, not an LLM-facing tool argument.
|
// Model selection is policy/config/session state, not an LLM-facing tool argument.
|
||||||
const model = agent.model ?? parent.model
|
const model = agent.model ?? parent.model
|
||||||
const child = yield* runtime.session
|
const child =
|
||||||
.create({
|
existing ??
|
||||||
parentID: context.sessionID,
|
(yield* runtime.session
|
||||||
title: input.description,
|
.create({
|
||||||
agent: Agent.ID.make(input.agent),
|
parentID: context.sessionID,
|
||||||
model,
|
title: input.description,
|
||||||
// TODO(opencode kkdvxn): derive restricted subagent permissions from the parent
|
agent: Agent.ID.make(input.agent),
|
||||||
// session (V1 deriveSubagentSessionPermission). MVP uses the agent's own permissions.
|
model,
|
||||||
})
|
// TODO(opencode kkdvxn): derive restricted subagent permissions from the parent
|
||||||
.pipe(
|
// session (V1 deriveSubagentSessionPermission). MVP uses the agent's own permissions.
|
||||||
Effect.mapError(
|
})
|
||||||
(error) => new ToolFailure({ message: `Parent session not found: ${context.sessionID}`, error }),
|
.pipe(
|
||||||
),
|
Effect.mapError(
|
||||||
)
|
(error) => new ToolFailure({ message: `Parent session not found: ${context.sessionID}`, error }),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
|
||||||
const background = input.background === true
|
const background = input.background === true
|
||||||
yield* context.progress({
|
yield* context.progress({
|
||||||
@@ -190,7 +224,10 @@ export const Plugin = {
|
|||||||
// The child session owns its agent/model (set at create); prompt only admits input.
|
// The child session owns its agent/model (set at create); prompt only admits input.
|
||||||
yield* runtime.session.prompt({
|
yield* runtime.session.prompt({
|
||||||
sessionID: child.id,
|
sessionID: child.id,
|
||||||
text: ["You are a subagent spawned by another session.", input.prompt].join("\n"),
|
text:
|
||||||
|
existing === undefined
|
||||||
|
? ["You are a subagent spawned by another session.", input.prompt].join("\n")
|
||||||
|
: input.prompt,
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
yield* runtime.session.resume(child.id)
|
yield* runtime.session.resume(child.id)
|
||||||
@@ -238,7 +275,10 @@ export const Plugin = {
|
|||||||
}).pipe(
|
}).pipe(
|
||||||
Effect.map((output) => ({
|
Effect.map((output) => ({
|
||||||
output,
|
output,
|
||||||
content: output.output,
|
content:
|
||||||
|
output.status === "completed"
|
||||||
|
? `<subagent sessionID="${output.sessionID}" state="completed">\n${output.output}\n</subagent>`
|
||||||
|
: output.output,
|
||||||
metadata: { sessionID: output.sessionID, status: output.status },
|
metadata: { sessionID: output.sessionID, status: output.status },
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ export * as WebSearchTool from "./websearch"
|
|||||||
|
|
||||||
import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin"
|
import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin"
|
||||||
import { ToolFailure } from "@opencode-ai/ai"
|
import { ToolFailure } from "@opencode-ai/ai"
|
||||||
import { Effect, Schema, Semaphore } from "effect"
|
import { Effect, Schema } from "effect"
|
||||||
import { Form } from "../../form"
|
import { Form } from "../../form"
|
||||||
import { KV } from "../../kv"
|
import { KV } from "../../kv"
|
||||||
import { Permission } from "../../permission"
|
import { Permission } from "../../permission"
|
||||||
@@ -10,7 +10,6 @@ import { WebSearch } from "../../websearch"
|
|||||||
|
|
||||||
export const name = "websearch"
|
export const name = "websearch"
|
||||||
export const NO_RESULTS = "No search results found. Please try a different query."
|
export const NO_RESULTS = "No search results found. Please try a different query."
|
||||||
const providerSelectionLock = Semaphore.makeUnsafe(1)
|
|
||||||
|
|
||||||
export const description = `Search the web using the user's selected search integration. Use this for current information beyond knowledge cutoff.
|
export const description = `Search the web using the user's selected search integration. Use this for current information beyond knowledge cutoff.
|
||||||
|
|
||||||
@@ -30,7 +29,6 @@ export const Plugin = {
|
|||||||
const permission = yield* Permission.Service
|
const permission = yield* Permission.Service
|
||||||
const forms = yield* Form.Service
|
const forms = yield* Form.Service
|
||||||
const kv = yield* KV.Service
|
const kv = yield* KV.Service
|
||||||
const websearch = yield* WebSearch.Service
|
|
||||||
|
|
||||||
yield* ctx.tool
|
yield* ctx.tool
|
||||||
.transform((draft) =>
|
.transform((draft) =>
|
||||||
@@ -51,90 +49,70 @@ export const Plugin = {
|
|||||||
agent: context.agent,
|
agent: context.agent,
|
||||||
source: { type: "tool", messageID: context.messageID, id: context.id },
|
source: { type: "tool", messageID: context.messageID, id: context.id },
|
||||||
})
|
})
|
||||||
const search = (): Effect.Effect<Effect.Success<ReturnType<typeof ctx.websearch.query>>, unknown> =>
|
const result = yield* ctx.websearch.query(input).pipe(
|
||||||
ctx.websearch.query(input).pipe(
|
Effect.catch((error) => {
|
||||||
Effect.catch((error) => {
|
if (!Schema.is(WebSearch.ProviderRequiredError)(error)) return Effect.fail(error)
|
||||||
if (!Schema.is(WebSearch.ProviderRequiredError)(error)) return Effect.fail(error)
|
return Effect.gen(function* () {
|
||||||
return providerSelectionLock
|
const providers = (yield* ctx.websearch.providers()).data
|
||||||
.withPermit(
|
const defaultProvider = providers[0]
|
||||||
Effect.gen(function* () {
|
if (!defaultProvider) return yield* new WebSearch.ProviderRequiredError()
|
||||||
if (yield* websearch.default()) return yield* Effect.void
|
const response = yield* forms.ask({
|
||||||
const providers = (yield* ctx.websearch.providers()).data
|
sessionID: context.sessionID,
|
||||||
const defaultProvider = providers[0]
|
title: "Web Search",
|
||||||
if (!defaultProvider) return yield* new WebSearch.ProviderRequiredError()
|
metadata: { kind: "websearch.provider" },
|
||||||
const response = yield* forms.ask({
|
fields: [
|
||||||
|
{
|
||||||
|
key: "choice",
|
||||||
|
description: "Allow OpenCode to search the web for up-to-date information?",
|
||||||
|
type: "string",
|
||||||
|
required: true,
|
||||||
|
custom: false,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "allow",
|
||||||
|
label: `Allow web search via ${defaultProvider.name}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "choose",
|
||||||
|
label: "Choose another provider",
|
||||||
|
},
|
||||||
|
{ value: "disable", label: "Disable web search" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
if (response.status === "cancelled") return yield* Effect.fail(new Error("Web search cancelled"))
|
||||||
|
if (response.answer.choice === "disable") {
|
||||||
|
yield* kv.set("websearch:provider", false)
|
||||||
|
return yield* new WebSearch.DisabledError()
|
||||||
|
}
|
||||||
|
const selection =
|
||||||
|
response.answer.choice === "choose"
|
||||||
|
? yield* forms.ask({
|
||||||
sessionID: context.sessionID,
|
sessionID: context.sessionID,
|
||||||
title: "Web Search",
|
title: "Choose a web search provider",
|
||||||
metadata: { kind: "websearch.provider" },
|
metadata: { kind: "websearch.provider" },
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
key: "choice",
|
key: "provider",
|
||||||
description: "Allow OpenCode to search the web for up-to-date information?",
|
description: "Choose a provider for web search.",
|
||||||
type: "string",
|
type: "string",
|
||||||
required: true,
|
required: true,
|
||||||
custom: false,
|
custom: false,
|
||||||
options: [
|
options: providers.map((provider) => ({ value: provider.id, label: provider.name })),
|
||||||
{
|
|
||||||
value: "allow",
|
|
||||||
label: `Allow web search via ${defaultProvider.name}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "choose",
|
|
||||||
label: "Choose another provider",
|
|
||||||
},
|
|
||||||
{ value: "disable", label: "Disable web search" },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
if (response.status === "cancelled")
|
: undefined
|
||||||
return yield* Effect.fail(new Error("Web search cancelled"))
|
if (selection?.status === "cancelled") return yield* Effect.fail(new Error("Web search cancelled"))
|
||||||
if (response.answer.choice === "disable") {
|
const providerID = selection?.answer.provider ?? defaultProvider.id
|
||||||
yield* kv.set("websearch:provider", false)
|
if (typeof providerID !== "string" || !providers.some((provider) => provider.id === providerID))
|
||||||
return yield* new WebSearch.DisabledError()
|
return yield* new WebSearch.ProviderRequiredError()
|
||||||
}
|
yield* kv.set("websearch:provider", providerID)
|
||||||
const selection =
|
return yield* ctx.websearch.query(input)
|
||||||
response.answer.choice === "choose"
|
})
|
||||||
? yield* forms.ask({
|
}),
|
||||||
sessionID: context.sessionID,
|
)
|
||||||
title: "Choose a web search provider",
|
|
||||||
metadata: { kind: "websearch.provider" },
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
key: "provider",
|
|
||||||
description: "Choose a provider for web search.",
|
|
||||||
type: "string",
|
|
||||||
required: true,
|
|
||||||
custom: false,
|
|
||||||
options: providers.map((provider) => ({
|
|
||||||
value: provider.id,
|
|
||||||
label: provider.name,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
: undefined
|
|
||||||
if (selection?.status === "cancelled")
|
|
||||||
return yield* Effect.fail(new Error("Web search cancelled"))
|
|
||||||
const providerID = selection?.answer.provider ?? defaultProvider.id
|
|
||||||
if (
|
|
||||||
typeof providerID !== "string" ||
|
|
||||||
!providers.some((provider) => provider.id === providerID)
|
|
||||||
)
|
|
||||||
return yield* new WebSearch.ProviderRequiredError()
|
|
||||||
return yield* kv.set("websearch:provider", providerID)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.pipe(
|
|
||||||
Effect.timeoutOrElse({
|
|
||||||
duration: "1 minute",
|
|
||||||
orElse: () => Effect.fail(new Error("Web search cancelled")),
|
|
||||||
}),
|
|
||||||
Effect.andThen(Effect.suspend(search)),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
const result = yield* search()
|
|
||||||
const output = {
|
const output = {
|
||||||
provider: result.data.providerID,
|
provider: result.data.providerID,
|
||||||
results: result.data.results,
|
results: result.data.results,
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
|||||||
import { FSUtil } from "@opencode-ai/util/fs-util"
|
import { FSUtil } from "@opencode-ai/util/fs-util"
|
||||||
import { Global } from "@opencode-ai/util/global"
|
import { Global } from "@opencode-ai/util/global"
|
||||||
import { Permission } from "@opencode-ai/core/permission"
|
import { Permission } from "@opencode-ai/core/permission"
|
||||||
import { AgentPlugin } from "@opencode-ai/core/plugin/agent"
|
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
import { ConfigMigrateV1 } from "@opencode-ai/core/v1/config/migrate"
|
import { ConfigMigrateV1 } from "@opencode-ai/core/v1/config/migrate"
|
||||||
import { advance, drain } from "../lib/clock"
|
import { advance, drain } from "../lib/clock"
|
||||||
@@ -51,11 +50,6 @@ describe("ConfigAgentPlugin.Plugin", () => {
|
|||||||
it.effect("matches Windows paths against home-relative permissions", () =>
|
it.effect("matches Windows paths against home-relative permissions", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const permissions = yield* loadHomePermissions("C:\\Users\\test")
|
const permissions = yield* loadHomePermissions("C:\\Users\\test")
|
||||||
expect(permissions).toContainEqual({
|
|
||||||
action: "external_directory",
|
|
||||||
resource: "C:\\Users\\test\\p\\**",
|
|
||||||
effect: "allow",
|
|
||||||
})
|
|
||||||
expect(
|
expect(
|
||||||
Permission.evaluate("external_directory", "C:\\Users\\test\\p\\opencode\\src\\*", permissions).effect,
|
Permission.evaluate("external_directory", "C:\\Users\\test\\p\\opencode\\src\\*", permissions).effect,
|
||||||
).toBe("allow")
|
).toBe("allow")
|
||||||
@@ -65,96 +59,6 @@ describe("ConfigAgentPlugin.Plugin", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("applies remote permission defaults before explicit global and build rules", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const agents = yield* Agent.Service
|
|
||||||
const global = yield* Global.Service
|
|
||||||
yield* AgentPlugin.Plugin.effect(host({ agent: agentHost(agents) }))
|
|
||||||
|
|
||||||
const entries = [
|
|
||||||
new Document({
|
|
||||||
type: "document",
|
|
||||||
info: decode(
|
|
||||||
ConfigMigrateV1.migrate({
|
|
||||||
permission: {
|
|
||||||
bash: "ask",
|
|
||||||
edit: "ask",
|
|
||||||
webfetch: "ask",
|
|
||||||
read: {
|
|
||||||
"*": "allow",
|
|
||||||
"*.env": "deny",
|
|
||||||
"*.env.*": "deny",
|
|
||||||
"*.env.example": "allow",
|
|
||||||
"*.dev.vars": "deny",
|
|
||||||
"~/.local/share/opencode/mcp-auth.json": "deny",
|
|
||||||
"$HOME/.local/share/opencode/mcp-auth.json": "deny",
|
|
||||||
},
|
|
||||||
external_directory: {
|
|
||||||
"*": "ask",
|
|
||||||
"~/.local/share/opencode/*": "deny",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
new Document({
|
|
||||||
type: "document",
|
|
||||||
info: decode({
|
|
||||||
permissions: [{ action: "*", resource: "*", effect: "allow" }],
|
|
||||||
agents: {
|
|
||||||
build: {
|
|
||||||
permissions: [
|
|
||||||
{ action: "external_directory", resource: "*", effect: "allow" },
|
|
||||||
{
|
|
||||||
action: "external_directory",
|
|
||||||
resource: "~/.local/share/opencode/*",
|
|
||||||
effect: "deny",
|
|
||||||
},
|
|
||||||
{ action: "read", resource: "*.env", effect: "deny" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
|
|
||||||
yield* ConfigAgentPlugin.Plugin.effect(host({ agent: agentHost(agents) })).pipe(
|
|
||||||
Effect.provide(Config.testLayer(entries)),
|
|
||||||
)
|
|
||||||
|
|
||||||
const build = yield* agents.get(Agent.defaultID)
|
|
||||||
if (!build) throw new Error("expected configured build agent")
|
|
||||||
const opencodeData = path.join(global.home, ".local", "share", "opencode", "*")
|
|
||||||
const mcpAuth = path.join(global.home, ".local", "share", "opencode", "mcp-auth.json")
|
|
||||||
expect(build.permissions).toEqual([
|
|
||||||
...defaultPermissions(global),
|
|
||||||
{ action: "question", resource: "*", effect: "allow" },
|
|
||||||
{ action: "shell", resource: "*", effect: "ask" },
|
|
||||||
{ action: "edit", resource: "*", effect: "ask" },
|
|
||||||
{ action: "webfetch", resource: "*", effect: "ask" },
|
|
||||||
{ action: "read", resource: "*", effect: "allow" },
|
|
||||||
{ action: "read", resource: "*.env", effect: "deny" },
|
|
||||||
{ action: "read", resource: "*.env.*", effect: "deny" },
|
|
||||||
{ action: "read", resource: "*.env.example", effect: "allow" },
|
|
||||||
{ action: "read", resource: "*.dev.vars", effect: "deny" },
|
|
||||||
{ action: "read", resource: mcpAuth, effect: "deny" },
|
|
||||||
{ action: "read", resource: mcpAuth, effect: "deny" },
|
|
||||||
{ action: "external_directory", resource: "*", effect: "ask" },
|
|
||||||
{ action: "external_directory", resource: opencodeData, effect: "deny" },
|
|
||||||
{ action: "*", resource: "*", effect: "allow" },
|
|
||||||
{ action: "external_directory", resource: "*", effect: "allow" },
|
|
||||||
{ action: "external_directory", resource: opencodeData, effect: "deny" },
|
|
||||||
{ action: "read", resource: "*.env", effect: "deny" },
|
|
||||||
])
|
|
||||||
expect(Permission.evaluate("shell", "bun test", build.permissions).effect).toBe("allow")
|
|
||||||
expect(Permission.evaluate("edit", "src/index.ts", build.permissions).effect).toBe("allow")
|
|
||||||
expect(Permission.evaluate("webfetch", "https://example.com", build.permissions).effect).toBe("allow")
|
|
||||||
expect(Permission.evaluate("read", ".env", build.permissions).effect).toBe("deny")
|
|
||||||
expect(Permission.evaluate("external_directory", opencodeData, build.permissions).effect).toBe("deny")
|
|
||||||
expect(Permission.evaluate("external_directory", "/outside/*", build.permissions).effect).toBe("allow")
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.effect("applies all global permissions before agent-specific permissions", () =>
|
it.effect("applies all global permissions before agent-specific permissions", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const agents = yield* Agent.Service
|
const agents = yield* Agent.Service
|
||||||
|
|||||||
@@ -307,7 +307,7 @@ describe("Config", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.live("loads authenticated wellknown config before user configuration", () =>
|
it.live("loads authenticated wellknown config below project config", () =>
|
||||||
Effect.acquireUseRelease(
|
Effect.acquireUseRelease(
|
||||||
Effect.promise(() => tmpdir()),
|
Effect.promise(() => tmpdir()),
|
||||||
(tmp) =>
|
(tmp) =>
|
||||||
@@ -370,13 +370,7 @@ describe("Config", () => {
|
|||||||
return yield* Effect.gen(function* () {
|
return yield* Effect.gen(function* () {
|
||||||
const config = yield* Config.Service
|
const config = yield* Config.Service
|
||||||
const bus = yield* Bus.Service
|
const bus = yield* Bus.Service
|
||||||
const initial = yield* config.entries()
|
expect(Config.latest(yield* config.entries(), "shell")).toBe("project")
|
||||||
expect(Config.latest(initial, "shell")).toBe("project")
|
|
||||||
expect(
|
|
||||||
initial.flatMap((entry) =>
|
|
||||||
entry.type === "document" && entry.info.shell ? [entry.info.shell] : [],
|
|
||||||
),
|
|
||||||
).toEqual(["secret", "global", "project"])
|
|
||||||
const updated = yield* bus
|
const updated = yield* bus
|
||||||
.subscribe(Event.Updated)
|
.subscribe(Event.Updated)
|
||||||
.pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
|
.pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
|
||||||
@@ -384,13 +378,7 @@ describe("Config", () => {
|
|||||||
key = "next"
|
key = "next"
|
||||||
yield* bus.publish(Integration.Event.ConnectionUpdated, { integrationID })
|
yield* bus.publish(Integration.Event.ConnectionUpdated, { integrationID })
|
||||||
expect(yield* Fiber.join(updated)).toHaveLength(1)
|
expect(yield* Fiber.join(updated)).toHaveLength(1)
|
||||||
const refreshed = yield* config.entries()
|
expect(Config.latest(yield* config.entries(), "shell")).toBe("project")
|
||||||
expect(Config.latest(refreshed, "shell")).toBe("project")
|
|
||||||
expect(
|
|
||||||
refreshed.flatMap((entry) =>
|
|
||||||
entry.type === "document" && entry.info.shell ? [entry.info.shell] : [],
|
|
||||||
),
|
|
||||||
).toEqual(["next", "global", "project"])
|
|
||||||
}).pipe(
|
}).pipe(
|
||||||
Effect.provide(testLayer(project, global, project, undefined, undefined, credentialNode, wellknownNode)),
|
Effect.provide(testLayer(project, global, project, undefined, undefined, credentialNode, wellknownNode)),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -237,11 +237,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
|
|||||||
models: {
|
models: {
|
||||||
chat: {
|
chat: {
|
||||||
name: "First",
|
name: "First",
|
||||||
compatibility: {
|
compatibility: { reasoningField: "vendor_reasoning" },
|
||||||
reasoningField: "vendor_reasoning",
|
|
||||||
maxTokensField: "max_completion_tokens",
|
|
||||||
requireFinishReason: false,
|
|
||||||
},
|
|
||||||
capabilities: { tools: true, input: ["text"], output: ["text"] },
|
capabilities: { tools: true, input: ["text"], output: ["text"] },
|
||||||
disabled: true,
|
disabled: true,
|
||||||
limit: { context: 100, output: 50 },
|
limit: { context: 100, output: 50 },
|
||||||
@@ -322,11 +318,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
|
|||||||
expect(model.id).toBe(modelID)
|
expect(model.id).toBe(modelID)
|
||||||
expect(model.modelID).toBe(Model.ID.make("api-chat"))
|
expect(model.modelID).toBe(Model.ID.make("api-chat"))
|
||||||
expect(model.name).toBe("Last")
|
expect(model.name).toBe("Last")
|
||||||
expect(model.compatibility).toEqual({
|
expect(model.compatibility).toEqual({ reasoningField: "vendor_reasoning" })
|
||||||
reasoningField: "vendor_reasoning",
|
|
||||||
maxTokensField: "max_completion_tokens",
|
|
||||||
requireFinishReason: false,
|
|
||||||
})
|
|
||||||
expect(model.capabilities).toEqual({ tools: true, input: ["text"], output: ["text"] })
|
expect(model.capabilities).toEqual({ tools: true, input: ["text"], output: ["text"] })
|
||||||
expect(model.enabled).toBe(false)
|
expect(model.enabled).toBe(false)
|
||||||
expect(model.limit).toEqual({ context: 100, output: 75 })
|
expect(model.limit).toEqual({ context: 100, output: 75 })
|
||||||
|
|||||||
@@ -194,11 +194,7 @@ describe("ModelResolver", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const resolved = yield* ModelResolver.fromCatalogModel(
|
const resolved = yield* ModelResolver.fromCatalogModel(
|
||||||
model(Provider.aisdk("@ai-sdk/openai-compatible"), {
|
model(Provider.aisdk("@ai-sdk/openai-compatible"), {
|
||||||
compatibility: {
|
compatibility: { reasoningField: "vendor_reasoning" },
|
||||||
reasoningField: "vendor_reasoning",
|
|
||||||
maxTokensField: "max_completion_tokens",
|
|
||||||
requireFinishReason: false,
|
|
||||||
},
|
|
||||||
settings: {
|
settings: {
|
||||||
apiKey: "settings-secret",
|
apiKey: "settings-secret",
|
||||||
baseURL: "https://compatible.example/v1",
|
baseURL: "https://compatible.example/v1",
|
||||||
@@ -208,8 +204,7 @@ describe("ModelResolver", () => {
|
|||||||
body: {},
|
body: {},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const request = LLM.request({ model: resolved, prompt: "Hello", generation: { maxTokens: 10 } })
|
const request = LLM.request({ model: resolved, prompt: "Hello" })
|
||||||
const prepared = yield* compileRequest(request)
|
|
||||||
const headers = yield* resolved.route.auth.apply({
|
const headers = yield* resolved.route.auth.apply({
|
||||||
request,
|
request,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -221,10 +216,6 @@ describe("ModelResolver", () => {
|
|||||||
expect(headers.authorization).toBe("Bearer settings-secret")
|
expect(headers.authorization).toBe("Bearer settings-secret")
|
||||||
expect(resolved.route.id).toBe("openai-compatible-chat")
|
expect(resolved.route.id).toBe("openai-compatible-chat")
|
||||||
expect(resolved.compatibility?.reasoningField).toBe("vendor_reasoning")
|
expect(resolved.compatibility?.reasoningField).toBe("vendor_reasoning")
|
||||||
expect(resolved.compatibility?.maxTokensField).toBe("max_completion_tokens")
|
|
||||||
expect(resolved.compatibility?.requireFinishReason).toBe(false)
|
|
||||||
expect(prepared.body).toMatchObject({ max_completion_tokens: 10 })
|
|
||||||
expect(prepared.body).not.toHaveProperty("max_tokens")
|
|
||||||
expect(resolved.route.endpoint.baseURL).toBe("https://compatible.example/v1")
|
expect(resolved.route.endpoint.baseURL).toBe("https://compatible.example/v1")
|
||||||
expect(resolved.route.defaults.http?.body).toEqual({})
|
expect(resolved.route.defaults.http?.body).toEqual({})
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import { SessionPending } from "@opencode-ai/core/session/pending"
|
|||||||
import { SessionEvent } from "@opencode-ai/core/session/event"
|
import { SessionEvent } from "@opencode-ai/core/session/event"
|
||||||
import { SessionTable } from "@opencode-ai/core/session/sql"
|
import { SessionTable } from "@opencode-ai/core/session/sql"
|
||||||
import { SessionStore } from "@opencode-ai/core/session/store"
|
import { SessionStore } from "@opencode-ai/core/session/store"
|
||||||
import { SessionTransfer } from "@opencode-ai/core/session/transfer"
|
|
||||||
import { Workspace } from "@opencode-ai/core/workspace"
|
import { Workspace } from "@opencode-ai/core/workspace"
|
||||||
import { testEffect } from "./lib/effect"
|
import { testEffect } from "./lib/effect"
|
||||||
import { tmpdir } from "./fixture/tmpdir"
|
import { tmpdir } from "./fixture/tmpdir"
|
||||||
@@ -38,14 +37,7 @@ const projects = Layer.succeed(
|
|||||||
)
|
)
|
||||||
const it = testEffect(
|
const it = testEffect(
|
||||||
AppNodeBuilder.build(
|
AppNodeBuilder.build(
|
||||||
LayerNode.group([
|
LayerNode.group([Database.node, Bus.node, SessionProjector.node, SessionStore.node, Session.node]),
|
||||||
Database.node,
|
|
||||||
Bus.node,
|
|
||||||
SessionProjector.node,
|
|
||||||
SessionStore.node,
|
|
||||||
Session.node,
|
|
||||||
SessionTransfer.node,
|
|
||||||
]),
|
|
||||||
[
|
[
|
||||||
[Bus.node, Bus.configured({ persist: true })],
|
[Bus.node, Bus.configured({ persist: true })],
|
||||||
[Project.node, projects],
|
[Project.node, projects],
|
||||||
@@ -748,77 +740,3 @@ describe("Session.create", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("SessionTransfer", () => {
|
|
||||||
it.effect("imports projected messages and reserves their aggregate sequence", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const session = yield* Session.Service
|
|
||||||
const transfer = yield* SessionTransfer.Service
|
|
||||||
const bus = yield* Bus.Service
|
|
||||||
const { db } = yield* Database.Service
|
|
||||||
const template = yield* session.create({ location, title: "Exported" })
|
|
||||||
const sessionID = Session.ID.create()
|
|
||||||
const sourceMessageID = SessionMessage.ID.create()
|
|
||||||
const errorMessageID = SessionMessage.ID.create()
|
|
||||||
|
|
||||||
const imported = yield* transfer.import({
|
|
||||||
data: {
|
|
||||||
info: { ...template, id: sessionID },
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
id: sourceMessageID,
|
|
||||||
type: "user",
|
|
||||||
text: "Imported message",
|
|
||||||
time: { created: DateTime.makeUnsafe(100) },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: errorMessageID,
|
|
||||||
type: "compaction",
|
|
||||||
status: "failed",
|
|
||||||
reason: "manual",
|
|
||||||
error: { type: "test_error", message: "Original error" },
|
|
||||||
time: { created: DateTime.makeUnsafe(101) },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
location,
|
|
||||||
})
|
|
||||||
const messages = yield* session.messages({ sessionID, order: "asc" })
|
|
||||||
|
|
||||||
expect(imported).toMatchObject({ id: sessionID, title: "Exported", location })
|
|
||||||
expect(messages).toMatchObject([
|
|
||||||
{ id: sourceMessageID, type: "user", text: "Imported message" },
|
|
||||||
{ id: errorMessageID, type: "compaction", error: { type: "test_error", message: "Original error" } },
|
|
||||||
])
|
|
||||||
expect(yield* Bus.latestSequence(db, sessionID)).toBe(2)
|
|
||||||
expect((yield* transfer.export({ sessionID })).messages).toEqual(messages)
|
|
||||||
expect((yield* transfer.export({ sessionID, sanitize: true })).messages).toMatchObject([
|
|
||||||
{ id: sourceMessageID, text: `[redacted:text:${sourceMessageID}]` },
|
|
||||||
{ id: errorMessageID, error: { type: "test_error", message: "Original error" } },
|
|
||||||
])
|
|
||||||
|
|
||||||
yield* session.prompt({ sessionID, text: "Continue", resume: false })
|
|
||||||
yield* SessionPending.promote(db, bus, sessionID, "steer")
|
|
||||||
|
|
||||||
expect((yield* session.messages({ sessionID, order: "asc" })).map((message) => message.type)).toEqual([
|
|
||||||
"user",
|
|
||||||
"compaction",
|
|
||||||
"user",
|
|
||||||
])
|
|
||||||
expect(yield* Bus.latestSequence(db, sessionID)).toBe(4)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.effect("rejects an existing session ID without changing its transcript", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const session = yield* Session.Service
|
|
||||||
const transfer = yield* SessionTransfer.Service
|
|
||||||
const existing = yield* session.create({ location, title: "Existing" })
|
|
||||||
const exit = yield* Effect.exit(transfer.import({ data: { info: existing, messages: [] }, location }))
|
|
||||||
|
|
||||||
expect(exit._tag).toBe("Failure")
|
|
||||||
expect((yield* session.get(existing.id)).title).toBe("Existing")
|
|
||||||
expect(yield* session.messages({ sessionID: existing.id })).toEqual([])
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ describe("SubagentTool", () => {
|
|||||||
expect(settled).toMatchObject({
|
expect(settled).toMatchObject({
|
||||||
status: "completed",
|
status: "completed",
|
||||||
metadata: { status: "completed" },
|
metadata: { status: "completed" },
|
||||||
content: [{ type: "text", text: childText }],
|
content: [{ type: "text", text: expect.stringContaining(childText) }],
|
||||||
})
|
})
|
||||||
expect(settled.metadata).toEqual({
|
expect(settled.metadata).toEqual({
|
||||||
sessionID: outputSessionID(settled.metadata),
|
sessionID: outputSessionID(settled.metadata),
|
||||||
@@ -283,9 +283,15 @@ describe("SubagentTool", () => {
|
|||||||
expect(settled).toMatchObject({
|
expect(settled).toMatchObject({
|
||||||
status: "completed",
|
status: "completed",
|
||||||
metadata: { status: "completed" },
|
metadata: { status: "completed" },
|
||||||
content: [{ type: "text", text: childText }],
|
content: [{ type: "text", text: expect.stringContaining(childText) }],
|
||||||
})
|
})
|
||||||
const child = yield* sessions.get(outputSessionID(settled.metadata))
|
const child = yield* sessions.get(outputSessionID(settled.metadata))
|
||||||
|
expect(settled.content).toEqual([
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `<subagent sessionID="${child.id}" state="completed">\n${childText}\n</subagent>`,
|
||||||
|
},
|
||||||
|
])
|
||||||
expect(settled.metadata).toEqual({ sessionID: child.id, status: "completed" })
|
expect(settled.metadata).toEqual({ sessionID: child.id, status: "completed" })
|
||||||
expect(progress[0]?.metadata).toEqual({ sessionID: child.id, status: "running" })
|
expect(progress[0]?.metadata).toEqual({ sessionID: child.id, status: "running" })
|
||||||
expect(child).toMatchObject({
|
expect(child).toMatchObject({
|
||||||
@@ -315,6 +321,144 @@ describe("SubagentTool", () => {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.live("continues an existing child session", () =>
|
||||||
|
Effect.acquireRelease(
|
||||||
|
Effect.promise(() => tmpdir()),
|
||||||
|
(dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
|
||||||
|
).pipe(
|
||||||
|
Effect.flatMap((dir) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const location = Location.Ref.make({ directory: AbsolutePath.make(dir.path) })
|
||||||
|
const sessions = yield* Session.Service
|
||||||
|
const parent = yield* sessions.create({ location, model: parentModel })
|
||||||
|
yield* withSubagent(parent.location)
|
||||||
|
const locations = yield* LocationServiceMap.Service
|
||||||
|
const registry = yield* Tool.Service.pipe(Effect.provide(locations.get(parent.location)))
|
||||||
|
yield* waitForTool(registry, SubagentTool.name)
|
||||||
|
|
||||||
|
const first = yield* executeTool(registry, {
|
||||||
|
sessionID: parent.id,
|
||||||
|
...toolIdentity,
|
||||||
|
call: {
|
||||||
|
type: "tool-call",
|
||||||
|
id: "call-subagent-first",
|
||||||
|
name: SubagentTool.name,
|
||||||
|
input: { agent: "reviewer", description: "review", prompt: "review this" },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const childID = outputSessionID(first.metadata)
|
||||||
|
const second = yield* executeTool(registry, {
|
||||||
|
sessionID: parent.id,
|
||||||
|
...toolIdentity,
|
||||||
|
call: {
|
||||||
|
type: "tool-call",
|
||||||
|
id: "call-subagent-second",
|
||||||
|
name: SubagentTool.name,
|
||||||
|
input: {
|
||||||
|
agent: "reviewer",
|
||||||
|
description: "follow up",
|
||||||
|
prompt: "continue this",
|
||||||
|
sessionID: childID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(outputSessionID(second.metadata)).toBe(childID)
|
||||||
|
expect((yield* sessions.list({ parentID: parent.id })).data).toHaveLength(1)
|
||||||
|
expect((yield* sessions.get(childID)).title).toBe("review")
|
||||||
|
expect(
|
||||||
|
(yield* sessions.pending(childID)).flatMap((message) =>
|
||||||
|
message.type === "user" ? [message.data.text] : [],
|
||||||
|
),
|
||||||
|
).toEqual(["You are a subagent spawned by another session.\nreview this", "continue this"])
|
||||||
|
expect(second.content).toEqual([
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `<subagent sessionID="${childID}" state="completed">\n${childText}\n</subagent>`,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.live("rejects background continuation", () =>
|
||||||
|
Effect.acquireRelease(
|
||||||
|
Effect.promise(() => tmpdir()),
|
||||||
|
(dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
|
||||||
|
).pipe(
|
||||||
|
Effect.flatMap((dir) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const location = Location.Ref.make({ directory: AbsolutePath.make(dir.path) })
|
||||||
|
const sessions = yield* Session.Service
|
||||||
|
const parent = yield* sessions.create({ location })
|
||||||
|
const child = yield* sessions.create({
|
||||||
|
parentID: parent.id,
|
||||||
|
title: "review",
|
||||||
|
agent: Agent.ID.make("reviewer"),
|
||||||
|
})
|
||||||
|
yield* withSubagent(parent.location)
|
||||||
|
const locations = yield* LocationServiceMap.Service
|
||||||
|
const registry = yield* Tool.Service.pipe(Effect.provide(locations.get(parent.location)))
|
||||||
|
yield* waitForTool(registry, SubagentTool.name)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
yield* executeTool(registry, {
|
||||||
|
sessionID: parent.id,
|
||||||
|
...toolIdentity,
|
||||||
|
call: {
|
||||||
|
type: "tool-call",
|
||||||
|
id: "call-background-continuation",
|
||||||
|
name: SubagentTool.name,
|
||||||
|
input: {
|
||||||
|
agent: "reviewer",
|
||||||
|
description: "follow up",
|
||||||
|
prompt: "continue this",
|
||||||
|
sessionID: child.id,
|
||||||
|
background: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
status: "error",
|
||||||
|
error: {
|
||||||
|
type: "tool.execution",
|
||||||
|
message: "Continuing a subagent in the background is not implemented yet",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const jobs = yield* Job.Service
|
||||||
|
yield* jobs.start({ id: child.id, type: "subagent", run: Effect.never })
|
||||||
|
yield* jobs.background(child.id)
|
||||||
|
expect(
|
||||||
|
yield* executeTool(registry, {
|
||||||
|
sessionID: parent.id,
|
||||||
|
...toolIdentity,
|
||||||
|
call: {
|
||||||
|
type: "tool-call",
|
||||||
|
id: "call-running-continuation",
|
||||||
|
name: SubagentTool.name,
|
||||||
|
input: {
|
||||||
|
agent: "reviewer",
|
||||||
|
description: "follow up",
|
||||||
|
prompt: "continue this",
|
||||||
|
sessionID: child.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
status: "error",
|
||||||
|
error: {
|
||||||
|
type: "tool.execution",
|
||||||
|
message: "Continuing a running subagent is not implemented yet",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
yield* jobs.cancel(child.id)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
it.live("returns child runner failures as tool errors", () =>
|
it.live("returns child runner failures as tool errors", () =>
|
||||||
Effect.acquireRelease(
|
Effect.acquireRelease(
|
||||||
Effect.promise(() => tmpdir()),
|
Effect.promise(() => tmpdir()),
|
||||||
@@ -393,7 +537,7 @@ describe("SubagentTool", () => {
|
|||||||
expect(settled.content).toEqual([{ type: "text", text: expect.stringContaining(`id: ${childID}`) }])
|
expect(settled.content).toEqual([{ type: "text", text: expect.stringContaining(`id: ${childID}`) }])
|
||||||
|
|
||||||
const admission = Array.from(yield* Fiber.join(admitted))[0]
|
const admission = Array.from(yield* Fiber.join(admitted))[0]
|
||||||
expect(admission?.data.input.data.text).toContain(`<subagent id="${childID}" state="completed"`)
|
expect(admission?.data.input.data.text).toContain(`<subagent sessionID="${childID}" state="completed"`)
|
||||||
expect(admission?.data.input.data).toMatchObject({
|
expect(admission?.data.input.data).toMatchObject({
|
||||||
description: "background review",
|
description: "background review",
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -407,7 +551,7 @@ describe("SubagentTool", () => {
|
|||||||
yield* SessionPending.promote(database.db, bus, parent.id, "steer")
|
yield* SessionPending.promote(database.db, bus, parent.id, "steer")
|
||||||
const synthetic = (yield* sessions.context(parent.id)).filter((message) => message.type === "synthetic")
|
const synthetic = (yield* sessions.context(parent.id)).filter((message) => message.type === "synthetic")
|
||||||
expect(synthetic).toHaveLength(1)
|
expect(synthetic).toHaveLength(1)
|
||||||
expect(synthetic[0]?.text).toContain(`<subagent id="${childID}" state="completed"`)
|
expect(synthetic[0]?.text).toContain(`<subagent sessionID="${childID}" state="completed"`)
|
||||||
expect(synthetic[0]?.text).toContain(childText)
|
expect(synthetic[0]?.text).toContain(childText)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { beforeEach, describe, expect } from "bun:test"
|
import { beforeEach, describe, expect } from "bun:test"
|
||||||
import { Deferred, Effect, Layer } from "effect"
|
import { Effect, Layer } from "effect"
|
||||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||||
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
||||||
import { Permission } from "@opencode-ai/core/permission"
|
import { Permission } from "@opencode-ai/core/permission"
|
||||||
@@ -39,8 +39,6 @@ const providers = [
|
|||||||
let providerRequired = false
|
let providerRequired = false
|
||||||
let formResponse: Form.TerminalState = { status: "cancelled" }
|
let formResponse: Form.TerminalState = { status: "cancelled" }
|
||||||
const formResponses: Form.TerminalState[] = []
|
const formResponses: Form.TerminalState[] = []
|
||||||
let queryBarrier: Deferred.Deferred<void> | undefined
|
|
||||||
let synchronizedQueries = 0
|
|
||||||
let result = new WebSearch.Response({
|
let result = new WebSearch.Response({
|
||||||
providerID: WebSearch.ID.make("exa"),
|
providerID: WebSearch.ID.make("exa"),
|
||||||
results: [{ url: "https://example.com", title: "Search results", content: "search results", time: {} }],
|
results: [{ url: "https://example.com", title: "Search results", content: "search results", time: {} }],
|
||||||
@@ -54,8 +52,6 @@ beforeEach(() => {
|
|||||||
providerRequired = false
|
providerRequired = false
|
||||||
formResponse = { status: "cancelled" }
|
formResponse = { status: "cancelled" }
|
||||||
formResponses.length = 0
|
formResponses.length = 0
|
||||||
queryBarrier = undefined
|
|
||||||
synchronizedQueries = 0
|
|
||||||
result = new WebSearch.Response({
|
result = new WebSearch.Response({
|
||||||
providerID: WebSearch.ID.make("exa"),
|
providerID: WebSearch.ID.make("exa"),
|
||||||
results: [{ url: "https://example.com", title: "Search results", content: "search results", time: {} }],
|
results: [{ url: "https://example.com", title: "Search results", content: "search results", time: {} }],
|
||||||
@@ -79,21 +75,11 @@ const websearch = Layer.succeed(
|
|||||||
transform: () => Effect.die("unused"),
|
transform: () => Effect.die("unused"),
|
||||||
reload: () => Effect.die("unused"),
|
reload: () => Effect.die("unused"),
|
||||||
providers: () => Effect.succeed(providers),
|
providers: () => Effect.succeed(providers),
|
||||||
default: () =>
|
default: () => Effect.succeed(undefined),
|
||||||
Effect.gen(function* () {
|
|
||||||
const stored = values.get("websearch:provider")
|
|
||||||
if (stored === false) return yield* new WebSearch.DisabledError()
|
|
||||||
return typeof stored === "string" ? providers.find((provider) => provider.id === stored) : undefined
|
|
||||||
}),
|
|
||||||
query: (input) =>
|
query: (input) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
queries.push(input)
|
queries.push(input)
|
||||||
const stored = values.get("websearch:provider")
|
const stored = values.get("websearch:provider")
|
||||||
if (queryBarrier && synchronizedQueries < 5) {
|
|
||||||
synchronizedQueries++
|
|
||||||
if (synchronizedQueries === 5) yield* Deferred.succeed(queryBarrier, undefined)
|
|
||||||
yield* Deferred.await(queryBarrier)
|
|
||||||
}
|
|
||||||
if (providerRequired && typeof stored !== "string") return yield* new WebSearch.ProviderRequiredError()
|
if (providerRequired && typeof stored !== "string") return yield* new WebSearch.ProviderRequiredError()
|
||||||
if (typeof stored === "string")
|
if (typeof stored === "string")
|
||||||
return new WebSearch.Response({ providerID: WebSearch.ID.make(stored), results: result.results })
|
return new WebSearch.Response({ providerID: WebSearch.ID.make(stored), results: result.results })
|
||||||
@@ -330,35 +316,6 @@ describe("WebSearchTool registration", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("shares provider consent across concurrent searches", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
providerRequired = true
|
|
||||||
formResponse = { status: "answered", answer: { choice: "allow" } }
|
|
||||||
queryBarrier = yield* Deferred.make<void>()
|
|
||||||
const registry = yield* Tool.Service
|
|
||||||
|
|
||||||
const results = yield* Effect.all(
|
|
||||||
Array.from({ length: 5 }, (_, index) =>
|
|
||||||
executeTool(registry, {
|
|
||||||
sessionID,
|
|
||||||
...toolIdentity,
|
|
||||||
call: {
|
|
||||||
type: "tool-call",
|
|
||||||
id: `call-concurrent-${index}`,
|
|
||||||
name: "websearch",
|
|
||||||
input: { query: `effect ${index}` },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
{ concurrency: "unbounded" },
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(results.every((item) => item.status === "completed")).toBe(true)
|
|
||||||
expect(formRequests).toHaveLength(1)
|
|
||||||
expect(values.get("websearch:provider")).toBe("exa")
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.effect("persists the choice to disable web search", () =>
|
it.effect("persists the choice to disable web search", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
providerRequired = true
|
providerRequired = true
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { SessionMessage } from "@opencode-ai/schema/session-message"
|
import { SessionMessage } from "@opencode-ai/schema/session-message"
|
||||||
import { SessionTransfer } from "@opencode-ai/schema/session-transfer"
|
|
||||||
import { SessionPending } from "@opencode-ai/schema/session-pending"
|
import { SessionPending } from "@opencode-ai/schema/session-pending"
|
||||||
import { PromptInput } from "@opencode-ai/schema/prompt-input"
|
import { PromptInput } from "@opencode-ai/schema/prompt-input"
|
||||||
import { Session } from "@opencode-ai/schema/session"
|
import { Session } from "@opencode-ai/schema/session"
|
||||||
@@ -151,8 +150,8 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
|||||||
payload: Schema.Struct({
|
payload: Schema.Struct({
|
||||||
id: Session.ID.pipe(Schema.optional),
|
id: Session.ID.pipe(Schema.optional),
|
||||||
title: Schema.String.pipe(Schema.optional),
|
title: Schema.String.pipe(Schema.optional),
|
||||||
agent: Agent.ID,
|
agent: Agent.ID.pipe(Schema.optional),
|
||||||
model: Model.Ref,
|
model: Model.Ref.pipe(Schema.optional),
|
||||||
location: Location.Ref.pipe(Schema.optional),
|
location: Location.Ref.pipe(Schema.optional),
|
||||||
}),
|
}),
|
||||||
success: Schema.Struct({ data: Session.Info }),
|
success: Schema.Struct({ data: Session.Info }),
|
||||||
@@ -160,37 +159,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
|||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "v2.session.create",
|
identifier: "v2.session.create",
|
||||||
summary: "Create session",
|
summary: "Create session",
|
||||||
description: "Create a session with an explicit agent and model at the requested location.",
|
description: "Create a session at the requested location.",
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.add(
|
|
||||||
HttpApiEndpoint.post("session.import", "/api/session/import", {
|
|
||||||
payload: Schema.Struct({
|
|
||||||
...SessionTransfer.Data.fields,
|
|
||||||
location: Location.Ref.pipe(Schema.optional),
|
|
||||||
}),
|
|
||||||
success: Schema.Struct({ data: Session.Info }),
|
|
||||||
error: ConflictError,
|
|
||||||
}).annotateMerge(
|
|
||||||
OpenApi.annotations({
|
|
||||||
identifier: "v2.session.import",
|
|
||||||
summary: "Import session",
|
|
||||||
description: "Import a projected session transcript at the requested location.",
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.add(
|
|
||||||
HttpApiEndpoint.get("session.export", "/api/session/:sessionID/export", {
|
|
||||||
params: { sessionID: Session.ID },
|
|
||||||
query: Schema.Struct({ sanitize: BooleanFromString.pipe(Schema.optional) }),
|
|
||||||
success: Schema.Struct({ data: SessionTransfer.Data }),
|
|
||||||
error: [SessionNotFoundError, UnknownError],
|
|
||||||
}).annotateMerge(
|
|
||||||
OpenApi.annotations({
|
|
||||||
identifier: "v2.session.export",
|
|
||||||
summary: "Export session",
|
|
||||||
description: "Export a complete projected session transcript.",
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ export { Vcs } from "./vcs.js"
|
|||||||
export { SessionPending } from "./session-pending.js"
|
export { SessionPending } from "./session-pending.js"
|
||||||
export { SessionError } from "./session-error.js"
|
export { SessionError } from "./session-error.js"
|
||||||
export { SessionMessage } from "./session-message.js"
|
export { SessionMessage } from "./session-message.js"
|
||||||
export { SessionTransfer } from "./session-transfer.js"
|
|
||||||
export { Snapshot } from "./snapshot.js"
|
export { Snapshot } from "./snapshot.js"
|
||||||
export { Shell } from "./shell.js"
|
export { Shell } from "./shell.js"
|
||||||
export { Skill } from "./skill.js"
|
export { Skill } from "./skill.js"
|
||||||
|
|||||||
@@ -47,16 +47,9 @@ export const ReasoningField: Schema.Codec<ReasoningField> = Schema.Union([
|
|||||||
Schema.String,
|
Schema.String,
|
||||||
]).annotate({ identifier: "Model.ReasoningField" })
|
]).annotate({ identifier: "Model.ReasoningField" })
|
||||||
|
|
||||||
export const MaxTokensField = Schema.Literals(["max_completion_tokens", "max_tokens"]).annotate({
|
|
||||||
identifier: "Model.MaxTokensField",
|
|
||||||
})
|
|
||||||
export type MaxTokensField = typeof MaxTokensField.Type
|
|
||||||
|
|
||||||
export interface Compatibility extends Schema.Schema.Type<typeof Compatibility> {}
|
export interface Compatibility extends Schema.Schema.Type<typeof Compatibility> {}
|
||||||
export const Compatibility = Schema.Struct({
|
export const Compatibility = Schema.Struct({
|
||||||
reasoningField: ReasoningField.pipe(optional),
|
reasoningField: ReasoningField.pipe(optional),
|
||||||
maxTokensField: MaxTokensField.pipe(optional),
|
|
||||||
requireFinishReason: Schema.Boolean.pipe(optional),
|
|
||||||
}).annotate({ identifier: "Model.Compatibility" })
|
}).annotate({ identifier: "Model.Compatibility" })
|
||||||
|
|
||||||
export interface Capabilities extends Schema.Schema.Type<typeof Capabilities> {}
|
export interface Capabilities extends Schema.Schema.Type<typeof Capabilities> {}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
export * as SessionTransfer from "./session-transfer.js"
|
|
||||||
|
|
||||||
import { Schema } from "effect"
|
|
||||||
import { Session } from "./session.js"
|
|
||||||
import { SessionMessage } from "./session-message.js"
|
|
||||||
|
|
||||||
export interface Data extends Schema.Schema.Type<typeof Data> {}
|
|
||||||
export const Data = Schema.Struct({
|
|
||||||
info: Session.Info,
|
|
||||||
messages: Schema.Array(SessionMessage.Info),
|
|
||||||
}).annotate({ identifier: "SessionTransfer.Data" })
|
|
||||||
@@ -30,22 +30,3 @@ describe("Model.ReasoningField", () => {
|
|||||||
expect(decode(field)).toBe(field)
|
expect(decode(field)).toBe(field)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("Model.Compatibility", () => {
|
|
||||||
test("decodes model compatibility overrides", () => {
|
|
||||||
const decode = Schema.decodeUnknownSync(Model.Compatibility)
|
|
||||||
|
|
||||||
expect(decode({})).toEqual({})
|
|
||||||
expect(
|
|
||||||
decode({
|
|
||||||
reasoningField: "vendor_reasoning",
|
|
||||||
maxTokensField: "max_completion_tokens",
|
|
||||||
requireFinishReason: false,
|
|
||||||
}),
|
|
||||||
).toEqual({
|
|
||||||
reasoningField: "vendor_reasoning",
|
|
||||||
maxTokensField: "max_completion_tokens",
|
|
||||||
requireFinishReason: false,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Session } from "@opencode-ai/core/session"
|
import { Session } from "@opencode-ai/core/session"
|
||||||
import { SessionTransfer } from "@opencode-ai/core/session/transfer"
|
|
||||||
import { InstructionEntry } from "@opencode-ai/core/session/instruction-entry"
|
import { InstructionEntry } from "@opencode-ai/core/session/instruction-entry"
|
||||||
import { DateTime, Effect, Stream } from "effect"
|
import { DateTime, Effect, Stream } from "effect"
|
||||||
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
||||||
@@ -25,7 +24,6 @@ const DefaultSessionsLimit = 50
|
|||||||
export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handlers) =>
|
export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handlers) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const session = yield* Session.Service
|
const session = yield* Session.Service
|
||||||
const transfer = yield* SessionTransfer.Service
|
|
||||||
|
|
||||||
return handlers
|
return handlers
|
||||||
.handle(
|
.handle(
|
||||||
@@ -88,56 +86,6 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.handle(
|
|
||||||
"session.import",
|
|
||||||
Effect.fn(function* (ctx) {
|
|
||||||
return {
|
|
||||||
data: yield* transfer
|
|
||||||
.import({
|
|
||||||
data: { info: ctx.payload.info, messages: ctx.payload.messages },
|
|
||||||
location: ctx.payload.location ?? { directory: AbsolutePath.make(process.cwd()) },
|
|
||||||
})
|
|
||||||
.pipe(
|
|
||||||
Effect.catchTag(
|
|
||||||
"SessionTransfer.ImportConflictError",
|
|
||||||
(error) =>
|
|
||||||
new ConflictError({
|
|
||||||
message: `Session already exists: ${error.sessionID}`,
|
|
||||||
resource: error.sessionID,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.handle(
|
|
||||||
"session.export",
|
|
||||||
Effect.fn(function* (ctx) {
|
|
||||||
return {
|
|
||||||
data: yield* transfer.export({ sessionID: ctx.params.sessionID, sanitize: ctx.query.sanitize }).pipe(
|
|
||||||
Effect.catchTag(
|
|
||||||
"Session.NotFoundError",
|
|
||||||
(error) =>
|
|
||||||
new SessionNotFoundError({
|
|
||||||
sessionID: error.sessionID,
|
|
||||||
message: `Session not found: ${error.sessionID}`,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
Effect.catchTag("Session.MessageDecodeError", (error) => {
|
|
||||||
const ref = `err_${crypto.randomUUID().slice(0, 8)}`
|
|
||||||
return Effect.logError("failed to decode session message").pipe(
|
|
||||||
Effect.annotateLogs({ ref, sessionID: error.sessionID, messageID: error.messageID }),
|
|
||||||
Effect.andThen(
|
|
||||||
Effect.fail(
|
|
||||||
new UnknownError({ message: "Unexpected server error. Check server logs for details.", ref }),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.handle(
|
.handle(
|
||||||
"session.active",
|
"session.active",
|
||||||
Effect.fn(function* () {
|
Effect.fn(function* () {
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import { PtyTicket } from "@opencode-ai/core/pty/ticket"
|
|||||||
import { Pty } from "@opencode-ai/core/pty"
|
import { Pty } from "@opencode-ai/core/pty"
|
||||||
import { Project } from "@opencode-ai/core/project"
|
import { Project } from "@opencode-ai/core/project"
|
||||||
import { Session } from "@opencode-ai/core/session"
|
import { Session } from "@opencode-ai/core/session"
|
||||||
import { SessionTransfer } from "@opencode-ai/core/session/transfer"
|
|
||||||
import { Shell } from "@opencode-ai/core/shell"
|
import { Shell } from "@opencode-ai/core/shell"
|
||||||
import { Job } from "@opencode-ai/core/job"
|
import { Job } from "@opencode-ai/core/job"
|
||||||
import { MCP } from "@opencode-ai/core/mcp/index"
|
import { MCP } from "@opencode-ai/core/mcp/index"
|
||||||
@@ -52,7 +51,6 @@ const applicationServices = LayerNode.group([
|
|||||||
Job.node,
|
Job.node,
|
||||||
Project.node,
|
Project.node,
|
||||||
Session.node,
|
Session.node,
|
||||||
SessionTransfer.node,
|
|
||||||
PluginRuntime.providerNode,
|
PluginRuntime.providerNode,
|
||||||
SdkPlugins.node,
|
SdkPlugins.node,
|
||||||
PermissionSaved.node,
|
PermissionSaved.node,
|
||||||
|
|||||||
@@ -512,7 +512,7 @@ function App(props: { pair?: DialogPairCredentials }) {
|
|||||||
const terminalTitleEnabled = () => config.data.terminal?.title ?? true
|
const terminalTitleEnabled = () => config.data.terminal?.title ?? true
|
||||||
const copyOnSelectEnabled = () => config.data.terminal?.copy_on_select ?? process.platform !== "win32"
|
const copyOnSelectEnabled = () => config.data.terminal?.copy_on_select ?? process.platform !== "win32"
|
||||||
const pasteSummaryEnabled = () => config.data.prompt?.paste !== "full"
|
const pasteSummaryEnabled = () => config.data.prompt?.paste !== "full"
|
||||||
const tabsVertical = () => config.data.tabs.layout === "vertical" && sessionTabsFitVertically(dimensions().width)
|
const tabsVertical = () => (config.data.tabs?.vertical ?? false) && sessionTabsFitVertically(dimensions().width)
|
||||||
const tabsVisible = () =>
|
const tabsVisible = () =>
|
||||||
sessionTabs.enabled() && (sessionTabs.tabs().length > 0 || sessionTabs.newTab()) && route.data.type !== "plugin"
|
sessionTabs.enabled() && (sessionTabs.tabs().length > 0 || sessionTabs.newTab()) && route.data.type !== "plugin"
|
||||||
|
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export const settings: Setting[] = [
|
|||||||
title: "Enabled",
|
title: "Enabled",
|
||||||
category: "Tabs",
|
category: "Tabs",
|
||||||
path: ["tabs", "enabled"],
|
path: ["tabs", "enabled"],
|
||||||
default: true,
|
default: false,
|
||||||
values: [false, true],
|
values: [false, true],
|
||||||
labels: ["off", "on"],
|
labels: ["off", "on"],
|
||||||
},
|
},
|
||||||
@@ -96,16 +96,17 @@ export const settings: Setting[] = [
|
|||||||
title: "Scope",
|
title: "Scope",
|
||||||
category: "Tabs",
|
category: "Tabs",
|
||||||
path: ["tabs", "scope"],
|
path: ["tabs", "scope"],
|
||||||
default: "cwd",
|
default: "global",
|
||||||
values: ["cwd", "global"],
|
values: ["cwd", "global"],
|
||||||
labels: ["current directory", "global"],
|
labels: ["current directory", "global"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Layout",
|
title: "Vertical",
|
||||||
category: "Tabs",
|
category: "Tabs",
|
||||||
path: ["tabs", "layout"],
|
path: ["tabs", "vertical"],
|
||||||
default: "horizontal",
|
default: false,
|
||||||
values: ["horizontal", "vertical"],
|
values: [false, true],
|
||||||
|
labels: ["off", "on"],
|
||||||
keywords: ["sidebar", "orientation", "left"],
|
keywords: ["sidebar", "orientation", "left"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,21 +8,17 @@ import * as fuzzysort from "fuzzysort"
|
|||||||
import { useConnected } from "./use-connected"
|
import { useConnected } from "./use-connected"
|
||||||
import { useData } from "../context/data"
|
import { useData } from "../context/data"
|
||||||
import { modelPreferenceKey } from "../model-preference"
|
import { modelPreferenceKey } from "../model-preference"
|
||||||
import { useLocation } from "../context/location"
|
|
||||||
|
|
||||||
export function DialogModel(props: { providerID?: string }) {
|
export function DialogModel(props: { providerID?: string }) {
|
||||||
const local = useLocal()
|
const local = useLocal()
|
||||||
const data = useData()
|
const data = useData()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const location = useLocation()
|
|
||||||
const [query, setQuery] = createSignal("")
|
const [query, setQuery] = createSignal("")
|
||||||
const favoritePriority = new Set(local.model.favorite().map(modelPreferenceKey))
|
const favoritePriority = new Set(local.model.favorite().map(modelPreferenceKey))
|
||||||
|
|
||||||
const connected = useConnected()
|
const connected = useConnected()
|
||||||
const providers = createMemo(
|
const providers = createMemo(() => new Map((data.location.provider.list() ?? []).map((item) => [item.id, item])))
|
||||||
() => new Map((data.location.provider.list(location.ref) ?? []).map((item) => [item.id, item])),
|
const models = createMemo(() => data.location.model.list() ?? [])
|
||||||
)
|
|
||||||
const models = createMemo(() => data.location.model.list(location.ref) ?? [])
|
|
||||||
|
|
||||||
const showExtra = createMemo(() => connected() && !props.providerID)
|
const showExtra = createMemo(() => connected() && !props.providerID)
|
||||||
|
|
||||||
|
|||||||
@@ -327,6 +327,10 @@ export function Prompt(props: PromptProps) {
|
|||||||
if (!session) return
|
if (!session) return
|
||||||
const agent = session.agent && local.agent.list().find((agent) => agent.id === session.agent)
|
const agent = session.agent && local.agent.list().find((agent) => agent.id === session.agent)
|
||||||
if (agent && !args.agent) local.agent.set(agent.id)
|
if (agent && !args.agent) local.agent.set(agent.id)
|
||||||
|
if (session.model) {
|
||||||
|
local.model.set({ providerID: session.model.providerID, modelID: session.model.id })
|
||||||
|
local.model.variant.set(session.model.variant)
|
||||||
|
}
|
||||||
syncedSessionID = sessionID
|
syncedSessionID = sessionID
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -939,43 +943,15 @@ export function Prompt(props: PromptProps) {
|
|||||||
await slash.command.run(slash.input)
|
await slash.command.run(slash.input)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
const inputText = expandTrackedPastedText(
|
|
||||||
store.prompt.text,
|
|
||||||
input.extmarks.getAllForTypeId(promptPartTypeId).flatMap((extmark) => {
|
|
||||||
const ref = store.extmarkToPart.get(extmark.id)
|
|
||||||
if (ref?.type !== "pasted") return []
|
|
||||||
const part = store.prompt.pasted[ref.index]
|
|
||||||
if (!part) return []
|
|
||||||
return [{ start: extmark.start, end: extmark.end, text: part.text }]
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
const slashHead = parseSlashHead(inputText, /\s/)
|
|
||||||
const isSkill =
|
|
||||||
slashHead !== undefined &&
|
|
||||||
(data.location.skill.list(currentLocation.ref) ?? []).some(
|
|
||||||
(skill) => skill.slash === true && skill.id === slashHead.name,
|
|
||||||
)
|
|
||||||
const isCommand =
|
|
||||||
slashHead !== undefined &&
|
|
||||||
(data.location.command.list(currentLocation.ref) ?? []).some((command) => command.name === slashHead.name)
|
|
||||||
const agent = local.agent.current()
|
const agent = local.agent.current()
|
||||||
if (!agent) return false
|
if (!agent) return false
|
||||||
const selection = local.model.selection()
|
const selectedModel = local.model.current()
|
||||||
if (!selection) {
|
if (!selectedModel) {
|
||||||
void promptModelWarning()
|
void promptModelWarning()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
const usesModel = !props.sessionID || (store.mode !== "shell" && !isSkill)
|
|
||||||
if (usesModel && !local.model.available(selection)) {
|
|
||||||
toast.show({
|
|
||||||
title: "Model unavailable",
|
|
||||||
message: `${selection.providerID}/${selection.modelID} is not available in this session's location`,
|
|
||||||
variant: "warning",
|
|
||||||
})
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const variant = selection.variant
|
const variant = local.model.variant.current()
|
||||||
let sessionID = props.sessionID
|
let sessionID = props.sessionID
|
||||||
let session = sessionID ? data.session.get(sessionID) : undefined
|
let session = sessionID ? data.session.get(sessionID) : undefined
|
||||||
let finishMoveProgress = false
|
let finishMoveProgress = false
|
||||||
@@ -993,8 +969,8 @@ export function Prompt(props: PromptProps) {
|
|||||||
location: directory ? { directory } : location,
|
location: directory ? { directory } : location,
|
||||||
agent: agent.id,
|
agent: agent.id,
|
||||||
model: {
|
model: {
|
||||||
providerID: selection.providerID,
|
providerID: selectedModel.providerID,
|
||||||
id: selection.modelID,
|
id: selectedModel.modelID,
|
||||||
variant,
|
variant,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -1014,6 +990,17 @@ export function Prompt(props: PromptProps) {
|
|||||||
session = created
|
session = created
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const inputText = expandTrackedPastedText(
|
||||||
|
store.prompt.text,
|
||||||
|
input.extmarks.getAllForTypeId(promptPartTypeId).flatMap((extmark) => {
|
||||||
|
const ref = store.extmarkToPart.get(extmark.id)
|
||||||
|
if (ref?.type !== "pasted") return []
|
||||||
|
const part = store.prompt.pasted[ref.index]
|
||||||
|
if (!part) return []
|
||||||
|
return [{ start: extmark.start, end: extmark.end, text: part.text }]
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
// Capture mode before it gets reset
|
// Capture mode before it gets reset
|
||||||
const currentMode = store.mode
|
const currentMode = store.mode
|
||||||
const editorSelection = editorContext()
|
const editorSelection = editorContext()
|
||||||
@@ -1026,30 +1013,43 @@ export function Prompt(props: PromptProps) {
|
|||||||
command: inputText,
|
command: inputText,
|
||||||
})
|
})
|
||||||
setStore("mode", "normal")
|
setStore("mode", "normal")
|
||||||
} else if (slashHead && isCommand) {
|
} else if (
|
||||||
|
inputText.startsWith("/") &&
|
||||||
|
(data.location.command.list(currentLocation.current) ?? []).some(
|
||||||
|
(command) => command.name === inputText.split("\n")[0].split(" ")[0].slice(1),
|
||||||
|
)
|
||||||
|
) {
|
||||||
move.startSubmit()
|
move.startSubmit()
|
||||||
const model = { providerID: selection.providerID, id: selection.modelID, variant }
|
// Parse command from first line, preserve multi-line content in arguments
|
||||||
const cancelCommit = local.model.trackSessionCommit(sessionID, model)
|
const firstLineEnd = inputText.indexOf("\n")
|
||||||
|
const firstLine = firstLineEnd === -1 ? inputText : inputText.slice(0, firstLineEnd)
|
||||||
|
const [command, ...firstLineArgs] = firstLine.split(" ")
|
||||||
|
const restOfInput = firstLineEnd === -1 ? "" : inputText.slice(firstLineEnd + 1)
|
||||||
|
const args = firstLineArgs.join(" ") + (restOfInput ? "\n" + restOfInput : "")
|
||||||
|
|
||||||
void client.api.session
|
void client.api.session
|
||||||
.command({
|
.command({
|
||||||
sessionID,
|
sessionID,
|
||||||
command: slashHead.name,
|
command: command.slice(1),
|
||||||
arguments: slashHead.arguments,
|
arguments: args,
|
||||||
agent: agent.id,
|
agent: agent.id,
|
||||||
model,
|
model: { providerID: selectedModel.providerID, id: selectedModel.modelID, variant },
|
||||||
files: store.prompt.files,
|
files: store.prompt.files,
|
||||||
agents: store.prompt.agents,
|
agents: store.prompt.agents,
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
cancelCommit()
|
|
||||||
toast.show({ title: "Failed to run command", message: errorMessage(error), variant: "error" })
|
toast.show({ title: "Failed to run command", message: errorMessage(error), variant: "error" })
|
||||||
})
|
})
|
||||||
} else if (isSkill) {
|
} else if (
|
||||||
|
inputText.startsWith("/") &&
|
||||||
|
(data.location.skill.list(currentLocation.current) ?? []).some(
|
||||||
|
(skill) => skill.slash === true && skill.id === inputText.split("\n")[0].split(" ")[0].slice(1),
|
||||||
|
)
|
||||||
|
) {
|
||||||
move.startSubmit()
|
move.startSubmit()
|
||||||
void client.api.session.skill({
|
void client.api.session.skill({
|
||||||
sessionID,
|
sessionID,
|
||||||
skill: slashHead!.name,
|
skill: inputText.split("\n")[0].split(" ")[0].slice(1),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
move.startSubmit()
|
move.startSubmit()
|
||||||
@@ -1061,15 +1061,13 @@ export function Prompt(props: PromptProps) {
|
|||||||
await client.api.session.switchAgent({ sessionID, agent: agent.id })
|
await client.api.session.switchAgent({ sessionID, agent: agent.id })
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
session?.model?.providerID !== selection.providerID ||
|
session?.model?.providerID !== selectedModel.providerID ||
|
||||||
session.model.id !== selection.modelID ||
|
session.model.id !== selectedModel.modelID ||
|
||||||
(session.model.variant ?? "default") !== (variant ?? "default")
|
(session.model.variant ?? "default") !== (variant ?? "default")
|
||||||
) {
|
) {
|
||||||
const model = { providerID: selection.providerID, id: selection.modelID, variant }
|
await client.api.session.switchModel({
|
||||||
const cancelCommit = local.model.trackSessionCommit(sessionID, model)
|
sessionID,
|
||||||
await client.api.session.switchModel({ sessionID, model }).catch((error) => {
|
model: { providerID: selectedModel.providerID, id: selectedModel.modelID, variant },
|
||||||
cancelCommit()
|
|
||||||
throw error
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (session?.revert) {
|
if (session?.revert) {
|
||||||
@@ -1322,7 +1320,10 @@ export function Prompt(props: PromptProps) {
|
|||||||
return `Ask anything... "${list()[store.placeholder % list().length]}"`
|
return `Ask anything... "${list()[store.placeholder % list().length]}"`
|
||||||
})()
|
})()
|
||||||
if (!value) return undefined
|
if (!value) return undefined
|
||||||
const width = dimensions().width < 44 ? dimensions().width - 5 : Math.min(75, dimensions().width - 4) - 5
|
const width =
|
||||||
|
dimensions().width < 44
|
||||||
|
? dimensions().width - 5
|
||||||
|
: Math.min(75, dimensions().width - 4) - 5
|
||||||
return Locale.takeWidth(value, Math.max(1, width)).trimEnd()
|
return Locale.takeWidth(value, Math.max(1, width)).trimEnd()
|
||||||
})
|
})
|
||||||
const locationLabel = createMemo(() => {
|
const locationLabel = createMemo(() => {
|
||||||
|
|||||||
@@ -132,8 +132,8 @@ export const Info = Schema.Struct({
|
|||||||
scope: Schema.optional(Schema.Literals(["global", "cwd"])).annotate({
|
scope: Schema.optional(Schema.Literals(["global", "cwd"])).annotate({
|
||||||
description: "Share tabs globally or keep a separate set for each working directory",
|
description: "Share tabs globally or keep a separate set for each working directory",
|
||||||
}),
|
}),
|
||||||
layout: Schema.optional(Schema.Literals(["horizontal", "vertical"])).annotate({
|
vertical: Schema.optional(Schema.Boolean).annotate({
|
||||||
description: "Show tabs in a horizontal strip or vertical sidebar",
|
description: "Show tabs in a left sidebar instead of a horizontal strip",
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
).annotate({ description: "Tab strip settings" }),
|
).annotate({ description: "Tab strip settings" }),
|
||||||
@@ -179,7 +179,7 @@ export const Info = Schema.Struct({
|
|||||||
})
|
})
|
||||||
export type Info = Schema.Schema.Type<typeof Info>
|
export type Info = Schema.Schema.Type<typeof Info>
|
||||||
|
|
||||||
export type Resolved = Omit<Info, "attention" | "keybinds" | "leader" | "mouse" | "tabs"> & {
|
export type Resolved = Omit<Info, "attention" | "keybinds" | "leader" | "mouse"> & {
|
||||||
attention: {
|
attention: {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
notifications: boolean
|
notifications: boolean
|
||||||
@@ -191,11 +191,6 @@ export type Resolved = Omit<Info, "attention" | "keybinds" | "leader" | "mouse"
|
|||||||
keybinds: TuiKeybind.BindingLookupView
|
keybinds: TuiKeybind.BindingLookupView
|
||||||
leader: { timeout: number }
|
leader: { timeout: number }
|
||||||
mouse: boolean
|
mouse: boolean
|
||||||
tabs: {
|
|
||||||
enabled: boolean
|
|
||||||
scope: "global" | "cwd"
|
|
||||||
layout: "horizontal" | "vertical"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolve(input: Info, options: { terminalSuspend: boolean }): Resolved {
|
export function resolve(input: Info, options: { terminalSuspend: boolean }): Resolved {
|
||||||
@@ -226,12 +221,6 @@ export function resolve(input: Info, options: { terminalSuspend: boolean }): Res
|
|||||||
}),
|
}),
|
||||||
leader: { timeout: input.leader?.timeout ?? 2000 },
|
leader: { timeout: input.leader?.timeout ?? 2000 },
|
||||||
mouse: input.mouse ?? true,
|
mouse: input.mouse ?? true,
|
||||||
tabs: {
|
|
||||||
...input.tabs,
|
|
||||||
enabled: input.tabs?.enabled ?? true,
|
|
||||||
scope: input.tabs?.scope ?? "cwd",
|
|
||||||
layout: input.tabs?.layout ?? "horizontal",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { dedupeWith } from "effect/Array"
|
import { dedupeWith } from "effect/Array"
|
||||||
import { createSimpleContext } from "./helper"
|
import { createSimpleContext } from "./helper"
|
||||||
import { batch, createMemo, onCleanup } from "solid-js"
|
import { batch, createMemo } from "solid-js"
|
||||||
import { useEvent } from "./event"
|
import { useEvent } from "./event"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { useTuiPaths } from "./runtime"
|
import { useTuiPaths } from "./runtime"
|
||||||
@@ -22,7 +22,6 @@ import { useToast } from "../ui/toast"
|
|||||||
import { useRoute } from "./route"
|
import { useRoute } from "./route"
|
||||||
import { useData } from "./data"
|
import { useData } from "./data"
|
||||||
import { usePermission } from "./permission"
|
import { usePermission } from "./permission"
|
||||||
import { useLocation } from "./location"
|
|
||||||
|
|
||||||
export function parseModel(model: string) {
|
export function parseModel(model: string) {
|
||||||
const [providerID, ...rest] = model.split("/")
|
const [providerID, ...rest] = model.split("/")
|
||||||
@@ -58,29 +57,26 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
const args = useArgs()
|
const args = useArgs()
|
||||||
const event = useEvent()
|
const event = useEvent()
|
||||||
const permission = usePermission()
|
const permission = usePermission()
|
||||||
const location = useLocation()
|
|
||||||
|
|
||||||
const models = () => data.location.model.list(location.ref)
|
|
||||||
const providers = () => data.location.provider.list(location.ref)
|
|
||||||
|
|
||||||
function isModelValid(model: ModelPreferenceModel) {
|
function isModelValid(model: ModelPreferenceModel) {
|
||||||
return !!models()?.some((item) => item.providerID === model.providerID && item.id === model.modelID)
|
return !!data.location.model
|
||||||
|
.list()
|
||||||
|
?.some((item) => item.providerID === model.providerID && item.id === model.modelID)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFirstValidModel(...modelFns: (() => ModelPreferenceModel | undefined)[]) {
|
function getFirstValidModel(...modelFns: (() => ModelPreferenceModel | undefined)[]) {
|
||||||
for (const modelFn of modelFns) {
|
for (const modelFn of modelFns) {
|
||||||
const model = modelFn()
|
const model = modelFn()
|
||||||
if (model && isModelValid(model)) return model
|
if (!model) continue
|
||||||
|
if (isModelValid(model)) return model
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createAgent() {
|
function createAgent() {
|
||||||
const agents = createMemo(() =>
|
const agents = createMemo(() =>
|
||||||
(data.location.agent.list(location.ref) ?? []).filter((agent) => agent.mode !== "subagent" && !agent.hidden),
|
(data.location.agent.list() ?? []).filter((agent) => agent.mode !== "subagent" && !agent.hidden),
|
||||||
)
|
|
||||||
const visibleAgents = createMemo(() =>
|
|
||||||
(data.location.agent.list(location.ref) ?? []).filter((agent) => !agent.hidden),
|
|
||||||
)
|
)
|
||||||
|
const visibleAgents = createMemo(() => (data.location.agent.list() ?? []).filter((agent) => !agent.hidden))
|
||||||
const [agentStore, setAgentStore] = createStore({
|
const [agentStore, setAgentStore] = createStore({
|
||||||
current: undefined as string | undefined,
|
current: undefined as string | undefined,
|
||||||
})
|
})
|
||||||
@@ -132,40 +128,35 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
const agent = createAgent()
|
const agent = createAgent()
|
||||||
|
|
||||||
function createModel() {
|
function createModel() {
|
||||||
type ModelSelection = ModelPreferenceModel & { variant?: string }
|
const [modelStore, setModelStore] = createStore<
|
||||||
const [preferences, setPreferences] = createStore<ModelPreference & { ready: boolean }>({
|
ModelPreference & {
|
||||||
|
ready: boolean
|
||||||
|
model: Record<string, ModelPreferenceModel>
|
||||||
|
}
|
||||||
|
>({
|
||||||
ready: false,
|
ready: false,
|
||||||
|
model: {},
|
||||||
recent: [],
|
recent: [],
|
||||||
favorite: [],
|
favorite: [],
|
||||||
variant: {},
|
variant: {},
|
||||||
})
|
})
|
||||||
const [selectionState, setSelectionState] = createStore<{
|
|
||||||
newSessionModelByLocationAgent: Record<string, ModelPreferenceModel | undefined>
|
|
||||||
draftBySession: Record<string, ModelSelection | undefined>
|
|
||||||
}>({
|
|
||||||
newSessionModelByLocationAgent: {},
|
|
||||||
draftBySession: {},
|
|
||||||
})
|
|
||||||
|
|
||||||
const repository = createModelPreferenceRepository(path.join(paths.state, "model.json"))
|
const repository = createModelPreferenceRepository(path.join(paths.state, "model.json"))
|
||||||
const pendingSelectionCommits = new Map<string, string>()
|
const state = {
|
||||||
const selectionKey = (value: ModelSelection) =>
|
|
||||||
`${modelPreferenceKey(value)}:${normalizeModelVariant(value.variant) ?? "default"}`
|
|
||||||
const saveState = {
|
|
||||||
pending: false,
|
pending: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
function savePreferences() {
|
function save() {
|
||||||
if (!preferences.ready) {
|
if (!modelStore.ready) {
|
||||||
saveState.pending = true
|
state.pending = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
saveState.pending = false
|
state.pending = false
|
||||||
void repository
|
void repository
|
||||||
.patch({
|
.patch({
|
||||||
recent: preferences.recent,
|
recent: modelStore.recent,
|
||||||
favorite: preferences.favorite,
|
favorite: modelStore.favorite,
|
||||||
variant: preferences.variant,
|
variant: modelStore.variant,
|
||||||
})
|
})
|
||||||
.catch(() => undefined)
|
.catch(() => undefined)
|
||||||
}
|
}
|
||||||
@@ -173,14 +164,14 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
repository
|
repository
|
||||||
.load()
|
.load()
|
||||||
.then((value) => {
|
.then((value) => {
|
||||||
setPreferences("recent", value.recent)
|
setModelStore("recent", value.recent)
|
||||||
setPreferences("favorite", value.favorite)
|
setModelStore("favorite", value.favorite)
|
||||||
setPreferences("variant", value.variant)
|
setModelStore("variant", value.variant)
|
||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setPreferences("ready", true)
|
setModelStore("ready", true)
|
||||||
if (saveState.pending) savePreferences()
|
if (state.pending) save()
|
||||||
})
|
})
|
||||||
|
|
||||||
const fallbackModel = createMemo(() => {
|
const fallbackModel = createMemo(() => {
|
||||||
@@ -194,13 +185,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const item of preferences.recent) {
|
for (const item of modelStore.recent) {
|
||||||
if (isModelValid(item)) {
|
if (isModelValid(item)) {
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const model = models()?.[0]
|
const model = data.location.model.list()?.[0]
|
||||||
if (!model) return undefined
|
if (!model) return undefined
|
||||||
return {
|
return {
|
||||||
providerID: model.providerID,
|
providerID: model.providerID,
|
||||||
@@ -208,134 +199,30 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const newSessionModel = createMemo(() => {
|
|
||||||
const a = agent.current()
|
|
||||||
return getFirstValidModel(
|
|
||||||
() => a && selectionState.newSessionModelByLocationAgent[locationAgentKey(a.id)],
|
|
||||||
() => a?.model && { providerID: a.model.providerID, modelID: a.model.id },
|
|
||||||
fallbackModel,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const currentSelection = createMemo<ModelSelection | undefined>(() => {
|
|
||||||
if (route.data.type === "session") return sessionSelection(route.data.sessionID)
|
|
||||||
const model = newSessionModel()
|
|
||||||
if (!model) return
|
|
||||||
return { ...model, variant: normalizeModelVariant(preferences.variant[modelPreferenceKey(model)]) }
|
|
||||||
})
|
|
||||||
|
|
||||||
const currentModel = createMemo(() => {
|
const currentModel = createMemo(() => {
|
||||||
const selection = currentSelection()
|
const a = agent.current()
|
||||||
if (!selection) return
|
return (
|
||||||
return { providerID: selection.providerID, modelID: selection.modelID }
|
getFirstValidModel(
|
||||||
})
|
() => a && modelStore.model[a.id],
|
||||||
|
() => a?.model && { providerID: a.model.providerID, modelID: a.model.id },
|
||||||
function locationAgentKey(agentID: string) {
|
fallbackModel,
|
||||||
const ref = location.ref ?? data.location.default()
|
) ?? undefined
|
||||||
return `${JSON.stringify([ref.directory, ref.workspaceID])}:${agentID}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function durableSelection(sessionID: string): ModelSelection | undefined {
|
|
||||||
const model = data.session.get(sessionID)?.model
|
|
||||||
if (!model) return
|
|
||||||
return {
|
|
||||||
providerID: model.providerID,
|
|
||||||
modelID: model.id,
|
|
||||||
variant: normalizeModelVariant(model.variant),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function sessionSelection(sessionID: string) {
|
|
||||||
return selectionState.draftBySession[sessionID] ?? durableSelection(sessionID)
|
|
||||||
}
|
|
||||||
|
|
||||||
function setSessionDraft(sessionID: string, selection: ModelSelection) {
|
|
||||||
const durable = durableSelection(sessionID)
|
|
||||||
setSelectionState(
|
|
||||||
"draftBySession",
|
|
||||||
sessionID,
|
|
||||||
durable && selectionKey(durable) === selectionKey(selection) ? undefined : selection,
|
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
function selectModel(model: ModelPreferenceModel) {
|
|
||||||
if (route.data.type === "session") {
|
|
||||||
const sessionID = route.data.sessionID
|
|
||||||
const current = sessionSelection(sessionID)
|
|
||||||
const preferred = normalizeModelVariant(
|
|
||||||
current?.providerID === model.providerID && current.modelID === model.modelID
|
|
||||||
? current.variant
|
|
||||||
: preferences.variant[modelPreferenceKey(model)],
|
|
||||||
)
|
|
||||||
const info = models()?.find((item) => item.providerID === model.providerID && item.id === model.modelID)
|
|
||||||
const variant = preferred && info?.variants?.some((item) => item.id === preferred) ? preferred : undefined
|
|
||||||
setSessionDraft(sessionID, { ...model, variant })
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
const current = agent.current()
|
|
||||||
if (!current) return false
|
|
||||||
setSelectionState("newSessionModelByLocationAgent", locationAgentKey(current.id), model)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
onCleanup(
|
|
||||||
event.on("session.model.selected", (evt) => {
|
|
||||||
const expected = pendingSelectionCommits.get(evt.data.sessionID)
|
|
||||||
if (!expected) return
|
|
||||||
const committed = selectionKey({
|
|
||||||
providerID: evt.data.model.providerID,
|
|
||||||
modelID: evt.data.model.id,
|
|
||||||
variant: evt.data.model.variant,
|
|
||||||
})
|
|
||||||
if (committed !== expected) return
|
|
||||||
pendingSelectionCommits.delete(evt.data.sessionID)
|
|
||||||
const draft = selectionState.draftBySession[evt.data.sessionID]
|
|
||||||
if (draft && selectionKey(draft) === committed)
|
|
||||||
setSelectionState("draftBySession", evt.data.sessionID, undefined)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
onCleanup(
|
|
||||||
event.on("session.deleted", (evt) => {
|
|
||||||
pendingSelectionCommits.delete(evt.data.sessionID)
|
|
||||||
setSelectionState("draftBySession", evt.data.sessionID, undefined)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
current: currentModel,
|
current: currentModel,
|
||||||
selection: currentSelection,
|
|
||||||
available(model = currentModel()) {
|
|
||||||
return model ? isModelValid(model) : false
|
|
||||||
},
|
|
||||||
trackSessionCommit(
|
|
||||||
sessionID: string,
|
|
||||||
value: {
|
|
||||||
providerID: string
|
|
||||||
id: string
|
|
||||||
variant?: string
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
const committed = selectionKey({ providerID: value.providerID, modelID: value.id, variant: value.variant })
|
|
||||||
pendingSelectionCommits.set(sessionID, committed)
|
|
||||||
return () => {
|
|
||||||
if (pendingSelectionCommits.get(sessionID) === committed) pendingSelectionCommits.delete(sessionID)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
get ready() {
|
get ready() {
|
||||||
return preferences.ready
|
return modelStore.ready
|
||||||
},
|
|
||||||
get catalogReady() {
|
|
||||||
return models() !== undefined
|
|
||||||
},
|
},
|
||||||
recent() {
|
recent() {
|
||||||
return preferences.recent
|
return modelStore.recent
|
||||||
},
|
},
|
||||||
favorite() {
|
favorite() {
|
||||||
return preferences.favorite
|
return modelStore.favorite
|
||||||
},
|
},
|
||||||
parsed: createMemo(() => {
|
parsed: createMemo(() => {
|
||||||
const value = currentSelection()
|
const value = currentModel()
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return {
|
return {
|
||||||
provider: "Connect a provider",
|
provider: "Connect a provider",
|
||||||
@@ -343,28 +230,33 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
reasoning: false,
|
reasoning: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const provider = providers()?.find((item) => item.id === value.providerID)
|
const provider = data.location.provider.list()?.find((item) => item.id === value.providerID)
|
||||||
const info = models()?.find((item) => item.providerID === value.providerID && item.id === value.modelID)
|
const info = data.location.model
|
||||||
|
.list()
|
||||||
|
?.find((item) => item.providerID === value.providerID && item.id === value.modelID)
|
||||||
return {
|
return {
|
||||||
provider: provider?.name ?? value.providerID,
|
provider: provider?.name ?? value.providerID,
|
||||||
model: info?.name ?? `${value.modelID} (unavailable)`,
|
model: info?.name ?? value.modelID,
|
||||||
reasoning: (info?.variants?.length ?? 0) !== 0,
|
reasoning: (info?.variants?.length ?? 0) !== 0,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
cycle(direction: 1 | -1) {
|
cycle(direction: 1 | -1) {
|
||||||
const current = currentSelection()
|
const current = currentModel()
|
||||||
if (!current) return
|
if (!current) return
|
||||||
const recent = recentModels(current, preferences.recent).filter(isModelValid)
|
const recent = modelStore.recent
|
||||||
const index = recent.findIndex((x) => x.providerID === current.providerID && x.modelID === current.modelID)
|
const index = recent.findIndex((x) => x.providerID === current.providerID && x.modelID === current.modelID)
|
||||||
let next = index === -1 ? (direction === 1 ? 0 : recent.length - 1) : index + direction
|
if (index === -1) return
|
||||||
|
let next = index + direction
|
||||||
if (next < 0) next = recent.length - 1
|
if (next < 0) next = recent.length - 1
|
||||||
if (next >= recent.length) next = 0
|
if (next >= recent.length) next = 0
|
||||||
const val = recent[next]
|
const val = recent[next]
|
||||||
if (!val) return
|
if (!val) return
|
||||||
selectModel({ ...val })
|
const a = agent.current()
|
||||||
|
if (!a) return
|
||||||
|
setModelStore("model", a.id, { ...val })
|
||||||
},
|
},
|
||||||
cycleFavorite(direction: 1 | -1) {
|
cycleFavorite(direction: 1 | -1) {
|
||||||
const favorites = preferences.favorite.filter((item) => isModelValid(item))
|
const favorites = modelStore.favorite.filter((item) => isModelValid(item))
|
||||||
if (!favorites.length) {
|
if (!favorites.length) {
|
||||||
toast.show({
|
toast.show({
|
||||||
variant: "info",
|
variant: "info",
|
||||||
@@ -373,7 +265,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const current = currentSelection()
|
const current = currentModel()
|
||||||
let index = -1
|
let index = -1
|
||||||
if (current) {
|
if (current) {
|
||||||
index = favorites.findIndex((x) => x.providerID === current.providerID && x.modelID === current.modelID)
|
index = favorites.findIndex((x) => x.providerID === current.providerID && x.modelID === current.modelID)
|
||||||
@@ -387,39 +279,45 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
}
|
}
|
||||||
const next = favorites[index]
|
const next = favorites[index]
|
||||||
if (!next) return
|
if (!next) return
|
||||||
if (!selectModel({ ...next })) return
|
const a = agent.current()
|
||||||
setPreferences("recent", recentModels(next, preferences.recent))
|
if (!a) return
|
||||||
savePreferences()
|
setModelStore("model", a.id, { ...next })
|
||||||
|
setModelStore("recent", recentModels(next, modelStore.recent))
|
||||||
|
save()
|
||||||
},
|
},
|
||||||
set(model: { providerID: string; modelID: string }, options?: { recent?: boolean }) {
|
set(model: { providerID: string; modelID: string }, options?: { recent?: boolean }) {
|
||||||
batch(() => {
|
batch(() => {
|
||||||
if (!isModelValid(model)) return
|
if (!isModelValid(model)) return
|
||||||
if (!selectModel(model)) return
|
const a = agent.current()
|
||||||
|
if (!a) return
|
||||||
|
setModelStore("model", a.id, model)
|
||||||
if (options?.recent) {
|
if (options?.recent) {
|
||||||
setPreferences("recent", recentModels(model, preferences.recent))
|
setModelStore("recent", recentModels(model, modelStore.recent))
|
||||||
savePreferences()
|
save()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
toggleFavorite(model: { providerID: string; modelID: string }) {
|
toggleFavorite(model: { providerID: string; modelID: string }) {
|
||||||
batch(() => {
|
batch(() => {
|
||||||
if (!isModelValid(model)) return
|
if (!isModelValid(model)) return
|
||||||
const exists = preferences.favorite.some(
|
const exists = modelStore.favorite.some(
|
||||||
(x) => x.providerID === model.providerID && x.modelID === model.modelID,
|
(x) => x.providerID === model.providerID && x.modelID === model.modelID,
|
||||||
)
|
)
|
||||||
const next = exists
|
const next = exists
|
||||||
? preferences.favorite.filter((x) => x.providerID !== model.providerID || x.modelID !== model.modelID)
|
? modelStore.favorite.filter((x) => x.providerID !== model.providerID || x.modelID !== model.modelID)
|
||||||
: [model, ...preferences.favorite]
|
: [model, ...modelStore.favorite]
|
||||||
setPreferences(
|
setModelStore(
|
||||||
"favorite",
|
"favorite",
|
||||||
next.map((x) => ({ providerID: x.providerID, modelID: x.modelID })),
|
next.map((x) => ({ providerID: x.providerID, modelID: x.modelID })),
|
||||||
)
|
)
|
||||||
savePreferences()
|
save()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
variant: {
|
variant: {
|
||||||
selected() {
|
selected() {
|
||||||
return currentSelection()?.variant
|
const m = currentModel()
|
||||||
|
if (!m) return undefined
|
||||||
|
return normalizeModelVariant(modelStore.variant[modelPreferenceKey(m)])
|
||||||
},
|
},
|
||||||
current() {
|
current() {
|
||||||
const v = this.selected()
|
const v = this.selected()
|
||||||
@@ -427,20 +325,18 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
return undefined
|
return undefined
|
||||||
},
|
},
|
||||||
list() {
|
list() {
|
||||||
const m = currentSelection()
|
const m = currentModel()
|
||||||
if (!m) return []
|
if (!m) return []
|
||||||
const info = models()?.find((item) => item.providerID === m.providerID && item.id === m.modelID)
|
const info = data.location.model
|
||||||
|
.list()
|
||||||
|
?.find((item) => item.providerID === m.providerID && item.id === m.modelID)
|
||||||
return info?.variants?.map((variant) => variant.id) ?? []
|
return info?.variants?.map((variant) => variant.id) ?? []
|
||||||
},
|
},
|
||||||
set(value: string | undefined) {
|
set(value: string | undefined) {
|
||||||
const m = currentSelection()
|
const m = currentModel()
|
||||||
if (!m) return
|
if (!m) return
|
||||||
if (route.data.type === "session") {
|
setModelStore("variant", modelPreferenceKey(m), normalizeModelVariant(value))
|
||||||
setSessionDraft(route.data.sessionID, { ...m, variant: normalizeModelVariant(value) })
|
save()
|
||||||
return
|
|
||||||
}
|
|
||||||
setPreferences("variant", modelPreferenceKey(m), normalizeModelVariant(value))
|
|
||||||
savePreferences()
|
|
||||||
},
|
},
|
||||||
cycle() {
|
cycle() {
|
||||||
const variants = this.list()
|
const variants = this.list()
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export const { use: useSessionTabs, provider: SessionTabsProvider } = createSimp
|
|||||||
const event = useEvent()
|
const event = useEvent()
|
||||||
const config = useConfig().data
|
const config = useConfig().data
|
||||||
const paths = useTuiPaths()
|
const paths = useTuiPaths()
|
||||||
const enabled = () => config.tabs.enabled
|
const enabled = () => config.tabs?.enabled ?? false
|
||||||
// Keyed reconcile keeps tab object identity across reorders, so strip rows move instead of
|
// Keyed reconcile keeps tab object identity across reorders, so strip rows move instead of
|
||||||
// mutating in place, which per-row animations and drag state depend on.
|
// mutating in place, which per-row animations and drag state depend on.
|
||||||
const [store, updateStore] = useStorage().store<PersistedState>("tabs", {
|
const [store, updateStore] = useStorage().store<PersistedState>("tabs", {
|
||||||
@@ -66,12 +66,12 @@ export const { use: useSessionTabs, provider: SessionTabsProvider } = createSimp
|
|||||||
let closedTabs: ClosedSessionTab[] = []
|
let closedTabs: ClosedSessionTab[] = []
|
||||||
|
|
||||||
function state() {
|
function state() {
|
||||||
if (config.tabs.scope === "cwd") return store.cwd[paths.cwd] ?? fallback
|
if (config.tabs?.scope === "cwd") return store.cwd[paths.cwd] ?? fallback
|
||||||
return store.global
|
return store.global
|
||||||
}
|
}
|
||||||
|
|
||||||
function update(mutation: (draft: TabsState) => void) {
|
function update(mutation: (draft: TabsState) => void) {
|
||||||
const scope = config.tabs.scope
|
const scope = config.tabs?.scope ?? "global"
|
||||||
void updateStore((draft) => mutation(scope === "cwd" ? (draft.cwd[paths.cwd] ??= empty()) : draft.global)).catch(
|
void updateStore((draft) => mutation(scope === "cwd" ? (draft.cwd[paths.cwd] ??= empty()) : draft.global)).catch(
|
||||||
// Failed writes lose only tab layout, but silence would hide tabs resetting every launch.
|
// Failed writes lose only tab layout, but silence would hide tabs resetting every launch.
|
||||||
(error) => console.error("Failed to persist session tabs", error),
|
(error) => console.error("Failed to persist session tabs", error),
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ export function Session() {
|
|||||||
const availableWidth = createMemo(
|
const availableWidth = createMemo(
|
||||||
() =>
|
() =>
|
||||||
dimensions().width -
|
dimensions().width -
|
||||||
(config.tabs?.enabled && config.tabs.layout === "vertical" && sessionTabsFitVertically(dimensions().width)
|
(config.tabs?.enabled && config.tabs.vertical && sessionTabsFitVertically(dimensions().width)
|
||||||
? SESSION_SIDEBAR_WIDTH
|
? SESSION_SIDEBAR_WIDTH
|
||||||
: 0),
|
: 0),
|
||||||
)
|
)
|
||||||
@@ -361,7 +361,7 @@ export function Session() {
|
|||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const current = prompt()
|
const current = prompt()
|
||||||
if (sent || !current || !synced() || !local.model.ready || !local.model.catalogReady) return
|
if (sent || !current || !synced() || !local.model.ready) return
|
||||||
if (!local.agent.current() || !local.model.current()) return
|
if (!local.agent.current() || !local.model.current()) return
|
||||||
if (!args.prompt || route.prompt?.text !== args.prompt || current.current.text !== args.prompt) return
|
if (!args.prompt || route.prompt?.text !== args.prompt || current.current.text !== args.prompt) return
|
||||||
sent = true
|
sent = true
|
||||||
@@ -824,13 +824,22 @@ export function Session() {
|
|||||||
if (options === null) return
|
if (options === null) return
|
||||||
|
|
||||||
const content =
|
const content =
|
||||||
options.format === "markdown"
|
options.format === "markdown"
|
||||||
? formatSessionTranscript(sessionData, messages(), options.thinking)
|
? formatSessionTranscript(sessionData, messages(), options.thinking)
|
||||||
: JSON.stringify(
|
: await (async () => {
|
||||||
await client.api.session.export({ sessionID: sessionData.id, sanitize: options.sanitize }),
|
const messages: unknown[] = []
|
||||||
null,
|
let cursor: string | undefined
|
||||||
2,
|
do {
|
||||||
) + EOL
|
const page = await client.api.message.list(
|
||||||
|
cursor
|
||||||
|
? { sessionID: sessionData.id, limit: 200, cursor }
|
||||||
|
: { sessionID: sessionData.id, limit: 200, order: "asc" },
|
||||||
|
)
|
||||||
|
messages.push(...page.data)
|
||||||
|
cursor = page.data.length ? (page.cursor.next ?? undefined) : undefined
|
||||||
|
} while (cursor)
|
||||||
|
return JSON.stringify({ info: sessionData, messages }, null, 2) + EOL
|
||||||
|
})()
|
||||||
|
|
||||||
if (options.action === "copy") {
|
if (options.action === "copy") {
|
||||||
await clipboard.write?.(content)
|
await clipboard.write?.(content)
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ export type ExportFormat = "markdown" | "json"
|
|||||||
|
|
||||||
export type DialogExportOptionsProps = {
|
export type DialogExportOptionsProps = {
|
||||||
defaultThinking: boolean
|
defaultThinking: boolean
|
||||||
onConfirm?: (options: { action: "copy" | "export"; format: ExportFormat; thinking: boolean; sanitize: boolean }) => void
|
onConfirm?: (options: { action: "copy" | "export"; format: ExportFormat; thinking: boolean }) => void
|
||||||
onCancel?: () => void
|
onCancel?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
type Active = ExportFormat | "thinking" | "sanitize" | "copy" | "export"
|
type Active = ExportFormat | "thinking" | "copy" | "export"
|
||||||
|
|
||||||
export function DialogExportOptions(props: DialogExportOptionsProps) {
|
export function DialogExportOptions(props: DialogExportOptionsProps) {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
@@ -22,7 +22,6 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
|
|||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
format: "markdown" as ExportFormat,
|
format: "markdown" as ExportFormat,
|
||||||
thinking: props.defaultThinking,
|
thinking: props.defaultThinking,
|
||||||
sanitize: false,
|
|
||||||
active: "markdown" as Active,
|
active: "markdown" as Active,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -31,7 +30,6 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
|
|||||||
action,
|
action,
|
||||||
format: store.format,
|
format: store.format,
|
||||||
thinking: store.thinking,
|
thinking: store.thinking,
|
||||||
sanitize: store.sanitize,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const activate = () => {
|
const activate = () => {
|
||||||
@@ -40,7 +38,6 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (store.active === "thinking") setStore("thinking", !store.thinking)
|
if (store.active === "thinking") setStore("thinking", !store.thinking)
|
||||||
if (store.active === "sanitize") setStore("sanitize", !store.sanitize)
|
|
||||||
if (store.active === "copy" || store.active === "export") confirm(store.active)
|
if (store.active === "copy" || store.active === "export") confirm(store.active)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +52,7 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
|
|||||||
const order: Active[] =
|
const order: Active[] =
|
||||||
store.format === "markdown"
|
store.format === "markdown"
|
||||||
? ["markdown", "json", "thinking", "copy", "export"]
|
? ["markdown", "json", "thinking", "copy", "export"]
|
||||||
: ["markdown", "json", "sanitize", "copy", "export"]
|
: ["markdown", "json", "copy", "export"]
|
||||||
setStore("active", order[(order.indexOf(store.active) + 1) % order.length])
|
setStore("active", order[(order.indexOf(store.active) + 1) % order.length])
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -156,46 +153,6 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
|
|||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={store.format === "json"}>
|
|
||||||
<box
|
|
||||||
flexDirection="row"
|
|
||||||
gap={1}
|
|
||||||
backgroundColor={
|
|
||||||
store.active === "sanitize"
|
|
||||||
? theme.background.formfield.focused
|
|
||||||
: store.sanitize
|
|
||||||
? theme.background.formfield.selected
|
|
||||||
: theme.background.formfield.default
|
|
||||||
}
|
|
||||||
onMouseUp={() => {
|
|
||||||
setStore("active", "sanitize")
|
|
||||||
setStore("sanitize", !store.sanitize)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<text
|
|
||||||
fg={
|
|
||||||
store.active === "sanitize"
|
|
||||||
? theme.text.formfield.focused
|
|
||||||
: store.sanitize
|
|
||||||
? theme.text.formfield.selected
|
|
||||||
: theme.text.formfield.default
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{store.sanitize ? "[x]" : "[ ]"}
|
|
||||||
</text>
|
|
||||||
<text
|
|
||||||
fg={
|
|
||||||
store.active === "sanitize"
|
|
||||||
? theme.text.formfield.focused
|
|
||||||
: store.sanitize
|
|
||||||
? theme.text.formfield.selected
|
|
||||||
: theme.text.formfield.default
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Sanitize sensitive data
|
|
||||||
</text>
|
|
||||||
</box>
|
|
||||||
</Show>
|
|
||||||
<box flexDirection="row" justifyContent="flex-end" gap={1} paddingBottom={1}>
|
<box flexDirection="row" justifyContent="flex-end" gap={1} paddingBottom={1}>
|
||||||
<box
|
<box
|
||||||
paddingLeft={4}
|
paddingLeft={4}
|
||||||
@@ -229,7 +186,6 @@ DialogExportOptions.show = (dialog: DialogContext, defaultThinking: boolean) =>
|
|||||||
action: "copy" | "export"
|
action: "copy" | "export"
|
||||||
format: ExportFormat
|
format: ExportFormat
|
||||||
thinking: boolean
|
thinking: boolean
|
||||||
sanitize: boolean
|
|
||||||
} | null>((resolve) => {
|
} | null>((resolve) => {
|
||||||
dialog.replace(
|
dialog.replace(
|
||||||
() => (
|
() => (
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { testRender } from "@opentui/solid"
|
|||||||
import { expect, test } from "bun:test"
|
import { expect, test } from "bun:test"
|
||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import { resolve, ConfigProvider, Info, useConfig, type Interface } from "../src/config"
|
import { resolve, ConfigProvider, Info, useConfig, type Interface } from "../src/config"
|
||||||
import { settings } from "../src/component/dialog-config"
|
|
||||||
|
|
||||||
test("validates mini replay settings", () => {
|
test("validates mini replay settings", () => {
|
||||||
const decode = Schema.decodeUnknownSync(Info)
|
const decode = Schema.decodeUnknownSync(Info)
|
||||||
@@ -18,10 +17,7 @@ test("validates mini replay settings", () => {
|
|||||||
test("validates the session tabs setting", () => {
|
test("validates the session tabs setting", () => {
|
||||||
const decode = Schema.decodeUnknownSync(Info)
|
const decode = Schema.decodeUnknownSync(Info)
|
||||||
|
|
||||||
expect(decode({ tabs: { enabled: true, layout: "vertical" } })).toEqual({
|
expect(decode({ tabs: { enabled: true, vertical: true } })).toEqual({ tabs: { enabled: true, vertical: true } })
|
||||||
tabs: { enabled: true, layout: "vertical" },
|
|
||||||
})
|
|
||||||
expect(() => decode({ tabs: { layout: true } })).toThrow()
|
|
||||||
expect(() => decode({ tabs: { enabled: "on" } })).toThrow()
|
expect(() => decode({ tabs: { enabled: "on" } })).toThrow()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -42,13 +38,6 @@ test("resolves nested config and keybind defaults", () => {
|
|||||||
expect(config.scroll).toEqual({ speed: 2, acceleration: true })
|
expect(config.scroll).toEqual({ speed: 2, acceleration: true })
|
||||||
expect(config.diffs).toEqual({ view: "split" })
|
expect(config.diffs).toEqual({ view: "split" })
|
||||||
expect(config.debug).toEqual({ devtools: true })
|
expect(config.debug).toEqual({ devtools: true })
|
||||||
expect(config.tabs).toEqual({ enabled: true, scope: "cwd", layout: "horizontal" })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("shows resolved tab defaults in settings", () => {
|
|
||||||
expect(settings.find((setting) => setting.path.join(".") === "tabs.enabled")?.default).toBe(true)
|
|
||||||
expect(settings.find((setting) => setting.path.join(".") === "tabs.scope")?.default).toBe("cwd")
|
|
||||||
expect(settings.find((setting) => setting.path.join(".") === "tabs.layout")?.default).toBe("horizontal")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("provides config and its host interface", async () => {
|
test("provides config and its host interface", async () => {
|
||||||
|
|||||||
@@ -60,8 +60,8 @@ async function renderSessionTabs(
|
|||||||
await Bun.write(
|
await Bun.write(
|
||||||
file,
|
file,
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
global: { tabs: [], unread: {} },
|
global: { tabs: options.persisted.map((sessionID) => ({ sessionID })), unread: {} },
|
||||||
cwd: { [directory]: { tabs: options.persisted.map((sessionID) => ({ sessionID })), unread: {} } },
|
cwd: {},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -153,15 +153,15 @@ test("loads persisted tab metadata concurrently on connect", async () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
test("stores session tabs for the current working directory by default", async () => {
|
test("stores session tabs globally by default", async () => {
|
||||||
const setup = await renderSessionTabs("first")
|
const setup = await renderSessionTabs("first")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const file = path.join(setup.state, "test", "tui", "tabs.json")
|
const file = path.join(setup.state, "test", "tui", "tabs.json")
|
||||||
await wait(() => Bun.file(file).size > 0)
|
await wait(() => Bun.file(file).size > 0)
|
||||||
expect(await Bun.file(file).json()).toEqual({
|
expect(await Bun.file(file).json()).toEqual({
|
||||||
global: { tabs: [], unread: {} },
|
global: { tabs: [{ sessionID: "first" }], unread: {} },
|
||||||
cwd: { [directory]: { tabs: [{ sessionID: "first" }], unread: {} } },
|
cwd: {},
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
setup.destroy()
|
setup.destroy()
|
||||||
@@ -180,7 +180,7 @@ test("concurrent TUIs do not alternate shared tab titles from divergent session
|
|||||||
await titled.data.session.sync("shared")
|
await titled.data.session.sync("shared")
|
||||||
await wait(async () => {
|
await wait(async () => {
|
||||||
if (!(await Bun.file(file).exists())) return false
|
if (!(await Bun.file(file).exists())) return false
|
||||||
return (await Bun.file(file).json()).cwd[directory]?.tabs[0]?.title === "Generated title"
|
return (await Bun.file(file).json()).global.tabs[0]?.title === "Generated title"
|
||||||
})
|
})
|
||||||
const observed = ["Generated title"]
|
const observed = ["Generated title"]
|
||||||
const pending = new Set<Promise<void>>()
|
const pending = new Set<Promise<void>>()
|
||||||
@@ -189,7 +189,7 @@ test("concurrent TUIs do not alternate shared tab titles from divergent session
|
|||||||
const read = Bun.file(file)
|
const read = Bun.file(file)
|
||||||
.json()
|
.json()
|
||||||
.then((value) => {
|
.then((value) => {
|
||||||
const title = value.cwd[directory]?.tabs[0]?.title
|
const title = value.global.tabs[0]?.title
|
||||||
if (title && observed.at(-1) !== title) observed.push(title)
|
if (title && observed.at(-1) !== title) observed.push(title)
|
||||||
})
|
})
|
||||||
.catch(() => undefined)
|
.catch(() => undefined)
|
||||||
|
|||||||
Reference in New Issue
Block a user