{t().title}
diff --git a/packages/desktop/src/utils/prompt.ts b/packages/desktop/src/utils/prompt.ts
new file mode 100644
index 000000000..45c5ce1f3
--- /dev/null
+++ b/packages/desktop/src/utils/prompt.ts
@@ -0,0 +1,47 @@
+import type { Part, TextPart, FilePart } from "@opencode-ai/sdk/v2"
+import type { Prompt, FileAttachmentPart } from "@/context/prompt"
+
+/**
+ * Extract prompt content from message parts for restoring into the prompt input.
+ * This is used by undo to restore the original user prompt.
+ */
+export function extractPromptFromParts(parts: Part[]): Prompt {
+ const result: Prompt = []
+ let position = 0
+
+ for (const part of parts) {
+ if (part.type === "text") {
+ const textPart = part as TextPart
+ if (!textPart.synthetic && textPart.text) {
+ result.push({
+ type: "text",
+ content: textPart.text,
+ start: position,
+ end: position + textPart.text.length,
+ })
+ position += textPart.text.length
+ }
+ } else if (part.type === "file") {
+ const filePart = part as FilePart
+ if (filePart.source?.type === "file") {
+ const path = filePart.source.path
+ const content = "@" + path
+ const attachment: FileAttachmentPart = {
+ type: "file",
+ path,
+ content,
+ start: position,
+ end: position + content.length,
+ }
+ result.push(attachment)
+ position += content.length
+ }
+ }
+ }
+
+ if (result.length === 0) {
+ result.push({ type: "text", content: "", start: 0, end: 0 })
+ }
+
+ return result
+}
diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json
index f265b3b27..36e8ef01c 100644
--- a/packages/enterprise/package.json
+++ b/packages/enterprise/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
- "version": "1.0.152",
+ "version": "1.0.163",
"private": true,
"type": "module",
"scripts": {
@@ -14,7 +14,7 @@
"@opencode-ai/util": "workspace:*",
"@opencode-ai/ui": "workspace:*",
"aws4fetch": "^1.0.20",
- "@pierre/precision-diffs": "catalog:",
+ "@pierre/diffs": "catalog:",
"@solidjs/router": "catalog:",
"@solidjs/start": "catalog:",
"@solidjs/meta": "catalog:",
diff --git a/packages/enterprise/src/routes/share/[shareID].tsx b/packages/enterprise/src/routes/share/[shareID].tsx
index 7cce15906..83cc030f9 100644
--- a/packages/enterprise/src/routes/share/[shareID].tsx
+++ b/packages/enterprise/src/routes/share/[shareID].tsx
@@ -19,7 +19,7 @@ import { createStore } from "solid-js/store"
import z from "zod"
import NotFound from "../[...404]"
import { Tabs } from "@opencode-ai/ui/tabs"
-import { preloadMultiFileDiff, PreloadMultiFileDiffResult } from "@pierre/precision-diffs/ssr"
+import { preloadMultiFileDiff, PreloadMultiFileDiffResult } from "@pierre/diffs/ssr"
import { Diff as SSRDiff } from "@opencode-ai/ui/diff-ssr"
import { clientOnly } from "@solidjs/start"
import { type IconName } from "@opencode-ai/ui/icons/provider"
@@ -138,18 +138,13 @@ const getData = query(async (shareID) => {
export default function () {
const params = useParams()
- const data = createAsync(
- async () => {
- if (!params.shareID) throw new Error("Missing shareID")
- const now = Date.now()
- const data = getData(params.shareID)
- console.log("getData", Date.now() - now)
- return data
- },
- {
- deferStream: true,
- },
- )
+ const data = createAsync(async () => {
+ if (!params.shareID) throw new Error("Missing shareID")
+ const now = Date.now()
+ const data = getData(params.shareID)
+ console.log("getData", Date.now() - now)
+ return data
+ })
createEffect(() => {
console.log(data())
diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml
index 649233b99..8d7a72cb9 100644
--- a/packages/extensions/zed/extension.toml
+++ b/packages/extensions/zed/extension.toml
@@ -1,7 +1,7 @@
id = "opencode"
name = "OpenCode"
description = "The open source coding agent."
-version = "1.0.152"
+version = "1.0.163"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/sst/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
-archive = "https://github.com/sst/opencode/releases/download/v1.0.152/opencode-darwin-arm64.zip"
+archive = "https://github.com/sst/opencode/releases/download/v1.0.163/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
-archive = "https://github.com/sst/opencode/releases/download/v1.0.152/opencode-darwin-x64.zip"
+archive = "https://github.com/sst/opencode/releases/download/v1.0.163/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
-archive = "https://github.com/sst/opencode/releases/download/v1.0.152/opencode-linux-arm64.tar.gz"
+archive = "https://github.com/sst/opencode/releases/download/v1.0.163/opencode-linux-arm64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
-archive = "https://github.com/sst/opencode/releases/download/v1.0.152/opencode-linux-x64.tar.gz"
+archive = "https://github.com/sst/opencode/releases/download/v1.0.163/opencode-linux-x64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
-archive = "https://github.com/sst/opencode/releases/download/v1.0.152/opencode-windows-x64.zip"
+archive = "https://github.com/sst/opencode/releases/download/v1.0.163/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
diff --git a/packages/function/package.json b/packages/function/package.json
index 42baa2787..7fdd342ce 100644
--- a/packages/function/package.json
+++ b/packages/function/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
- "version": "1.0.152",
+ "version": "1.0.163",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
@@ -12,7 +12,7 @@
},
"dependencies": {
"@octokit/auth-app": "8.0.1",
- "@octokit/rest": "22.0.0",
+ "@octokit/rest": "catalog:",
"hono": "catalog:",
"jose": "6.0.11"
}
diff --git a/packages/opencode/Dockerfile b/packages/opencode/Dockerfile
index 99f593581..f92b48a6d 100644
--- a/packages/opencode/Dockerfile
+++ b/packages/opencode/Dockerfile
@@ -1,10 +1,18 @@
-FROM alpine
+FROM alpine AS base
# Disable the runtime transpiler cache by default inside Docker containers.
# On ephemeral containers, the cache is not useful
ARG BUN_RUNTIME_TRANSPILER_CACHE_PATH=0
ENV BUN_RUNTIME_TRANSPILER_CACHE_PATH=${BUN_RUNTIME_TRANSPILER_CACHE_PATH}
RUN apk add libgcc libstdc++ ripgrep
-ADD ./dist/opencode-linux-x64-baseline-musl/bin/opencode /usr/local/bin/opencode
+
+FROM base AS build-amd64
+COPY dist/opencode-linux-x64-baseline-musl/bin/opencode /usr/local/bin/opencode
+
+FROM base AS build-arm64
+COPY dist/opencode-linux-arm64-musl/bin/opencode /usr/local/bin/opencode
+
+ARG TARGETARCH
+FROM build-${TARGETARCH}
RUN opencode --version
ENTRYPOINT ["opencode"]
diff --git a/packages/opencode/package.json b/packages/opencode/package.json
index 362f5b1f2..d65b1dec2 100644
--- a/packages/opencode/package.json
+++ b/packages/opencode/package.json
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
- "version": "1.0.152",
+ "version": "1.0.163",
"name": "opencode",
"type": "module",
"private": true,
@@ -64,7 +64,7 @@
"@hono/zod-validator": "catalog:",
"@modelcontextprotocol/sdk": "1.15.1",
"@octokit/graphql": "9.0.2",
- "@octokit/rest": "22.0.0",
+ "@octokit/rest": "catalog:",
"@openauthjs/openauth": "catalog:",
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/script": "workspace:*",
@@ -74,7 +74,7 @@
"@opentui/core": "0.0.0-20251211-4403a69a",
"@opentui/solid": "0.0.0-20251211-4403a69a",
"@parcel/watcher": "2.5.1",
- "@pierre/precision-diffs": "catalog:",
+ "@pierre/diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2",
"@standard-schema/spec": "1.0.0",
"@zip.js/zip.js": "2.7.62",
diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts
index 5a6ac2584..a85fde9e2 100755
--- a/packages/opencode/script/build.ts
+++ b/packages/opencode/script/build.ts
@@ -117,6 +117,9 @@ for (const item of targets) {
compile: {
autoloadBunfig: false,
autoloadDotenv: false,
+ //@ts-ignore (bun types aren't up to date)
+ autoloadTsconfig: true,
+ autoloadPackageJson: true,
target: name.replace(pkg.name, "bun") as any,
outfile: `dist/${name}/bin/opencode`,
execArgv: [`--user-agent=opencode/${Script.version}`, "--"],
diff --git a/packages/opencode/script/publish.ts b/packages/opencode/script/publish.ts
index ff75bbb8d..72632992f 100755
--- a/packages/opencode/script/publish.ts
+++ b/packages/opencode/script/publish.ts
@@ -244,8 +244,8 @@ if (!Script.preview) {
await $`cd ./dist/homebrew-tap && git push`
const image = "ghcr.io/sst/opencode"
- await $`docker build -t ${image}:${Script.version} .`
- await $`docker push ${image}:${Script.version}`
- await $`docker tag ${image}:${Script.version} ${image}:latest`
- await $`docker push ${image}:latest`
+ const platforms = "linux/amd64,linux/arm64"
+ const tags = [`${image}:${Script.version}`, `${image}:latest`]
+ const tagFlags = tags.flatMap((t) => ["-t", t])
+ await $`docker buildx build --platform ${platforms} ${tagFlags} --push .`
}
diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts
index 94127e51c..add120f91 100644
--- a/packages/opencode/src/agent/agent.ts
+++ b/packages/opencode/src/agent/agent.ts
@@ -2,18 +2,24 @@ import { Config } from "../config/config"
import z from "zod"
import { Provider } from "../provider/provider"
import { generateObject, type ModelMessage } from "ai"
-import PROMPT_GENERATE from "./generate.txt"
import { SystemPrompt } from "../session/system"
import { Instance } from "../project/instance"
import { mergeDeep } from "remeda"
+import PROMPT_GENERATE from "./generate.txt"
+import PROMPT_COMPACTION from "./prompt/compaction.txt"
+import PROMPT_EXPLORE from "./prompt/explore.txt"
+import PROMPT_SUMMARY from "./prompt/summary.txt"
+import PROMPT_TITLE from "./prompt/title.txt"
+
export namespace Agent {
export const Info = z
.object({
name: z.string(),
description: z.string().optional(),
mode: z.enum(["subagent", "primary", "all"]),
- builtIn: z.boolean(),
+ native: z.boolean().optional(),
+ hidden: z.boolean().optional(),
topP: z.number().optional(),
temperature: z.number().optional(),
color: z.string().optional(),
@@ -101,6 +107,24 @@ export namespace Agent {
)
const result: Record
= {
+ build: {
+ name: "build",
+ tools: { ...defaultTools },
+ options: {},
+ permission: agentPermission,
+ mode: "primary",
+ native: true,
+ },
+ plan: {
+ name: "plan",
+ options: {},
+ permission: planPermission,
+ tools: {
+ ...defaultTools,
+ },
+ mode: "primary",
+ native: true,
+ },
general: {
name: "general",
description: `General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel.`,
@@ -112,7 +136,8 @@ export namespace Agent {
options: {},
permission: agentPermission,
mode: "subagent",
- builtIn: true,
+ native: true,
+ hidden: true,
},
explore: {
name: "explore",
@@ -124,48 +149,43 @@ export namespace Agent {
...defaultTools,
},
description: `Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.`,
- prompt: [
- `You are a file search specialist. You excel at thoroughly navigating and exploring codebases.`,
- ``,
- `Your strengths:`,
- `- Rapidly finding files using glob patterns`,
- `- Searching code and text with powerful regex patterns`,
- `- Reading and analyzing file contents`,
- ``,
- `Guidelines:`,
- `- Use Glob for broad file pattern matching`,
- `- Use Grep for searching file contents with regex`,
- `- Use Read when you know the specific file path you need to read`,
- `- Use Bash for file operations like copying, moving, or listing directory contents`,
- `- Adapt your search approach based on the thoroughness level specified by the caller`,
- `- Return file paths as absolute paths in your final response`,
- `- For clear communication, avoid using emojis`,
- `- Do not create any files, or run bash commands that modify the user's system state in any way`,
- ``,
- `Complete the user's search request efficiently and report your findings clearly.`,
- ].join("\n"),
+ prompt: PROMPT_EXPLORE,
options: {},
permission: agentPermission,
mode: "subagent",
- builtIn: true,
+ native: true,
},
- build: {
- name: "build",
- tools: { ...defaultTools },
+ compaction: {
+ name: "compaction",
+ mode: "primary",
+ native: true,
+ hidden: true,
+ prompt: PROMPT_COMPACTION,
+ tools: {
+ "*": false,
+ },
options: {},
permission: agentPermission,
- mode: "primary",
- builtIn: true,
},
- plan: {
- name: "plan",
- options: {},
- permission: planPermission,
- tools: {
- ...defaultTools,
- },
+ title: {
+ name: "title",
mode: "primary",
- builtIn: true,
+ options: {},
+ native: true,
+ hidden: true,
+ permission: agentPermission,
+ prompt: PROMPT_TITLE,
+ tools: {},
+ },
+ summary: {
+ name: "summary",
+ mode: "primary",
+ options: {},
+ native: true,
+ hidden: true,
+ permission: agentPermission,
+ prompt: PROMPT_SUMMARY,
+ tools: {},
},
}
for (const [key, value] of Object.entries(cfg.agent ?? {})) {
@@ -181,7 +201,7 @@ export namespace Agent {
permission: agentPermission,
options: {},
tools: {},
- builtIn: false,
+ native: false,
}
const {
name,
@@ -236,9 +256,9 @@ export namespace Agent {
return state().then((x) => Object.values(x))
}
- export async function generate(input: { description: string }) {
+ export async function generate(input: { description: string; model?: { providerID: string; modelID: string } }) {
const cfg = await Config.get()
- const defaultModel = await Provider.defaultModel()
+ const defaultModel = input.model ?? (await Provider.defaultModel())
const model = await Provider.getModel(defaultModel.providerID, defaultModel.modelID)
const language = await Provider.getLanguage(model)
const system = SystemPrompt.header(defaultModel.providerID)
diff --git a/packages/opencode/src/session/prompt/compaction.txt b/packages/opencode/src/agent/prompt/compaction.txt
similarity index 100%
rename from packages/opencode/src/session/prompt/compaction.txt
rename to packages/opencode/src/agent/prompt/compaction.txt
diff --git a/packages/opencode/src/agent/prompt/explore.txt b/packages/opencode/src/agent/prompt/explore.txt
new file mode 100644
index 000000000..5761077cb
--- /dev/null
+++ b/packages/opencode/src/agent/prompt/explore.txt
@@ -0,0 +1,18 @@
+You are a file search specialist. You excel at thoroughly navigating and exploring codebases.
+
+Your strengths:
+- Rapidly finding files using glob patterns
+- Searching code and text with powerful regex patterns
+- Reading and analyzing file contents
+
+Guidelines:
+- Use Glob for broad file pattern matching
+- Use Grep for searching file contents with regex
+- Use Read when you know the specific file path you need to read
+- Use Bash for file operations like copying, moving, or listing directory contents
+- Adapt your search approach based on the thoroughness level specified by the caller
+- Return file paths as absolute paths in your final response
+- For clear communication, avoid using emojis
+- Do not create any files, or run bash commands that modify the user's system state in any way
+
+Complete the user's search request efficiently and report your findings clearly.
diff --git a/packages/opencode/src/session/prompt/summarize.txt b/packages/opencode/src/agent/prompt/summary.txt
similarity index 100%
rename from packages/opencode/src/session/prompt/summarize.txt
rename to packages/opencode/src/agent/prompt/summary.txt
diff --git a/packages/opencode/src/session/prompt/title.txt b/packages/opencode/src/agent/prompt/title.txt
similarity index 84%
rename from packages/opencode/src/session/prompt/title.txt
rename to packages/opencode/src/agent/prompt/title.txt
index e297dc460..f67aaa95b 100644
--- a/packages/opencode/src/session/prompt/title.txt
+++ b/packages/opencode/src/agent/prompt/title.txt
@@ -22,8 +22,8 @@ Your output must be:
- The title should NEVER include "summarizing" or "generating" when generating a title
- DO NOT SAY YOU CANNOT GENERATE A TITLE OR COMPLAIN ABOUT THE INPUT
- Always output something meaningful, even if the input is minimal.
-- If the user message is short or conversational (e.g. “hello”, “lol”, “whats up”, “hey”):
- → create a title that reflects the user’s tone or intent (such as Greeting, Quick check-in, Light chat, Intro message, etc.)
+- If the user message is short or conversational (e.g. "hello", "lol", "whats up", "hey"):
+ → create a title that reflects the user's tone or intent (such as Greeting, Quick check-in, Light chat, Intro message, etc.)
diff --git a/packages/opencode/src/bun/index.ts b/packages/opencode/src/bun/index.ts
index c0f90e6ca..5456d0a5b 100644
--- a/packages/opencode/src/bun/index.ts
+++ b/packages/opencode/src/bun/index.ts
@@ -85,47 +85,16 @@ export namespace BunProc {
version,
})
- const total = 3
- const wait = 500
-
- const runInstall = async (count: number = 1): Promise => {
- log.info("bun install attempt", {
- pkg,
- version,
- attempt: count,
- total,
- })
- await BunProc.run(args, {
- cwd: Global.Path.cache,
- }).catch(async (error) => {
- log.warn("bun install failed", {
- pkg,
- version,
- attempt: count,
- total,
- error,
- })
- if (count >= total) {
- throw new InstallFailedError(
- { pkg, version },
- {
- cause: error,
- },
- )
- }
- const delay = wait * count
- log.info("bun install retrying", {
- pkg,
- version,
- next: count + 1,
- delay,
- })
- await Bun.sleep(delay)
- return runInstall(count + 1)
- })
- }
-
- await runInstall()
+ await BunProc.run(args, {
+ cwd: Global.Path.cache,
+ }).catch((e) => {
+ throw new InstallFailedError(
+ { pkg, version },
+ {
+ cause: e,
+ },
+ )
+ })
// Resolve actual version from installed package when using "latest"
// This ensures subsequent starts use the cached version until explicitly updated
diff --git a/packages/opencode/src/cli/cmd/agent.ts b/packages/opencode/src/cli/cmd/agent.ts
index 812e97423..60dd9cc75 100644
--- a/packages/opencode/src/cli/cmd/agent.ts
+++ b/packages/opencode/src/cli/cmd/agent.ts
@@ -3,6 +3,7 @@ import * as prompts from "@clack/prompts"
import { UI } from "../ui"
import { Global } from "../../global"
import { Agent } from "../../agent/agent"
+import { Provider } from "../../provider/provider"
import path from "path"
import fs from "fs/promises"
import matter from "gray-matter"
@@ -47,6 +48,11 @@ const AgentCreateCommand = cmd({
.option("tools", {
type: "string",
describe: `comma-separated list of tools to enable (default: all). Available: "${AVAILABLE_TOOLS.join(", ")}"`,
+ })
+ .option("model", {
+ type: "string",
+ alias: ["m"],
+ describe: "model to use in the format of provider/model",
}),
async handler(args) {
await Instance.provide({
@@ -114,7 +120,8 @@ const AgentCreateCommand = cmd({
// Generate agent
const spinner = prompts.spinner()
spinner.start("Generating agent configuration...")
- const generated = await Agent.generate({ description }).catch((error) => {
+ const model = args.model ? Provider.parseModel(args.model) : undefined
+ const generated = await Agent.generate({ description, model }).catch((error) => {
spinner.stop(`LLM failed to generate agent: ${error.message}`, 1)
if (isFullyNonInteractive) process.exit(1)
throw new UI.CancelledError()
@@ -227,8 +234,8 @@ const AgentListCommand = cmd({
async fn() {
const agents = await Agent.list()
const sortedAgents = agents.sort((a, b) => {
- if (a.builtIn !== b.builtIn) {
- return a.builtIn ? -1 : 1
+ if (a.native !== b.native) {
+ return a.native ? -1 : 1
}
return a.name.localeCompare(b.name)
})
diff --git a/packages/opencode/src/cli/cmd/github.ts b/packages/opencode/src/cli/cmd/github.ts
index 55d9fb19d..c7d403395 100644
--- a/packages/opencode/src/cli/cmd/github.ts
+++ b/packages/opencode/src/cli/cmd/github.ts
@@ -128,6 +128,19 @@ const AGENT_USERNAME = "opencode-agent[bot]"
const AGENT_REACTION = "eyes"
const WORKFLOW_FILE = ".github/workflows/opencode.yml"
+// Parses GitHub remote URLs in various formats:
+// - https://github.com/owner/repo.git
+// - https://github.com/owner/repo
+// - git@github.com:owner/repo.git
+// - git@github.com:owner/repo
+// - ssh://git@github.com/owner/repo.git
+// - ssh://git@github.com/owner/repo
+export function parseGitHubRemote(url: string): { owner: string; repo: string } | null {
+ const match = url.match(/^(?:(?:https?|ssh):\/\/)?(?:git@)?github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?$/)
+ if (!match) return null
+ return { owner: match[1], repo: match[2] }
+}
+
export const GithubCommand = cmd({
command: "github",
describe: "manage GitHub agent",
@@ -197,20 +210,12 @@ export const GithubInstallCommand = cmd({
// Get repo info
const info = (await $`git remote get-url origin`.quiet().nothrow().text()).trim()
- // match https or git pattern
- // ie. https://github.com/sst/opencode.git
- // ie. https://github.com/sst/opencode
- // ie. git@github.com:sst/opencode.git
- // ie. git@github.com:sst/opencode
- // ie. ssh://git@github.com/sst/opencode.git
- // ie. ssh://git@github.com/sst/opencode
- const parsed = info.match(/^(?:(?:https?|ssh):\/\/)?(?:git@)?github\.com[:/]([^/]+)\/([^/.]+?)(?:\.git)?$/)
+ const parsed = parseGitHubRemote(info)
if (!parsed) {
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
throw new UI.CancelledError()
}
- const [, owner, repo] = parsed
- return { owner, repo, root: Instance.worktree }
+ return { owner: parsed.owner, repo: parsed.repo, root: Instance.worktree }
}
async function promptProvider() {
@@ -278,7 +283,7 @@ export const GithubInstallCommand = cmd({
process.platform === "darwin"
? `open "${url}"`
: process.platform === "win32"
- ? `start "${url}"`
+ ? `start "" "${url}"`
: `xdg-open "${url}"`
exec(command, (error) => {
@@ -411,17 +416,30 @@ export const GithubRunCommand = cmd({
let exitCode = 0
type PromptFiles = Awaited>["promptFiles"]
const triggerCommentId = payload.comment.id
+ const useGithubToken = normalizeUseGithubToken()
try {
- const actionToken = isMock ? args.token! : await getOidcToken()
- appToken = await exchangeForAppToken(actionToken)
+ if (useGithubToken) {
+ const githubToken = process.env["GITHUB_TOKEN"]
+ if (!githubToken) {
+ throw new Error(
+ "GITHUB_TOKEN environment variable is not set. When using use_github_token, you must provide GITHUB_TOKEN.",
+ )
+ }
+ appToken = githubToken
+ } else {
+ const actionToken = isMock ? args.token! : await getOidcToken()
+ appToken = await exchangeForAppToken(actionToken)
+ }
octoRest = new Octokit({ auth: appToken })
octoGraph = graphql.defaults({
headers: { authorization: `token ${appToken}` },
})
const { userPrompt, promptFiles } = await getUserPrompt()
- await configureGit(appToken)
+ if (!useGithubToken) {
+ await configureGit(appToken)
+ }
await assertPermissions()
await addReaction()
@@ -514,8 +532,10 @@ export const GithubRunCommand = cmd({
// Also output the clean error message for the action to capture
//core.setOutput("prepare_error", e.message);
} finally {
- await restoreGitConfig()
- await revokeAppToken()
+ if (!useGithubToken) {
+ await restoreGitConfig()
+ await revokeAppToken()
+ }
}
process.exit(exitCode)
@@ -544,6 +564,14 @@ export const GithubRunCommand = cmd({
throw new Error(`Invalid share value: ${value}. Share must be a boolean.`)
}
+ function normalizeUseGithubToken() {
+ const value = process.env["USE_GITHUB_TOKEN"]
+ if (!value) return false
+ if (value === "true") return true
+ if (value === "false") return false
+ throw new Error(`Invalid use_github_token value: ${value}. Must be a boolean.`)
+ }
+
function isIssueCommentEvent(
event: IssueCommentEvent | PullRequestReviewCommentEvent,
): event is IssueCommentEvent {
diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts
index c57711b4c..3a0b2f23f 100644
--- a/packages/opencode/src/cli/cmd/run.ts
+++ b/packages/opencode/src/cli/cmd/run.ts
@@ -88,7 +88,9 @@ export const RunCommand = cmd({
})
},
handler: async (args) => {
- let message = [...args.message, ...(args["--"] || [])].join(" ")
+ let message = [...args.message, ...(args["--"] || [])]
+ .map((arg) => (arg.includes(" ") ? `"${arg.replace(/"/g, '\\"')}"` : arg))
+ .join(" ")
const fileParts: any[] = []
if (args.file) {
@@ -277,8 +279,8 @@ export const RunCommand = cmd({
}
return { error }
})
- if (!shareResult.error) {
- UI.println(UI.Style.TEXT_INFO_BOLD + "~ https://opencode.ai/s/" + sessionID.slice(-8))
+ if (!shareResult.error && "data" in shareResult && shareResult.data?.share?.url) {
+ UI.println(UI.Style.TEXT_INFO_BOLD + "~ " + shareResult.data.share.url)
}
}
@@ -330,8 +332,8 @@ export const RunCommand = cmd({
}
return { error }
})
- if (!shareResult.error) {
- UI.println(UI.Style.TEXT_INFO_BOLD + "~ https://opencode.ai/s/" + sessionID.slice(-8))
+ if (!shareResult.error && "data" in shareResult && shareResult.data?.share?.url) {
+ UI.println(UI.Style.TEXT_INFO_BOLD + "~ " + shareResult.data.share.url)
}
}
diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx
index 28e841122..a1a8a5e80 100644
--- a/packages/opencode/src/cli/cmd/tui/app.tsx
+++ b/packages/opencode/src/cli/cmd/tui/app.tsx
@@ -218,7 +218,9 @@ function App() {
let continued = false
createEffect(() => {
if (continued || sync.status !== "complete" || !args.continue) return
- const match = sync.data.session.at(0)?.id
+ const match = sync.data.session
+ .toSorted((a, b) => b.time.updated - a.time.updated)
+ .find((x) => x.parentID === undefined)?.id
if (match) {
continued = true
route.navigate({ type: "session", sessionID: match })
@@ -295,6 +297,24 @@ function App() {
local.model.cycle(-1)
},
},
+ {
+ title: "Favorite cycle",
+ value: "model.cycle_favorite",
+ keybind: "model_cycle_favorite",
+ category: "Agent",
+ onSelect: () => {
+ local.model.cycleFavorite(1)
+ },
+ },
+ {
+ title: "Favorite cycle reverse",
+ value: "model.cycle_favorite_reverse",
+ keybind: "model_cycle_favorite_reverse",
+ category: "Agent",
+ onSelect: () => {
+ local.model.cycleFavorite(-1)
+ },
+ },
{
title: "Switch agent",
value: "agent.list",
diff --git a/packages/opencode/src/cli/cmd/tui/attach.ts b/packages/opencode/src/cli/cmd/tui/attach.ts
index 7da6507ea..5d1a4ded2 100644
--- a/packages/opencode/src/cli/cmd/tui/attach.ts
+++ b/packages/opencode/src/cli/cmd/tui/attach.ts
@@ -14,12 +14,17 @@ export const AttachCommand = cmd({
.option("dir", {
type: "string",
description: "directory to run in",
+ })
+ .option("session", {
+ alias: ["s"],
+ type: "string",
+ describe: "session id to continue",
}),
handler: async (args) => {
if (args.dir) process.chdir(args.dir)
await tui({
url: args.url,
- args: {},
+ args: { sessionID: args.session },
})
},
})
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-agent.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-agent.tsx
index 65aaeb22b..365a22445 100644
--- a/packages/opencode/src/cli/cmd/tui/component/dialog-agent.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-agent.tsx
@@ -12,7 +12,7 @@ export function DialogAgent() {
return {
value: item.name,
title: item.name,
- description: item.builtIn ? "native" : item.description,
+ description: item.native ? "native" : item.description,
}
}),
)
diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx
index c40aa114a..6fde66944 100644
--- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx
@@ -184,7 +184,7 @@ export function Autocomplete(props: {
const agents = createMemo(() => {
const agents = sync.data.agent
return agents
- .filter((agent) => !agent.builtIn && agent.mode !== "primary")
+ .filter((agent) => !agent.hidden && agent.mode !== "primary")
.map(
(agent): AutocompleteOption => ({
display: "@" + agent.name,
@@ -364,6 +364,13 @@ export function Autocomplete(props: {
const result = fuzzysort.go(currentFilter, mixed, {
keys: [(obj) => obj.display.trimEnd(), "description", (obj) => obj.aliases?.join(" ") ?? ""],
limit: 10,
+ scoreFn: (objResults) => {
+ const displayResult = objResults[0]
+ if (displayResult && displayResult.target.startsWith(store.visible + currentFilter)) {
+ return objResults.score * 2
+ }
+ return objResults.score
+ },
})
return result.map((arr) => arr.obj)
})
diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx
index e55479c02..e90503e9f 100644
--- a/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx
@@ -9,6 +9,7 @@ import type { AgentPart, FilePart, TextPart } from "@opencode-ai/sdk/v2"
export type PromptInfo = {
input: string
+ mode?: "normal" | "shell"
parts: (
| Omit
| Omit
diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
index 1ea701c83..938405f68 100644
--- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
@@ -44,6 +44,7 @@ export type PromptRef = {
reset(): void
blur(): void
focus(): void
+ submit(): void
}
const PLACEHOLDERS = ["Fix a TODO in the codebase", "What is the tech stack of this project?", "Fix broken tests"]
@@ -447,11 +448,14 @@ export function Prompt(props: PromptProps) {
})
setStore("extmarkToPartIndex", new Map())
},
+ submit() {
+ submit()
+ },
})
async function submit() {
if (props.disabled) return
- if (autocomplete.visible) return
+ if (autocomplete?.visible) return
if (!store.prompt.input) return
const trimmed = store.prompt.input.trim()
if (trimmed === "exit" || trimmed === "quit" || trimmed === ":q") {
@@ -491,6 +495,9 @@ export function Prompt(props: PromptProps) {
// Filter out text parts (pasted content) since they're now expanded inline
const nonTextParts = store.prompt.parts.filter((part) => part.type !== "text")
+ // Capture mode before it gets reset
+ const currentMode = store.mode
+
if (store.mode === "shell") {
sdk.client.session.shell({
sessionID,
@@ -539,7 +546,10 @@ export function Prompt(props: PromptProps) {
],
})
}
- history.append(store.prompt)
+ history.append({
+ ...store.prompt,
+ mode: currentMode,
+ })
input.extmarks.clear()
setStore("prompt", {
input: "",
@@ -705,8 +715,8 @@ export function Prompt(props: PromptProps) {
>