Merge branch 'dev' into sqlite2

This commit is contained in:
Dax Raad
2026-01-28 18:45:12 -05:00
85 changed files with 2952 additions and 883 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.1.36",
"version": "1.1.40",
"name": "opencode",
"type": "module",
"license": "MIT",
+2 -5
View File
@@ -1,6 +1,5 @@
import path from "path"
import { Global } from "../global"
import fs from "fs/promises"
import z from "zod"
export const OAUTH_DUMMY_KEY = "opencode-oauth-dummy-key"
@@ -59,15 +58,13 @@ export namespace Auth {
export async function set(key: string, info: Info) {
const file = Bun.file(filepath)
const data = await all()
await Bun.write(file, JSON.stringify({ ...data, [key]: info }, null, 2))
await fs.chmod(file.name!, 0o600)
await Bun.write(file, JSON.stringify({ ...data, [key]: info }, null, 2), { mode: 0o600 })
}
export async function remove(key: string) {
const file = Bun.file(filepath)
const data = await all()
delete data[key]
await Bun.write(file, JSON.stringify(data, null, 2))
await fs.chmod(file.name!, 0o600)
await Bun.write(file, JSON.stringify(data, null, 2), { mode: 0o600 })
}
}
@@ -1,16 +1,13 @@
import { TextAttributes, RGBA } from "@opentui/core"
import { For, type JSX } from "solid-js"
import { useTheme, tint } from "@tui/context/theme"
import { logo, marks } from "@/cli/logo"
// Shadow markers (rendered chars in parens):
// _ = full shadow cell (space with bg=shadow)
// ^ = letter top, shadow bottom (▀ with fg=letter, bg=shadow)
// ~ = shadow top only (▀ with fg=shadow)
const SHADOW_MARKER = /[_^~]/
const LOGO_LEFT = [` `, `█▀▀█ █▀▀█ █▀▀█ █▀▀▄`, `█__█ █__█ █^^^ █__█`, `▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀~~▀`]
const LOGO_RIGHT = [``, `█▀▀▀ █▀▀█ █▀▀█ █▀▀█`, `█___ █__█ █__█ █^^^`, `▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀`]
const SHADOW_MARKER = new RegExp(`[${marks}]`)
export function Logo() {
const { theme } = useTheme()
@@ -75,11 +72,11 @@ export function Logo() {
return (
<box>
<For each={LOGO_LEFT}>
<For each={logo.left}>
{(line, index) => (
<box flexDirection="row" gap={1}>
<box flexDirection="row">{renderLine(line, theme.textMuted, false)}</box>
<box flexDirection="row">{renderLine(LOGO_RIGHT[index()], theme.text, true)}</box>
<box flexDirection="row">{renderLine(logo.right[index()], theme.text, true)}</box>
</box>
)}
</For>
@@ -58,6 +58,7 @@ import { DialogTimeline } from "./dialog-timeline"
import { DialogForkFromTimeline } from "./dialog-fork-from-timeline"
import { DialogSessionRename } from "../../component/dialog-session-rename"
import { Sidebar } from "./sidebar"
import { Flag } from "@/flag/flag"
import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
import parsers from "../../../../../../parsers-config.ts"
import { Clipboard } from "../../util/clipboard"
@@ -1338,15 +1339,27 @@ function TextPart(props: { last: boolean; part: TextPart; message: AssistantMess
return (
<Show when={props.part.text.trim()}>
<box id={"text-" + props.part.id} paddingLeft={3} marginTop={1} flexShrink={0}>
<code
filetype="markdown"
drawUnstyledText={false}
streaming={true}
syntaxStyle={syntax()}
content={props.part.text.trim()}
conceal={ctx.conceal()}
fg={theme.text}
/>
<Switch>
<Match when={Flag.OPENCODE_EXPERIMENTAL_MARKDOWN}>
<markdown
syntaxStyle={syntax()}
streaming={true}
content={props.part.text.trim()}
conceal={ctx.conceal()}
/>
</Match>
<Match when={!Flag.OPENCODE_EXPERIMENTAL_MARKDOWN}>
<code
filetype="markdown"
drawUnstyledText={false}
streaming={true}
syntaxStyle={syntax()}
content={props.part.text.trim()}
conceal={ctx.conceal()}
fg={theme.text}
/>
</Match>
</Switch>
</box>
</Show>
)
+6
View File
@@ -0,0 +1,6 @@
export const logo = {
left: [" ", "█▀▀█ █▀▀█ █▀▀█ █▀▀▄", "█__█ █__█ █^^^ █__█", "▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀~~▀"],
right: [" ▄ ", "█▀▀▀ █▀▀█ █▀▀█ █▀▀█", "█___ █__█ █__█ █^^^", "▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀"],
}
export const marks = "_^~"
+44 -15
View File
@@ -1,15 +1,9 @@
import z from "zod"
import { EOL } from "os"
import { NamedError } from "@opencode-ai/util/error"
import { logo as glyphs } from "./logo"
export namespace UI {
const LOGO = [
[`  `, ``],
[`█▀▀█ █▀▀█ █▀▀█ █▀▀▄ `, `█▀▀▀ █▀▀█ █▀▀█ █▀▀█`],
[`█░░█ █░░█ █▀▀▀ █░░█ `, `█░░░ █░░█ █░░█ █▀▀▀`],
[`▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ `, `▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀`],
]
export const CancelledError = NamedError.create("UICancelledError", z.void())
export const Style = {
@@ -47,15 +41,50 @@ export namespace UI {
}
export function logo(pad?: string) {
const result = []
for (const row of LOGO) {
if (pad) result.push(pad)
result.push(Bun.color("gray", "ansi"))
result.push(row[0])
result.push("\x1b[0m")
result.push(row[1])
result.push(EOL)
const result: string[] = []
const reset = "\x1b[0m"
const left = {
fg: Bun.color("gray", "ansi") ?? "",
shadow: "\x1b[38;5;235m",
bg: "\x1b[48;5;235m",
}
const right = {
fg: reset,
shadow: "\x1b[38;5;238m",
bg: "\x1b[48;5;238m",
}
const gap = " "
const draw = (line: string, fg: string, shadow: string, bg: string) => {
const parts: string[] = []
for (const char of line) {
if (char === "_") {
parts.push(bg, " ", reset)
continue
}
if (char === "^") {
parts.push(fg, bg, "▀", reset)
continue
}
if (char === "~") {
parts.push(shadow, "▀", reset)
continue
}
if (char === " ") {
parts.push(" ")
continue
}
parts.push(fg, char, reset)
}
return parts.join("")
}
glyphs.left.forEach((row, index) => {
if (pad) result.push(pad)
result.push(draw(row, left.fg, left.shadow, left.bg))
result.push(gap)
const other = glyphs.right[index] ?? ""
result.push(draw(other, right.fg, right.shadow, right.bg))
result.push(EOL)
})
return result.join("").trimEnd()
}
+43 -29
View File
@@ -1104,20 +1104,23 @@ export namespace Config {
mergeDeep(await loadFile(path.join(Global.Path.config, "opencode.jsonc"))),
)
await import(path.join(Global.Path.config, "config"), {
with: {
type: "toml",
},
})
.then(async (mod) => {
const { provider, model, ...rest } = mod.default
if (provider && model) result.model = `${provider}/${model}`
result["$schema"] = "https://opencode.ai/config.json"
result = mergeDeep(result, rest)
await Bun.write(path.join(Global.Path.config, "config.json"), JSON.stringify(result, null, 2))
await fs.unlink(path.join(Global.Path.config, "config"))
const legacy = path.join(Global.Path.config, "config")
if (existsSync(legacy)) {
await import(pathToFileURL(legacy).href, {
with: {
type: "toml",
},
})
.catch(() => {})
.then(async (mod) => {
const { provider, model, ...rest } = mod.default
if (provider && model) result.model = `${provider}/${model}`
result["$schema"] = "https://opencode.ai/config.json"
result = mergeDeep(result, rest)
await Bun.write(path.join(Global.Path.config, "config.json"), JSON.stringify(result, null, 2))
await fs.unlink(legacy)
})
.catch(() => {})
}
return result
})
@@ -1341,24 +1344,35 @@ export namespace Config {
throw new JsonError({ path: filepath }, { cause: err })
})
if (!filepath.endsWith(".jsonc")) {
const existing = parseConfig(before, filepath)
await Bun.write(filepath, JSON.stringify(mergeDeep(existing, config), null, 2))
} else {
const next = patchJsonc(before, config)
parseConfig(next, filepath)
await Bun.write(filepath, next)
}
const next = await (async () => {
if (!filepath.endsWith(".jsonc")) {
const existing = parseConfig(before, filepath)
const merged = mergeDeep(existing, config)
await Bun.write(filepath, JSON.stringify(merged, null, 2))
return merged
}
const updated = patchJsonc(before, config)
const merged = parseConfig(updated, filepath)
await Bun.write(filepath, updated)
return merged
})()
global.reset()
await Instance.disposeAll()
GlobalBus.emit("event", {
directory: "global",
payload: {
type: Event.Disposed.type,
properties: {},
},
})
void Instance.disposeAll()
.catch(() => undefined)
.finally(() => {
GlobalBus.emit("event", {
directory: "global",
payload: {
type: Event.Disposed.type,
properties: {},
},
})
})
return next
}
export async function directories() {
+1
View File
@@ -46,6 +46,7 @@ export namespace Flag {
export const OPENCODE_EXPERIMENTAL_LSP_TOOL = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_LSP_TOOL")
export const OPENCODE_DISABLE_FILETIME_CHECK = truthy("OPENCODE_DISABLE_FILETIME_CHECK")
export const OPENCODE_EXPERIMENTAL_PLAN_MODE = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_PLAN_MODE")
export const OPENCODE_EXPERIMENTAL_MARKDOWN = truthy("OPENCODE_EXPERIMENTAL_MARKDOWN")
export const OPENCODE_MODELS_URL = process.env["OPENCODE_MODELS_URL"]
function number(key: string) {
+1 -1
View File
@@ -33,7 +33,7 @@ await Promise.all([
fs.mkdir(Global.Path.bin, { recursive: true }),
])
const CACHE_VERSION = "19"
const CACHE_VERSION = "21"
const version = await Bun.file(path.join(Global.Path.cache, "version"))
.text()
+2 -5
View File
@@ -1,5 +1,4 @@
import path from "path"
import fs from "fs/promises"
import z from "zod"
import { Global } from "../global"
@@ -65,16 +64,14 @@ export namespace McpAuth {
if (serverUrl) {
entry.serverUrl = serverUrl
}
await Bun.write(file, JSON.stringify({ ...data, [mcpName]: entry }, null, 2))
await fs.chmod(file.name!, 0o600)
await Bun.write(file, JSON.stringify({ ...data, [mcpName]: entry }, null, 2), { mode: 0o600 })
}
export async function remove(mcpName: string): Promise<void> {
const file = Bun.file(filepath)
const data = await all()
delete data[mcpName]
await Bun.write(file, JSON.stringify(data, null, 2))
await fs.chmod(file.name!, 0o600)
await Bun.write(file, JSON.stringify(data, null, 2), { mode: 0o600 })
}
export async function updateTokens(mcpName: string, tokens: Tokens, serverUrl?: string): Promise<void> {
+85 -1
View File
@@ -10,6 +10,7 @@ const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"
const ISSUER = "https://auth.openai.com"
const CODEX_API_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses"
const OAUTH_PORT = 1455
const OAUTH_POLLING_SAFETY_MARGIN_MS = 3000
interface PkceCodes {
verifier: string
@@ -461,7 +462,7 @@ export async function CodexAuthPlugin(input: PluginInput): Promise<Hooks> {
},
methods: [
{
label: "ChatGPT Pro/Plus",
label: "ChatGPT Pro/Plus (browser)",
type: "oauth",
authorize: async () => {
const { redirectUri } = await startOAuthServer()
@@ -490,6 +491,89 @@ export async function CodexAuthPlugin(input: PluginInput): Promise<Hooks> {
}
},
},
{
label: "ChatGPT Pro/Plus (headless)",
type: "oauth",
authorize: async () => {
const deviceResponse = await fetch(`${ISSUER}/api/accounts/deviceauth/usercode`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"User-Agent": `opencode/${Installation.VERSION}`,
},
body: JSON.stringify({ client_id: CLIENT_ID }),
})
if (!deviceResponse.ok) throw new Error("Failed to initiate device authorization")
const deviceData = (await deviceResponse.json()) as {
device_auth_id: string
user_code: string
interval: string
}
const interval = Math.max(parseInt(deviceData.interval) || 5, 1) * 1000
return {
url: `${ISSUER}/codex/device`,
instructions: `Enter code: ${deviceData.user_code}`,
method: "auto" as const,
async callback() {
while (true) {
const response = await fetch(`${ISSUER}/api/accounts/deviceauth/token`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"User-Agent": `opencode/${Installation.VERSION}`,
},
body: JSON.stringify({
device_auth_id: deviceData.device_auth_id,
user_code: deviceData.user_code,
}),
})
if (response.ok) {
const data = (await response.json()) as {
authorization_code: string
code_verifier: string
}
const tokenResponse = await fetch(`${ISSUER}/oauth/token`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
code: data.authorization_code,
redirect_uri: `${ISSUER}/deviceauth/callback`,
client_id: CLIENT_ID,
code_verifier: data.code_verifier,
}).toString(),
})
if (!tokenResponse.ok) {
throw new Error(`Token exchange failed: ${tokenResponse.status}`)
}
const tokens: TokenResponse = await tokenResponse.json()
return {
type: "success" as const,
refresh: tokens.refresh_token,
access: tokens.access_token,
expires: Date.now() + (tokens.expires_in ?? 3600) * 1000,
accountId: extractAccountId(tokens),
}
}
if (response.status !== 403 && response.status !== 404) {
return { type: "failed" as const }
}
await Bun.sleep(interval + OAUTH_POLLING_SAFETY_MARGIN_MS)
}
},
}
},
},
{
label: "Manually enter API Key",
type: "api",
+24 -1
View File
@@ -61,12 +61,13 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
const info = await getAuth()
if (info.type !== "oauth") return fetch(request, init)
const url = request instanceof URL ? request.href : request.toString()
const { isVision, isAgent } = iife(() => {
try {
const body = typeof init?.body === "string" ? JSON.parse(init.body) : init?.body
// Completions API
if (body?.messages) {
if (body?.messages && url.includes("completions")) {
const last = body.messages[body.messages.length - 1]
return {
isVision: body.messages.some(
@@ -88,6 +89,28 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
isAgent: last?.role !== "user",
}
}
// Messages API
if (body?.messages) {
const last = body.messages[body.messages.length - 1]
const hasNonToolCalls =
Array.isArray(last?.content) && last.content.some((part: any) => part?.type !== "tool_result")
return {
isVision: body.messages.some(
(item: any) =>
Array.isArray(item?.content) &&
item.content.some(
(part: any) =>
part?.type === "image" ||
// images can be nested inside tool_result content
(part?.type === "tool_result" &&
Array.isArray(part?.content) &&
part.content.some((nested: any) => nested?.type === "image")),
),
),
isAgent: !(last?.role === "user" && hasNonToolCalls),
}
}
} catch {}
return { isVision: false, isAgent: false }
})
+1 -1
View File
@@ -15,7 +15,7 @@ import { CopilotAuthPlugin } from "./copilot"
export namespace Plugin {
const log = Log.create({ service: "plugin" })
const BUILTIN = ["opencode-anthropic-auth@0.0.10", "@gitlab/opencode-gitlab-auth@1.3.2"]
const BUILTIN = ["opencode-anthropic-auth@0.0.13", "@gitlab/opencode-gitlab-auth@1.3.2"]
// Built-in plugins that are directly imported (not installed from npm)
const INTERNAL_PLUGINS: PluginInstance[] = [CodexAuthPlugin, CopilotAuthPlugin]
+30 -7
View File
@@ -14,6 +14,10 @@ interface Context {
const context = Context.create<Context>("instance")
const cache = new Map<string, Promise<Context>>()
const disposal = {
all: undefined as Promise<void> | undefined,
}
export const Instance = {
async provide<R>(input: { directory: string; init?: () => Promise<any>; fn: () => R }): Promise<R> {
let existing = cache.get(input.directory)
@@ -77,15 +81,34 @@ export const Instance = {
})
},
async disposeAll() {
Log.Default.info("disposing all instances")
for (const [_key, value] of cache) {
const awaited = await value.catch(() => {})
if (awaited) {
await context.provide(await value, async () => {
if (disposal.all) return disposal.all
disposal.all = iife(async () => {
Log.Default.info("disposing all instances")
const entries = [...cache.entries()]
for (const [key, value] of entries) {
if (cache.get(key) !== value) continue
const ctx = await value.catch((error) => {
Log.Default.warn("instance dispose failed", { key, error })
return undefined
})
if (!ctx) {
if (cache.get(key) === value) cache.delete(key)
continue
}
if (cache.get(key) !== value) continue
await context.provide(ctx, async () => {
await Instance.dispose()
})
}
}
cache.clear()
}).finally(() => {
disposal.all = undefined
})
return disposal.all
},
}
+7 -3
View File
@@ -46,20 +46,24 @@ export namespace State {
}, 10000).unref()
const tasks: Promise<void>[] = []
for (const entry of entries.values()) {
for (const [init, entry] of entries) {
if (!entry.dispose) continue
const label = typeof init === "function" ? init.name : String(init)
const task = Promise.resolve(entry.state)
.then((state) => entry.dispose!(state))
.catch((error) => {
log.error("Error while disposing state:", { error, key })
log.error("Error while disposing state:", { error, key, init: label })
})
tasks.push(task)
}
await Promise.all(tasks)
entries.clear()
recordsByKey.delete(key)
await Promise.all(tasks)
disposalFinished = true
log.info("state disposal completed", { key })
}
+49 -1
View File
@@ -1,5 +1,5 @@
import { Hono } from "hono"
import { describeRoute, resolver } from "hono-openapi"
import { describeRoute, resolver, validator } from "hono-openapi"
import { streamSSE } from "hono/streaming"
import z from "zod"
import { BusEvent } from "@/bus/bus-event"
@@ -8,6 +8,8 @@ import { Instance } from "../../project/instance"
import { Installation } from "@/installation"
import { Log } from "../../util/log"
import { lazy } from "../../util/lazy"
import { Config } from "../../config/config"
import { errors } from "../error"
const log = Log.create({ service: "server" })
@@ -103,6 +105,52 @@ export const GlobalRoutes = lazy(() =>
})
},
)
.get(
"/config",
describeRoute({
summary: "Get global configuration",
description: "Retrieve the current global OpenCode configuration settings and preferences.",
operationId: "global.config.get",
responses: {
200: {
description: "Get global config info",
content: {
"application/json": {
schema: resolver(Config.Info),
},
},
},
},
}),
async (c) => {
return c.json(await Config.getGlobal())
},
)
.patch(
"/config",
describeRoute({
summary: "Update global configuration",
description: "Update global OpenCode configuration settings and preferences.",
operationId: "global.config.update",
responses: {
200: {
description: "Successfully updated global config",
content: {
"application/json": {
schema: resolver(Config.Info),
},
},
},
...errors(400),
},
}),
validator("json", Config.Info),
async (c) => {
const config = c.req.valid("json")
const next = await Config.updateGlobal(config)
return c.json(next)
},
)
.post(
"/dispose",
describeRoute({
+62 -62
View File
@@ -122,6 +122,68 @@ export namespace Server {
}),
)
.route("/global", GlobalRoutes())
.put(
"/auth/:providerID",
describeRoute({
summary: "Set auth credentials",
description: "Set authentication credentials",
operationId: "auth.set",
responses: {
200: {
description: "Successfully set authentication credentials",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
...errors(400),
},
}),
validator(
"param",
z.object({
providerID: z.string(),
}),
),
validator("json", Auth.Info),
async (c) => {
const providerID = c.req.valid("param").providerID
const info = c.req.valid("json")
await Auth.set(providerID, info)
return c.json(true)
},
)
.delete(
"/auth/:providerID",
describeRoute({
summary: "Remove auth credentials",
description: "Remove authentication credentials",
operationId: "auth.remove",
responses: {
200: {
description: "Successfully removed authentication credentials",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
...errors(400),
},
}),
validator(
"param",
z.object({
providerID: z.string(),
}),
),
async (c) => {
const providerID = c.req.valid("param").providerID
await Auth.remove(providerID)
return c.json(true)
},
)
.use(async (c, next) => {
let directory = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd()
try {
@@ -409,68 +471,6 @@ export namespace Server {
return c.json(await Format.status())
},
)
.put(
"/auth/:providerID",
describeRoute({
summary: "Set auth credentials",
description: "Set authentication credentials",
operationId: "auth.set",
responses: {
200: {
description: "Successfully set authentication credentials",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
...errors(400),
},
}),
validator(
"param",
z.object({
providerID: z.string(),
}),
),
validator("json", Auth.Info),
async (c) => {
const providerID = c.req.valid("param").providerID
const info = c.req.valid("json")
await Auth.set(providerID, info)
return c.json(true)
},
)
.delete(
"/auth/:providerID",
describeRoute({
summary: "Remove auth credentials",
description: "Remove authentication credentials",
operationId: "auth.remove",
responses: {
200: {
description: "Successfully removed authentication credentials",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
...errors(400),
},
}),
validator(
"param",
z.object({
providerID: z.string(),
}),
),
async (c) => {
const providerID = c.req.valid("param").providerID
await Auth.remove(providerID)
return c.json(true)
},
)
.get(
"/event",
describeRoute({
+29 -2
View File
@@ -41,6 +41,32 @@ async function resolveRelative(instruction: string): Promise<string[]> {
}
export namespace InstructionPrompt {
const state = Instance.state(() => {
return {
claims: new Map<string, Set<string>>(),
}
})
function isClaimed(messageID: string, filepath: string) {
const claimed = state().claims.get(messageID)
if (!claimed) return false
return claimed.has(filepath)
}
function claim(messageID: string, filepath: string) {
const current = state()
let claimed = current.claims.get(messageID)
if (!claimed) {
claimed = new Set()
current.claims.set(messageID, claimed)
}
claimed.add(filepath)
}
export function clear(messageID: string) {
state().claims.delete(messageID)
}
export async function systemPaths() {
const config = await Config.get()
const paths = new Set<string>()
@@ -137,7 +163,7 @@ export namespace InstructionPrompt {
}
}
export async function resolve(messages: MessageV2.WithParts[], filepath: string) {
export async function resolve(messages: MessageV2.WithParts[], filepath: string, messageID: string) {
const system = await systemPaths()
const already = loaded(messages)
const results: { filepath: string; content: string }[] = []
@@ -147,7 +173,8 @@ export namespace InstructionPrompt {
while (current.startsWith(root)) {
const found = await find(current)
if (found && !system.has(found) && !already.has(found)) {
if (found && !system.has(found) && !already.has(found) && !isClaimed(messageID, found)) {
claim(messageID, found)
const content = await Bun.file(found)
.text()
.catch(() => undefined)
@@ -177,6 +177,8 @@ export namespace MessageV2 {
})
.optional(),
command: z.string().optional(),
}).meta({
ref: "SubtaskPart",
})
export type SubtaskPart = z.infer<typeof SubtaskPart>
+2
View File
@@ -549,6 +549,7 @@ export namespace SessionPrompt {
model,
abort,
})
using _ = defer(() => InstructionPrompt.clear(processor.message.id))
// Check if user explicitly invoked an agent via @ in this turn
const lastUserMsg = msgs.findLast((m) => m.info.role === "user")
@@ -837,6 +838,7 @@ export namespace SessionPrompt {
system: input.system,
variant: input.variant,
}
using _ = defer(() => InstructionPrompt.clear(info.id))
const parts = await Promise.all(
input.parts.map(async (part): Promise<MessageV2.Part[]> => {
+1 -1
View File
@@ -60,7 +60,7 @@ export const ReadTool = Tool.define("read", {
throw new Error(`File not found: ${filepath}`)
}
const instructions = await InstructionPrompt.resolve(ctx.messages, filepath)
const instructions = await InstructionPrompt.resolve(ctx.messages, filepath, ctx.messageID)
// Exclude SVG (XML-based) and vnd.fastbidsheet (.fbs extension, commonly FlatBuffers schema files)
const isImage =
+3 -1
View File
@@ -159,8 +159,10 @@ export const TaskTool = Tool.define("task", async (ctx) => {
...Object.fromEntries((config.experimental?.primary_tools ?? []).map((t) => [t, false])),
},
parts: promptParts,
}).finally(() => {
unsub()
})
unsub()
const messages = await Session.messages({ sessionID: session.id })
const summary = messages
.filter((x) => x.info.role === "assistant")
@@ -18,7 +18,7 @@ describe("InstructionPrompt.resolve", () => {
const system = await InstructionPrompt.systemPaths()
expect(system.has(path.join(tmp.path, "AGENTS.md"))).toBe(true)
const results = await InstructionPrompt.resolve([], path.join(tmp.path, "src", "file.ts"))
const results = await InstructionPrompt.resolve([], path.join(tmp.path, "src", "file.ts"), "test-message-1")
expect(results).toEqual([])
},
})
@@ -37,7 +37,11 @@ describe("InstructionPrompt.resolve", () => {
const system = await InstructionPrompt.systemPaths()
expect(system.has(path.join(tmp.path, "subdir", "AGENTS.md"))).toBe(false)
const results = await InstructionPrompt.resolve([], path.join(tmp.path, "subdir", "nested", "file.ts"))
const results = await InstructionPrompt.resolve(
[],
path.join(tmp.path, "subdir", "nested", "file.ts"),
"test-message-2",
)
expect(results.length).toBe(1)
expect(results[0].filepath).toBe(path.join(tmp.path, "subdir", "AGENTS.md"))
},