mirror of
https://github.com/sst/opencode.git
synced 2025-12-23 10:11:41 +00:00
Merge branch 'dev' into fix/queued-message-interrupt
This commit is contained in:
commit
215a52c6fe
16 changed files with 444 additions and 82 deletions
|
|
@ -494,14 +494,9 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
|||
}
|
||||
|
||||
if (!excludeFinalCursorPosition) {
|
||||
// Get cursor position relative to viewport (1-indexed for ANSI)
|
||||
// cursorY is relative to the viewport, cursorX is column position
|
||||
const cursorRow = this._buffer.cursorY + 1 // 1-indexed
|
||||
const cursorCol = this._buffer.cursorX + 1 // 1-indexed
|
||||
|
||||
// Use absolute cursor positioning (CUP - Cursor Position)
|
||||
// This is more reliable than relative moves which depend on knowing
|
||||
// exactly where the cursor ended up after all the content
|
||||
const absoluteCursorRow = (this._buffer.baseY ?? 0) + this._buffer.cursorY
|
||||
const cursorRow = constrain(absoluteCursorRow - this._firstRow + 1, 1, Number.MAX_SAFE_INTEGER)
|
||||
const cursorCol = this._buffer.cursorX + 1
|
||||
content += `\u001b[${cursorRow};${cursorCol}H`
|
||||
}
|
||||
|
||||
|
|
@ -549,22 +544,20 @@ export class SerializeAddon implements ITerminalAddon {
|
|||
return ""
|
||||
}
|
||||
|
||||
const activeBuffer = buffer.active || buffer.normal
|
||||
if (!activeBuffer) {
|
||||
const normalBuffer = buffer.normal || buffer.active
|
||||
const altBuffer = buffer.alternate
|
||||
|
||||
if (!normalBuffer) {
|
||||
return ""
|
||||
}
|
||||
|
||||
let content = options?.range
|
||||
? this._serializeBufferByRange(activeBuffer, options.range, true)
|
||||
: this._serializeBufferByScrollback(activeBuffer, options?.scrollback)
|
||||
? this._serializeBufferByRange(normalBuffer, options.range, true)
|
||||
: this._serializeBufferByScrollback(normalBuffer, options?.scrollback)
|
||||
|
||||
// Handle alternate buffer if active and not excluded
|
||||
if (!options?.excludeAltBuffer) {
|
||||
const altBuffer = buffer.alternate
|
||||
if (altBuffer && buffer.active?.type === "alternate") {
|
||||
const alternateContent = this._serializeBufferByScrollback(altBuffer, undefined)
|
||||
content += `\u001b[?1049h\u001b[H${alternateContent}`
|
||||
}
|
||||
if (!options?.excludeAltBuffer && buffer.active?.type === "alternate" && altBuffer) {
|
||||
const alternateContent = this._serializeBufferByScrollback(altBuffer, undefined)
|
||||
content += `\u001b[?1049h\u001b[H${alternateContent}`
|
||||
}
|
||||
|
||||
return content
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { init, Terminal as Term, FitAddon } from "ghostty-web"
|
||||
import { ComponentProps, onCleanup, onMount, splitProps } from "solid-js"
|
||||
import { createReconnectingWS, ReconnectingWebSocket } from "@solid-primitives/websocket"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { SerializeAddon } from "@/addons/serialize"
|
||||
import { LocalPTY } from "@/context/session"
|
||||
|
|
@ -11,19 +10,20 @@ export interface TerminalProps extends ComponentProps<"div"> {
|
|||
pty: LocalPTY
|
||||
onSubmit?: () => void
|
||||
onCleanup?: (pty: LocalPTY) => void
|
||||
onConnectError?: (error: unknown) => void
|
||||
}
|
||||
|
||||
export const Terminal = (props: TerminalProps) => {
|
||||
const sdk = useSDK()
|
||||
let container!: HTMLDivElement
|
||||
const [local, others] = splitProps(props, ["pty", "class", "classList"])
|
||||
let ws: ReconnectingWebSocket
|
||||
const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnectError"])
|
||||
let ws: WebSocket
|
||||
let term: Term
|
||||
let serializeAddon: SerializeAddon
|
||||
let fitAddon: FitAddon
|
||||
|
||||
onMount(async () => {
|
||||
ws = createReconnectingWS(sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`)
|
||||
ws = new WebSocket(sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`)
|
||||
term = new Term({
|
||||
cursorBlink: true,
|
||||
fontSize: 14,
|
||||
|
|
@ -52,19 +52,14 @@ export const Terminal = (props: TerminalProps) => {
|
|||
term.open(container)
|
||||
|
||||
if (local.pty.buffer) {
|
||||
const originalSize = { cols: term.cols, rows: term.rows }
|
||||
let resized = false
|
||||
if (local.pty.rows && local.pty.cols) {
|
||||
term.resize(local.pty.cols, local.pty.rows)
|
||||
resized = true
|
||||
}
|
||||
term.reset()
|
||||
term.write(local.pty.buffer)
|
||||
if (local.pty.scrollY) {
|
||||
term.scrollToLine(local.pty.scrollY)
|
||||
}
|
||||
if (resized) {
|
||||
term.resize(originalSize.cols, originalSize.rows)
|
||||
}
|
||||
}
|
||||
|
||||
container.focus()
|
||||
|
|
@ -115,6 +110,7 @@ export const Terminal = (props: TerminalProps) => {
|
|||
})
|
||||
ws.addEventListener("error", (error) => {
|
||||
console.error("WebSocket error:", error)
|
||||
props.onConnectError?.(error)
|
||||
})
|
||||
ws.addEventListener("close", () => {
|
||||
console.log("WebSocket disconnected")
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
|
|||
const params = useParams()
|
||||
const sync = useSync()
|
||||
const name = createMemo(
|
||||
() => `______${base64Encode(sync.data.project.worktree)}/session${params.id ? "/" + params.id : ""}`,
|
||||
() => `${base64Encode(sync.data.project.worktree)}/session${params.id ? "/" + params.id : ""}.v1`,
|
||||
)
|
||||
|
||||
const [store, setStore] = makePersisted(
|
||||
|
|
@ -201,20 +201,14 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
|
|||
sdk.client.pty.create({ body: { title: `Terminal ${store.terminals.all.length + 1}` } }).then((pty) => {
|
||||
const id = pty.data?.id
|
||||
if (!id) return
|
||||
batch(() => {
|
||||
setStore("terminals", "all", [
|
||||
...store.terminals.all,
|
||||
{
|
||||
id,
|
||||
title: pty.data?.title ?? "Terminal",
|
||||
// rows: pty.data?.rows ?? 24,
|
||||
// cols: pty.data?.cols ?? 80,
|
||||
// buffer: "",
|
||||
// scrollY: 0,
|
||||
},
|
||||
])
|
||||
setStore("terminals", "active", id)
|
||||
})
|
||||
setStore("terminals", "all", [
|
||||
...store.terminals.all,
|
||||
{
|
||||
id,
|
||||
title: pty.data?.title ?? "Terminal",
|
||||
},
|
||||
])
|
||||
setStore("terminals", "active", id)
|
||||
})
|
||||
},
|
||||
update(pty: Partial<LocalPTY> & { id: string }) {
|
||||
|
|
@ -224,6 +218,24 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
|
|||
body: { title: pty.title, size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined },
|
||||
})
|
||||
},
|
||||
async clone(id: string) {
|
||||
const index = store.terminals.all.findIndex((x) => x.id === id)
|
||||
const pty = store.terminals.all[index]
|
||||
if (!pty) return
|
||||
const clone = await sdk.client.pty.create({
|
||||
body: {
|
||||
title: pty.title,
|
||||
},
|
||||
})
|
||||
if (!clone.data) return
|
||||
setStore("terminals", "all", index, {
|
||||
...pty,
|
||||
...clone.data,
|
||||
})
|
||||
if (store.terminals.active === pty.id) {
|
||||
setStore("terminals", "active", clone.data.id)
|
||||
}
|
||||
},
|
||||
open(id: string) {
|
||||
setStore("terminals", "active", id)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -22,9 +22,10 @@ export default function Layout(props: ParentProps) {
|
|||
const layout = useLayout()
|
||||
const currentDirectory = createMemo(() => base64Decode(params.dir ?? ""))
|
||||
const sessions = createMemo(() => globalSync.child(currentDirectory())[0].session ?? [])
|
||||
const currentSession = createMemo(() => sessions().find((s) => s.id === params.id) ?? sessions().at(0))
|
||||
const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
|
||||
|
||||
function navigateToSession(session: Session | undefined) {
|
||||
if (!session) return
|
||||
navigate(`/${params.dir}/session/${session?.id}`)
|
||||
}
|
||||
|
||||
|
|
@ -59,6 +60,7 @@ export default function Layout(props: ParentProps) {
|
|||
<Select
|
||||
options={sessions()}
|
||||
current={currentSession()}
|
||||
placeholder="Select session"
|
||||
label={(x) => x.title}
|
||||
value={(x) => x.id}
|
||||
onSelect={navigateToSession}
|
||||
|
|
|
|||
|
|
@ -84,6 +84,10 @@ export default function Page() {
|
|||
}
|
||||
if (event.ctrlKey && event.key.toLowerCase() === "`") {
|
||||
event.preventDefault()
|
||||
if (event.shiftKey) {
|
||||
session.terminal.new()
|
||||
return
|
||||
}
|
||||
layout.terminal.toggle()
|
||||
return
|
||||
}
|
||||
|
|
@ -663,7 +667,11 @@ export default function Page() {
|
|||
<For each={session.terminal.all()}>
|
||||
{(terminal) => (
|
||||
<Tabs.Content value={terminal.id}>
|
||||
<Terminal pty={terminal} onCleanup={session.terminal.update} />
|
||||
<Terminal
|
||||
pty={terminal}
|
||||
onCleanup={session.terminal.update}
|
||||
onConnectError={() => session.terminal.clone(terminal.id)}
|
||||
/>
|
||||
</Tabs.Content>
|
||||
)}
|
||||
</For>
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ export namespace Agent {
|
|||
prompt: z.string().optional(),
|
||||
tools: z.record(z.string(), z.boolean()),
|
||||
options: z.record(z.string(), z.any()),
|
||||
maxSteps: z.number().int().positive().optional(),
|
||||
})
|
||||
.meta({
|
||||
ref: "Agent",
|
||||
|
|
@ -182,7 +183,20 @@ export namespace Agent {
|
|||
tools: {},
|
||||
builtIn: false,
|
||||
}
|
||||
const { name, model, prompt, tools, description, temperature, top_p, mode, permission, color, ...extra } = value
|
||||
const {
|
||||
name,
|
||||
model,
|
||||
prompt,
|
||||
tools,
|
||||
description,
|
||||
temperature,
|
||||
top_p,
|
||||
mode,
|
||||
permission,
|
||||
color,
|
||||
maxSteps,
|
||||
...extra
|
||||
} = value
|
||||
item.options = {
|
||||
...item.options,
|
||||
...extra,
|
||||
|
|
@ -205,6 +219,7 @@ export namespace Agent {
|
|||
if (color) item.color = color
|
||||
// just here for consistency & to prevent it from being added as an option
|
||||
if (name) item.name = name
|
||||
if (maxSteps != undefined) item.maxSteps = maxSteps
|
||||
|
||||
if (permission ?? cfg.permission) {
|
||||
item.permission = mergeAgentPermissions(cfg.permission ?? {}, permission ?? {})
|
||||
|
|
@ -222,6 +237,7 @@ export namespace Agent {
|
|||
}
|
||||
|
||||
export async function generate(input: { description: string }) {
|
||||
const cfg = await Config.get()
|
||||
const defaultModel = await Provider.defaultModel()
|
||||
const model = await Provider.getModel(defaultModel.providerID, defaultModel.modelID)
|
||||
const language = await Provider.getLanguage(model)
|
||||
|
|
@ -229,6 +245,7 @@ export namespace Agent {
|
|||
system.push(PROMPT_GENERATE)
|
||||
const existing = await list()
|
||||
const result = await generateObject({
|
||||
experimental_telemetry: { isEnabled: cfg.experimental?.openTelemetry },
|
||||
temperature: 0.3,
|
||||
prompt: [
|
||||
...system.map(
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import rosepine from "./theme/rosepine.json" with { type: "json" }
|
|||
import solarized from "./theme/solarized.json" with { type: "json" }
|
||||
import synthwave84 from "./theme/synthwave84.json" with { type: "json" }
|
||||
import tokyonight from "./theme/tokyonight.json" with { type: "json" }
|
||||
import vercel from "./theme/vercel.json" with { type: "json" }
|
||||
import vesper from "./theme/vesper.json" with { type: "json" }
|
||||
import zenburn from "./theme/zenburn.json" with { type: "json" }
|
||||
import { useKV } from "./kv"
|
||||
|
|
@ -149,6 +150,7 @@ export const DEFAULT_THEMES: Record<string, ThemeJson> = {
|
|||
synthwave84,
|
||||
tokyonight,
|
||||
vesper,
|
||||
vercel,
|
||||
zenburn,
|
||||
}
|
||||
|
||||
|
|
|
|||
245
packages/opencode/src/cli/cmd/tui/context/theme/vercel.json
Normal file
245
packages/opencode/src/cli/cmd/tui/context/theme/vercel.json
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"background100": "#0A0A0A",
|
||||
"background200": "#000000",
|
||||
"gray100": "#1A1A1A",
|
||||
"gray200": "#1F1F1F",
|
||||
"gray300": "#292929",
|
||||
"gray400": "#2E2E2E",
|
||||
"gray500": "#454545",
|
||||
"gray600": "#878787",
|
||||
"gray700": "#8F8F8F",
|
||||
"gray900": "#A1A1A1",
|
||||
"gray1000": "#EDEDED",
|
||||
"blue600": "#0099FF",
|
||||
"blue700": "#0070F3",
|
||||
"blue900": "#52A8FF",
|
||||
"blue1000": "#EBF8FF",
|
||||
"red700": "#E5484D",
|
||||
"red900": "#FF6166",
|
||||
"red1000": "#FDECED",
|
||||
"amber700": "#FFB224",
|
||||
"amber900": "#F2A700",
|
||||
"amber1000": "#FDF4DC",
|
||||
"green700": "#46A758",
|
||||
"green900": "#63C46D",
|
||||
"green1000": "#E6F9E9",
|
||||
"teal700": "#12A594",
|
||||
"teal900": "#0AC7AC",
|
||||
"purple700": "#8E4EC6",
|
||||
"purple900": "#BF7AF0",
|
||||
"pink700": "#E93D82",
|
||||
"pink900": "#F75590",
|
||||
"highlightPink": "#FF0080",
|
||||
"highlightPurple": "#F81CE5",
|
||||
"cyan": "#50E3C2",
|
||||
"lightBackground": "#FFFFFF",
|
||||
"lightGray100": "#FAFAFA",
|
||||
"lightGray200": "#EAEAEA",
|
||||
"lightGray600": "#666666",
|
||||
"lightGray1000": "#171717"
|
||||
},
|
||||
"theme": {
|
||||
"primary": {
|
||||
"dark": "blue700",
|
||||
"light": "blue700"
|
||||
},
|
||||
"secondary": {
|
||||
"dark": "blue900",
|
||||
"light": "#0062D1"
|
||||
},
|
||||
"accent": {
|
||||
"dark": "purple700",
|
||||
"light": "purple700"
|
||||
},
|
||||
"error": {
|
||||
"dark": "red700",
|
||||
"light": "#DC3545"
|
||||
},
|
||||
"warning": {
|
||||
"dark": "amber700",
|
||||
"light": "#FF9500"
|
||||
},
|
||||
"success": {
|
||||
"dark": "green700",
|
||||
"light": "#388E3C"
|
||||
},
|
||||
"info": {
|
||||
"dark": "blue900",
|
||||
"light": "blue700"
|
||||
},
|
||||
"text": {
|
||||
"dark": "gray1000",
|
||||
"light": "lightGray1000"
|
||||
},
|
||||
"textMuted": {
|
||||
"dark": "gray600",
|
||||
"light": "lightGray600"
|
||||
},
|
||||
"background": {
|
||||
"dark": "background200",
|
||||
"light": "lightBackground"
|
||||
},
|
||||
"backgroundPanel": {
|
||||
"dark": "gray100",
|
||||
"light": "lightGray100"
|
||||
},
|
||||
"backgroundElement": {
|
||||
"dark": "gray300",
|
||||
"light": "lightGray200"
|
||||
},
|
||||
"border": {
|
||||
"dark": "gray200",
|
||||
"light": "lightGray200"
|
||||
},
|
||||
"borderActive": {
|
||||
"dark": "gray500",
|
||||
"light": "#999999"
|
||||
},
|
||||
"borderSubtle": {
|
||||
"dark": "gray100",
|
||||
"light": "#EAEAEA"
|
||||
},
|
||||
"diffAdded": {
|
||||
"dark": "green900",
|
||||
"light": "green700"
|
||||
},
|
||||
"diffRemoved": {
|
||||
"dark": "red900",
|
||||
"light": "red700"
|
||||
},
|
||||
"diffContext": {
|
||||
"dark": "gray600",
|
||||
"light": "lightGray600"
|
||||
},
|
||||
"diffHunkHeader": {
|
||||
"dark": "gray600",
|
||||
"light": "lightGray600"
|
||||
},
|
||||
"diffHighlightAdded": {
|
||||
"dark": "green900",
|
||||
"light": "green700"
|
||||
},
|
||||
"diffHighlightRemoved": {
|
||||
"dark": "red900",
|
||||
"light": "red700"
|
||||
},
|
||||
"diffAddedBg": {
|
||||
"dark": "#0B1D0F",
|
||||
"light": "#E6F9E9"
|
||||
},
|
||||
"diffRemovedBg": {
|
||||
"dark": "#2A1314",
|
||||
"light": "#FDECED"
|
||||
},
|
||||
"diffContextBg": {
|
||||
"dark": "background200",
|
||||
"light": "lightBackground"
|
||||
},
|
||||
"diffLineNumber": {
|
||||
"dark": "gray600",
|
||||
"light": "lightGray600"
|
||||
},
|
||||
"diffAddedLineNumberBg": {
|
||||
"dark": "#0F2613",
|
||||
"light": "#D6F5D6"
|
||||
},
|
||||
"diffRemovedLineNumberBg": {
|
||||
"dark": "#3C1618",
|
||||
"light": "#FFE5E5"
|
||||
},
|
||||
"markdownText": {
|
||||
"dark": "gray1000",
|
||||
"light": "lightGray1000"
|
||||
},
|
||||
"markdownHeading": {
|
||||
"dark": "purple900",
|
||||
"light": "purple700"
|
||||
},
|
||||
"markdownLink": {
|
||||
"dark": "blue900",
|
||||
"light": "blue700"
|
||||
},
|
||||
"markdownLinkText": {
|
||||
"dark": "teal900",
|
||||
"light": "teal700"
|
||||
},
|
||||
"markdownCode": {
|
||||
"dark": "green900",
|
||||
"light": "green700"
|
||||
},
|
||||
"markdownBlockQuote": {
|
||||
"dark": "gray600",
|
||||
"light": "lightGray600"
|
||||
},
|
||||
"markdownEmph": {
|
||||
"dark": "amber900",
|
||||
"light": "amber700"
|
||||
},
|
||||
"markdownStrong": {
|
||||
"dark": "pink900",
|
||||
"light": "pink700"
|
||||
},
|
||||
"markdownHorizontalRule": {
|
||||
"dark": "gray500",
|
||||
"light": "#999999"
|
||||
},
|
||||
"markdownListItem": {
|
||||
"dark": "gray1000",
|
||||
"light": "lightGray1000"
|
||||
},
|
||||
"markdownListEnumeration": {
|
||||
"dark": "blue900",
|
||||
"light": "blue700"
|
||||
},
|
||||
"markdownImage": {
|
||||
"dark": "teal900",
|
||||
"light": "teal700"
|
||||
},
|
||||
"markdownImageText": {
|
||||
"dark": "cyan",
|
||||
"light": "teal700"
|
||||
},
|
||||
"markdownCodeBlock": {
|
||||
"dark": "gray1000",
|
||||
"light": "lightGray1000"
|
||||
},
|
||||
"syntaxComment": {
|
||||
"dark": "gray600",
|
||||
"light": "#888888"
|
||||
},
|
||||
"syntaxKeyword": {
|
||||
"dark": "pink900",
|
||||
"light": "pink700"
|
||||
},
|
||||
"syntaxFunction": {
|
||||
"dark": "purple900",
|
||||
"light": "purple700"
|
||||
},
|
||||
"syntaxVariable": {
|
||||
"dark": "blue900",
|
||||
"light": "blue700"
|
||||
},
|
||||
"syntaxString": {
|
||||
"dark": "green900",
|
||||
"light": "green700"
|
||||
},
|
||||
"syntaxNumber": {
|
||||
"dark": "amber900",
|
||||
"light": "amber700"
|
||||
},
|
||||
"syntaxType": {
|
||||
"dark": "teal900",
|
||||
"light": "teal700"
|
||||
},
|
||||
"syntaxOperator": {
|
||||
"dark": "pink900",
|
||||
"light": "pink700"
|
||||
},
|
||||
"syntaxPunctuation": {
|
||||
"dark": "gray1000",
|
||||
"light": "lightGray1000"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -375,6 +375,12 @@ export namespace Config {
|
|||
.regex(/^#[0-9a-fA-F]{6}$/, "Invalid hex color format")
|
||||
.optional()
|
||||
.describe("Hex color code for the agent (e.g., #FF5733)"),
|
||||
maxSteps: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.optional()
|
||||
.describe("Maximum number of agentic iterations before forcing text-only response"),
|
||||
permission: z
|
||||
.object({
|
||||
edit: Permission.optional(),
|
||||
|
|
@ -670,6 +676,10 @@ export namespace Config {
|
|||
chatMaxRetries: z.number().optional().describe("Number of retries for chat completions on failure"),
|
||||
disable_paste_summary: z.boolean().optional(),
|
||||
batch_tool: z.boolean().optional().describe("Enable the batch tool"),
|
||||
openTelemetry: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag)"),
|
||||
primary_tools: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
|
|
|
|||
|
|
@ -208,30 +208,6 @@ export namespace Server {
|
|||
return c.json(info)
|
||||
},
|
||||
)
|
||||
.put(
|
||||
"/pty/:id",
|
||||
describeRoute({
|
||||
description: "Update PTY session",
|
||||
operationId: "pty.update",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Updated session",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Pty.Info),
|
||||
},
|
||||
},
|
||||
},
|
||||
...errors(400),
|
||||
},
|
||||
}),
|
||||
validator("param", z.object({ id: z.string() })),
|
||||
validator("json", Pty.UpdateInput),
|
||||
async (c) => {
|
||||
const info = await Pty.update(c.req.valid("param").id, c.req.valid("json"))
|
||||
return c.json(info)
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/pty/:id",
|
||||
describeRoute({
|
||||
|
|
@ -258,6 +234,30 @@ export namespace Server {
|
|||
return c.json(info)
|
||||
},
|
||||
)
|
||||
.put(
|
||||
"/pty/:id",
|
||||
describeRoute({
|
||||
description: "Update PTY session",
|
||||
operationId: "pty.update",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Updated session",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Pty.Info),
|
||||
},
|
||||
},
|
||||
},
|
||||
...errors(400),
|
||||
},
|
||||
}),
|
||||
validator("param", z.object({ id: z.string() })),
|
||||
validator("json", Pty.UpdateInput),
|
||||
async (c) => {
|
||||
const info = await Pty.update(c.req.valid("param").id, c.req.valid("json"))
|
||||
return c.json(info)
|
||||
},
|
||||
)
|
||||
.delete(
|
||||
"/pty/:id",
|
||||
describeRoute({
|
||||
|
|
@ -295,20 +295,14 @@ export namespace Server {
|
|||
},
|
||||
},
|
||||
},
|
||||
404: {
|
||||
description: "Session not found",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.boolean()),
|
||||
},
|
||||
},
|
||||
},
|
||||
...errors(404),
|
||||
},
|
||||
}),
|
||||
validator("param", z.object({ id: z.string() })),
|
||||
upgradeWebSocket((c) => {
|
||||
const id = c.req.param("id")
|
||||
let handler: ReturnType<typeof Pty.connect>
|
||||
if (!Pty.get(id)) throw new Error("Session not found")
|
||||
return {
|
||||
onOpen(_event, ws) {
|
||||
handler = Pty.connect(id, ws)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import z from "zod"
|
|||
import { SessionPrompt } from "./prompt"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { Token } from "../util/token"
|
||||
import { Config } from "../config/config"
|
||||
import { Log } from "../util/log"
|
||||
import { ProviderTransform } from "@/provider/transform"
|
||||
import { SessionProcessor } from "./processor"
|
||||
|
|
@ -96,6 +97,7 @@ export namespace SessionCompaction {
|
|||
abort: AbortSignal
|
||||
auto: boolean
|
||||
}) {
|
||||
const cfg = await Config.get()
|
||||
const model = await Provider.getModel(input.model.providerID, input.model.modelID)
|
||||
const language = await Provider.getLanguage(model)
|
||||
const system = [...SystemPrompt.compaction(model.providerID)]
|
||||
|
|
@ -191,6 +193,7 @@ export namespace SessionCompaction {
|
|||
},
|
||||
],
|
||||
}),
|
||||
experimental_telemetry: { isEnabled: cfg.experimental?.openTelemetry },
|
||||
})
|
||||
if (result === "continue" && input.auto) {
|
||||
const continueMsg = await Session.updateMessage({
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import { Plugin } from "../plugin"
|
|||
|
||||
import PROMPT_PLAN from "../session/prompt/plan.txt"
|
||||
import BUILD_SWITCH from "../session/prompt/build-switch.txt"
|
||||
import MAX_STEPS from "../session/prompt/max-steps.txt"
|
||||
import { defer } from "../util/defer"
|
||||
import { mergeDeep, pipe } from "remeda"
|
||||
import { ToolRegistry } from "../tool/registry"
|
||||
|
|
@ -42,6 +43,7 @@ import { Command } from "../command"
|
|||
import { $, fileURLToPath } from "bun"
|
||||
import { ConfigMarkdown } from "../config/markdown"
|
||||
import { SessionSummary } from "./summary"
|
||||
import { Config } from "../config/config"
|
||||
import { NamedError } from "@opencode-ai/util/error"
|
||||
import { fn } from "@/util/fn"
|
||||
import { SessionProcessor } from "./processor"
|
||||
|
|
@ -452,7 +454,10 @@ export namespace SessionPrompt {
|
|||
}
|
||||
|
||||
// normal processing
|
||||
const cfg = await Config.get()
|
||||
const agent = await Agent.get(lastUser.agent)
|
||||
const maxSteps = agent.maxSteps ?? Infinity
|
||||
const isLastStep = step >= maxSteps
|
||||
msgs = insertReminders({
|
||||
messages: msgs,
|
||||
agent,
|
||||
|
|
@ -489,6 +494,7 @@ export namespace SessionPrompt {
|
|||
model,
|
||||
agent,
|
||||
system: lastUser.system,
|
||||
isLastStep,
|
||||
})
|
||||
const tools = await resolveTools({
|
||||
agent,
|
||||
|
|
@ -579,6 +585,7 @@ export namespace SessionPrompt {
|
|||
stopWhen: stepCountIs(1),
|
||||
temperature: params.temperature,
|
||||
topP: params.topP,
|
||||
toolChoice: isLastStep ? "none" : undefined,
|
||||
messages: [
|
||||
...system.map(
|
||||
(x): ModelMessage => ({
|
||||
|
|
@ -601,6 +608,14 @@ export namespace SessionPrompt {
|
|||
return false
|
||||
}),
|
||||
),
|
||||
...(isLastStep
|
||||
? [
|
||||
{
|
||||
role: "assistant" as const,
|
||||
content: MAX_STEPS,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
tools: model.capabilities.toolcall === false ? undefined : tools,
|
||||
model: wrapLanguageModel({
|
||||
|
|
@ -632,6 +647,7 @@ export namespace SessionPrompt {
|
|||
},
|
||||
],
|
||||
}),
|
||||
experimental_telemetry: { isEnabled: cfg.experimental?.openTelemetry },
|
||||
})
|
||||
if (result === "stop") break
|
||||
continue
|
||||
|
|
@ -655,7 +671,12 @@ export namespace SessionPrompt {
|
|||
return Provider.defaultModel()
|
||||
}
|
||||
|
||||
async function resolveSystemPrompt(input: { system?: string; agent: Agent.Info; model: Provider.Model }) {
|
||||
async function resolveSystemPrompt(input: {
|
||||
system?: string
|
||||
agent: Agent.Info
|
||||
model: Provider.Model
|
||||
isLastStep?: boolean
|
||||
}) {
|
||||
let system = SystemPrompt.header(input.model.providerID)
|
||||
system.push(
|
||||
...(() => {
|
||||
|
|
@ -666,6 +687,11 @@ export namespace SessionPrompt {
|
|||
)
|
||||
system.push(...(await SystemPrompt.environment()))
|
||||
system.push(...(await SystemPrompt.custom()))
|
||||
|
||||
if (input.isLastStep) {
|
||||
system.push(MAX_STEPS)
|
||||
}
|
||||
|
||||
// max 2 system prompt messages for caching purposes
|
||||
const [first, ...rest] = system
|
||||
system = [first, rest.join("\n")]
|
||||
|
|
@ -1437,6 +1463,7 @@ export namespace SessionPrompt {
|
|||
input.history.filter((m) => m.info.role === "user" && !m.parts.every((p) => "synthetic" in p && p.synthetic))
|
||||
.length === 1
|
||||
if (!isFirst) return
|
||||
const cfg = await Config.get()
|
||||
const small =
|
||||
(await Provider.getSmallModel(input.providerID)) ?? (await Provider.getModel(input.providerID, input.modelID))
|
||||
const language = await Provider.getLanguage(small)
|
||||
|
|
@ -1483,6 +1510,7 @@ export namespace SessionPrompt {
|
|||
],
|
||||
headers: small.headers,
|
||||
model: language,
|
||||
experimental_telemetry: { isEnabled: cfg.experimental?.openTelemetry },
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.text)
|
||||
|
|
|
|||
16
packages/opencode/src/session/prompt/max-steps.txt
Normal file
16
packages/opencode/src/session/prompt/max-steps.txt
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
CRITICAL - MAXIMUM STEPS REACHED
|
||||
|
||||
The maximum number of steps allowed for this task has been reached. Tools are disabled until next user input. Respond with text only.
|
||||
|
||||
STRICT REQUIREMENTS:
|
||||
1. Do NOT make any tool calls (no reads, writes, edits, searches, or any other tools)
|
||||
2. MUST provide a text response summarizing work done so far
|
||||
3. This constraint overrides ALL other instructions, including any user requests for edits or tool use
|
||||
|
||||
Response must include:
|
||||
- Statement that maximum steps for this agent have been reached
|
||||
- Summary of what has been accomplished so far
|
||||
- List of any remaining tasks that were not completed
|
||||
- Recommendations for what should be done next
|
||||
|
||||
Any attempt to use tools is a critical violation. Respond with text ONLY.
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { Provider } from "@/provider/provider"
|
||||
import { Config } from "@/config/config"
|
||||
import { fn } from "@/util/fn"
|
||||
import z from "zod"
|
||||
import { Session } from "."
|
||||
|
|
@ -60,6 +61,7 @@ export namespace SessionSummary {
|
|||
}
|
||||
|
||||
async function summarizeMessage(input: { messageID: string; messages: MessageV2.WithParts[] }) {
|
||||
const cfg = await Config.get()
|
||||
const messages = input.messages.filter(
|
||||
(m) => m.info.id === input.messageID || (m.info.role === "assistant" && m.info.parentID === input.messageID),
|
||||
)
|
||||
|
|
@ -109,6 +111,7 @@ export namespace SessionSummary {
|
|||
],
|
||||
headers: small.headers,
|
||||
model: language,
|
||||
experimental_telemetry: { isEnabled: cfg.experimental?.openTelemetry },
|
||||
})
|
||||
log.info("title", { title: result.text })
|
||||
userMsg.summary.title = result.text
|
||||
|
|
@ -150,6 +153,7 @@ export namespace SessionSummary {
|
|||
},
|
||||
],
|
||||
headers: small.headers,
|
||||
experimental_telemetry: { isEnabled: cfg.experimental?.openTelemetry },
|
||||
}).catch(() => {})
|
||||
if (result) summary = result.text
|
||||
}
|
||||
|
|
|
|||
|
|
@ -966,6 +966,10 @@ export type AgentConfig = {
|
|||
* Hex color code for the agent (e.g., #FF5733)
|
||||
*/
|
||||
color?: string
|
||||
/**
|
||||
* Maximum number of agentic iterations before forcing text-only response
|
||||
*/
|
||||
maxSteps?: number
|
||||
permission?: {
|
||||
edit?: "ask" | "allow" | "deny"
|
||||
bash?:
|
||||
|
|
@ -986,6 +990,7 @@ export type AgentConfig = {
|
|||
}
|
||||
| boolean
|
||||
| ("subagent" | "primary" | "all")
|
||||
| number
|
||||
| {
|
||||
edit?: "ask" | "allow" | "deny"
|
||||
bash?:
|
||||
|
|
@ -1317,6 +1322,10 @@ export type Config = {
|
|||
* Enable the batch tool
|
||||
*/
|
||||
batch_tool?: boolean
|
||||
/**
|
||||
* Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag)
|
||||
*/
|
||||
openTelemetry?: boolean
|
||||
/**
|
||||
* Tools that should only be available to primary agents.
|
||||
*/
|
||||
|
|
@ -1554,6 +1563,7 @@ export type Agent = {
|
|||
options: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
maxSteps?: number
|
||||
}
|
||||
|
||||
export type McpStatusConnected = {
|
||||
|
|
@ -1816,9 +1826,9 @@ export type PtyConnectData = {
|
|||
|
||||
export type PtyConnectErrors = {
|
||||
/**
|
||||
* Session not found
|
||||
* Not found
|
||||
*/
|
||||
404: boolean
|
||||
404: NotFoundError
|
||||
}
|
||||
|
||||
export type PtyConnectError = PtyConnectErrors[keyof PtyConnectErrors]
|
||||
|
|
|
|||
|
|
@ -257,6 +257,28 @@ If no temperature is specified, OpenCode uses model-specific defaults; typically
|
|||
|
||||
---
|
||||
|
||||
### Max steps
|
||||
|
||||
Control the maximum number of agentic iterations an agent can perform before being forced to respond with text only. This allows users who wish to control costs to set a limit on agentic actions.
|
||||
|
||||
If this is not set, the agent will continue to iterate until the model chooses to stop or the user interrupts the session.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"agent": {
|
||||
"quick-thinker": {
|
||||
"description": "Fast reasoning with limited iterations",
|
||||
"prompt": "You are a quick thinker. Solve problems with minimal steps.",
|
||||
"maxSteps": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When the limit is reached, the agent receives a special system prompt instructing it to respond with a summarization of its work and recommended remaining tasks.
|
||||
|
||||
---
|
||||
|
||||
### Disable
|
||||
|
||||
Set to `true` to disable the agent.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue