Compare commits

..
22 Commits
Author SHA1 Message Date
Dax Raad 4bbbbac5f6 vercel ai gateway
publish / publish (push) Has been cancelled
2025-07-19 10:08:36 -04:00
GitHub Action 3c3a997d2a ignore: update download stats 2025-07-19 2025-07-19 12:04:11 +00:00
CodinCatandGitHub 1676f8b5dd fix table heading rendering (#1138) 2025-07-18 20:17:22 -05:00
Dax Raad c87a7469a0 ci: rollback install script 2025-07-18 18:57:58 -04:00
Michael HansonandGitHub 132e26ddbf docs: Clarify MCP config instructions (#1026)
publish / publish (push) Has been cancelled
2025-07-18 16:04:29 -04:00
Rami ChowdhuryandGitHub f1da70b1de feat(provider): add Gemini tool schema sanitization (#1132) 2025-07-18 16:02:54 -04:00
Aiden ClineandGitHub 5c9d1910af fix: func called before definition (#1134) 2025-07-18 15:00:32 -05:00
Timo ClasenandGitHub 18abcab208 feat(config): make small model configurable (#1030)
publish / publish (push) Has been cancelled
2025-07-18 14:16:50 -04:00
opencode-agent[bot]GitHubopencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>thdxr
01e7dc2d02 Added install dir priority & user feedback (#1129)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: thdxr <thdxr@users.noreply.github.com>
2025-07-18 14:15:10 -04:00
adamdotdevin 611854e4b6 feat(tui): simpler layout, always stretched 2025-07-18 13:03:27 -05:00
DaxandGitHub d56dec4ba7 wip: optional IDs in api (#1128)
publish / publish (push) Has been cancelled
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 KhuuandGitHub 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
publish / publish (push) Has been cancelled
2025-07-18 05:08:35 -05:00
Timo ClasenandGitHub df33143396 feat(tui): parse for file attachments when exiting EDITOR (#1117) 2025-07-18 04:47:20 -05:00
Aiden ClineandGitHub 571aeaaea2 tweak: remove needless resorting (#1116) 2025-07-18 04:42:43 -05:00
Aiden ClineandGitHub edfea03917 tweak: fix [object Object] in logging (#1114) 2025-07-18 04:41:23 -05:00
TomandGitHub 81c88cc742 fix(tui): ensure viewport scrolls to bottom on new messages (#1110) 2025-07-18 04:41:03 -05:00
Mike WallioandGitHub 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
publish / publish (push) Has been cancelled
2025-07-17 18:07:06 -04:00
30 changed files with 841 additions and 541 deletions
+15
View File
@@ -33,6 +33,21 @@ paru -S opencode-bin # Arch Linux
> [!TIP]
> Remove versions older than 0.1.x before installing.
#### Installation Directory
The install script respects the following priority order for the installation path:
1. `$OPENCODE_INSTALL_DIR` - Custom installation directory
2. `$XDG_BIN_DIR` - XDG Base Directory Specification compliant path
3. `$HOME/bin` - Standard user binary directory (if exists or can be created)
4. `$HOME/.opencode/bin` - Default fallback
```bash
# Examples
OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash
XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
```
### Documentation
For more info on how to configure opencode [**head over to our docs**](https://opencode.ai/docs).
+3
View File
@@ -19,3 +19,6 @@
| 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) |
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
-1
View File
@@ -186,4 +186,3 @@ if [ -n "${GITHUB_ACTIONS-}" ] && [ "${GITHUB_ACTIONS}" == "true" ]; then
echo "$INSTALL_DIR" >> $GITHUB_PATH
print_message info "Added $INSTALL_DIR to \$GITHUB_PATH"
fi
+7 -1
View File
@@ -78,6 +78,8 @@ export const AuthLoginCommand = cmd({
"github-copilot": 1,
openai: 2,
google: 3,
openrouter: 4,
vercel: 5,
}
let provider = await prompts.select({
message: "Select provider",
@@ -108,7 +110,7 @@ export const AuthLoginCommand = cmd({
if (provider === "other") {
provider = await prompts.text({
message: "Enter provider id",
validate: (x) => (x.match(/^[a-z-]+$/) ? undefined : "a-z and hyphens only"),
validate: (x) => (x.match(/^[0-9a-z-]+$/) ? undefined : "a-z, 0-9 and hyphens only"),
})
if (prompts.isCancel(provider)) throw new UI.CancelledError()
provider = provider.replace(/^@ai-sdk\//, "")
@@ -265,6 +267,10 @@ export const AuthLoginCommand = cmd({
return
}
if (provider === "vercel") {
prompts.log.info("You can create an api key in the dashboard")
}
const key = await prompts.password({
message: "Enter your API key",
validate: (x) => (x.length > 0 ? undefined : "Required"),
-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,
},
+7 -4
View File
@@ -31,9 +31,6 @@ export namespace Config {
const os = await import("os")
result.username = os.userInfo().username
}
if (!result.layout) {
result.layout = "auto"
}
log.info("loaded", result)
@@ -152,6 +149,12 @@ export namespace Config {
autoupdate: z.boolean().optional().describe("Automatically update to the latest version"),
disabled_providers: z.array(z.string()).optional().describe("Disable providers that are loaded automatically"),
model: z.string().describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(),
small_model: z
.string()
.describe(
"Small model to use for tasks like summarization and title generation in the format of provider/model",
)
.optional(),
username: z
.string()
.optional()
@@ -176,7 +179,7 @@ export namespace Config {
.describe("Custom provider configurations and model overrides"),
mcp: z.record(z.string(), Mcp).optional().describe("MCP (Model Context Protocol) server configurations"),
instructions: z.array(z.string()).optional().describe("Additional instruction files or patterns to include"),
layout: Layout.optional().describe("Layout to use for the TUI"),
layout: Layout.optional().describe("@deprecated Always uses stretch layout."),
experimental: z
.object({
hook: z
+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
},
}
+1 -1
View File
@@ -27,7 +27,7 @@ await Promise.all([
fs.mkdir(Global.Path.state, { recursive: true }),
])
const CACHE_VERSION = "2"
const CACHE_VERSION = "3"
const version = await Bun.file(path.join(Global.Path.cache, "version"))
.text()
+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")
+79 -9
View File
@@ -272,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,
@@ -410,6 +416,13 @@ export namespace Provider {
}
export async function getSmallModel(providerID: string) {
const cfg = await Config.get()
if (cfg.small_model) {
const parsed = parseModel(cfg.small_model)
return getModel(parsed.providerID, parsed.modelID)
}
const provider = await state().then((state) => state.providers[providerID])
if (!provider) return
const priority = ["3-5-haiku", "3.5-haiku", "gemini-2.5-flash"]
@@ -481,7 +494,10 @@ export namespace Provider {
...t,
parameters: optionalToNullable(t.parameters),
})),
google: TOOLS,
google: TOOLS.map((t) => ({
...t,
parameters: sanitizeGeminiParameters(t.parameters),
})),
}
export async function tools(providerID: string) {
@@ -495,6 +511,60 @@ export namespace Provider {
return TOOL_MAPPING[providerID] ?? TOOLS
}
function sanitizeGeminiParameters(schema: z.ZodTypeAny, visited = new Set()): z.ZodTypeAny {
if (!schema || visited.has(schema)) {
return schema
}
visited.add(schema)
if (schema instanceof z.ZodDefault) {
const innerSchema = schema.removeDefault()
// Handle Gemini's incompatibility with `default` on `anyOf` (unions).
if (innerSchema instanceof z.ZodUnion) {
// The schema was `z.union(...).default(...)`, which is not allowed.
// We strip the default and return the sanitized union.
return sanitizeGeminiParameters(innerSchema, visited)
}
// Otherwise, the default is on a regular type, which is allowed.
// We recurse on the inner type and then re-apply the default.
return sanitizeGeminiParameters(innerSchema, visited).default(schema._def.defaultValue())
}
if (schema instanceof z.ZodOptional) {
return z.optional(sanitizeGeminiParameters(schema.unwrap(), visited))
}
if (schema instanceof z.ZodObject) {
const newShape: Record<string, z.ZodTypeAny> = {}
for (const [key, value] of Object.entries(schema.shape)) {
newShape[key] = sanitizeGeminiParameters(value as z.ZodTypeAny, visited)
}
return z.object(newShape)
}
if (schema instanceof z.ZodArray) {
return z.array(sanitizeGeminiParameters(schema.element, visited))
}
if (schema instanceof z.ZodUnion) {
// This schema corresponds to `anyOf` in JSON Schema.
// We recursively sanitize each option in the union.
const sanitizedOptions = schema.options.map((option: z.ZodTypeAny) => sanitizeGeminiParameters(option, visited))
return z.union(sanitizedOptions as [z.ZodTypeAny, z.ZodTypeAny, ...z.ZodTypeAny[]])
}
if (schema instanceof z.ZodString) {
const newSchema = z.string({ description: schema.description })
const safeChecks = ["min", "max", "length", "regex", "startsWith", "endsWith", "includes", "trim"]
// rome-ignore lint/suspicious/noExplicitAny: <explanation>
;(newSchema._def as any).checks = (schema._def as z.ZodStringDef).checks.filter((check) =>
safeChecks.includes(check.kind),
)
return newSchema
}
return schema
}
function optionalToNullable(schema: z.ZodTypeAny): z.ZodTypeAny {
if (schema instanceof z.ZodObject) {
const shape = schema.shape
+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>
-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
+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),
})
}
}
-4
View File
@@ -1,4 +0,0 @@
package app
const MAX_CONTAINER_WIDTH = 86
const EDIT_DIFF_MAX_WIDTH = 180
+122 -71
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)
}
@@ -63,10 +64,7 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width = min(msg.Width-4, app.MAX_CONTAINER_WIDTH)
if m.app.Config.Layout == opencode.LayoutConfigStretch {
m.width = msg.Width - 4
}
m.width = msg.Width - 4
return m, nil
case spinner.TickMsg:
m.spinner, cmd = m.spinner.Update(msg)
@@ -94,51 +92,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 +133,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
@@ -235,6 +177,11 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
func (m *editorComponent) Content() string {
width := m.width
if m.app.Session.ID == "" {
width = min(width, 80)
}
t := theme.CurrentTheme()
base := styles.NewStyle().Foreground(t.Text()).Background(t.Background()).Render
muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
@@ -243,7 +190,7 @@ func (m *editorComponent) Content() string {
Bold(true)
prompt := promptStyle.Render(">")
m.textarea.SetWidth(m.width - 6)
m.textarea.SetWidth(width - 6)
textarea := lipgloss.JoinHorizontal(
lipgloss.Top,
prompt,
@@ -255,7 +202,7 @@ func (m *editorComponent) Content() string {
}
textarea = styles.NewStyle().
Background(t.BackgroundElement()).
Width(m.width).
Width(width).
PaddingTop(1).
PaddingBottom(1).
BorderStyle(lipgloss.ThickBorder()).
@@ -291,7 +238,7 @@ func (m *editorComponent) Content() string {
model = muted(m.app.Provider.Name) + base(" "+m.app.Model.Name)
}
space := m.width - 2 - lipgloss.Width(model) - lipgloss.Width(hint)
space := width - 2 - lipgloss.Width(model) - lipgloss.Width(hint)
spacer := styles.NewStyle().Background(t.Background()).Width(space).Render("")
info := hint + spacer + model
@@ -302,9 +249,14 @@ func (m *editorComponent) Content() string {
}
func (m *editorComponent) View() string {
width := m.width
if m.app.Session.ID == "" {
width = min(width, 80)
}
if m.Lines() > 1 {
return lipgloss.Place(
m.width,
width,
5,
lipgloss.Center,
lipgloss.Center,
@@ -352,7 +304,8 @@ func (m *editorComponent) Submit() (tea.Model, tea.Cmd) {
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
}
@@ -360,10 +313,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),
@@ -424,6 +377,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
}
@@ -504,3 +489,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,
}
}
+257 -254
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,176 @@ 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 := m.width // always use full 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 +303,43 @@ 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)
}
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,67 +351,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()),
)
error = lipgloss.PlaceHorizontal(
m.width,
lipgloss.Center,
error,
styles.WhitespaceStyle(t.Background()),
)
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 {
@@ -400,10 +409,7 @@ func (m *messagesComponent) renderHeader() string {
return ""
}
headerWidth := min(m.width, app.MAX_CONTAINER_WIDTH)
if m.app.Config.Layout == opencode.LayoutConfigStretch {
headerWidth = m.width
}
headerWidth := m.width
t := theme.CurrentTheme()
base := styles.NewStyle().Foreground(t.Text()).Background(t.Background()).Render
@@ -549,7 +555,7 @@ func formatTokensAndCost(
func (m *messagesComponent) View() string {
t := theme.CurrentTheme()
if m.rendering {
if m.loading {
return lipgloss.Place(
m.width,
m.height,
@@ -566,10 +572,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) {
@@ -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 && len(c.providers) > 1 {
if query != "" && providersWithResults > 1 {
t := theme.CurrentTheme()
baseStyle := styles.NewStyle().Background(t.BackgroundElement())
// Create a slice of display values for fuzzy matching
-1
View File
@@ -284,7 +284,6 @@ func generateMarkdownStyleConfig(backgroundColor compat.AdaptiveColor) ansi.Styl
Table: ansi.StyleTable{
StyleBlock: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
BlockPrefix: "\n",
BlockSuffix: "\n",
},
},
+2 -2
View File
@@ -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)
@@ -439,7 +439,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.WindowSizeMsg:
msg.Height -= 2 // Make space for the status bar
a.width, a.height = msg.Width, msg.Height
container := min(a.width, app.MAX_CONTAINER_WIDTH)
container := min(a.width, 86)
layout.Current = &layout.LayoutInfo{
Viewport: layout.Dimensions{
Width: a.width,
+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-a1da357fcefd3105736841fbf44018022fade78e67ffc81e178cf9196da723ee.yml
openapi_spec_hash: 9bd27afcc5b8f43d8e4223f7c984035f
config_hash: 62b73a3397120578a992bffd1e69386a
+2 -3
View File
@@ -64,7 +64,6 @@ Response 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#Config">Config</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#KeybindsConfig">KeybindsConfig</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#LayoutConfig">LayoutConfig</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#McpLocalConfig">McpLocalConfig</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#McpRemoteConfig">McpRemoteConfig</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#ModeConfig">ModeConfig</a>
@@ -77,8 +76,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:
+33 -25
View File
@@ -55,8 +55,8 @@ type Config struct {
Instructions []string `json:"instructions"`
// Custom keybind configurations
Keybinds KeybindsConfig `json:"keybinds"`
// Layout to use for the TUI
Layout LayoutConfig `json:"layout"`
// @deprecated Always uses stretch layout.
Layout ConfigLayout `json:"layout"`
// Minimum log level to write to log files
LogLevel LogLevel `json:"log_level"`
// MCP (Model Context Protocol) server configurations
@@ -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"`
@@ -197,6 +197,22 @@ func (r configExperimentalHookSessionCompletedJSON) RawJSON() string {
return r.raw
}
// @deprecated Always uses stretch layout.
type ConfigLayout string
const (
ConfigLayoutAuto ConfigLayout = "auto"
ConfigLayoutStretch ConfigLayout = "stretch"
)
func (r ConfigLayout) IsKnown() bool {
switch r {
case ConfigLayoutAuto, ConfigLayoutStretch:
return true
}
return false
}
type ConfigMcp struct {
// Type of MCP server connection
Type ConfigMcpType `json:"type,required"`
@@ -206,6 +222,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 +236,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 +446,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 +529,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"`
@@ -570,21 +590,6 @@ func (r keybindsConfigJSON) RawJSON() string {
return r.raw
}
type LayoutConfig string
const (
LayoutConfigAuto LayoutConfig = "auto"
LayoutConfigStretch LayoutConfig = "stretch"
)
func (r LayoutConfig) IsKnown() bool {
switch r {
case LayoutConfigAuto, LayoutConfigStretch:
return true
}
return false
}
type McpLocalConfig struct {
// Command and arguments to run the MCP server
Command []string `json:"command,required"`
@@ -638,7 +643,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 +654,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 {
@@ -18,19 +18,24 @@ You can define MCP servers in your opencode config under `mcp`.
### Local
Add a local MCP servers under `mcp.localmcp`.
Add local MCP servers under `mcp` with `"type": "local"`.
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"localmcp": {
"my-local-mcp-server": {
"type": "local",
"command": ["bun", "x", "my-mcp-command"],
"enabled": true,
"environment": {
"MY_ENV_VAR": "my_env_var_value"
}
}, {
"my-different-local-mcp-server": {
"type": "local",
"command": ["bun", "x", "my-other-mcp-command"],
"enabled": true
}
}
}
@@ -40,13 +45,13 @@ You can also disable a server by setting `enabled` to `false`. This is useful if
### Remote
Add a remote MCP servers under `mcp.remotemcp`.
Add remote MCP servers under `mcp` with `"type": "remote"`.
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"remotemcp": {
"my-remote-mcp": {
"type": "remote",
"url": "https://my-mcp-server.com",
"enabled": true,
+2 -1
View File
@@ -82,7 +82,6 @@ resources:
mcpLocalConfig: McpLocalConfig
mcpRemoteConfig: McpRemoteConfig
modeConfig: ModeConfig
layoutConfig: LayoutConfig
methods:
get: get /config
@@ -92,7 +91,9 @@ resources:
message: Message
part: Part
textPart: TextPart
textPartInput: TextPartInput
filePart: FilePart
filePartInput: FilePartInput
toolPart: ToolPart
stepStartPart: StepStartPart
stepFinishPart: StepFinishPart