mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-24 17:41:37 +08:00
Compare commits
22 Commits
github-v1.0.3
...
v0.3.80
| Author | SHA1 | Date | |
|---|---|---|---|
| 081f100c93 | |||
| f2bdb8159f | |||
| 10d749a85e | |||
| a07d149e28 | |||
| 3eb982c8cd | |||
| 45c4e0b8f8 | |||
| b18b646f8e | |||
| 9741a6703c | |||
| 27a079d9cb | |||
| 2eeb987680 | |||
| e827294c9b | |||
| 7cf4ed6ad6 | |||
| ad8a4bc744 | |||
| 2630104f18 | |||
| 670f470eee | |||
| c2b3c52b76 | |||
| a007d65f62 | |||
| 2c924b9fdb | |||
| e8eaa77bf1 | |||
| a07f37073b | |||
| 4d760a1984 | |||
| 6b7058fe1c |
@@ -7,12 +7,11 @@ on:
|
||||
jobs:
|
||||
opencode:
|
||||
if: |
|
||||
startsWith(github.event.comment.body, 'opencode') ||
|
||||
startsWith(github.event.comment.body, 'hi opencode') ||
|
||||
startsWith(github.event.comment.body, 'hey opencode') ||
|
||||
contains(github.event.comment.body, '@opencode-agent')
|
||||
contains(github.event.comment.body, '/oc') ||
|
||||
contains(github.event.comment.body, '/opencode')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -25,4 +24,4 @@ jobs:
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
with:
|
||||
model: anthropic/claude-sonnet-4-20250514
|
||||
model: anthropic/claude-sonnet-4-20250514
|
||||
@@ -27,4 +27,4 @@ jobs:
|
||||
git config --global user.email "opencode@sst.dev"
|
||||
git config --global user.name "opencode"
|
||||
./script/publish
|
||||
working-directory: ./sdks/github
|
||||
working-directory: ./github
|
||||
|
||||
@@ -97,7 +97,7 @@ $ bun run packages/opencode/src/index.ts
|
||||
It's very similar to Claude Code in terms of capability. Here are the key differences:
|
||||
|
||||
- 100% open source
|
||||
- Not coupled to any provider. Although Anthropic is recommended, opencode can be used with OpenAI, Google or even local models. As models evolve the gaps between them will close and pricing will drop so being provider agnostic is important.
|
||||
- Not coupled to any provider. Although Anthropic is recommended, opencode can be used with OpenAI, Google or even local models. As models evolve the gaps between them will close and pricing will drop so being provider-agnostic is important.
|
||||
- A focus on TUI. opencode is built by neovim users and the creators of [terminal.shop](https://terminal.shop); we are going to push the limits of what's possible in the terminal.
|
||||
- A client/server architecture. This for example can allow opencode to run on your computer, while you can drive it remotely from a mobile app. Meaning that the TUI frontend is just one of the possible clients.
|
||||
|
||||
|
||||
@@ -27,3 +27,6 @@
|
||||
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
|
||||
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
|
||||
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
|
||||
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
|
||||
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
|
||||
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
|
||||
|
||||
+63
-59
@@ -2,73 +2,77 @@
|
||||
|
||||
A GitHub Action that integrates [opencode](https://opencode.ai) directly into your GitHub workflow.
|
||||
|
||||
Start your comment with `hey opencode`, and opencode will take action via your GitHub Actions runner.
|
||||
Mention `/opencode` in your comment, and opencode will execute tasks within your GitHub Actions runner.
|
||||
|
||||
## Features
|
||||
|
||||
#### Triage and explain issues
|
||||
#### Explain an issues
|
||||
|
||||
Leave the following comment on a GitHub issue. `opencode` will read the entire thread, including all comments, and reply with a clear explanation.
|
||||
|
||||
```
|
||||
hey opencode, explain this issue
|
||||
/opencode explain this issue
|
||||
```
|
||||
|
||||
#### Fix or implement issues - opencode will create a PR with the changes.
|
||||
#### Fix an issues
|
||||
|
||||
Leave the following comment on a GitHub issue. opencode will create a new branch, implement the changes, and open a PR with the changes.
|
||||
|
||||
```
|
||||
hi opencode, fix this
|
||||
/opencode fix this
|
||||
```
|
||||
|
||||
- Review PRs and make changes
|
||||
#### Review PRs and make changes
|
||||
|
||||
Leave the following comment on a GitHub PR. opencode will implement the requested change and commit it to the same PR.
|
||||
|
||||
```
|
||||
Delete the attachment from S3 when the note is removed @opencode-agent
|
||||
Delete the attachment from S3 when the note is removed /oc
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
Run the following command in the terminal from your GitHub repo:
|
||||
|
||||
```
|
||||
```bash
|
||||
opencode github install
|
||||
```
|
||||
|
||||
This will walk you through installing the GitHub app, configuring the workflow, and setting up secrets.
|
||||
This will walk you through installing the GitHub app, creating the workflow, and setting up secrets.
|
||||
|
||||
### Manual Setup
|
||||
|
||||
1. Install the GitHub app https://github.com/apps/opencode-agent. Make sure it is installed on the target repository.
|
||||
2. Add the following workflow file to `.github/workflows/opencode.yml` in your repo. Set the appropriate `model` and required API keys in `env`.
|
||||
|
||||
```yml
|
||||
name: opencode
|
||||
```yml
|
||||
name: opencode
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
opencode:
|
||||
if: |
|
||||
startsWith(github.event.comment.body, 'opencode') ||
|
||||
startsWith(github.event.comment.body, 'hi opencode') ||
|
||||
startsWith(github.event.comment.body, 'hey opencode') ||
|
||||
contains(github.event.comment.body, '@opencode-agent')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
jobs:
|
||||
opencode:
|
||||
if: |
|
||||
contains(github.event.comment.body, '/oc') ||
|
||||
contains(github.event.comment.body, '/opencode')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run opencode
|
||||
uses: sst/opencode/github@latest
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
with:
|
||||
model: anthropic/claude-sonnet-4-20250514
|
||||
```
|
||||
- name: Run opencode
|
||||
uses: sst/opencode/github@latest
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
with:
|
||||
model: anthropic/claude-sonnet-4-20250514
|
||||
```
|
||||
|
||||
3. Store the API keys in secrets. In your organization or project **settings**, expand **Secrets and variables** on the left and select **Actions**. Add the required API keys.
|
||||
|
||||
@@ -82,32 +86,32 @@ To test locally:
|
||||
|
||||
1. Navigate to a test repo (e.g. `hello-world`):
|
||||
|
||||
```
|
||||
cd hello-world
|
||||
```
|
||||
```bash
|
||||
cd hello-world
|
||||
```
|
||||
|
||||
2. Run:
|
||||
|
||||
```
|
||||
MODEL=anthropic/claude-sonnet-4-20250514 \
|
||||
ANTHROPIC_API_KEY=sk-ant-api03-1234567890 \
|
||||
GITHUB_RUN_ID=dummy \
|
||||
bun /path/to/opencode/packages/opencode/src/index.ts github run \
|
||||
--token 'github_pat_1234567890' \
|
||||
--event '{"eventName":"issue_comment",...}'
|
||||
```
|
||||
```bash
|
||||
MODEL=anthropic/claude-sonnet-4-20250514 \
|
||||
ANTHROPIC_API_KEY=sk-ant-api03-1234567890 \
|
||||
GITHUB_RUN_ID=dummy \
|
||||
bun /path/to/opencode/packages/opencode/src/index.ts github run \
|
||||
--token 'github_pat_1234567890' \
|
||||
--event '{"eventName":"issue_comment",...}'
|
||||
```
|
||||
|
||||
- `MODEL`: The model used by opencode. Same as the `MODEL` defined in the GitHub workflow.
|
||||
- `ANTHROPIC_API_KEY`: Your model provider API key. Same as the keys defined in the GitHub workflow.
|
||||
- `GITHUB_RUN_ID`: Dummy value to emulate GitHub action environment.
|
||||
- `/path/to/opencode`: Path to your cloned opencode repo. `bun /path/to/opencode/packages/opencode/src/index.ts` runs your local version of `opencode`.
|
||||
- `--token`: A GitHub persontal access token. This token is used to verify you have `admin` or `write` access to the test repo. Generate a token [here](https://github.com/settings/personal-access-tokens).
|
||||
- `--event`: Mock GitHub event payload (see templates below).
|
||||
- `MODEL`: The model used by opencode. Same as the `MODEL` defined in the GitHub workflow.
|
||||
- `ANTHROPIC_API_KEY`: Your model provider API key. Same as the keys defined in the GitHub workflow.
|
||||
- `GITHUB_RUN_ID`: Dummy value to emulate GitHub action environment.
|
||||
- `/path/to/opencode`: Path to your cloned opencode repo. `bun /path/to/opencode/packages/opencode/src/index.ts` runs your local version of `opencode`.
|
||||
- `--token`: A GitHub persontal access token. This token is used to verify you have `admin` or `write` access to the test repo. Generate a token [here](https://github.com/settings/personal-access-tokens).
|
||||
- `--event`: Mock GitHub event payload (see templates below).
|
||||
|
||||
#### Issue comment event
|
||||
### Issue comment event
|
||||
|
||||
```
|
||||
--event '{"eventName":"issue_comment","repo":{"owner":"sst","repo":"hello-world"},"actor":"fwang","payload":{"issue":{"number":4},"comment":{"id":1,"body":"hey opencode, summarize thread"}}}'
|
||||
--event '{"eventName":"issue_comment","repo":{"owner":"sst","repo":"hello-world"},"actor":"fwang","payload":{"issue":{"number":4},"comment":{"id":1,"body":"hey opencode, summarize thread"}}}'
|
||||
```
|
||||
|
||||
Replace:
|
||||
@@ -118,16 +122,16 @@ Replace:
|
||||
- `"number":4` with the GitHub issue id
|
||||
- `"body":"hey opencode, summarize thread"` with comment body
|
||||
|
||||
#### Issue comment with image attachment.
|
||||
### Issue comment with image attachment.
|
||||
|
||||
```
|
||||
--event '{"eventName":"issue_comment","repo":{"owner":"sst","repo":"hello-world"},"actor":"fwang","payload":{"issue":{"number":4},"comment":{"id":1,"body":"hey opencode, what is in my image "}}}'
|
||||
--event '{"eventName":"issue_comment","repo":{"owner":"sst","repo":"hello-world"},"actor":"fwang","payload":{"issue":{"number":4},"comment":{"id":1,"body":"hey opencode, what is in my image "}}}'
|
||||
```
|
||||
|
||||
Replace the image URL `https://github.com/user-attachments/assets/xxxxxxxx` with a valid GitHub attachment (you can generate one by commenting with an image in any issue).
|
||||
|
||||
#### PR comment event
|
||||
### PR comment event
|
||||
|
||||
```
|
||||
--event '{"eventName":"issue_comment","repo":{"owner":"sst","repo":"hello-world"},"actor":"fwang","payload":{"issue":{"number":4,"pull_request":{}},"comment":{"id":1,"body":"hey opencode, summarize thread"}}}'
|
||||
--event '{"eventName":"issue_comment","repo":{"owner":"sst","repo":"hello-world"},"actor":"fwang","payload":{"issue":{"number":4,"pull_request":{}},"comment":{"id":1,"body":"hey opencode, summarize thread"}}}'
|
||||
```
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ branding:
|
||||
inputs:
|
||||
model:
|
||||
description: "Model to use"
|
||||
required: false
|
||||
required: true
|
||||
|
||||
share:
|
||||
description: "Share the opencode session (defaults to true for public repos)"
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
}
|
||||
},
|
||||
"mcp": {
|
||||
"context7": {
|
||||
"type": "remote",
|
||||
"url": "https://mcp.context7.com/sse"
|
||||
},
|
||||
"weather": {
|
||||
"type": "local",
|
||||
"command": ["opencode", "x", "@h1deya/mcp-server-weather"]
|
||||
|
||||
@@ -156,21 +156,23 @@ export const GithubInstallCommand = cmd({
|
||||
step2 =
|
||||
"Configure OIDC in AWS - https://docs.github.com/en/actions/how-tos/security-for-github-actions/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services"
|
||||
} else {
|
||||
const url = `https://github.com/organizations/${app.owner}/settings/secrets/actions`
|
||||
const env = providers[provider].env
|
||||
const envStr =
|
||||
env.length === 1
|
||||
? `\`${env[0]}\` secret`
|
||||
: `\`${[env.slice(0, -1).join("\`, \`"), ...env.slice(-1)].join("\` and \`")}\` secrets`
|
||||
step2 = `Add ${envStr} for ${providers[provider].name} - ${url}`
|
||||
step2 = [
|
||||
` 2. Add the following secrets in org or repo (${app.owner}/${app.repo}) settings`,
|
||||
"",
|
||||
...providers[provider].env.map((e) => ` - ${e}`),
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
prompts.outro(
|
||||
[
|
||||
"Next steps:",
|
||||
` 1. Commit "${WORKFLOW_FILE}" file and push`,
|
||||
` 2. ${step2}`,
|
||||
" 3. Learn how to use the GitHub agent - https://docs.opencode.ai/docs/github/getting-started",
|
||||
"",
|
||||
` 1. Commit the \`${WORKFLOW_FILE}\` file and push`,
|
||||
step2,
|
||||
"",
|
||||
" 3. Go to a GitHub issue and comment `/oc summarize` to see the agent in action",
|
||||
"",
|
||||
" Learn more about the GitHub agent - https://opencode.ai/docs/github/#usage-examples",
|
||||
].join("\n"),
|
||||
)
|
||||
}
|
||||
@@ -316,18 +318,15 @@ on:
|
||||
jobs:
|
||||
opencode:
|
||||
if: |
|
||||
startsWith(github.event.comment.body, 'opencode') ||
|
||||
startsWith(github.event.comment.body, 'hi opencode') ||
|
||||
startsWith(github.event.comment.body, 'hey opencode') ||
|
||||
contains(github.event.comment.body, '@opencode-agent')
|
||||
contains(github.event.comment.body, '/oc') ||
|
||||
contains(github.event.comment.body, '/opencode')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run opencode
|
||||
uses: sst/opencode/github@latest${envStr}
|
||||
@@ -375,9 +374,6 @@ export const GithubRunCommand = cmd({
|
||||
const runUrl = `/${owner}/${repo}/actions/runs/${runId}`
|
||||
const shareBaseUrl = isMock ? "https://dev.opencode.ai" : "https://opencode.ai"
|
||||
|
||||
// TODO
|
||||
console.log("payload", payload)
|
||||
|
||||
let appToken: string
|
||||
let octoRest: Octokit
|
||||
let octoGraph: typeof graphql
|
||||
@@ -428,7 +424,7 @@ export const GithubRunCommand = cmd({
|
||||
const response = await chat(`${userPrompt}\n\n${dataPrompt}`, promptFiles)
|
||||
if (await branchIsDirty()) {
|
||||
const summary = await summarize(response)
|
||||
await pushToCurrentBranch(summary)
|
||||
await pushToLocalBranch(summary)
|
||||
}
|
||||
const hasShared = prData.comments.nodes.some((c) => c.body.includes(`${shareBaseUrl}/s/${shareId}`))
|
||||
await updateComment(`${response}${footer({ image: !hasShared })}`)
|
||||
@@ -454,7 +450,7 @@ export const GithubRunCommand = cmd({
|
||||
const response = await chat(`${userPrompt}\n\n${dataPrompt}`, promptFiles)
|
||||
if (await branchIsDirty()) {
|
||||
const summary = await summarize(response)
|
||||
await pushToCurrentBranch(summary)
|
||||
await pushToNewBranch(summary, branch)
|
||||
const pr = await createPR(
|
||||
repoData.data.default_branch,
|
||||
branch,
|
||||
@@ -462,8 +458,9 @@ export const GithubRunCommand = cmd({
|
||||
`${response}\n\nCloses #${issueId}${footer({ image: true })}`,
|
||||
)
|
||||
await updateComment(`Created PR #${pr}${footer({ image: true })}`)
|
||||
} else {
|
||||
await updateComment(`${response}${footer({ image: true })}`)
|
||||
}
|
||||
await updateComment(`${response}${footer({ image: true })}`)
|
||||
}
|
||||
} catch (e: any) {
|
||||
exitCode = 1
|
||||
@@ -511,16 +508,10 @@ export const GithubRunCommand = cmd({
|
||||
|
||||
async function getUserPrompt() {
|
||||
let prompt = (() => {
|
||||
const body = payload.comment.body
|
||||
if (body.match("@opencode-agent")) return body
|
||||
|
||||
const match = body.match(/^(?:hey|hi)?\s*opencode(?:-agent)?,?\s*(.*)$/is)
|
||||
if (match?.[1] === undefined)
|
||||
throw new Error(
|
||||
"Command must mention @opencode-agent, or start with `opencode`, `hi opencode`, or `hey opencode` followed by instructions",
|
||||
)
|
||||
if (match[1] === "") return "Summarize this thread"
|
||||
return match[1]
|
||||
const body = payload.comment.body.trim()
|
||||
if (body === "/opencode" || body === "/oc") return "Summarize this thread"
|
||||
if (body.includes("/opencode") || body.includes("/oc")) return body
|
||||
throw new Error("Comments must mention `/opencode` or `/oc`")
|
||||
})()
|
||||
|
||||
// Handle images
|
||||
@@ -540,6 +531,7 @@ export const GithubRunCommand = cmd({
|
||||
const mdMatches = prompt.matchAll(/!?\[.*?\]\((https:\/\/github\.com\/user-attachments\/[^)]+)\)/gi)
|
||||
const tagMatches = prompt.matchAll(/<img .*?src="(https:\/\/github\.com\/user-attachments\/[^"]+)" \/>/gi)
|
||||
const matches = [...mdMatches, ...tagMatches].sort((a, b) => a.index - b.index)
|
||||
console.log("Images", JSON.stringify(matches, null, 2))
|
||||
|
||||
let offset = 0
|
||||
for (const m of matches) {
|
||||
@@ -556,9 +548,6 @@ export const GithubRunCommand = cmd({
|
||||
},
|
||||
})
|
||||
if (!res.ok) {
|
||||
// TODO
|
||||
//console.log(res)
|
||||
//throw new Error("manual")
|
||||
console.error(`Failed to download image: ${url}`)
|
||||
continue
|
||||
}
|
||||
@@ -791,8 +780,17 @@ export const GithubRunCommand = cmd({
|
||||
return `opencode/${type}${issueId}-${timestamp}`
|
||||
}
|
||||
|
||||
async function pushToCurrentBranch(summary: string) {
|
||||
console.log("Pushing to current branch...")
|
||||
async function pushToNewBranch(summary: string, branch: string) {
|
||||
console.log("Pushing to new branch...")
|
||||
await $`git add .`
|
||||
await $`git commit -m "${summary}
|
||||
|
||||
Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
|
||||
await $`git push -u origin ${branch}`
|
||||
}
|
||||
|
||||
async function pushToLocalBranch(summary: string) {
|
||||
console.log("Pushing to local branch...")
|
||||
await $`git add .`
|
||||
await $`git commit -m "${summary}
|
||||
|
||||
|
||||
@@ -123,19 +123,15 @@ export const TuiCommand = cmd({
|
||||
const method = await Installation.method()
|
||||
if (method === "unknown") return
|
||||
await Installation.upgrade(method, latest)
|
||||
.then(() => {
|
||||
Bus.publish(Installation.Event.Updated, { version: latest })
|
||||
})
|
||||
.then(() => Bus.publish(Installation.Event.Updated, { version: latest }))
|
||||
.catch(() => {})
|
||||
})()
|
||||
;(async () => {
|
||||
if (Ide.alreadyInstalled()) return
|
||||
const ide = await Ide.ide()
|
||||
const ide = Ide.ide()
|
||||
if (ide === "unknown") return
|
||||
await Ide.install(ide)
|
||||
.then(() => {
|
||||
Bus.publish(Ide.Event.Installed, { ide })
|
||||
})
|
||||
.then(() => Bus.publish(Ide.Event.Installed, { ide }))
|
||||
.catch(() => {})
|
||||
})()
|
||||
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { $ } from "bun"
|
||||
import { spawn } from "bun"
|
||||
import { z } from "zod"
|
||||
import { NamedError } from "../util/error"
|
||||
import { Log } from "../util/log"
|
||||
import { Bus } from "../bus"
|
||||
|
||||
const SUPPORTED_IDES = ["Windsurf", "Visual Studio Code", "Cursor", "VSCodium"] as const
|
||||
const SUPPORTED_IDES = [
|
||||
{ name: "Windsurf" as const, cmd: "windsurf" },
|
||||
{ name: "Visual Studio Code" as const, cmd: "code" },
|
||||
{ name: "Cursor" as const, cmd: "cursor" },
|
||||
{ name: "VSCodium" as const, cmd: "codium" },
|
||||
]
|
||||
|
||||
export namespace Ide {
|
||||
const log = Log.create({ service: "ide" })
|
||||
@@ -18,8 +23,6 @@ export namespace Ide {
|
||||
),
|
||||
}
|
||||
|
||||
export type Ide = Awaited<ReturnType<typeof ide>>
|
||||
|
||||
export const AlreadyInstalledError = NamedError.create("AlreadyInstalledError", z.object({}))
|
||||
|
||||
export const InstallFailedError = NamedError.create(
|
||||
@@ -29,11 +32,11 @@ export namespace Ide {
|
||||
}),
|
||||
)
|
||||
|
||||
export async function ide() {
|
||||
export function ide() {
|
||||
if (process.env["TERM_PROGRAM"] === "vscode") {
|
||||
const v = process.env["GIT_ASKPASS"]
|
||||
for (const ide of SUPPORTED_IDES) {
|
||||
if (v?.includes(ide)) return ide
|
||||
if (v?.includes(ide.name)) return ide.name
|
||||
}
|
||||
}
|
||||
return "unknown"
|
||||
@@ -43,32 +46,29 @@ export namespace Ide {
|
||||
return process.env["OPENCODE_CALLER"] === "vscode"
|
||||
}
|
||||
|
||||
export async function install(ide: Ide) {
|
||||
const cmd = (() => {
|
||||
switch (ide) {
|
||||
case "Windsurf":
|
||||
return $`windsurf --install-extension sst-dev.opencode`
|
||||
case "Visual Studio Code":
|
||||
return $`code --install-extension sst-dev.opencode`
|
||||
case "Cursor":
|
||||
return $`cursor --install-extension sst-dev.opencode`
|
||||
case "VSCodium":
|
||||
return $`codium --install-extension sst-dev.opencode`
|
||||
default:
|
||||
throw new Error(`Unknown IDE: ${ide}`)
|
||||
}
|
||||
})()
|
||||
// TODO: check OPENCODE_CALLER
|
||||
const result = await cmd.quiet().throws(false)
|
||||
export async function install(ide: (typeof SUPPORTED_IDES)[number]["name"]) {
|
||||
const cmd = SUPPORTED_IDES.find((i) => i.name === ide)?.cmd
|
||||
if (!cmd) throw new Error(`Unknown IDE: ${ide}`)
|
||||
|
||||
const p = spawn([cmd, "--install-extension", "sst-dev.opencode"], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
})
|
||||
await p.exited
|
||||
const stdout = await new Response(p.stdout).text()
|
||||
const stderr = await new Response(p.stderr).text()
|
||||
|
||||
log.info("installed", {
|
||||
ide,
|
||||
stdout: result.stdout.toString(),
|
||||
stderr: result.stderr.toString(),
|
||||
stdout,
|
||||
stderr,
|
||||
})
|
||||
if (result.exitCode !== 0)
|
||||
throw new InstallFailedError({
|
||||
stderr: result.stderr.toString("utf8"),
|
||||
})
|
||||
if (result.stdout.toString().includes("already installed")) throw new AlreadyInstalledError({})
|
||||
|
||||
if (p.exitCode !== 0) {
|
||||
throw new InstallFailedError({ stderr })
|
||||
}
|
||||
if (stdout.includes("already installed")) {
|
||||
throw new AlreadyInstalledError({})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { experimental_createMCPClient, type Tool } from "ai"
|
||||
import { Experimental_StdioMCPTransport } from "ai/mcp-stdio"
|
||||
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
|
||||
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
|
||||
import { App } from "../app/app"
|
||||
import { Config } from "../config/config"
|
||||
import { Log } from "../util/log"
|
||||
@@ -32,15 +34,28 @@ export namespace MCP {
|
||||
}
|
||||
log.info("found", { key, type: mcp.type })
|
||||
if (mcp.type === "remote") {
|
||||
const client = await experimental_createMCPClient({
|
||||
name: key,
|
||||
transport: {
|
||||
type: "sse",
|
||||
url: mcp.url,
|
||||
headers: mcp.headers,
|
||||
},
|
||||
}).catch(() => {})
|
||||
if (!client) {
|
||||
const transports = [
|
||||
new StreamableHTTPClientTransport(new URL(mcp.url), {
|
||||
requestInit: {
|
||||
headers: mcp.headers,
|
||||
},
|
||||
}),
|
||||
new SSEClientTransport(new URL(mcp.url), {
|
||||
requestInit: {
|
||||
headers: mcp.headers,
|
||||
},
|
||||
}),
|
||||
]
|
||||
for (const transport of transports) {
|
||||
const client = await experimental_createMCPClient({
|
||||
name: key,
|
||||
transport,
|
||||
}).catch(() => {})
|
||||
if (!client) continue
|
||||
clients[key] = client
|
||||
break
|
||||
}
|
||||
if (!clients[key])
|
||||
Bus.publish(Session.Event.Error, {
|
||||
error: {
|
||||
name: "UnknownError",
|
||||
@@ -49,16 +64,13 @@ export namespace MCP {
|
||||
},
|
||||
},
|
||||
})
|
||||
continue
|
||||
}
|
||||
clients[key] = client
|
||||
}
|
||||
|
||||
if (mcp.type === "local") {
|
||||
const [cmd, ...args] = mcp.command
|
||||
const client = await experimental_createMCPClient({
|
||||
name: key,
|
||||
transport: new Experimental_StdioMCPTransport({
|
||||
transport: new StdioClientTransport({
|
||||
stderr: "ignore",
|
||||
command: cmd,
|
||||
args,
|
||||
|
||||
@@ -23,8 +23,9 @@ export namespace ProviderTransform {
|
||||
}
|
||||
|
||||
for (const msg of unique([...system, ...final])) {
|
||||
const shouldUseContentOptions = providerID !== "anthropic" && Array.isArray(msg.content) && msg.content.length > 0
|
||||
|
||||
const shouldUseContentOptions =
|
||||
providerID !== "anthropic" && Array.isArray(msg.content) && msg.content.length > 0
|
||||
|
||||
if (shouldUseContentOptions) {
|
||||
const lastContent = msg.content[msg.content.length - 1]
|
||||
if (lastContent && typeof lastContent === "object") {
|
||||
@@ -35,7 +36,7 @@ export namespace ProviderTransform {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
msg.providerOptions = {
|
||||
...msg.providerOptions,
|
||||
...providerOptions,
|
||||
@@ -46,7 +47,7 @@ export namespace ProviderTransform {
|
||||
}
|
||||
|
||||
export function temperature(_providerID: string, modelID: string) {
|
||||
if (modelID.includes("qwen")) return 0.55
|
||||
if (modelID.toLowerCase().includes("qwen")) return 0.55
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,9 +419,7 @@ export namespace Session {
|
||||
case "file:":
|
||||
// have to normalize, symbol search returns absolute paths
|
||||
// Decode the pathname since URL constructor doesn't automatically decode it
|
||||
const pathname = decodeURIComponent(url.pathname)
|
||||
const relativePath = pathname.replace(app.path.cwd, ".")
|
||||
const filePath = path.join(app.path.cwd, relativePath)
|
||||
const filePath = decodeURIComponent(url.pathname)
|
||||
|
||||
if (part.mime === "text/plain") {
|
||||
let offset: number | undefined = undefined
|
||||
@@ -501,7 +499,7 @@ export namespace Session {
|
||||
messageID: userMsg.id,
|
||||
sessionID: input.sessionID,
|
||||
type: "text",
|
||||
text: `Called the Read tool with the following input: {\"filePath\":\"${pathname}\"}`,
|
||||
text: `Called the Read tool with the following input: {\"filePath\":\"${filePath}\"}`,
|
||||
synthetic: true,
|
||||
},
|
||||
{
|
||||
@@ -640,7 +638,9 @@ export namespace Session {
|
||||
.then((result) => {
|
||||
if (result.text)
|
||||
return Session.update(input.sessionID, (draft) => {
|
||||
draft.title = result.text
|
||||
const cleaned = result.text.replace(/<think>[\s\S]*?<\/think>\s*/g, "")
|
||||
const title = cleaned.length > 100 ? cleaned.substring(0, 97) + "..." : cleaned
|
||||
draft.title = title
|
||||
})
|
||||
})
|
||||
.catch(() => {})
|
||||
@@ -882,7 +882,7 @@ export namespace Session {
|
||||
|
||||
case "tool-input-start":
|
||||
const part = await updatePart({
|
||||
id: Identifier.ascending("part"),
|
||||
id: toolCalls[value.id]?.id ?? Identifier.ascending("part"),
|
||||
messageID: assistantMsg.id,
|
||||
sessionID: assistantMsg.sessionID,
|
||||
type: "tool",
|
||||
@@ -1029,17 +1029,17 @@ export namespace Session {
|
||||
case "text":
|
||||
if (currentText) {
|
||||
currentText.text += value.text
|
||||
await updatePart(currentText)
|
||||
if (currentText.text) await updatePart(currentText)
|
||||
}
|
||||
break
|
||||
|
||||
case "text-end":
|
||||
if (currentText && currentText.text) {
|
||||
if (currentText) {
|
||||
currentText.text = currentText.text.trimEnd()
|
||||
currentText.time = {
|
||||
start: Date.now(),
|
||||
end: Date.now(),
|
||||
}
|
||||
currentText.text = currentText.text.trimEnd()
|
||||
await updatePart(currentText)
|
||||
}
|
||||
currentText = undefined
|
||||
|
||||
@@ -29,7 +29,7 @@ You MUST use the ToolRead tool to verify that all steps are complete or cancelle
|
||||
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. Fetch any URL's provided by the user using the `webfetch` tool.
|
||||
1. Fetch any URL 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?
|
||||
@@ -84,7 +84,7 @@ Carefully read the issue and think hard about a plan to solve it before coding.
|
||||
- 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.
|
||||
- If the build fails or tests fail, debug why before proceeding, update the plan as needed.
|
||||
|
||||
## 7. Debugging
|
||||
- Use the `lsp_diagnostics` tool to check for any problems in the code.
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<system-reminder>
|
||||
Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supercedes any other instructions you have received (for example, to make edits).
|
||||
Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supersedes any other instructions you have received (for example, to make edits).
|
||||
</system-reminder>
|
||||
|
||||
@@ -10,7 +10,7 @@ You are generating titles for a coding assistant conversation.
|
||||
- Max 50 chars, single line
|
||||
- Focus on the specific action or question
|
||||
- Keep technical terms, numbers, and filenames exactly as written
|
||||
- Preserve HTTP status codes (401, 404, 500, etc) as numbers
|
||||
- Preserve HTTP status codes (401, 404, 500, etc.) as numbers
|
||||
- For file references, include the filename
|
||||
- Avoid filler words: the, this, my, a, an, properly
|
||||
- NEVER assume their tech stack or domain
|
||||
|
||||
@@ -64,12 +64,12 @@ export namespace ToolRegistry {
|
||||
}
|
||||
|
||||
export function enabled(_providerID: string, modelID: string): Record<string, boolean> {
|
||||
if (modelID.includes("claude")) {
|
||||
if (modelID.toLowerCase().includes("claude")) {
|
||||
return {
|
||||
patch: false,
|
||||
}
|
||||
}
|
||||
if (modelID.includes("qwen")) {
|
||||
if (modelID.toLowerCase().includes("qwen")) {
|
||||
return {
|
||||
patch: false,
|
||||
todowrite: false,
|
||||
|
||||
@@ -777,7 +777,7 @@ var seed = flag.Int64("seed", 0, "random seed (0 to autoselect)")
|
||||
// the seed flag was set.
|
||||
func genRandomData(logfn func(int64), length int) randTest {
|
||||
// We'll use a random source. However, we give the user the option
|
||||
// to override it to a specific value for reproduceability.
|
||||
// to override it to a specific value for reproducibility.
|
||||
s := *seed
|
||||
if s == 0 {
|
||||
s = time.Now().UnixNano()
|
||||
|
||||
@@ -206,7 +206,7 @@ func buildKeysTable(flags int, term string) map[string]Key {
|
||||
table["\x1bOc"] = Key{Code: KeyRight, Mod: ModCtrl}
|
||||
table["\x1bOd"] = Key{Code: KeyLeft, Mod: ModCtrl}
|
||||
//nolint:godox
|
||||
// TODO: invistigate if shift-ctrl arrow keys collide with DECCKM keys i.e.
|
||||
// TODO: investigate if shift-ctrl arrow keys collide with DECCKM keys i.e.
|
||||
// "\x1bOA", "\x1bOB", "\x1bOC", "\x1bOD"
|
||||
|
||||
// URxvt modifier CSI ~ keys
|
||||
|
||||
@@ -311,13 +311,13 @@ func read(t Format) (buf []byte, err error) {
|
||||
format = cFmtUnicodeText
|
||||
}
|
||||
|
||||
// check if clipboard is avaliable for the requested format
|
||||
// check if clipboard is available for the requested format
|
||||
r, _, err := isClipboardFormatAvailable.Call(format)
|
||||
if r == 0 {
|
||||
return nil, errUnavailable
|
||||
}
|
||||
|
||||
// try again until open clipboard successed
|
||||
// try again until open clipboard succeeds
|
||||
for {
|
||||
r, _, _ = openClipboard.Call()
|
||||
if r == 0 {
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
@@ -732,7 +731,7 @@ func (m *editorComponent) createAttachmentFromFile(filePath string) *attachment.
|
||||
ID: uuid.NewString(),
|
||||
Type: "file",
|
||||
Display: "@" + filePath,
|
||||
URL: fmt.Sprintf("file://./%s", filePath),
|
||||
URL: fmt.Sprintf("file://%s", absolutePath),
|
||||
Filename: filePath,
|
||||
MediaType: mediaType,
|
||||
Source: &attachment.FileSource{
|
||||
@@ -783,7 +782,7 @@ func (m *editorComponent) createAttachmentFromPath(filePath string) *attachment.
|
||||
ID: uuid.NewString(),
|
||||
Type: "file",
|
||||
Display: "@" + filePath,
|
||||
URL: fmt.Sprintf("file://./%s", url.PathEscape(filePath)),
|
||||
URL: fmt.Sprintf("file://%s", absolutePath),
|
||||
Filename: filePath,
|
||||
MediaType: mediaType,
|
||||
Source: &attachment.FileSource{
|
||||
|
||||
@@ -281,6 +281,9 @@ func (m *messagesComponent) renderView() tea.Cmd {
|
||||
if part.Synthetic {
|
||||
continue
|
||||
}
|
||||
if part.Text == "" {
|
||||
continue
|
||||
}
|
||||
remainingParts := message.Parts[partIndex+1:]
|
||||
fileParts := make([]opencode.FilePart, 0)
|
||||
for _, part := range remainingParts {
|
||||
@@ -365,6 +368,9 @@ func (m *messagesComponent) renderView() tea.Cmd {
|
||||
if reverted {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(part.Text) == "" {
|
||||
continue
|
||||
}
|
||||
hasTextPart = true
|
||||
finished := part.Time.End > 0
|
||||
remainingParts := message.Parts[partIndex+1:]
|
||||
|
||||
@@ -270,7 +270,7 @@ func (m Model) GetContent() string {
|
||||
return strings.Join(m.lines, "\n")
|
||||
}
|
||||
|
||||
// calculateLine taking soft wraping into account, returns the total viewable
|
||||
// calculateLine taking soft wrapping into account, returns the total viewable
|
||||
// lines and the real-line index for the given yoffset.
|
||||
func (m Model) calculateLine(yoffset int) (total, idx int) {
|
||||
if !m.SoftWrap {
|
||||
|
||||
@@ -68,6 +68,7 @@ export default defineConfig({
|
||||
"docs/modes",
|
||||
"docs/agents",
|
||||
"docs/rules",
|
||||
"docs/github",
|
||||
"docs/config",
|
||||
"docs/models",
|
||||
"docs/themes",
|
||||
|
||||
@@ -63,16 +63,19 @@ opencode comes with two built-in modes: _build_, the default with all tools enab
|
||||
|
||||
### Models
|
||||
|
||||
You can configure the providers and models you want to use in your opencode config through the `provider` and `model` options.
|
||||
You can configure the providers and models you want to use in your opencode config through the `provider`, `model` and `small_model` options.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"provider": {},
|
||||
"model": ""
|
||||
"model": "anthropic/claude-sonnet-4-20250514",
|
||||
"small_model": "anthropic/claude-3-5-haiku-20241022"
|
||||
}
|
||||
```
|
||||
|
||||
The `small_model` option configures a separate model for lightweight tasks like summarization and title generation. By default, opencode tries to use a cheaper model if one is available from your provider, otherwise it falls back to your main model.
|
||||
|
||||
You can also configure [local models](/docs/models#local). [Learn more](/docs/models).
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
---
|
||||
title: GitHub
|
||||
description: Use opencode in GitHub Issues and Pull-Requests
|
||||
---
|
||||
|
||||
opencode integrates directly into your GitHub workflow. Mention `/opencode` or `/oc` in your comment, and opencode will execute tasks within your GitHub Actions runner.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- **Triage Issues**: Ask opencode to look into an issue and explain it to you.
|
||||
- **Fix and Implement**: Ask opencode to fix an issue or implement a feature. And it will work in a new branch and submits a PR with all the changes.
|
||||
- **Secure**: opencode runs inside your GitHub's runners.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
Run the following command in the terminal from your GitHub repo:
|
||||
|
||||
```bash
|
||||
opencode github install
|
||||
```
|
||||
|
||||
This will walk you through installing the GitHub app, creating the workflow, and setting up secrets.
|
||||
|
||||
---
|
||||
|
||||
### Manual Setup
|
||||
|
||||
1. Install the GitHub app https://github.com/apps/opencode-agent. Make sure it is installed on the target repository.
|
||||
2. Add the following workflow file to `.github/workflows/opencode.yml` in your repo. Set the appropriate `model` and required API keys in `env`.
|
||||
|
||||
```yml
|
||||
name: opencode
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
opencode:
|
||||
if: |
|
||||
contains(github.event.comment.body, '/oc') ||
|
||||
contains(github.event.comment.body, '/opencode')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run opencode
|
||||
uses: sst/opencode/github@latest
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
with:
|
||||
model: anthropic/claude-sonnet-4-20250514
|
||||
# share: true
|
||||
```
|
||||
|
||||
3. Store the API keys in secrets. In your organization or project **settings**, expand **Secrets and variables** on the left and select **Actions**. Add the required API keys.
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
- `model`: The model used by opencode. Takes the format of `provider/model` (**required**)
|
||||
- `share`: Share the session. Sessions are shared by default for public repos.
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
- Explain an issue
|
||||
|
||||
Leave the following comment on a GitHub issue. `opencode` will read the entire thread, including all comments, and reply with a clear explanation.
|
||||
|
||||
```
|
||||
/opencode explain this issue
|
||||
```
|
||||
|
||||
- Fix an issue
|
||||
|
||||
Leave the following comment on a GitHub issue. opencode will create a new branch, implement the changes, and open a PR with the changes.
|
||||
|
||||
```
|
||||
/opencode fix this
|
||||
```
|
||||
|
||||
- Review PRs and make changes
|
||||
|
||||
Leave the following comment on a GitHub PR. opencode will implement the requested change and commit it to the same PR.
|
||||
|
||||
```
|
||||
Delete the attachment from S3 when the note is removed /oc
|
||||
```
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: IDE
|
||||
description: Using opencode with VS Code, Cursor, and other IDEs
|
||||
description: The opencode extension for VS Code, Cursor, and other IDEs
|
||||
---
|
||||
|
||||
opencode integrates with VS Code, Cursor, or any IDE that supports a terminal. Just run `opencode` in the terminal to get started.
|
||||
@@ -9,7 +9,8 @@ opencode integrates with VS Code, Cursor, or any IDE that supports a terminal. J
|
||||
|
||||
## Usage
|
||||
|
||||
- **Quick Launch**: Open opencode with `Cmd+Esc` (Mac) or `Ctrl+Esc` (Windows/Linux), or click the opencode button in the UI.
|
||||
- **Quick Launch**: Use `Cmd+Esc` (Mac) or `Ctrl+Esc` (Windows/Linux) to open opencode in a split terminal view, or focus an existing terminal session if one is already running.
|
||||
- **New Session**: Use `Cmd+Shift+Esc` (Mac) or `Ctrl+Shift+Esc` (Windows/Linux) to start a new opencode terminal session, even if one is already open. You can also click the opencode button in the UI.
|
||||
- **Context Awareness**: Automatically share your current selection or tab with opencode.
|
||||
- **File Reference Shortcuts**: Use `Cmd+Option+K` (Mac) or `Alt+Ctrl+K` (Linux/Windows) to insert file references. For example, `@File#L37-42`.
|
||||
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
# opencode VS Code Extension
|
||||
|
||||
A VS Code extension that integrates [opencode](https://opencode.ai) directly into your development environment.
|
||||
A Visual Studio Code extension that integrates [opencode](https://opencode.ai) directly into your development workflow.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
This extension requires [opencode](https://opencode.ai) to be installed on your system. Visit [opencode.ai](https://opencode.ai) for installation instructions.
|
||||
This extension requires the [opencode CLI](https://opencode.ai) to be installed on your system. Visit [opencode.ai](https://opencode.ai) for installation instructions.
|
||||
|
||||
## Features
|
||||
|
||||
- **Cmd+Escape**: Launch opencode in a split terminal view
|
||||
- **Alt+Cmd+K**: Send selected code to opencode's prompt
|
||||
- **Tab awareness**: opencode automatically detects which files you have open
|
||||
- **Quick Launch**: Use `Cmd+Esc` (Mac) or `Ctrl+Esc` (Windows/Linux) to open opencode in a split terminal view, or focus an existing terminal session if one is already running.
|
||||
- **New Session**: Use `Cmd+Shift+Esc` (Mac) or `Ctrl+Shift+Esc` (Windows/Linux) to start a new opencode terminal session, even if one is already open. You can also click the opencode button in the UI.
|
||||
- **Context Awareness**: Automatically share your current selection or tab with opencode.
|
||||
- **File Reference Shortcuts**: Use `Cmd+Option+K` (Mac) or `Alt+Ctrl+K` (Linux/Windows) to insert file references. For example, `@File#L37-42`.
|
||||
|
||||
## Support
|
||||
|
||||
|
||||
@@ -26,7 +26,15 @@
|
||||
"commands": [
|
||||
{
|
||||
"command": "opencode.openTerminal",
|
||||
"title": "Open Terminal with Opencode",
|
||||
"title": "Open opencode",
|
||||
"icon": {
|
||||
"light": "images/button-dark.svg",
|
||||
"dark": "images/button-light.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "opencode.openNewTerminal",
|
||||
"title": "Open opencode in new tab",
|
||||
"icon": {
|
||||
"light": "images/button-dark.svg",
|
||||
"dark": "images/button-light.svg"
|
||||
@@ -40,8 +48,7 @@
|
||||
"menus": {
|
||||
"editor/title": [
|
||||
{
|
||||
"command": "opencode.openTerminal",
|
||||
"when": "editorTextFocus",
|
||||
"command": "opencode.openNewTerminal",
|
||||
"group": "navigation"
|
||||
}
|
||||
]
|
||||
@@ -55,6 +62,14 @@
|
||||
"win": "ctrl+escape",
|
||||
"linux": "ctrl+escape"
|
||||
},
|
||||
{
|
||||
"command": "opencode.openNewTerminal",
|
||||
"title": "Run opencode",
|
||||
"key": "cmd+shift+escape",
|
||||
"mac": "cmd+shift+escape",
|
||||
"win": "ctrl+shift+escape",
|
||||
"linux": "ctrl+shift+escape"
|
||||
},
|
||||
{
|
||||
"command": "opencode.addFilepathToTerminal",
|
||||
"title": "opencode: Insert At-Mentioned",
|
||||
|
||||
@@ -3,11 +3,42 @@ export function deactivate() {}
|
||||
|
||||
import * as vscode from "vscode"
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
const TERMINAL_NAME = "opencode"
|
||||
const TERMINAL_NAME = "opencode"
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
let openNewTerminalDisposable = vscode.commands.registerCommand("opencode.openNewTerminal", async () => {
|
||||
await openTerminal()
|
||||
})
|
||||
|
||||
// Register command to open terminal in split screen and run opencode
|
||||
let openTerminalDisposable = vscode.commands.registerCommand("opencode.openTerminal", async () => {
|
||||
// An opencode terminal already exists => focus it
|
||||
const existingTerminal = vscode.window.terminals.find((t) => t.name === TERMINAL_NAME)
|
||||
if (existingTerminal) {
|
||||
existingTerminal.show()
|
||||
return
|
||||
}
|
||||
|
||||
await openTerminal()
|
||||
})
|
||||
|
||||
let addFilepathDisposable = vscode.commands.registerCommand("opencode.addFilepathToTerminal", async () => {
|
||||
const fileRef = getActiveFile()
|
||||
if (!fileRef) return
|
||||
|
||||
const terminal = vscode.window.activeTerminal
|
||||
if (!terminal) return
|
||||
|
||||
if (terminal.name === TERMINAL_NAME) {
|
||||
// @ts-ignore
|
||||
const port = terminal.creationOptions.env?.["_EXTENSION_OPENCODE_PORT"]
|
||||
port ? await appendPrompt(parseInt(port), fileRef) : terminal.sendText(fileRef)
|
||||
terminal.show()
|
||||
}
|
||||
})
|
||||
|
||||
context.subscriptions.push(openTerminalDisposable, addFilepathDisposable)
|
||||
|
||||
async function openTerminal() {
|
||||
// Create a new terminal in split screen
|
||||
const port = Math.floor(Math.random() * (65535 - 16384 + 1)) + 16384
|
||||
const terminal = vscode.window.createTerminal({
|
||||
@@ -50,64 +81,46 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
await appendPrompt(port, `In ${fileRef}`)
|
||||
terminal.show()
|
||||
}
|
||||
})
|
||||
|
||||
// Register command to add filepath to terminal
|
||||
let addFilepathDisposable = vscode.commands.registerCommand("opencode.addFilepathToTerminal", async () => {
|
||||
const fileRef = getActiveFile()
|
||||
if (!fileRef) return
|
||||
|
||||
const terminal = vscode.window.activeTerminal
|
||||
if (!terminal) return
|
||||
|
||||
if (terminal.name === TERMINAL_NAME) {
|
||||
// @ts-ignore
|
||||
const port = terminal.creationOptions.env?.["_EXTENSION_OPENCODE_PORT"]
|
||||
port ? await appendPrompt(parseInt(port), fileRef) : terminal.sendText(fileRef)
|
||||
terminal.show()
|
||||
}
|
||||
})
|
||||
|
||||
context.subscriptions.push(openTerminalDisposable, addFilepathDisposable)
|
||||
}
|
||||
|
||||
async function appendPrompt(port: number, text: string) {
|
||||
await fetch(`http://localhost:${port}/tui/append-prompt`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ text }),
|
||||
})
|
||||
}
|
||||
|
||||
function getActiveFile() {
|
||||
const activeEditor = vscode.window.activeTextEditor
|
||||
if (!activeEditor) return
|
||||
|
||||
const document = activeEditor.document
|
||||
const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri)
|
||||
if (!workspaceFolder) return
|
||||
|
||||
// Get the relative path from workspace root
|
||||
const relativePath = vscode.workspace.asRelativePath(document.uri)
|
||||
let filepathWithAt = `@${relativePath}`
|
||||
|
||||
// Check if there's a selection and add line numbers
|
||||
const selection = activeEditor.selection
|
||||
if (!selection.isEmpty) {
|
||||
// Convert to 1-based line numbers
|
||||
const startLine = selection.start.line + 1
|
||||
const endLine = selection.end.line + 1
|
||||
|
||||
if (startLine === endLine) {
|
||||
// Single line selection
|
||||
filepathWithAt += `#L${startLine}`
|
||||
} else {
|
||||
// Multi-line selection
|
||||
filepathWithAt += `#L${startLine}-${endLine}`
|
||||
}
|
||||
}
|
||||
|
||||
return filepathWithAt
|
||||
async function appendPrompt(port: number, text: string) {
|
||||
await fetch(`http://localhost:${port}/tui/append-prompt`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ text }),
|
||||
})
|
||||
}
|
||||
|
||||
function getActiveFile() {
|
||||
const activeEditor = vscode.window.activeTextEditor
|
||||
if (!activeEditor) return
|
||||
|
||||
const document = activeEditor.document
|
||||
const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri)
|
||||
if (!workspaceFolder) return
|
||||
|
||||
// Get the relative path from workspace root
|
||||
const relativePath = vscode.workspace.asRelativePath(document.uri)
|
||||
let filepathWithAt = `@${relativePath}`
|
||||
|
||||
// Check if there's a selection and add line numbers
|
||||
const selection = activeEditor.selection
|
||||
if (!selection.isEmpty) {
|
||||
// Convert to 1-based line numbers
|
||||
const startLine = selection.start.line + 1
|
||||
const endLine = selection.end.line + 1
|
||||
|
||||
if (startLine === endLine) {
|
||||
// Single line selection
|
||||
filepathWithAt += `#L${startLine}`
|
||||
} else {
|
||||
// Multi-line selection
|
||||
filepathWithAt += `#L${startLine}-${endLine}`
|
||||
}
|
||||
}
|
||||
|
||||
return filepathWithAt
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user