Compare commits

...

34 Commits

Author SHA1 Message Date
Dax d56dec4ba7 wip: optional IDs in api (#1128) 2025-07-18 13:42:50 -04:00
Dax Raad c952e9ae3d message rendering performance improvements 2025-07-18 13:40:07 -04:00
GitHub Action 6470243095 ignore: update download stats 2025-07-18 2025-07-18 12:04:28 +00:00
GitHub Action c8321cfbd9 ignore: update download stats 2025-07-18 2025-07-18 12:02:18 +00:00
Yihui Khuu 46c246e01f fix: \{return} should be replaced with new line on all lines (#1119) 2025-07-18 06:22:36 -05:00
adamdotdevin 9964d8e6c0 fix: model cost overrides 2025-07-18 05:08:35 -05:00
Timo Clasen df33143396 feat(tui): parse for file attachments when exiting EDITOR (#1117) 2025-07-18 04:47:20 -05:00
Aiden Cline 571aeaaea2 tweak: remove needless resorting (#1116) 2025-07-18 04:42:43 -05:00
Aiden Cline edfea03917 tweak: fix [object Object] in logging (#1114) 2025-07-18 04:41:23 -05:00
Tom 81c88cc742 fix(tui): ensure viewport scrolls to bottom on new messages (#1110) 2025-07-18 04:41:03 -05:00
Mike Wallio 99b9390d80 Update to a customized beast mode v3 for opencode. (#1109) 2025-07-17 20:10:06 -05:00
Dax Raad 23c30521d8 only enable ruff if it seems to be used 2025-07-17 18:07:06 -04:00
Wendell Misiedjan e681d610de feat: support AWS_BEARER_TOKEN_BEDROCK for amazon bedrock provider autoloading (#1094) 2025-07-17 09:12:30 -05:00
Aiden Cline a1fdeded3e tweak: allow mcp servers to include headers (#1096) 2025-07-17 09:11:48 -05:00
GitHub Action 2051312d12 ignore: update download stats 2025-07-17 2025-07-17 14:07:13 +00:00
Alexander Drottsgård 20cb7a76af feat(tui): highlight current session in sessions modal (#1093) 2025-07-17 07:40:15 -05:00
Timo Clasen a493aec174 feat(tui): remove share commands from help if sharing is disabled (#1087) 2025-07-17 04:28:12 -05:00
Aiden Cline 3ce3ac8e61 fix: message error centering (#1085) 2025-07-17 04:27:40 -05:00
Timo Clasen 91ad64feda fix(tui): user defined ctrl+z should take precedence over suspending (#1088) 2025-07-17 04:27:02 -05:00
Timo Clasen 60b55f9d92 feat(tui): remove sharing info from session header when sharing is disabled (#1076) 2025-07-16 17:36:48 -05:00
Timo Clasen 3c6c2bf13b docs(share): add explicit manual share mode (#1074) 2025-07-16 16:08:25 -05:00
Aiden Cline d4f9375548 fix: type 'reasoning' was provided without its required following item (#1072) 2025-07-16 15:59:40 -05:00
Jay V 28b39f547e docs: edit 2025-07-16 16:59:12 -04:00
Jay V 7520f5efa8 docs: update enterprise doc 2025-07-16 16:44:28 -04:00
Jay V eb4cdf4b20 docs: config doc 2025-07-16 16:27:44 -04:00
Jay V 9f6fc1c3c5 docs: edits 2025-07-16 16:20:09 -04:00
Mike Wallio dfede9ae6e Remove binary file opencode (#1069) 2025-07-16 15:10:40 -05:00
Daniel Saldarriaga López fc45c0c944 docs: fix keybinds documentation to match actual config schema (#867) 2025-07-16 15:34:52 -04:00
adamdotdevin 9d869f784c fix(tui): expand edit calls 2025-07-16 14:33:57 -05:00
adamdotdevin bd244f73af fix(tui): slightly faster scroll speed 2025-07-16 14:26:46 -05:00
Dax Raad dd34556e9c only include severity 1 diagnostics from lsp in edit tool output 2025-07-16 15:25:37 -04:00
adamdotdevin f7dd48e60d feat(tui): more ways to quit 2025-07-16 14:20:28 -05:00
Dax Raad 93c779cf48 docs: better variable examples 2025-07-16 14:56:24 -04:00
adamdotdevin 360c04c542 docs: copying text 2025-07-16 13:26:26 -05:00
40 changed files with 1002 additions and 602 deletions
+21 -18
View File
@@ -1,20 +1,23 @@
# Download Stats
| Date | GitHub Downloads | npm Downloads | Total |
| ---------- | ---------------- | --------------- | ----------------- |
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
| 2025-07-10 | 43,796 (+5,744) | 71,402 (+6,934) | 115,198 (+12,678) |
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
| Date | GitHub Downloads | npm Downloads | Total |
| ---------- | ---------------- | ---------------- | ----------------- |
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
| 2025-07-10 | 43,796 (+5,744) | 71,402 (+6,934) | 115,198 (+12,678) |
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
| 2025-07-18 | 70,380 (+1) | 102,587 (+0) | 172,967 (+1) |
+4 -4
View File
@@ -100,7 +100,7 @@
],
"catalog": {
"@types/node": "22.13.9",
"ai": "5.0.0-beta.18",
"ai": "5.0.0-beta.21",
"typescript": "5.8.2",
"zod": "3.25.49",
},
@@ -109,11 +109,11 @@
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@1.2.12", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-YSzjlko7JvuiyQFmI9RN1tNZdEiZxc+6xld/0tq/VkJaHpEzGAb1yiNxxvmYVcjvfu/PcvCxAAYXmTYQQ63IHQ=="],
"@ai-sdk/gateway": ["@ai-sdk/gateway@1.0.0-beta.7", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-beta.1", "@ai-sdk/provider-utils": "3.0.0-beta.2" }, "peerDependencies": { "zod": "^3.25.49" } }, "sha512-0TOWFetxYximugqdmA/uxk+NRkz51Lyo+anntc3hPzm2OnsbE/Q2wWkKJ14YzoCAfWZ9ZhvxsJRp8xzvvQREsA=="],
"@ai-sdk/gateway": ["@ai-sdk/gateway@1.0.0-beta.8", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-beta.1", "@ai-sdk/provider-utils": "3.0.0-beta.3" }, "peerDependencies": { "zod": "^3.25.49 || ^4" } }, "sha512-D2SqYRT/42JTiRxUuiWtn5cYQFscpb9Z14UNvJx7lnurBUXx57zy7TbLH0h7O+WbCluTQN5G6146JpUZ/SRyzw=="],
"@ai-sdk/provider": ["@ai-sdk/provider@2.0.0-beta.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Z8SPncMtS3RsoXITmT7NVwrAq6M44dmw0DoUOYJqNNtCu8iMWuxB8Nxsoqpa0uEEy9R1V1ZThJAXTYgjTUxl3w=="],
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0-beta.2", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-beta.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.49" } }, "sha512-H4K+4weOVgWqrDDeAbQWoA4U5mN4WrQPHQFdH7ynQYcnhj/pzctU9Q6mGlR5ESMWxaXxazxlOblSITlXo9bahA=="],
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0-beta.3", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-beta.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.49 || ^4" } }, "sha512-4gZ392GxjzMF7TnReF2eTKhOSyiSS3ydRVq4I7jxkeV5sdEuMoH3gzfItmlctsqGxlMU1/+zKPwl5yYz9O2dzg=="],
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
@@ -543,7 +543,7 @@
"acorn-walk": ["acorn-walk@8.3.2", "", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="],
"ai": ["ai@5.0.0-beta.18", "", { "dependencies": { "@ai-sdk/gateway": "1.0.0-beta.7", "@ai-sdk/provider": "2.0.0-beta.1", "@ai-sdk/provider-utils": "3.0.0-beta.2", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.49" } }, "sha512-gUkO9WF315aT6Al7FxBpY41z3aOVE11BdQUIdIekP1sebys4dElMmKjs9AoaNeYPcf+PklwQ6ZofztiT0egd4A=="],
"ai": ["ai@5.0.0-beta.21", "", { "dependencies": { "@ai-sdk/gateway": "1.0.0-beta.8", "@ai-sdk/provider": "2.0.0-beta.1", "@ai-sdk/provider-utils": "3.0.0-beta.3", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.49 || ^4" }, "bin": { "ai": "dist/bin/ai.min.js" } }, "sha512-ZmgUoEIXb2G2HLtK1U3UB+hSDa3qrVIeAfgXf3SIE9r5Vqj6xHG1pN/7fHIZDSgb1TCaypG0ANVB0O9WmnMfiw=="],
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
+1 -1
View File
@@ -18,7 +18,7 @@
"typescript": "5.8.2",
"@types/node": "22.13.9",
"zod": "3.25.49",
"ai": "5.0.0-beta.18"
"ai": "5.0.0-beta.21"
}
},
"devDependencies": {
+1 -1
View File
@@ -120,7 +120,7 @@ export const AuthLoginCommand = cmd({
if (provider === "amazon-bedrock") {
prompts.log.info(
"Amazon bedrock can be configured with standard AWS environment variables like AWS_PROFILE or AWS_ACCESS_KEY_ID",
"Amazon bedrock can be configured with standard AWS environment variables like AWS_BEARER_TOKEN_BEDROCK, AWS_PROFILE or AWS_ACCESS_KEY_ID",
)
prompts.outro("Done")
return
-2
View File
@@ -172,8 +172,6 @@ export const RunCommand = cmd({
parts: [
{
id: Identifier.ascending("part"),
sessionID: session.id,
messageID: messageID,
type: "text",
text: message,
},
+8 -2
View File
@@ -31,6 +31,9 @@ export namespace Config {
const os = await import("os")
result.username = os.userInfo().username
}
if (!result.layout) {
result.layout = "auto"
}
log.info("loaded", result)
@@ -57,6 +60,7 @@ export namespace Config {
type: z.literal("remote").describe("Type of MCP server connection"),
url: z.string().describe("URL of the remote MCP server"),
enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"),
headers: z.record(z.string(), z.string()).optional().describe("Headers to send with the request"),
})
.strict()
.openapi({
@@ -136,9 +140,11 @@ export namespace Config {
theme: z.string().optional().describe("Theme name to use for the interface"),
keybinds: Keybinds.optional().describe("Custom keybind configurations"),
share: z
.enum(["auto", "disabled"])
.enum(["manual", "auto", "disabled"])
.optional()
.describe("Control sharing behavior: 'auto' enables automatic sharing, 'disabled' disables all sharing"),
.describe(
"Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing",
),
autoshare: z
.boolean()
.optional()
+23 -1
View File
@@ -105,7 +105,29 @@ export const ruff: Info = {
command: ["ruff", "format", "$FILE"],
extensions: [".py", ".pyi"],
async enabled() {
return Bun.which("ruff") !== null
if (!Bun.which("ruff")) return false
const app = App.info()
const configs = ["pyproject.toml", "ruff.toml", ".ruff.toml"]
for (const config of configs) {
const found = await Filesystem.findUp(config, app.path.cwd, app.path.root)
if (found.length > 0) {
if (config === "pyproject.toml") {
const content = await Bun.file(found[0]).text()
if (content.includes("[tool.ruff]")) return true
} else {
return true
}
}
}
const deps = ["requirements.txt", "pyproject.toml", "Pipfile"]
for (const dep of deps) {
const found = await Filesystem.findUp(dep, app.path.cwd, app.path.root)
if (found.length > 0) {
const content = await Bun.file(found[0]).text()
if (content.includes("ruff")) return true
}
}
return false
},
}
+4 -1
View File
@@ -17,6 +17,9 @@ import { DebugCommand } from "./cli/cmd/debug"
import { StatsCommand } from "./cli/cmd/stats"
import { McpCommand } from "./cli/cmd/mcp"
import { InstallGithubCommand } from "./cli/cmd/install-github"
import { Trace } from "./trace"
Trace.init()
const cancel = new AbortController()
@@ -42,7 +45,7 @@ const cli = yargs(hideBin(process.argv))
type: "boolean",
})
.middleware(async () => {
await Log.init({ print: process.argv.includes("--print-logs") })
await Log.init({ print: process.argv.includes("--print-logs"), dev: Installation.isDev() })
try {
const { Config } = await import("./config/config")
+1
View File
@@ -37,6 +37,7 @@ export namespace MCP {
transport: {
type: "sse",
url: mcp.url,
headers: mcp.headers,
},
}).catch(() => {})
if (!client) {
+16 -9
View File
@@ -139,7 +139,8 @@ export namespace Provider {
}
},
"amazon-bedrock": async () => {
if (!process.env["AWS_PROFILE"] && !process.env["AWS_ACCESS_KEY_ID"]) return { autoload: false }
if (!process.env["AWS_PROFILE"] && !process.env["AWS_ACCESS_KEY_ID"] && !process.env["AWS_BEARER_TOKEN_BEDROCK"])
return { autoload: false }
const region = process.env["AWS_REGION"] ?? "us-east-1"
@@ -271,14 +272,20 @@ export namespace Provider {
reasoning: model.reasoning ?? existing?.reasoning ?? false,
temperature: model.temperature ?? existing?.temperature ?? false,
tool_call: model.tool_call ?? existing?.tool_call ?? true,
cost: {
...existing?.cost,
...model.cost,
input: 0,
output: 0,
cache_read: 0,
cache_write: 0,
},
cost:
!model.cost && !existing?.cost
? {
input: 0,
output: 0,
cache_read: 0,
cache_write: 0,
}
: {
cache_read: 0,
cache_write: 0,
...existing?.cost,
...model.cost,
},
options: {
...existing?.options,
...model.options,
+1 -10
View File
@@ -451,16 +451,7 @@ export namespace Server {
id: z.string().openapi({ description: "Session ID" }),
}),
),
zValidator(
"json",
z.object({
messageID: z.string(),
providerID: z.string(),
modelID: z.string(),
mode: z.string(),
parts: z.union([MessageV2.FilePart, MessageV2.TextPart]).array(),
}),
),
zValidator("json", Session.ChatInput.omit({ sessionID: true })),
async (c) => {
const sessionID = c.req.valid("param").id
const body = c.req.valid("json")
+42 -12
View File
@@ -319,14 +319,39 @@ export namespace Session {
return part
}
export async function chat(input: {
sessionID: string
messageID: string
providerID: string
modelID: string
mode?: string
parts: (MessageV2.TextPart | MessageV2.FilePart)[]
}) {
export const ChatInput = z.object({
sessionID: Identifier.schema("session"),
messageID: Identifier.schema("message").optional(),
providerID: z.string(),
modelID: z.string(),
mode: z.string().optional(),
parts: z.array(
z.discriminatedUnion("type", [
MessageV2.TextPart.omit({
messageID: true,
sessionID: true,
})
.partial({
id: true,
})
.openapi({
ref: "TextPartInput",
}),
MessageV2.FilePart.omit({
messageID: true,
sessionID: true,
})
.partial({
id: true,
})
.openapi({
ref: "FilePartInput",
}),
]),
),
})
export async function chat(input: z.infer<typeof ChatInput>) {
const l = log.clone().tag("session", input.sessionID)
l.info("chatting")
@@ -384,7 +409,7 @@ export namespace Session {
if (lastSummary) msgs = msgs.filter((msg) => msg.info.id >= lastSummary.info.id)
const userMsg: MessageV2.Info = {
id: input.messageID,
id: input.messageID ?? Identifier.ascending("message"),
role: "user",
sessionID: input.sessionID,
time: {
@@ -490,7 +515,14 @@ export namespace Session {
]
}
}
return [part]
return [
{
id: Identifier.ascending("part"),
...part,
messageID: userMsg.id,
sessionID: input.sessionID,
},
]
}),
).then((x) => x.flat())
@@ -1104,8 +1136,6 @@ export namespace Session {
parts: [
{
id: Identifier.ascending("part"),
sessionID: input.sessionID,
messageID: input.messageID,
type: "text",
text: PROMPT_INITIALIZE.replace("${path}", app.path.root),
},
+80 -58
View File
@@ -1,95 +1,117 @@
You are an agent known as opencode - please keep going until the users query is completely resolved, before ending your turn and yielding back to the user.
You are opencode, an autonomous agent - please keep going until the user's query is completely resolved, before ending your turn and yielding back to the user.
Your thinking should be thorough and so it's fine if it's very long. However, avoid unnecessary repetition and verbosity. You should be concise, but thorough.
You MUST iterate and keep going until the problem is solved.
I want you to fully solve this autonomously before coming back to me.
You have everything you need to resolve this problem. I want you to fully solve this autonomously before coming back to me.
Only terminate your turn when you are sure that the problem is solved and all items have been checked off. Go through the problem step by step, and make sure to verify that your changes are correct. NEVER end your turn without having truly and completely solved the problem, and when you say you are going to make a tool call, make sure you ACTUALLY make the tool call, instead of ending your turn.
Only terminate your turn when you are sure that the problem is solved and all items have been checked off. Use the TodoWrite and TodoRead tools to track and manage steps. Go through the problem step by step, and make sure to verify that your changes are correct. Once each step is finished mark it as completed with the TodoWrite tool. NEVER end your turn without having truly and completely solved the problem, use the TodoRead tool to make sure all steps are complete, and when you say you are going to make a tool call, make sure you ACTUALLY make the tool call, instead of ending your turn. If a step is impossible to complete, mark it as cancelled using the TodoWrite tool.
Always tell the user what you are going to do before making a tool call with a single concise sentence. This will help them understand what you are doing and why.
THE PROBLEM CAN NOT BE SOLVED WITHOUT EXTENSIVE INTERNET RESEARCH.
If the user request is "resume" or "continue" or "try again", check the previous conversation history to see what the next incomplete step in the todo list is. Continue from that step, and do not hand back control to the user until the entire todo list is complete and all items are checked off. Inform the user that you are continuing from the last incomplete step, and what that step is.
You must use the webfetch tool to recursively gather all information from URLs provided to you by the user, as well as any links you find in the content of those pages.
Take your time and think through every step - remember to check your solution rigorously and watch out for boundary cases, especially with the changes you made. Your solution must be perfect. If not, continue working on it. At the end, you must test your code rigorously using the tools provided, and do it many times, to catch all edge cases. If it is not robust, iterate more and make it perfect. Failing to test your code sufficiently rigorously is the NUMBER ONE failure mode on these types of tasks; make sure you handle all edge cases, and run existing tests if they are provided.
Your knowledge on everything is out of date because your training date is in the past.
You MUST plan extensively before each function call, and reflect extensively on the outcomes of the previous function calls. DO NOT do this entire process by making function calls only, as this can impair your ability to solve the problem and think insightfully.
You CANNOT successfully complete this task without using Bing to verify your understanding of third party packages and dependencies is up to date. You must use the webfetch tool to search bing for how to properly use libraries, packages, frameworks, dependencies, etc. every single time you install or implement one. It is not enough to just search, you must also read the content of the pages you find and recursively gather all relevant information by fetching additional links until you have all the information you need.
If the user request is "resume" or "continue" or "try again",use the TodoRead tool to find the next pending step. Continue from that step, and do not hand back control to the user until the entire todo list is complete and all steps are marked as complete or cancelled. Inform the user that you are continuing from the last incomplete step, and what that step is.
Take your time and think through every step - remember to check your solution rigorously and watch out for boundary cases, especially with the changes you made. Use the sequential thinking tool if available. Your solution must be perfect. If not, continue working on it. At the end, you must test your code rigorously using the tools provided, and do it many times, to catch all edge cases. If it is not robust, update the plan and iterate more and make it perfect. Failing to test your code sufficiently rigorously is the NUMBER ONE failure mode on these types of tasks; run the build, and verify that the changes you made actually build; make sure you handle all edge cases, and run existing tests if they are provided.
You MUST plan extensively before each tool call, and reflect extensively on the outcomes of the previous tool calls. DO NOT do this entire process by making tool calls only, as this can impair your ability to solve the problem and think insightfully.
You MUST keep working until the problem is completely solved, and all steps in the todo list are complete. Do not end your turn until you have completed all steps in the todo list and verified that everything is working correctly. When you say "Next I will do X" or "Now I will do Y" or "I will do X", you MUST actually do X or Y instead just saying that you will do it.
You MUST use the ToolRead tool to verify that all steps are complete or cancelled before ending your turn. If any steps are incomplete, you MUST continue working on them until they are all complete.
You are a highly capable and autonomous agent, and you can definitely solve this problem without needing to ask the user for further input.
# Workflow
1. Understand the problem deeply. Carefully read the issue and think critically about what is required.
2. Investigate the codebase. Explore relevant files, search for key functions, and gather context.
3. Develop a clear, step-by-step plan. Break down the fix into manageable, incremental steps. Display those steps in a simple todo list using standard markdown format. Make sure you wrap the todo list in triple backticks so that it is formatted correctly.
4. Implement the fix incrementally. Make small, testable code changes.
5. Debug as needed. Use debugging techniques to isolate and resolve issues.
6. Test frequently. Run tests after each change to verify correctness.
7. Iterate until the root cause is fixed and all tests pass.
8. Reflect and validate comprehensively. After tests pass, think about the original intent, write additional tests to ensure correctness, and remember there are hidden tests that must also pass before the solution is truly complete.
1. Fetch any URL's provided by the user using the `webfetch` tool.
2. Understand the problem deeply. Carefully read the issue and think critically about what is required. Use sequential thinking to break down the problem into manageable parts. Consider the following:
- What is the expected behavior?
- What are the edge cases?
- What are the potential pitfalls?
- How does this fit into the larger context of the codebase?
- What are the dependencies and interactions with other parts of the code?
3. Investigate the codebase. Explore relevant files, search for key functions, and gather context.
4. Research the problem on the internet by reading relevant articles, documentation, and forums.
5. Develop a clear, step-by-step plan. Break down the fix into manageable, incremental steps. Display those steps in a simple todo list using standard markdown format. Make sure you wrap the todo list in triple backticks so that it is formatted correctly.
6. Implement the fix incrementally. Make small, testable code changes.
7. Debug as needed. Use debugging techniques to isolate and resolve issues.
8. Test frequently. Run tests after each change to verify correctness.
9. Iterate until the root cause is fixed and all tests pass.
10. Reflect and validate comprehensively. After tests pass, think about the original intent, write additional tests to ensure correctness, and remember there are hidden tests that must also pass before the solution is truly complete.
Refer to the detailed sections below for more information on each step.
## 1. Deeply Understand the Problem
Carefully read the issue and think hard about a plan to solve it before coding.
## 1. Fetch Provided URLs
- If the user provides a URL, use the `webfetch` tool to retrieve the content of the provided URL.
- After fetching, review the content returned by the fetch tool.
- If you find any additional URLs or links that are relevant, use the `webfetch` tool again to retrieve those links.
- Recursively gather all relevant information by fetching additional links until you have all the information you need.
## 2. Codebase Investigation
## 2. Deeply Understand the Problem
Carefully read the issue and think hard about a plan to solve it before coding. Use the sequential thinking tool if available.
## 3. Codebase Investigation
- Explore relevant files and directories.
- Search for key functions, classes, or variables related to the issue.
- Read and understand relevant code snippets.
- Identify the root cause of the problem.
- Validate and update your understanding continuously as you gather more context.
## 3. Fetch Provided URLs
- If the user provides a URL, use the `functions.fetch_webpage` tool to retrieve the content of the provided URL.
## 4. Internet Research
- Use the `webfetch` tool to search bing by fetching the URL `https://www.bing.com/search?q=your+search+query`.
- After fetching, review the content returned by the fetch tool.
- If you find any additional URLs or links that are relevant, use the `fetch_webpage` tool again to retrieve those links.
- If you find any additional URLs or links that are relevant, use the `webfetch` tool again to retrieve those links.
- Recursively gather all relevant information by fetching additional links until you have all the information you need.
## 4. Develop a Detailed Plan
## 5. Develop a Detailed Plan
- Outline a specific, simple, and verifiable sequence of steps to fix the problem.
- Create a todo list in markdown format to track your progress.
- Each time you complete a step, check it off using `[x]` syntax.
- Each time you check off a step, display the updated todo list to the user.
- Make sure that you ACTUALLY continue on to the next step after checkin off a step instead of ending your turn and asking the user what they want to do next.
- Add steps using the TodoWrite tool.
- Each time you complete a step, mark it as complete using the TodoWrite tool.
- Each time you check off a step, use the TodoRead tool and display the updated todo list to the user in markdown format.
- You MUST continue on to the next step after checking off a step instead of ending your turn and asking the user what they want to do next.
- You may only end your turn when all steps in the todo list are marked as complete or cancelled.
## 5. Making Code Changes
## 6. Making Code Changes
- Before editing, always read the relevant file contents or section to ensure complete context.
- Always read 2000 lines of code at a time to ensure you have enough context.
- If a patch is not applied correctly, attempt to reapply it.
- Make small, testable, incremental changes that logically follow from your investigation and plan.
- When using the edit tool, include 3-5 lines of unchanged code before and after the string you want to replace, to make it unambiguous which part of the file should be edited.
- If a patch or edit is not applied correctly, attempt to reapply it.
- Always validate that your changes build and pass tests after each change.
- If the build fails or test fail, debug why before proceeding, update the plan as needed.
## 6. Debugging
- Make code changes only if you have high confidence they can solve the problem
- When debugging, try to determine the root cause rather than addressing symptoms
- Debug for as long as needed to identify the root cause and identify a fix
- Use the #problems tool to check for any problems in the code
- Use print statements, logs, or temporary code to inspect program state, including descriptive statements or error messages to understand what's happening
- To test hypotheses, you can also add test statements or functions
## 7. Debugging
- Use the `lsp_diagnostics` tool to check for any problems in the code.
- Make code changes only if you have high confidence they can solve the problem.
- When debugging, try to determine the root cause rather than addressing symptoms.
- Debug for as long as needed to identify the root cause and identify a fix.
- Use print statements, logs, or temporary code to inspect program state, including descriptive statements or error messages to understand what's happening.
- To test hypotheses, you can also add test statements or functions.
- Revisit your assumptions if unexpected behavior occurs.
# Fetch Webpage
Use the `webfetch` tool when the user provides a URL. Follow these steps exactly.
1. Use the `webfetch` tool to retrieve the content of the provided URL.
2. After fetching, review the content returned by the fetch tool.
3. If you find any additional URLs or links that are relevant, use the `webfetch` tool again to retrieve those links.
4. Go back to step 2 and repeat until you have all the information you need.
IMPORTANT: Recursively fetching links is crucial. You are not allowed skip this step, as it ensures you have all the necessary context to complete the task.
# How to create a Todo List
Use the following format to create a todo list:
```markdown
- [ ] Step 1: Description of the first step
- [ ] Step 2: Description of the second step
- [ ] Step 3: Description of the third step
```
Use the following format to show the todo list:
```markdown
- [ ] Step 1: Description of the first step
- [ ] Step 2: Description of the second step
- [ ] Step 3: Description of the third step
```
Do not ever use HTML tags or any other formatting for the todo list, as it will not be rendered correctly. Always use the markdown format shown above.
# Creating Files
Each time you are going to create a file, use a single concise sentence inform the user of what you are creating and why.
# Communication Guidelines
Always communicate clearly and concisely in a casual, friendly yet professional tone.
# Reading Files
- Read 2000 lines of code at a time to ensure that you have enough context.
- Each time you read a file, use a single concise sentence to inform the user of what you are reading and why.
<examples>
"Let me fetch the URL you provided to gather more information."
"Ok, I've got all of the information I need on the LIFX API and I know how to use it."
"Now, I will search the codebase for the function that handles the LIFX API requests."
"I need to update several files here - stand by"
"OK! Now let's run the tests to make sure everything is working correctly."
"Whelp - I see we have some problems. Let's fix those up."
</examples>
+4 -1
View File
@@ -86,7 +86,10 @@ export const EditTool = Tool.define({
output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</file_diagnostics>\n`
continue
}
output += `\n<project_diagnostics>\n${file}\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</project_diagnostics>\n`
output += `\n<project_diagnostics>\n${file}\n${issues
.filter((item) => item.severity === 1)
.map(LSP.Diagnostic.pretty)
.join("\n")}\n</project_diagnostics>\n`
}
return {
-2
View File
@@ -44,8 +44,6 @@ export const TaskTool = Tool.define({
parts: [
{
id: Identifier.ascending("part"),
messageID,
sessionID: session.id,
type: "text",
text: params.prompt,
},
+53
View File
@@ -0,0 +1,53 @@
import { Global } from "../global"
import { Installation } from "../installation"
import path from "path"
export namespace Trace {
export function init() {
if (!Installation.isDev()) return
const writer = Bun.file(path.join(Global.Path.data, "log", "fetch.log")).writer()
const originalFetch = globalThis.fetch
// @ts-expect-error
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url
const method = init?.method || "GET"
const urlObj = new URL(url)
writer.write(`\n${method} ${urlObj.pathname}${urlObj.search} HTTP/1.1\n`)
writer.write(`Host: ${urlObj.host}\n`)
if (init?.headers) {
if (init.headers instanceof Headers) {
init.headers.forEach((value, key) => {
writer.write(`${key}: ${value}\n`)
})
} else {
for (const [key, value] of Object.entries(init.headers)) {
writer.write(`${key}: ${value}\n`)
}
}
}
if (init?.body) {
writer.write(`\n${init.body}`)
}
writer.flush()
const response = await originalFetch(input, init)
const clonedResponse = response.clone()
writer.write(`\nHTTP/1.1 ${response.status} ${response.statusText}\n`)
response.headers.forEach((value, key) => {
writer.write(`${key}: ${value}\n`)
})
if (clonedResponse.body) {
clonedResponse.text().then(async (x) => {
writer.write(`\n${x}\n`)
})
}
writer.flush()
return response
}
}
}
+13 -8
View File
@@ -50,6 +50,7 @@ export namespace Log {
export interface Options {
print: boolean
dev?: boolean
level?: Level
}
@@ -63,7 +64,10 @@ export namespace Log {
await fs.mkdir(dir, { recursive: true })
cleanup(dir)
if (options.print) return
logpath = path.join(dir, new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log")
logpath = path.join(
dir,
options.dev ? "dev.log" : new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log",
)
const logfile = Bun.file(logpath)
await fs.truncate(logpath).catch(() => {})
const writer = logfile.writer()
@@ -75,15 +79,16 @@ export namespace Log {
}
async function cleanup(dir: string) {
const entries = await fs.readdir(dir, { withFileTypes: true })
const files = entries
.filter((entry) => entry.isFile() && entry.name.endsWith(".log"))
.map((entry) => path.join(dir, entry.name))
const glob = new Bun.Glob("????-??-??T??????.log")
const files = await Array.fromAsync(
glob.scan({
cwd: dir,
absolute: true,
}),
)
if (files.length <= 5) return
const filesToDelete = files.slice(0, -10)
await Promise.all(filesToDelete.map((file) => fs.unlink(file).catch(() => {})))
}
@@ -105,7 +110,7 @@ export namespace Log {
...extra,
})
.filter(([_, value]) => value !== undefined && value !== null)
.map(([key, value]) => `${key}=${value}`)
.map(([key, value]) => `${key}=${typeof value === "object" ? JSON.stringify(value) : value}`)
.join(" ")
const next = new Date()
const diff = next.getTime() - last
+1
View File
@@ -1 +1,2 @@
opencode-test
cmd/opencode/opencode
Binary file not shown.
+12 -20
View File
@@ -63,14 +63,11 @@ type SessionClearedMsg struct{}
type CompactSessionMsg struct{}
type SendMsg struct {
Text string
Attachments []opencode.FilePartParam
Attachments []opencode.FilePartInputParam
}
type SetEditorContentMsg struct {
Text string
}
type OptimisticMessageAddedMsg struct {
Message opencode.MessageUnion
}
type FileRenderedMsg struct {
FilePath string
}
@@ -465,7 +462,7 @@ func (a *App) CreateSession(ctx context.Context) (*opencode.Session, error) {
func (a *App) SendChatMessage(
ctx context.Context,
text string,
attachments []opencode.FilePartParam,
attachments []opencode.FilePartInputParam,
) (*App, tea.Cmd) {
var cmds []tea.Cmd
if a.Session.ID == "" {
@@ -508,29 +505,24 @@ func (a *App) SendChatMessage(
}
a.Messages = append(a.Messages, Message{Info: message, Parts: parts})
cmds = append(cmds, util.CmdHandler(OptimisticMessageAddedMsg{Message: message}))
cmds = append(cmds, func() tea.Msg {
partsParam := []opencode.SessionChatParamsPartUnion{}
for _, part := range parts {
switch casted := part.(type) {
case opencode.TextPart:
partsParam = append(partsParam, opencode.TextPartParam{
ID: opencode.F(casted.ID),
MessageID: opencode.F(casted.MessageID),
SessionID: opencode.F(casted.SessionID),
Type: opencode.F(casted.Type),
Text: opencode.F(casted.Text),
partsParam = append(partsParam, opencode.TextPartInputParam{
ID: opencode.F(casted.ID),
Type: opencode.F(opencode.TextPartInputType(casted.Type)),
Text: opencode.F(casted.Text),
})
case opencode.FilePart:
partsParam = append(partsParam, opencode.FilePartParam{
ID: opencode.F(casted.ID),
Mime: opencode.F(casted.Mime),
MessageID: opencode.F(casted.MessageID),
SessionID: opencode.F(casted.SessionID),
Type: opencode.F(casted.Type),
URL: opencode.F(casted.URL),
Filename: opencode.F(casted.Filename),
partsParam = append(partsParam, opencode.FilePartInputParam{
ID: opencode.F(casted.ID),
Mime: opencode.F(casted.Mime),
Type: opencode.F(opencode.FilePartInputType(casted.Type)),
URL: opencode.F(casted.URL),
Filename: opencode.F(casted.Filename),
})
}
}
+5 -1
View File
@@ -336,7 +336,7 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
Name: AppExitCommand,
Description: "exit the app",
Keybindings: parseBindings("ctrl+c", "<leader>q"),
Trigger: []string{"exit", "quit"},
Trigger: []string{"exit", "quit", "q"},
},
}
registry := make(CommandRegistry)
@@ -344,6 +344,10 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
marshalled, _ := json.Marshal(config.Keybinds)
json.Unmarshal(marshalled, &keybinds)
for _, command := range defaults {
// Remove share/unshare commands if sharing is disabled
if config.Share == opencode.ConfigShareDisabled && (command.Name == SessionShareCommand || command.Name == SessionUnshareCommand) {
continue
}
if keybind, ok := keybinds[string(command.Name)]; ok && keybind != "" {
command.Keybindings = parseBindings(keybind)
}
@@ -77,7 +77,6 @@ func (c *CommandCompletionProvider) GetChildEntries(
return items, nil
}
// Use fuzzy matching for commands
var commandNames []string
commandMap := make(map[string]CompletionSuggestion)
@@ -86,17 +85,13 @@ func (c *CommandCompletionProvider) GetChildEntries(
continue
}
space := space - lipgloss.Width(cmd.PrimaryTrigger())
// Add all triggers as searchable options
for _, trigger := range cmd.Trigger {
commandNames = append(commandNames, trigger)
commandMap[trigger] = c.getCommandCompletionItem(cmd, space)
}
}
// Find fuzzy matches
matches := fuzzy.RankFindFold(query, commandNames)
// Sort by score (best matches first)
sort.Sort(matches)
// Convert matches to completion items, deduplicating by command name
+113 -63
View File
@@ -40,6 +40,7 @@ type EditorComponent interface {
Paste() (tea.Model, tea.Cmd)
Newline() (tea.Model, tea.Cmd)
SetValue(value string)
SetValueWithAttachments(value string)
SetInterruptKeyInDebounce(inDebounce bool)
SetExitKeyInDebounce(inDebounce bool)
}
@@ -94,51 +95,13 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
filePath := text
ext := strings.ToLower(filepath.Ext(filePath))
mediaType := ""
switch ext {
case ".jpg":
mediaType = "image/jpeg"
case ".png", ".jpeg", ".gif", ".webp":
mediaType = "image/" + ext[1:]
case ".pdf":
mediaType = "application/pdf"
default:
attachment := &textarea.Attachment{
ID: uuid.NewString(),
Display: "@" + filePath,
URL: fmt.Sprintf("file://./%s", filePath),
Filename: filePath,
MediaType: "text/plain",
}
m.textarea.InsertAttachment(attachment)
m.textarea.InsertString(" ")
return m, nil
}
fileBytes, err := os.ReadFile(filePath)
if err != nil {
slog.Error("Failed to read file", "error", err)
attachment := m.createAttachmentFromFile(filePath)
if attachment == nil {
m.textarea.InsertRunesFromUserInput([]rune(msg))
return m, nil
}
base64EncodedFile := base64.StdEncoding.EncodeToString(fileBytes)
url := fmt.Sprintf("data:%s;base64,%s", mediaType, base64EncodedFile)
attachmentCount := len(m.textarea.GetAttachments())
attachmentIndex := attachmentCount + 1
label := "File"
if strings.HasPrefix(mediaType, "image/") {
label = "Image"
}
attachment := &textarea.Attachment{
ID: uuid.NewString(),
MediaType: mediaType,
Display: fmt.Sprintf("[%s #%d]", label, attachmentIndex),
URL: url,
Filename: filePath,
}
m.textarea.InsertAttachment(attachment)
m.textarea.InsertString(" ")
case tea.ClipboardMsg:
@@ -173,25 +136,7 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Now, insert the attachment at the position where the '@' was.
// The cursor is now at `atIndex` after the replacement.
filePath := msg.Item.Value
extension := filepath.Ext(filePath)
mediaType := ""
switch extension {
case ".jpg":
mediaType = "image/jpeg"
case ".png", ".jpeg", ".gif", ".webp":
mediaType = "image/" + extension[1:]
case ".pdf":
mediaType = "application/pdf"
default:
mediaType = "text/plain"
}
attachment := &textarea.Attachment{
ID: uuid.NewString(),
Display: "@" + filePath,
URL: fmt.Sprintf("file://./%s", url.PathEscape(filePath)),
Filename: filePath,
MediaType: mediaType,
}
attachment := m.createAttachmentFromPath(filePath)
m.textarea.InsertAttachment(attachment)
m.textarea.InsertString(" ")
return m, nil
@@ -344,9 +289,16 @@ func (m *editorComponent) Submit() (tea.Model, tea.Cmd) {
if value == "" {
return m, nil
}
switch value {
case "exit", "quit", "q", ":q":
return m, tea.Quit
}
if len(value) > 0 && value[len(value)-1] == '\\' {
// If the last character is a backslash, remove it and add a newline
m.textarea.ReplaceRange(len(value)-1, len(value), "")
backslashCol := m.textarea.CurrentRowLength() - 1
m.textarea.ReplaceRange(backslashCol, backslashCol+1, "")
m.textarea.InsertString("\n")
return m, nil
}
@@ -354,10 +306,10 @@ func (m *editorComponent) Submit() (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
attachments := m.textarea.GetAttachments()
fileParts := make([]opencode.FilePartParam, 0)
fileParts := make([]opencode.FilePartInputParam, 0)
for _, attachment := range attachments {
fileParts = append(fileParts, opencode.FilePartParam{
Type: opencode.F(opencode.FilePartTypeFile),
fileParts = append(fileParts, opencode.FilePartInputParam{
Type: opencode.F(opencode.FilePartInputTypeFile),
Mime: opencode.F(attachment.MediaType),
URL: opencode.F(attachment.URL),
Filename: opencode.F(attachment.Filename),
@@ -418,6 +370,38 @@ func (m *editorComponent) SetValue(value string) {
m.textarea.SetValue(value)
}
func (m *editorComponent) SetValueWithAttachments(value string) {
m.textarea.Reset()
i := 0
for i < len(value) {
// Check if filepath and add attachment
if value[i] == '@' {
start := i + 1
end := start
for end < len(value) && value[end] != ' ' && value[end] != '\t' && value[end] != '\n' && value[end] != '\r' {
end++
}
if end > start {
filePath := value[start:end]
if _, err := os.Stat(filePath); err == nil {
attachment := m.createAttachmentFromFile(filePath)
if attachment != nil {
m.textarea.InsertAttachment(attachment)
i = end
continue
}
}
}
}
// Not a valid file path, insert the character normally
m.textarea.InsertRune(rune(value[i]))
i++
}
}
func (m *editorComponent) SetExitKeyInDebounce(inDebounce bool) {
m.exitKeyInDebounce = inDebounce
}
@@ -498,3 +482,69 @@ func NewEditorComponent(app *app.App) EditorComponent {
return m
}
func getMediaTypeFromExtension(ext string) string {
switch strings.ToLower(ext) {
case ".jpg":
return "image/jpeg"
case ".png", ".jpeg", ".gif", ".webp":
return "image/" + ext[1:]
case ".pdf":
return "application/pdf"
default:
return "text/plain"
}
}
func (m *editorComponent) createAttachmentFromFile(filePath string) *textarea.Attachment {
ext := strings.ToLower(filepath.Ext(filePath))
mediaType := getMediaTypeFromExtension(ext)
// For text files, create a simple file reference
if mediaType == "text/plain" {
return &textarea.Attachment{
ID: uuid.NewString(),
Display: "@" + filePath,
URL: fmt.Sprintf("file://./%s", filePath),
Filename: filePath,
MediaType: mediaType,
}
}
// For binary files (images, PDFs), read and encode
fileBytes, err := os.ReadFile(filePath)
if err != nil {
slog.Error("Failed to read file", "error", err)
return nil
}
base64EncodedFile := base64.StdEncoding.EncodeToString(fileBytes)
url := fmt.Sprintf("data:%s;base64,%s", mediaType, base64EncodedFile)
attachmentCount := len(m.textarea.GetAttachments())
attachmentIndex := attachmentCount + 1
label := "File"
if strings.HasPrefix(mediaType, "image/") {
label = "Image"
}
return &textarea.Attachment{
ID: uuid.NewString(),
MediaType: mediaType,
Display: fmt.Sprintf("[%s #%d]", label, attachmentIndex),
URL: url,
Filename: filePath,
}
}
func (m *editorComponent) createAttachmentFromPath(filePath string) *textarea.Attachment {
extension := filepath.Ext(filePath)
mediaType := getMediaTypeFromExtension(extension)
return &textarea.Attachment{
ID: uuid.NewString(),
Display: "@" + filePath,
URL: fmt.Sprintf("file://./%s", url.PathEscape(filePath)),
Filename: filePath,
MediaType: mediaType,
}
}
+282 -252
View File
@@ -36,13 +36,14 @@ type messagesComponent struct {
header string
viewport viewport.Model
cache *PartCache
rendering bool
loading bool
showToolDetails bool
rendering bool
dirty bool
tail bool
partCount int
lineCount int
}
type renderFinishedMsg struct{}
type ToggleToolDetailsMsg struct{}
@@ -62,34 +63,24 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.width = effectiveWidth
m.height = msg.Height - 7
m.viewport.SetWidth(m.width)
m.header = m.renderHeader()
m.loading = true
return m, m.Reload()
case app.SendMsg:
m.viewport.GotoBottom()
m.tail = true
return m, nil
case app.OptimisticMessageAddedMsg:
m.tail = true
m.rendering = true
return m, m.Reload()
case dialog.ThemeSelectedMsg:
m.cache.Clear()
m.rendering = true
m.loading = true
return m, m.Reload()
case ToggleToolDetailsMsg:
m.showToolDetails = !m.showToolDetails
m.rendering = true
return m, m.Reload()
case app.SessionLoadedMsg, app.SessionClearedMsg:
m.cache.Clear()
m.tail = true
m.rendering = true
m.loading = true
return m, m.Reload()
case renderFinishedMsg:
m.rendering = false
if m.tail {
m.viewport.GotoBottom()
}
case opencode.EventListResponseEventSessionUpdated:
if msg.Properties.Info.ID == m.app.Session.ID {
@@ -97,17 +88,24 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
case opencode.EventListResponseEventMessageUpdated:
if msg.Properties.Info.SessionID == m.app.Session.ID {
m.renderView()
if m.tail {
m.viewport.GotoBottom()
}
cmds = append(cmds, m.renderView())
}
case opencode.EventListResponseEventMessagePartUpdated:
if msg.Properties.Part.SessionID == m.app.Session.ID {
m.renderView()
if m.tail {
m.viewport.GotoBottom()
}
cmds = append(cmds, m.renderView())
}
case renderCompleteMsg:
m.partCount = msg.partCount
m.lineCount = msg.lineCount
m.rendering = false
m.loading = false
m.viewport.SetHeight(m.height - lipgloss.Height(m.header))
m.viewport.SetContent(msg.content)
if m.tail {
m.viewport.GotoBottom()
}
if m.dirty {
cmds = append(cmds, m.renderView())
}
}
@@ -119,144 +117,179 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Batch(cmds...)
}
func (m *messagesComponent) renderView() {
measure := util.Measure("messages.renderView")
defer measure("messageCount", len(m.app.Messages))
type renderCompleteMsg struct {
content string
partCount int
lineCount int
}
func (m *messagesComponent) renderView() tea.Cmd {
m.header = m.renderHeader()
t := theme.CurrentTheme()
blocks := make([]string, 0)
m.partCount = 0
m.lineCount = 0
orphanedToolCalls := make([]opencode.ToolPart, 0)
width := min(m.width, app.MAX_CONTAINER_WIDTH)
if m.app.Config.Layout == opencode.LayoutConfigStretch {
width = m.width
if m.rendering {
m.dirty = true
return func() tea.Msg {
return nil
}
}
m.dirty = false
m.rendering = true
for _, message := range m.app.Messages {
var content string
var cached bool
return func() tea.Msg {
measure := util.Measure("messages.renderView")
defer measure()
switch casted := message.Info.(type) {
case opencode.UserMessage:
for partIndex, part := range message.Parts {
switch part := part.(type) {
case opencode.TextPart:
if part.Synthetic {
continue
}
remainingParts := message.Parts[partIndex+1:]
fileParts := make([]opencode.FilePart, 0)
for _, part := range remainingParts {
switch part := part.(type) {
case opencode.FilePart:
fileParts = append(fileParts, part)
t := theme.CurrentTheme()
blocks := make([]string, 0)
partCount := 0
lineCount := 0
orphanedToolCalls := make([]opencode.ToolPart, 0)
width := min(m.width, app.MAX_CONTAINER_WIDTH)
if m.app.Config.Layout == opencode.LayoutConfigStretch {
width = m.width
}
for _, message := range m.app.Messages {
var content string
var cached bool
switch casted := message.Info.(type) {
case opencode.UserMessage:
for partIndex, part := range message.Parts {
switch part := part.(type) {
case opencode.TextPart:
if part.Synthetic {
continue
}
}
flexItems := []layout.FlexItem{}
if len(fileParts) > 0 {
fileStyle := styles.NewStyle().Background(t.BackgroundElement()).Foreground(t.TextMuted()).Padding(0, 1)
mediaTypeStyle := styles.NewStyle().Background(t.Secondary()).Foreground(t.BackgroundPanel()).Padding(0, 1)
for _, filePart := range fileParts {
mediaType := ""
switch filePart.Mime {
case "text/plain":
mediaType = "txt"
case "image/png", "image/jpeg", "image/gif", "image/webp":
mediaType = "img"
mediaTypeStyle = mediaTypeStyle.Background(t.Accent())
case "application/pdf":
mediaType = "pdf"
mediaTypeStyle = mediaTypeStyle.Background(t.Primary())
}
flexItems = append(flexItems, layout.FlexItem{
View: mediaTypeStyle.Render(mediaType) + fileStyle.Render(filePart.Filename),
})
}
}
bgColor := t.BackgroundPanel()
files := layout.Render(
layout.FlexOptions{
Background: &bgColor,
Width: width - 6,
Direction: layout.Column,
},
flexItems...,
)
key := m.cache.GenerateKey(casted.ID, part.Text, width, files)
content, cached = m.cache.Get(key)
if !cached {
content = renderText(
m.app,
message.Info,
part.Text,
m.app.Config.Username,
m.showToolDetails,
width,
files,
)
content = lipgloss.PlaceHorizontal(
m.width,
lipgloss.Center,
content,
styles.WhitespaceStyle(t.Background()),
)
m.cache.Set(key, content)
}
if content != "" {
m.partCount++
m.lineCount += lipgloss.Height(content) + 1
blocks = append(blocks, content)
}
}
}
case opencode.AssistantMessage:
hasTextPart := false
for partIndex, p := range message.Parts {
switch part := p.(type) {
case opencode.TextPart:
hasTextPart = true
finished := casted.Time.Completed > 0
remainingParts := message.Parts[partIndex+1:]
toolCallParts := make([]opencode.ToolPart, 0)
// sometimes tool calls happen without an assistant message
// these should be included in this assistant message as well
if len(orphanedToolCalls) > 0 {
toolCallParts = append(toolCallParts, orphanedToolCalls...)
orphanedToolCalls = make([]opencode.ToolPart, 0)
}
remaining := true
for _, part := range remainingParts {
if !remaining {
break
}
switch part := part.(type) {
case opencode.TextPart:
// we only want tool calls associated with the current text part.
// if we hit another text part, we're done.
remaining = false
case opencode.ToolPart:
toolCallParts = append(toolCallParts, part)
if part.State.Status != opencode.ToolPartStateStatusCompleted && part.State.Status != opencode.ToolPartStateStatusError {
// i don't think there's a case where a tool call isn't in result state
// and the message time is 0, but just in case
finished = false
remainingParts := message.Parts[partIndex+1:]
fileParts := make([]opencode.FilePart, 0)
for _, part := range remainingParts {
switch part := part.(type) {
case opencode.FilePart:
fileParts = append(fileParts, part)
}
}
}
flexItems := []layout.FlexItem{}
if len(fileParts) > 0 {
fileStyle := styles.NewStyle().Background(t.BackgroundElement()).Foreground(t.TextMuted()).Padding(0, 1)
mediaTypeStyle := styles.NewStyle().Background(t.Secondary()).Foreground(t.BackgroundPanel()).Padding(0, 1)
for _, filePart := range fileParts {
mediaType := ""
switch filePart.Mime {
case "text/plain":
mediaType = "txt"
case "image/png", "image/jpeg", "image/gif", "image/webp":
mediaType = "img"
mediaTypeStyle = mediaTypeStyle.Background(t.Accent())
case "application/pdf":
mediaType = "pdf"
mediaTypeStyle = mediaTypeStyle.Background(t.Primary())
}
flexItems = append(flexItems, layout.FlexItem{
View: mediaTypeStyle.Render(mediaType) + fileStyle.Render(filePart.Filename),
})
}
}
bgColor := t.BackgroundPanel()
files := layout.Render(
layout.FlexOptions{
Background: &bgColor,
Width: width - 6,
Direction: layout.Column,
},
flexItems...,
)
if finished {
key := m.cache.GenerateKey(casted.ID, part.Text, width, m.showToolDetails)
key := m.cache.GenerateKey(casted.ID, part.Text, width, files)
content, cached = m.cache.Get(key)
if !cached {
content = renderText(
m.app,
message.Info,
part.Text,
m.app.Config.Username,
m.showToolDetails,
width,
files,
)
content = lipgloss.PlaceHorizontal(
m.width,
lipgloss.Center,
content,
styles.WhitespaceStyle(t.Background()),
)
m.cache.Set(key, content)
}
if content != "" {
partCount++
lineCount += lipgloss.Height(content) + 1
blocks = append(blocks, content)
}
}
}
case opencode.AssistantMessage:
hasTextPart := false
for partIndex, p := range message.Parts {
switch part := p.(type) {
case opencode.TextPart:
hasTextPart = true
finished := part.Time.End > 0
remainingParts := message.Parts[partIndex+1:]
toolCallParts := make([]opencode.ToolPart, 0)
// sometimes tool calls happen without an assistant message
// these should be included in this assistant message as well
if len(orphanedToolCalls) > 0 {
toolCallParts = append(toolCallParts, orphanedToolCalls...)
orphanedToolCalls = make([]opencode.ToolPart, 0)
}
remaining := true
for _, part := range remainingParts {
if !remaining {
break
}
switch part := part.(type) {
case opencode.TextPart:
// we only want tool calls associated with the current text part.
// if we hit another text part, we're done.
remaining = false
case opencode.ToolPart:
toolCallParts = append(toolCallParts, part)
if part.State.Status != opencode.ToolPartStateStatusCompleted && part.State.Status != opencode.ToolPartStateStatusError {
// i don't think there's a case where a tool call isn't in result state
// and the message time is 0, but just in case
finished = false
}
}
}
if finished {
key := m.cache.GenerateKey(casted.ID, part.Text, width, m.showToolDetails)
content, cached = m.cache.Get(key)
if !cached {
content = renderText(
m.app,
message.Info,
part.Text,
casted.ModelID,
m.showToolDetails,
width,
"",
toolCallParts...,
)
content = lipgloss.PlaceHorizontal(
m.width,
lipgloss.Center,
content,
styles.WhitespaceStyle(t.Background()),
)
m.cache.Set(key, content)
}
} else {
content = renderText(
m.app,
message.Info,
@@ -273,54 +306,50 @@ func (m *messagesComponent) renderView() {
content,
styles.WhitespaceStyle(t.Background()),
)
m.cache.Set(key, content)
}
} else {
content = renderText(
m.app,
message.Info,
part.Text,
casted.ModelID,
m.showToolDetails,
width,
"",
toolCallParts...,
)
content = lipgloss.PlaceHorizontal(
m.width,
lipgloss.Center,
content,
styles.WhitespaceStyle(t.Background()),
)
}
if content != "" {
m.partCount++
m.lineCount += lipgloss.Height(content) + 1
blocks = append(blocks, content)
}
case opencode.ToolPart:
if !m.showToolDetails {
if !hasTextPart {
orphanedToolCalls = append(orphanedToolCalls, part)
if content != "" {
partCount++
lineCount += lipgloss.Height(content) + 1
blocks = append(blocks, content)
}
case opencode.ToolPart:
if !m.showToolDetails {
if !hasTextPart {
orphanedToolCalls = append(orphanedToolCalls, part)
}
continue
}
continue
}
width := width
if m.app.Config.Layout == opencode.LayoutConfigAuto &&
part.Tool == "edit" &&
part.State.Error == "" {
width = min(m.width, app.EDIT_DIFF_MAX_WIDTH)
}
width := width
if m.app.Config.Layout == opencode.LayoutConfigAuto &&
part.Tool == "edit" &&
part.State.Error == "" {
width = min(m.width, app.EDIT_DIFF_MAX_WIDTH)
}
if part.State.Status == opencode.ToolPartStateStatusCompleted || part.State.Status == opencode.ToolPartStateStatusError {
key := m.cache.GenerateKey(casted.ID,
part.ID,
m.showToolDetails,
width,
)
content, cached = m.cache.Get(key)
if !cached {
if part.State.Status == opencode.ToolPartStateStatusCompleted || part.State.Status == opencode.ToolPartStateStatusError {
key := m.cache.GenerateKey(casted.ID,
part.ID,
m.showToolDetails,
width,
)
content, cached = m.cache.Get(key)
if !cached {
content = renderToolDetails(
m.app,
part,
width,
)
content = lipgloss.PlaceHorizontal(
m.width,
lipgloss.Center,
content,
styles.WhitespaceStyle(t.Background()),
)
m.cache.Set(key, content)
}
} else {
// if the tool call isn't finished, don't cache
content = renderToolDetails(
m.app,
part,
@@ -332,61 +361,57 @@ func (m *messagesComponent) renderView() {
content,
styles.WhitespaceStyle(t.Background()),
)
m.cache.Set(key, content)
}
} else {
// if the tool call isn't finished, don't cache
content = renderToolDetails(
m.app,
part,
width,
)
content = lipgloss.PlaceHorizontal(
m.width,
lipgloss.Center,
content,
styles.WhitespaceStyle(t.Background()),
)
}
if content != "" {
m.partCount++
m.lineCount += lipgloss.Height(content) + 1
blocks = append(blocks, content)
if content != "" {
partCount++
lineCount += lipgloss.Height(content) + 1
blocks = append(blocks, content)
}
}
}
}
}
error := ""
if assistant, ok := message.Info.(opencode.AssistantMessage); ok {
switch err := assistant.Error.AsUnion().(type) {
case nil:
case opencode.AssistantMessageErrorMessageOutputLengthError:
error = "Message output length exceeded"
case opencode.ProviderAuthError:
error = err.Data.Message
case opencode.MessageAbortedError:
error = "Request was aborted"
case opencode.UnknownError:
error = err.Data.Message
error := ""
if assistant, ok := message.Info.(opencode.AssistantMessage); ok {
switch err := assistant.Error.AsUnion().(type) {
case nil:
case opencode.AssistantMessageErrorMessageOutputLengthError:
error = "Message output length exceeded"
case opencode.ProviderAuthError:
error = err.Data.Message
case opencode.MessageAbortedError:
error = "Request was aborted"
case opencode.UnknownError:
error = err.Data.Message
}
}
if error != "" {
error = styles.NewStyle().Width(width - 6).Render(error)
error = renderContentBlock(
m.app,
error,
width,
WithBorderColor(t.Error()),
)
error = lipgloss.PlaceHorizontal(
m.width,
lipgloss.Center,
error,
styles.WhitespaceStyle(t.Background()),
)
blocks = append(blocks, error)
lineCount += lipgloss.Height(error) + 1
}
}
if error != "" {
error = styles.NewStyle().Width(width - 6).Render(error)
error = renderContentBlock(
m.app,
error,
width,
WithBorderColor(t.Error()),
)
blocks = append(blocks, error)
m.lineCount += lipgloss.Height(error) + 1
content := "\n" + strings.Join(blocks, "\n\n")
return renderCompleteMsg{
content: content,
partCount: partCount,
lineCount: lineCount,
}
}
m.viewport.SetHeight(m.height - lipgloss.Height(m.header))
m.viewport.SetContent("\n" + strings.Join(blocks, "\n\n"))
}
func (m *messagesComponent) renderHeader() string {
@@ -448,23 +473,30 @@ func (m *messagesComponent) renderHeader() string {
Render(formatTokensAndCost(tokens, contextWindow, cost, isSubscriptionModel))
background := t.Background()
share = layout.Render(
var items []layout.FlexItem
justify := layout.JustifyEnd
if m.app.Config.Share != opencode.ConfigShareDisabled {
items = append(items, layout.FlexItem{View: share})
justify = layout.JustifySpaceBetween
}
items = append(items, layout.FlexItem{View: sessionInfo})
headerRow := layout.Render(
layout.FlexOptions{
Background: &background,
Direction: layout.Row,
Justify: layout.JustifySpaceBetween,
Justify: justify,
Align: layout.AlignStretch,
Width: headerWidth - 6,
},
layout.FlexItem{
View: share,
},
layout.FlexItem{
View: sessionInfo,
},
items...,
)
headerLines = append(headerLines, share)
headerLines = append(headerLines, headerRow)
header := strings.Join(headerLines, "\n")
header = styles.NewStyle().
Background(t.Background()).
@@ -536,7 +568,7 @@ func formatTokensAndCost(
func (m *messagesComponent) View() string {
t := theme.CurrentTheme()
if m.rendering {
if m.loading {
return lipgloss.Place(
m.width,
m.height,
@@ -553,10 +585,7 @@ func (m *messagesComponent) View() string {
}
func (m *messagesComponent) Reload() tea.Cmd {
return func() tea.Msg {
m.renderView()
return renderFinishedMsg{}
}
return m.renderView()
}
func (m *messagesComponent) PageUp() (tea.Model, tea.Cmd) {
@@ -616,6 +645,7 @@ func (m *messagesComponent) CopyLastMessage() (tea.Model, tea.Cmd) {
func NewMessagesComponent(app *app.App) MessagesComponent {
vp := viewport.New()
vp.KeyMap = viewport.KeyMap{}
vp.MouseWheelDelta = 4
return &messagesComponent{
app: app,
@@ -67,6 +67,7 @@ func (c *completionDialogComponent) Init() tea.Cmd {
func (c *completionDialogComponent) getAllCompletions(query string) tea.Cmd {
return func() tea.Msg {
allItems := make([]completions.CompletionSuggestion, 0)
providersWithResults := 0
// Collect results from all providers
for _, provider := range c.providers {
@@ -81,11 +82,14 @@ func (c *completionDialogComponent) getAllCompletions(query string) tea.Cmd {
)
continue
}
allItems = append(allItems, items...)
if len(items) > 0 {
providersWithResults++
allItems = append(allItems, items...)
}
}
// If there's a query, use fuzzy ranking to sort results
if query != "" && len(allItems) > 0 {
if query != "" && providersWithResults > 1 {
t := theme.CurrentTheme()
baseStyle := styles.NewStyle().Background(t.BackgroundElement())
// Create a slice of display values for fuzzy matching
@@ -94,10 +98,7 @@ func (c *completionDialogComponent) getAllCompletions(query string) tea.Cmd {
displayValues[i] = item.Display(baseStyle)
}
// Get fuzzy matches with ranking
matches := fuzzy.RankFindFold(query, displayValues)
// Sort by score (best matches first)
sort.Sort(matches)
// Reorder items based on fuzzy ranking
@@ -28,6 +28,7 @@ type SessionDialog interface {
type sessionItem struct {
title string
isDeleteConfirming bool
isCurrentSession bool
}
func (s sessionItem) Render(
@@ -42,7 +43,11 @@ func (s sessionItem) Render(
if s.isDeleteConfirming {
text = "Press again to confirm delete"
} else {
text = s.title
if s.isCurrentSession {
text = "● " + s.title
} else {
text = s.title
}
}
truncatedStr := truncate.StringWithTail(text, uint(width-1), "...")
@@ -56,6 +61,14 @@ func (s sessionItem) Render(
Foreground(t.BackgroundElement()).
Width(width).
PaddingLeft(1)
} else if s.isCurrentSession {
// Different style for current session when selected
itemStyle = baseStyle.
Background(t.Primary()).
Foreground(t.BackgroundElement()).
Width(width).
PaddingLeft(1).
Bold(true)
} else {
// Normal selection
itemStyle = baseStyle.
@@ -70,6 +83,12 @@ func (s sessionItem) Render(
itemStyle = baseStyle.
Foreground(t.Error()).
PaddingLeft(1)
} else if s.isCurrentSession {
// Highlight current session when not selected
itemStyle = baseStyle.
Foreground(t.Primary()).
PaddingLeft(1).
Bold(true)
} else {
itemStyle = baseStyle.
PaddingLeft(1)
@@ -194,6 +213,7 @@ func (s *sessionDialog) updateListItems() {
item := sessionItem{
title: sess.Title,
isDeleteConfirming: s.deleteConfirmation == i,
isCurrentSession: s.app.Session != nil && s.app.Session.ID == sess.ID,
}
items = append(items, item)
}
@@ -229,6 +249,7 @@ func NewSessionDialog(app *app.App) SessionDialog {
items = append(items, sessionItem{
title: sess.Title,
isDeleteConfirming: false,
isCurrentSession: app.Session != nil && app.Session.ID == sess.ID,
})
}
+6 -6
View File
@@ -110,11 +110,6 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.KeyPressMsg:
keyString := msg.String()
// Handle Ctrl+Z for suspend
if keyString == "ctrl+z" {
return a, tea.Suspend
}
// 1. Handle active modal
if a.modal != nil {
switch keyString {
@@ -277,6 +272,11 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return a, util.CmdHandler(commands.ExecuteCommandsMsg(matches))
}
// Fallback: suspend if ctrl+z is pressed and no user keybind matched
if keyString == "ctrl+z" {
return a, tea.Suspend
}
// 10. Fallback to editor. This is for other characters like backspace, tab, etc.
updatedEditor, cmd := a.editor.Update(msg)
a.editor = updatedEditor.(chat.EditorComponent)
@@ -334,7 +334,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
cmds = append(cmds, cmd)
case app.SetEditorContentMsg:
// Set the editor content without sending
a.editor.SetValue(msg.Text)
a.editor.SetValueWithAttachments(msg.Text)
updated, cmd := a.editor.Focus()
a.editor = updated.(chat.EditorComponent)
cmds = append(cmds, cmd)
+3 -3
View File
@@ -1,4 +1,4 @@
configured_endpoints: 22
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-8792f91dd070f7b4ee671fc86e8a03976dc7fb6ee49f8c99ad989e1597003774.yml
openapi_spec_hash: fe9dc3a074be560de0b97df9b5af2c1b
config_hash: b7f3d9742335715c458494988498b183
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-d34620b462127c45497743c97fd3569f9e629d9fbd97c0707087eeddbd4b3de1.yml
openapi_spec_hash: 23864c98d555350fe56f1d0e56f205c5
config_hash: a8441af7cb2db855d79fd372ee3b9fb1
+2 -2
View File
@@ -77,8 +77,8 @@ Methods:
Params Types:
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FilePartParam">FilePartParam</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TextPartParam">TextPartParam</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FilePartInputParam">FilePartInputParam</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TextPartInputParam">TextPartInputParam</a>
Response Types:
+15 -8
View File
@@ -67,8 +67,8 @@ type Config struct {
Model string `json:"model"`
// Custom provider configurations and model overrides
Provider map[string]ConfigProvider `json:"provider"`
// Control sharing behavior: 'auto' enables automatic sharing, 'disabled' disables
// all sharing
// Control sharing behavior:'manual' allows manual sharing via commands, 'auto'
// enables automatic sharing, 'disabled' disables all sharing
Share ConfigShare `json:"share"`
// Theme name to use for the interface
Theme string `json:"theme"`
@@ -206,6 +206,8 @@ type ConfigMcp struct {
Enabled bool `json:"enabled"`
// This field can have the runtime type of [map[string]string].
Environment interface{} `json:"environment"`
// This field can have the runtime type of [map[string]string].
Headers interface{} `json:"headers"`
// URL of the remote MCP server
URL string `json:"url"`
JSON configMcpJSON `json:"-"`
@@ -218,6 +220,7 @@ type configMcpJSON struct {
Command apijson.Field
Enabled apijson.Field
Environment apijson.Field
Headers apijson.Field
URL apijson.Field
raw string
ExtraFields map[string]apijson.Field
@@ -427,18 +430,19 @@ func (r configProviderModelsLimitJSON) RawJSON() string {
return r.raw
}
// Control sharing behavior: 'auto' enables automatic sharing, 'disabled' disables
// all sharing
// Control sharing behavior:'manual' allows manual sharing via commands, 'auto'
// enables automatic sharing, 'disabled' disables all sharing
type ConfigShare string
const (
ConfigShareManual ConfigShare = "manual"
ConfigShareAuto ConfigShare = "auto"
ConfigShareDisabled ConfigShare = "disabled"
)
func (r ConfigShare) IsKnown() bool {
switch r {
case ConfigShareAuto, ConfigShareDisabled:
case ConfigShareManual, ConfigShareAuto, ConfigShareDisabled:
return true
}
return false
@@ -509,9 +513,9 @@ type KeybindsConfig struct {
SessionShare string `json:"session_share,required"`
// Unshare current session
SessionUnshare string `json:"session_unshare,required"`
// Switch mode
// Next mode
SwitchMode string `json:"switch_mode,required"`
// Switch mode reverse
// Previous Mode
SwitchModeReverse string `json:"switch_mode_reverse,required"`
// List available themes
ThemeList string `json:"theme_list,required"`
@@ -638,7 +642,9 @@ type McpRemoteConfig struct {
// URL of the remote MCP server
URL string `json:"url,required"`
// Enable or disable the MCP server on startup
Enabled bool `json:"enabled"`
Enabled bool `json:"enabled"`
// Headers to send with the request
Headers map[string]string `json:"headers"`
JSON mcpRemoteConfigJSON `json:"-"`
}
@@ -647,6 +653,7 @@ type mcpRemoteConfigJSON struct {
Type apijson.Field
URL apijson.Field
Enabled apijson.Field
Headers apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
+53 -30
View File
@@ -481,21 +481,33 @@ func (r FilePartType) IsKnown() bool {
return false
}
type FilePartParam struct {
ID param.Field[string] `json:"id,required"`
MessageID param.Field[string] `json:"messageID,required"`
Mime param.Field[string] `json:"mime,required"`
SessionID param.Field[string] `json:"sessionID,required"`
Type param.Field[FilePartType] `json:"type,required"`
URL param.Field[string] `json:"url,required"`
Filename param.Field[string] `json:"filename"`
type FilePartInputParam struct {
Mime param.Field[string] `json:"mime,required"`
Type param.Field[FilePartInputType] `json:"type,required"`
URL param.Field[string] `json:"url,required"`
ID param.Field[string] `json:"id"`
Filename param.Field[string] `json:"filename"`
}
func (r FilePartParam) MarshalJSON() (data []byte, err error) {
func (r FilePartInputParam) MarshalJSON() (data []byte, err error) {
return apijson.MarshalRoot(r)
}
func (r FilePartParam) implementsSessionChatParamsPartUnion() {}
func (r FilePartInputParam) implementsSessionChatParamsPartUnion() {}
type FilePartInputType string
const (
FilePartInputTypeFile FilePartInputType = "file"
)
func (r FilePartInputType) IsKnown() bool {
switch r {
case FilePartInputTypeFile:
return true
}
return false
}
type Message struct {
ID string `json:"id,required"`
@@ -1076,28 +1088,40 @@ func (r textPartTimeJSON) RawJSON() string {
return r.raw
}
type TextPartParam struct {
ID param.Field[string] `json:"id,required"`
MessageID param.Field[string] `json:"messageID,required"`
SessionID param.Field[string] `json:"sessionID,required"`
Text param.Field[string] `json:"text,required"`
Type param.Field[TextPartType] `json:"type,required"`
Synthetic param.Field[bool] `json:"synthetic"`
Time param.Field[TextPartTimeParam] `json:"time"`
type TextPartInputParam struct {
Text param.Field[string] `json:"text,required"`
Type param.Field[TextPartInputType] `json:"type,required"`
ID param.Field[string] `json:"id"`
Synthetic param.Field[bool] `json:"synthetic"`
Time param.Field[TextPartInputTimeParam] `json:"time"`
}
func (r TextPartParam) MarshalJSON() (data []byte, err error) {
func (r TextPartInputParam) MarshalJSON() (data []byte, err error) {
return apijson.MarshalRoot(r)
}
func (r TextPartParam) implementsSessionChatParamsPartUnion() {}
func (r TextPartInputParam) implementsSessionChatParamsPartUnion() {}
type TextPartTimeParam struct {
type TextPartInputType string
const (
TextPartInputTypeText TextPartInputType = "text"
)
func (r TextPartInputType) IsKnown() bool {
switch r {
case TextPartInputTypeText:
return true
}
return false
}
type TextPartInputTimeParam struct {
Start param.Field[float64] `json:"start,required"`
End param.Field[float64] `json:"end"`
}
func (r TextPartTimeParam) MarshalJSON() (data []byte, err error) {
func (r TextPartInputTimeParam) MarshalJSON() (data []byte, err error) {
return apijson.MarshalRoot(r)
}
@@ -1574,11 +1598,11 @@ func (r sessionMessagesResponseJSON) RawJSON() string {
}
type SessionChatParams struct {
MessageID param.Field[string] `json:"messageID,required"`
Mode param.Field[string] `json:"mode,required"`
ModelID param.Field[string] `json:"modelID,required"`
Parts param.Field[[]SessionChatParamsPartUnion] `json:"parts,required"`
ProviderID param.Field[string] `json:"providerID,required"`
MessageID param.Field[string] `json:"messageID"`
Mode param.Field[string] `json:"mode"`
}
func (r SessionChatParams) MarshalJSON() (data []byte, err error) {
@@ -1586,10 +1610,8 @@ func (r SessionChatParams) MarshalJSON() (data []byte, err error) {
}
type SessionChatParamsPart struct {
ID param.Field[string] `json:"id,required"`
MessageID param.Field[string] `json:"messageID,required"`
SessionID param.Field[string] `json:"sessionID,required"`
Type param.Field[SessionChatParamsPartsType] `json:"type,required"`
ID param.Field[string] `json:"id"`
Filename param.Field[string] `json:"filename"`
Mime param.Field[string] `json:"mime"`
Synthetic param.Field[bool] `json:"synthetic"`
@@ -1604,7 +1626,8 @@ func (r SessionChatParamsPart) MarshalJSON() (data []byte, err error) {
func (r SessionChatParamsPart) implementsSessionChatParamsPartUnion() {}
// Satisfied by [FilePartParam], [TextPartParam], [SessionChatParamsPart].
// Satisfied by [TextPartInputParam], [FilePartInputParam],
// [SessionChatParamsPart].
type SessionChatParamsPartUnion interface {
implementsSessionChatParamsPartUnion()
}
@@ -1612,13 +1635,13 @@ type SessionChatParamsPartUnion interface {
type SessionChatParamsPartsType string
const (
SessionChatParamsPartsTypeFile SessionChatParamsPartsType = "file"
SessionChatParamsPartsTypeText SessionChatParamsPartsType = "text"
SessionChatParamsPartsTypeFile SessionChatParamsPartsType = "file"
)
func (r SessionChatParamsPartsType) IsKnown() bool {
switch r {
case SessionChatParamsPartsTypeFile, SessionChatParamsPartsTypeText:
case SessionChatParamsPartsTypeText, SessionChatParamsPartsTypeFile:
return true
}
return false
+12 -11
View File
@@ -101,7 +101,7 @@ func TestSessionAbort(t *testing.T) {
}
}
func TestSessionChat(t *testing.T) {
func TestSessionChatWithOptionalParams(t *testing.T) {
t.Skip("skipped: tests are disabled for the time being")
baseURL := "http://localhost:4010"
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
@@ -117,19 +117,20 @@ func TestSessionChat(t *testing.T) {
context.TODO(),
"id",
opencode.SessionChatParams{
MessageID: opencode.F("messageID"),
Mode: opencode.F("mode"),
ModelID: opencode.F("modelID"),
Parts: opencode.F([]opencode.SessionChatParamsPartUnion{opencode.FilePartParam{
ModelID: opencode.F("modelID"),
Parts: opencode.F([]opencode.SessionChatParamsPartUnion{opencode.TextPartInputParam{
Text: opencode.F("text"),
Type: opencode.F(opencode.TextPartInputTypeText),
ID: opencode.F("id"),
MessageID: opencode.F("messageID"),
Mime: opencode.F("mime"),
SessionID: opencode.F("sessionID"),
Type: opencode.F(opencode.FilePartTypeFile),
URL: opencode.F("url"),
Filename: opencode.F("filename"),
Synthetic: opencode.F(true),
Time: opencode.F(opencode.TextPartInputTimeParam{
Start: opencode.F(0.000000),
End: opencode.F(0.000000),
}),
}}),
ProviderID: opencode.F("providerID"),
MessageID: opencode.F("msg"),
Mode: opencode.F("mode"),
},
)
if err != nil {
+75 -5
View File
@@ -10,7 +10,6 @@ You can configure opencode using a JSON config file.
"$schema": "https://opencode.ai/config.json",
"theme": "opencode",
"model": "anthropic/claude-sonnet-4-20250514",
"autoshare": false,
"autoupdate": true
}
```
@@ -93,6 +92,24 @@ You can configure the theme you want to use in your opencode config through the
---
### Layout
You can configure the layout of the TUI with the `layout` option.
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"layout": "stretch"
}
```
This takes:
- `"auto"`: Centers content with padding. This is the default.
- `"stretch"`: Uses full terminal width.
---
### Logging
Logs are written to:
@@ -118,8 +135,28 @@ With the following options:
| `WARN` | Warnings and errors only |
| `ERROR` | Errors only |
The **default** log level is `INFO`. If you are running opencode locally in
development mode it's set to `DEBUG`.
The **default** log level is `INFO`. If you are running opencode locally in development mode it's set to `DEBUG`.
---
### Sharing
You can configure the [share](/docs/share) feature through the `share` option.
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"share": "manual"
}
```
This takes:
- `"manual"` - Allow manual sharing via commands (default)
- `"auto"` - Automatically share new conversations
- `"disabled"` - Disable sharing entirely
By default, sharing is set to manual mode where you need to explicitly share conversations using the `/share` command.
---
@@ -138,6 +175,19 @@ You can customize your keybinds through the `keybinds` option.
---
### Autoupdate
opencode will automatically download any new updates when it starts up. You can disable this with the `autoupdate` option.
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"autoupdate": false
}
```
---
### MCP servers
You can configure MCP servers you want to use through the `mcp` option.
@@ -153,6 +203,22 @@ You can configure MCP servers you want to use through the `mcp` option.
---
### Instructions
You can configure the instructions for the model you're using through the `instructions` option.
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"instructions": ["CONTRIBUTING.md", "docs/guidelines.md", ".cursor/rules/*.md"]
}
```
This takes an array of paths and glob patterns to instruction files. [Learn more
about rules here](/docs/rules).
---
### Disabled providers
You can disable providers that are loaded automatically through the `disabled_providers` option. This is useful when you want to prevent certain providers from being loaded even if their credentials are available.
@@ -188,7 +254,9 @@ Use `{env:VARIABLE_NAME}` to substitute environment variables:
"model": "{env:OPENCODE_MODEL}",
"provider": {
"anthropic": {
"api_key": "{env:ANTHROPIC_API_KEY}"
"options": {
"apiKey": "{env:ANTHROPIC_API_KEY}"
}
}
}
}
@@ -208,7 +276,9 @@ Use `{file:path/to/file}` to substitute the contents of a file:
"instructions": ["{file:./custom-instructions.md}"],
"provider": {
"openai": {
"api_key": "{file:~/.secrets/openai-key}"
"options": {
"apiKey": "{file:~/.secrets/openai-key}"
}
}
}
}
@@ -23,7 +23,7 @@ Since opencode is open source and does not store any of your code or context dat
**opencode does not store your code or context data.** All processing happens locally or through direct API calls to your AI provider.
The only caveat here is the optional `/share` feature that must be manually enabled.
The only caveat here is the optional `/share` feature.
---
@@ -33,6 +33,17 @@ If a user enables the `/share` feature, the conversation and the data associated
The data is currently served through our CDN's edge network, and is cached on the edge near your users.
We recommend you disable this for your trial.
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"share": "disabled"
}
```
[Learn more about sharing](/docs/share).
---
### Code ownership
@@ -51,9 +62,37 @@ pricing and implementation options.
### SSO
SSO integration can be implemented for enterprise deployments after your trial. Currently users manage and configure individual API keys locally.
SSO integration can be implemented for enterprise deployments after your trial.
This will allow your team's session data and shared conversations to be protected
by your enterprise's authentication system.
This can be switched to a centralized authentication system that your organization uses.
---
### Private NPM
opencode supports private npm registries through Bun's native `.npmrc` file support. If your organization uses a private registry, such as JFrog Artifactory, Nexus, or similar, ensure developers are authenticated before running opencode.
To set up authentication with your private registry:
```bash
npm login --registry=https://your-company.jfrog.io/api/npm/npm-virtual/
```
This creates `~/.npmrc` with authentication details. opencode will automatically
pick this up.
:::caution
You must be logged into the private registry before running opencode.
:::
Alternatively, you can manually configure a `.npmrc` file:
```bash title="~/.npmrc"
registry=https://your-company.jfrog.io/api/npm/npm-virtual/
//your-company.jfrog.io/api/npm/npm-virtual/:_authToken=${NPM_AUTH_TOKEN}
```
Developers must be logged into the private registry before running opencode to ensure packages can be installed from your enterprise registry.
---
@@ -61,30 +100,3 @@ This can be switched to a centralized authentication system that your organizati
The share feature can be self-hosted and the share pages can be made accessible
only after the user has been authenticated.
---
### Private NPM registries
opencode supports private npm registries through Bun's native `.npmrc` file support. If your organization uses a private registry (such as JFrog Artifactory, Nexus, or similar), ensure developers are authenticated before running opencode.
To set up authentication with your private registry:
```bash
# Authenticate with your enterprise registry
npm login --registry=https://your-company.jfrog.io/api/npm/npm-virtual/
# This creates ~/.npmrc with authentication
# opencode will automatically use it through Bun's native support
```
Alternatively, you can manually configure a `.npmrc` file:
```bash
# ~/.npmrc
registry=https://your-company.jfrog.io/api/npm/npm-virtual/
//your-company.jfrog.io/api/npm/npm-virtual/:_authToken=${NPM_AUTH_TOKEN}
```
**Important**: Developers must be logged into the private registry before running opencode
to ensure packages can be installed from your enterprise registry.
@@ -9,32 +9,45 @@ opencode has a list of keybinds that you can customize through the opencode conf
{
"$schema": "https://opencode.ai/config.json",
"keybinds": {
"leader": "ctrl+x",
"help": "<leader>h",
"app_help": "<leader>h",
"switch_mode": "tab",
"editor_open": "<leader>e",
"session_new": "<leader>n",
"session_list": "<leader>l",
"session_share": "<leader>s",
"session_unshare": "<leader>u",
"session_interrupt": "esc",
"session_compact": "<leader>c",
"tool_details": "<leader>d",
"model_list": "<leader>m",
"theme_list": "<leader>t",
"project_init": "<leader>i",
"file_list": "<leader>f",
"file_close": "esc",
"file_diff_toggle": "<leader>v",
"input_clear": "ctrl+c",
"input_paste": "ctrl+v",
"input_submit": "enter",
"input_newline": "shift+enter,ctrl+j",
"history_previous": "up",
"history_next": "down",
"messages_page_up": "pgup",
"messages_page_down": "pgdown",
"messages_half_page_up": "ctrl+alt+u",
"messages_half_page_down": "ctrl+alt+d",
"messages_previous": "ctrl+alt+k",
"messages_next": "ctrl+alt+j",
"messages_previous": "ctrl+up",
"messages_next": "ctrl+down",
"messages_first": "ctrl+g",
"messages_last": "ctrl+alt+g",
"messages_layout_toggle": "<leader>p",
"messages_copy": "<leader>y",
"messages_revert": "<leader>r",
"app_exit": "ctrl+c,<leader>q"
}
}
@@ -49,7 +49,10 @@ Add a remote MCP servers under `mcp.remotemcp`.
"remotemcp": {
"type": "remote",
"url": "https://my-mcp-server.com",
"enabled": true
"enabled": true,
"headers": {
"Authorization": "Bearer MY_API_KEY"
}
}
}
}
@@ -46,14 +46,14 @@ You can add custom providers by specifying the npm package for the provider and
You can customize the base URL for any provider by setting the `baseURL` option. This is useful when using proxy services or custom endpoints.
```json title="opencode.json" {6-7}
```json title="opencode.json" {6}
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"anthropic": {
"options": {
"baseURL": "https://api.anthropic.com/v1"
},
}
}
}
}
+42 -17
View File
@@ -23,13 +23,13 @@ When you share a conversation, opencode:
## Sharing
You can manually share a conversation or enable automatic sharing for all new conversations.
opencode supports three sharing modes that control how conversations are shared:
---
### Manual
### Manual (default)
Use the `/share` command in any conversation to create a shareable link:
By default, opencode uses manual sharing mode. Sessions are not shared automatically, but you can manually share them using the `/share` command:
```
/share
@@ -37,24 +37,48 @@ Use the `/share` command in any conversation to create a shareable link:
This will generate a unique URL that'll be copied to your clipboard.
---
### Autoshare
You can enable automatic sharing for all new conversations through the `autoshare` option in your [config file](/docs/config).
To explicitly set manual mode in your [config file](/docs/config):
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"autoshare": true
"share": "manual"
}
```
By default, `autoshare` is disabled.
---
### Auto-share
You can enable automatic sharing for all new conversations by setting the `share` option to `"auto"` in your [config file](/docs/config):
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"share": "auto"
}
```
With auto-share enabled, every new conversation will automatically be shared and a link will be generated.
---
## Unsharing
### Disabled
You can disable sharing entirely by setting the `share` option to `"disabled"` in your [config file](/docs/config):
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"share": "disabled"
}
```
To enforce this across your team for a given project, add it to the `opencode.json` in your project and check into Git.
---
## Un-sharing
To stop sharing a conversation and remove it from public access:
@@ -85,10 +109,11 @@ includes:
### Recommendations
- Only share conversations that don't contain sensitive information
- Review conversation content before sharing
- Unshare conversations when collaboration is complete
- Avoid sharing conversations with proprietary code or confidential data
- Only share conversations that don't contain sensitive information.
- Review conversation content before sharing.
- Unshare conversations when collaboration is complete.
- Avoid sharing conversations with proprietary code or confidential data.
- For sensitive projects, disable sharing entirely.
---
@@ -96,8 +121,8 @@ includes:
For enterprise deployments, the share feature can be:
- **Self-hosted** on your own infrastructure
- **Restricted** to authenticated users only
- **Disabled** entirely for security compliance
- **Restricted** to users authenticated through SSO only
- **Self-hosted** on your own infrastructure
[Learn more](/docs/enterprise) about using opencode in your organization.
@@ -119,17 +119,25 @@ opencode will detect if you're using Wayland and prefer `wl-clipboard`, otherwis
---
### How to select and copy text in the TUI
There are several ways to copy text from opencode's TUI:
- **Copy latest message**: Use `<leader>y` to copy the most recent message in your current session to the clipboard
- **Export session**: Use `/export` (or `<leader>x`) to open the current session as plain text in your `$EDITOR` (requires the `EDITOR` environment variable to be set)
We're working on adding click & drag text selection in a future update.
---
### TUI not rendering full width
By default, opencode's TUI uses an "auto" layout that centers content with padding. If you want the TUI to use the full width of your terminal, you can configure the layout setting:
```json
```json title="opencode.json"
{
"layout": "stretch"
}
```
Add this to your `opencode.json` configuration file. The available layout options are:
- `"auto"` (default) - Centers content with padding
- `"stretch"` - Uses full terminal width
Read more about this in the [config docs](/docs/config#layout).
+2
View File
@@ -92,7 +92,9 @@ resources:
message: Message
part: Part
textPart: TextPart
textPartInput: TextPartInput
filePart: FilePart
filePartInput: FilePartInput
toolPart: ToolPart
stepStartPart: StepStartPart
stepFinishPart: StepFinishPart