Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ffc231bd8b | ||
|
|
3cf4ef56fb | ||
|
|
c738e26438 | ||
|
|
9c6aa82ac1 | ||
|
|
ef74d97491 | ||
|
|
af892e5432 | ||
|
|
d7aca6230d | ||
|
|
0f9c2c5c27 | ||
|
|
6a261dedb4 | ||
|
|
ec928d88b5 | ||
|
|
59a5f120c0 | ||
|
|
ce07f80b19 | ||
|
|
168fd9b2e3 | ||
|
|
df13b155f9 | ||
|
|
eeed5b8718 | ||
|
|
148ef90210 | ||
|
|
67023bb007 | ||
|
|
a316aed4fe | ||
|
|
9f7c0bd599 | ||
|
|
c7e1068f90 | ||
|
|
e2052d790b | ||
|
|
d3b2763c14 | ||
|
|
c6492de7ac |
@@ -1,37 +0,0 @@
|
|||||||
name: build
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- dev
|
|
||||||
|
|
||||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
packages: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- run: git fetch --force --tags
|
|
||||||
|
|
||||||
- uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version: ">=1.23.2"
|
|
||||||
cache: true
|
|
||||||
cache-dependency-path: go.sum
|
|
||||||
|
|
||||||
- run: go mod download
|
|
||||||
|
|
||||||
- uses: goreleaser/goreleaser-action@v6
|
|
||||||
with:
|
|
||||||
distribution: goreleaser
|
|
||||||
version: latest
|
|
||||||
args: build --snapshot --clean
|
|
||||||
@@ -4,7 +4,7 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- dontlook
|
- dev
|
||||||
tags:
|
tags:
|
||||||
- "*"
|
- "*"
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,92 @@
|
|||||||
|
[](https://github.com/sst/opencode)
|
||||||
|
|
||||||
|
AI coding agent, built for the terminal.
|
||||||
|
|
||||||
|
⚠️ **Note:** version 0.1.x is a full rewrite and we do not have proper documentation for it yet. Should have this out week of June 17th 2025 📚
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# YOLO
|
||||||
|
curl -fsSL https://opencode.ai/install | bash
|
||||||
|
|
||||||
|
# Package managers
|
||||||
|
npm i -g opencode-ai@latest # or bun/pnpm/yarn
|
||||||
|
brew install sst/tap/opencode # macOS
|
||||||
|
paru -S opencode-bin # Arch Linux
|
||||||
```
|
```
|
||||||
█▀▀█ █▀▀█ █▀▀ █▀▀▄ █▀▀ █▀▀█ █▀▀▄ █▀▀
|
|
||||||
█░░█ █░░█ █▀▀ █░░█ █░░ █░░█ █░░█ █▀▀
|
> **Note:** Remove previous versions < 0.1.x first if installed
|
||||||
▀▀▀▀ █▀▀▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀▀
|
|
||||||
|
### Providers
|
||||||
|
|
||||||
|
The recommended approach is to sign up for claude pro or max and do `opencode auth login` and select Anthropic. It is the most cost effective way to use this tool.
|
||||||
|
|
||||||
|
Additionally opencode is powered by the provider list at [models.dev](https://models.dev) so you can use `opencode auth login` to configure api keys for any provider you'd like to use. This is stored in `~/.local/share/opencode/auth.json`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ opencode auth login
|
||||||
|
|
||||||
|
┌ Add credential
|
||||||
|
│
|
||||||
|
◆ Select provider
|
||||||
|
│ ● Anthropic (recommended)
|
||||||
|
│ ○ OpenAI
|
||||||
|
│ ○ Google
|
||||||
|
│ ○ Amazon Bedrock
|
||||||
|
│ ○ Azure
|
||||||
|
│ ○ DeepSeek
|
||||||
|
│ ○ Groq
|
||||||
|
│ ...
|
||||||
|
└
|
||||||
|
```
|
||||||
|
|
||||||
|
The models.dev dataset is also used to detect common environment variables like `OPENAI_API_KEY` to autoload that provider.
|
||||||
|
|
||||||
|
If there are additional providers you want to use you can submit a PR to the [models.dev repo](https://github.com/sst/models.dev). If configuring just for yourself check out the Config section below
|
||||||
|
|
||||||
|
### Project Config
|
||||||
|
|
||||||
|
Project configuration is optional. You can place an `opencode.json` file in the root of your repo and it will be loaded.
|
||||||
|
|
||||||
|
```json title="opencode.json"
|
||||||
|
{
|
||||||
|
"$schema": "http://opencode.ai/config.json"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### MCP
|
||||||
|
|
||||||
|
```json title="opencode.json"
|
||||||
|
{
|
||||||
|
"$schema": "http://opencode.ai/config.json",
|
||||||
|
"mcp": {
|
||||||
|
"localmcp": {
|
||||||
|
"type": "local",
|
||||||
|
"command": ["bun", "x", "my-mcp-command"],
|
||||||
|
"environment": {
|
||||||
|
"MY_ENV_VAR": "my_env_var_value"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"remotemcp": {
|
||||||
|
"type": "remote",
|
||||||
|
"url": "https://my-mcp-server.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Contributing
|
||||||
|
|
||||||
|
To run opencode locally you need
|
||||||
|
|
||||||
|
- bun
|
||||||
|
- golang 1.24.x
|
||||||
|
|
||||||
|
To run
|
||||||
|
|
||||||
|
```
|
||||||
|
$ bun install
|
||||||
|
$ cd packages/opencode
|
||||||
|
$ bun run src/index.ts
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
"@types/turndown": "5.0.5",
|
"@types/turndown": "5.0.5",
|
||||||
"@types/yargs": "17.0.33",
|
"@types/yargs": "17.0.33",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
|
"zod-to-json-schema": "3.24.5",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
|
|||||||
@@ -12,23 +12,28 @@ requested_version=${VERSION:-}
|
|||||||
|
|
||||||
os=$(uname -s | tr '[:upper:]' '[:lower:]')
|
os=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||||
if [[ "$os" == "darwin" ]]; then
|
if [[ "$os" == "darwin" ]]; then
|
||||||
os="mac"
|
os="darwin"
|
||||||
fi
|
fi
|
||||||
arch=$(uname -m)
|
arch=$(uname -m)
|
||||||
|
|
||||||
if [[ "$arch" == "aarch64" ]]; then
|
if [[ "$arch" == "aarch64" ]]; then
|
||||||
arch="arm64"
|
arch="arm64"
|
||||||
|
elif [[ "$arch" == "x86_64" ]]; then
|
||||||
|
arch="x64"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
filename="$APP-$os-$arch.tar.gz"
|
filename="$APP-$os-$arch.zip"
|
||||||
|
|
||||||
|
|
||||||
case "$filename" in
|
case "$filename" in
|
||||||
*"-linux-"*)
|
*"-linux-"*)
|
||||||
[[ "$arch" == "x86_64" || "$arch" == "arm64" || "$arch" == "i386" ]] || exit 1
|
[[ "$arch" == "x64" || "$arch" == "arm64" ]] || exit 1
|
||||||
;;
|
;;
|
||||||
*"-mac-"*)
|
*"-darwin-"*)
|
||||||
[[ "$arch" == "x86_64" || "$arch" == "arm64" ]] || exit 1
|
[[ "$arch" == "x64" || "$arch" == "arm64" ]] || exit 1
|
||||||
|
;;
|
||||||
|
*"-windows-"*)
|
||||||
|
[[ "$arch" == "x64" ]] || exit 1
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "${RED}Unsupported OS/Arch: $os/$arch${NC}"
|
echo "${RED}Unsupported OS/Arch: $os/$arch${NC}"
|
||||||
@@ -88,8 +93,9 @@ check_version() {
|
|||||||
download_and_install() {
|
download_and_install() {
|
||||||
print_message info "Downloading ${ORANGE}opencode ${GREEN}version: ${YELLOW}$specific_version ${GREEN}..."
|
print_message info "Downloading ${ORANGE}opencode ${GREEN}version: ${YELLOW}$specific_version ${GREEN}..."
|
||||||
mkdir -p opencodetmp && cd opencodetmp
|
mkdir -p opencodetmp && cd opencodetmp
|
||||||
curl -# -L $url | tar xz
|
curl -# -L -o "$filename" "$url"
|
||||||
mv opencode $INSTALL_DIR
|
unzip -q "$filename"
|
||||||
|
mv opencode "$INSTALL_DIR"
|
||||||
cd .. && rm -rf opencodetmp
|
cd .. && rm -rf opencodetmp
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +107,9 @@ add_to_path() {
|
|||||||
local config_file=$1
|
local config_file=$1
|
||||||
local command=$2
|
local command=$2
|
||||||
|
|
||||||
if [[ -w $config_file ]]; then
|
if grep -Fxq "$command" "$config_file"; then
|
||||||
|
print_message info "Command already exists in $config_file, skipping write."
|
||||||
|
elif [[ -w $config_file ]]; then
|
||||||
echo -e "\n# opencode" >> "$config_file"
|
echo -e "\n# opencode" >> "$config_file"
|
||||||
echo "$command" >> "$config_file"
|
echo "$command" >> "$config_file"
|
||||||
print_message info "Successfully added ${ORANGE}opencode ${GREEN}to \$PATH in $config_file"
|
print_message info "Successfully added ${ORANGE}opencode ${GREEN}to \$PATH in $config_file"
|
||||||
@@ -167,6 +175,7 @@ if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
|
|||||||
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
|
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
|
export PATH=$INSTALL_DIR:$PATH
|
||||||
print_message warning "Manually add the directory to $config_file (or similar):"
|
print_message warning "Manually add the directory to $config_file (or similar):"
|
||||||
print_message info " export PATH=$INSTALL_DIR:\$PATH"
|
print_message info " export PATH=$INSTALL_DIR:\$PATH"
|
||||||
;;
|
;;
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"$schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"provider": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"models": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"attachment": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"reasoning": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"temperature": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"cost": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"input": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"output": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"inputCached": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"outputCached": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"input",
|
||||||
|
"output",
|
||||||
|
"inputCached",
|
||||||
|
"outputCached"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"limit": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"context": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"output": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"context",
|
||||||
|
"output"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"models"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mcp": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "local"
|
||||||
|
},
|
||||||
|
"command": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"environment": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"type",
|
||||||
|
"command"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "remote"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"type",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||||
|
}
|
||||||
@@ -5,7 +5,8 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit",
|
||||||
|
"dev": "bun run ./src/index.ts"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
"./*": [
|
"./*": [
|
||||||
@@ -18,7 +19,8 @@
|
|||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"@types/turndown": "5.0.5",
|
"@types/turndown": "5.0.5",
|
||||||
"@types/yargs": "17.0.33",
|
"@types/yargs": "17.0.33",
|
||||||
"typescript": "catalog:"
|
"typescript": "catalog:",
|
||||||
|
"zod-to-json-schema": "3.24.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/prompts": "0.11.0",
|
"@clack/prompts": "0.11.0",
|
||||||
|
|||||||
@@ -110,7 +110,8 @@ if (!snapshot) {
|
|||||||
return (
|
return (
|
||||||
!lower.includes("chore:") &&
|
!lower.includes("chore:") &&
|
||||||
!lower.includes("ci:") &&
|
!lower.includes("ci:") &&
|
||||||
!lower.includes("docs:")
|
!lower.includes("docs:") &&
|
||||||
|
!lower.includes("doc:")
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.join("\n")
|
.join("\n")
|
||||||
|
|||||||
Executable
+8
@@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
import "zod-openapi/extend"
|
||||||
|
import { Config } from "../src/config/config"
|
||||||
|
import { zodToJsonSchema } from "zod-to-json-schema"
|
||||||
|
|
||||||
|
const result = zodToJsonSchema(Config.Info)
|
||||||
|
await Bun.write("config.schema.json", JSON.stringify(result, null, 2))
|
||||||
@@ -2,6 +2,7 @@ import { Server } from "../../server/server"
|
|||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import type { CommandModule } from "yargs"
|
import type { CommandModule } from "yargs"
|
||||||
|
import { Config } from "../../config/config"
|
||||||
|
|
||||||
export const GenerateCommand = {
|
export const GenerateCommand = {
|
||||||
command: "generate",
|
command: "generate",
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export namespace Config {
|
|||||||
|
|
||||||
export const Info = z
|
export const Info = z
|
||||||
.object({
|
.object({
|
||||||
|
$schema: z.string().optional(),
|
||||||
provider: z
|
provider: z
|
||||||
.record(
|
.record(
|
||||||
ModelsDev.Provider.partial().extend({
|
ModelsDev.Provider.partial().extend({
|
||||||
@@ -58,11 +59,6 @@ export namespace Config {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
tool: z
|
|
||||||
.object({
|
|
||||||
provider: z.record(z.string(), z.string().array()).optional(),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
mcp: z.record(z.string(), Mcp).optional(),
|
mcp: z.record(z.string(), Mcp).optional(),
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
|
|||||||
+11
-11
@@ -1,4 +1,3 @@
|
|||||||
import { App } from "../app/app"
|
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
@@ -6,6 +5,7 @@ import { z } from "zod"
|
|||||||
import { NamedError } from "../util/error"
|
import { NamedError } from "../util/error"
|
||||||
import { lazy } from "../util/lazy"
|
import { lazy } from "../util/lazy"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
|
import { $ } from "bun"
|
||||||
|
|
||||||
export namespace Fzf {
|
export namespace Fzf {
|
||||||
const log = Log.create({ service: "fzf" })
|
const log = Log.create({ service: "fzf" })
|
||||||
@@ -117,18 +117,18 @@ export namespace Fzf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function search(cwd: string, query: string) {
|
export async function search(cwd: string, query: string) {
|
||||||
const process = Bun.spawn({
|
const results = await $`${await filepath()} --filter ${query}`
|
||||||
cwd,
|
.quiet()
|
||||||
stdin: "inherit",
|
.throws(false)
|
||||||
stdout: "pipe",
|
.cwd(cwd)
|
||||||
stderr: "pipe",
|
.text()
|
||||||
cmd: [await filepath(), "--filter", query],
|
const split = results
|
||||||
})
|
|
||||||
await process.exited
|
|
||||||
const stdout = await Bun.readableStreamToText(process.stdout)
|
|
||||||
return stdout
|
|
||||||
.trim()
|
.trim()
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.filter((line) => line.length > 0)
|
.filter((line) => line.length > 0)
|
||||||
|
log.info("results", {
|
||||||
|
count: split.length,
|
||||||
|
})
|
||||||
|
return split
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export namespace Flag {
|
||||||
|
export const OPENCODE_AUTO_SHARE = truthy("OPENCODE_AUTO_SHARE")
|
||||||
|
|
||||||
|
function truthy(key: string) {
|
||||||
|
const value = process.env[key]?.toLowerCase()
|
||||||
|
return value === "true" || value === "1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -72,7 +72,7 @@ const cli = yargs(hideBin(process.argv))
|
|||||||
cmd = [binary]
|
cmd = [binary]
|
||||||
}
|
}
|
||||||
const proc = Bun.spawn({
|
const proc = Bun.spawn({
|
||||||
cmd,
|
cmd: [...cmd, ...process.argv.slice(2)],
|
||||||
cwd,
|
cwd,
|
||||||
stdout: "inherit",
|
stdout: "inherit",
|
||||||
stderr: "inherit",
|
stderr: "inherit",
|
||||||
|
|||||||
@@ -289,11 +289,13 @@ export namespace Provider {
|
|||||||
google: TOOLS,
|
google: TOOLS,
|
||||||
}
|
}
|
||||||
export async function tools(providerID: string) {
|
export async function tools(providerID: string) {
|
||||||
|
/*
|
||||||
const cfg = await Config.get()
|
const cfg = await Config.get()
|
||||||
if (cfg.tool?.provider?.[providerID])
|
if (cfg.tool?.provider?.[providerID])
|
||||||
return cfg.tool.provider[providerID].map(
|
return cfg.tool.provider[providerID].map(
|
||||||
(id) => TOOLS.find((t) => t.id === id)!,
|
(id) => TOOLS.find((t) => t.id === id)!,
|
||||||
)
|
)
|
||||||
|
*/
|
||||||
return TOOL_MAPPING[providerID] ?? TOOLS
|
return TOOL_MAPPING[providerID] ?? TOOLS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,12 +56,16 @@ export namespace Server {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.use((c, next) => {
|
.use(async (c, next) => {
|
||||||
log.info("request", {
|
log.info("request", {
|
||||||
method: c.req.method,
|
method: c.req.method,
|
||||||
path: c.req.path,
|
path: c.req.path,
|
||||||
})
|
})
|
||||||
return next()
|
const start = Date.now()
|
||||||
|
await next()
|
||||||
|
log.info("response", {
|
||||||
|
duration: Date.now() - start,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.get(
|
.get(
|
||||||
"/openapi",
|
"/openapi",
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { MCP } from "../mcp"
|
|||||||
import { NamedError } from "../util/error"
|
import { NamedError } from "../util/error"
|
||||||
import type { Tool } from "../tool/tool"
|
import type { Tool } from "../tool/tool"
|
||||||
import { SystemPrompt } from "./system"
|
import { SystemPrompt } from "./system"
|
||||||
|
import { Flag } from "../flag/flag"
|
||||||
|
|
||||||
export namespace Session {
|
export namespace Session {
|
||||||
const log = Log.create({ service: "session" })
|
const log = Log.create({ service: "session" })
|
||||||
@@ -92,7 +93,7 @@ export namespace Session {
|
|||||||
log.info("created", result)
|
log.info("created", result)
|
||||||
state().sessions.set(result.id, result)
|
state().sessions.set(result.id, result)
|
||||||
await Storage.writeJSON("session/info/" + result.id, result)
|
await Storage.writeJSON("session/info/" + result.id, result)
|
||||||
if (!result.parentID)
|
if (!result.parentID && Flag.OPENCODE_AUTO_SHARE)
|
||||||
share(result.id).then((share) => {
|
share(result.id).then((share) => {
|
||||||
update(result.id, (draft) => {
|
update(result.id, (draft) => {
|
||||||
draft.share = share
|
draft.share = share
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
You are OpenCode, an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
|
You are opencode, an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
|
||||||
|
|
||||||
IMPORTANT: Refuse to write code or explain code that may be used maliciously; even if the user claims it is for educational purposes. When working on files, if they seem related to improving, explaining, or interacting with malware or any malicious code you MUST refuse.
|
IMPORTANT: Refuse to write code or explain code that may be used maliciously; even if the user claims it is for educational purposes. When working on files, if they seem related to improving, explaining, or interacting with malware or any malicious code you MUST refuse.
|
||||||
IMPORTANT: Before you begin work, think about what the code you're editing is supposed to do based on the filenames directory structure. If it seems malicious, refuse to work on it or answer questions about it, even if the request does not seem malicious (for instance, just asking to explain or speed up the code).
|
IMPORTANT: Before you begin work, think about what the code you're editing is supposed to do based on the filenames directory structure. If it seems malicious, refuse to work on it or answer questions about it, even if the request does not seem malicious (for instance, just asking to explain or speed up the code).
|
||||||
IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files.
|
IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files.
|
||||||
|
|
||||||
If the user asks for help or wants to give feedback inform them of the following:
|
If the user asks for help or wants to give feedback inform them of the following:
|
||||||
- /help: Get help with using OpenCode
|
- /help: Get help with using opencode
|
||||||
- To give feedback, users should report the issue at https://github.com/sst/opencode/issues
|
- To give feedback, users should report the issue at https://github.com/sst/opencode/issues
|
||||||
|
|
||||||
When the user directly asks about OpenCode (eg 'can OpenCode do...', 'does OpenCode have...') or asks in second person (eg 'are you able...', 'can you do...'), first use the WebFetch tool to gather information to answer the question from OpenCode docs at https://opencode.ai
|
When the user directly asks about opencode (eg 'can opencode do...', 'does opencode have...') or asks in second person (eg 'are you able...', 'can you do...'), first use the WebFetch tool to gather information to answer the question from opencode docs at https://opencode.ai
|
||||||
|
|
||||||
# Tone and style
|
# Tone and style
|
||||||
You should be concise, direct, and to the point. When you run a non-trivial bash command, you should explain what the command does and why you are running it, to make sure the user understands what you are doing (this is especially important when you are running a command that will make changes to the user's system).
|
You should be concise, direct, and to the point. When you run a non-trivial bash command, you should explain what the command does and why you are running it, to make sure the user understands what you are doing (this is especially important when you are running a command that will make changes to the user's system).
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ Usage notes:
|
|||||||
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
|
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
|
||||||
- If the output exceeds 30000 characters, output will be truncated before being returned to you.
|
- If the output exceeds 30000 characters, output will be truncated before being returned to you.
|
||||||
- VERY IMPORTANT: You MUST avoid using search commands like `find` and `grep`. Instead use Grep, Glob, or Task to search. You MUST avoid read tools like `cat`, `head`, `tail`, and `ls`, and use Read and LS to read files.
|
- VERY IMPORTANT: You MUST avoid using search commands like `find` and `grep`. Instead use Grep, Glob, or Task to search. You MUST avoid read tools like `cat`, `head`, `tail`, and `ls`, and use Read and LS to read files.
|
||||||
- If you _still_ need to run `grep`, STOP. ALWAYS USE ripgrep at `rg` (or /usr/bin/rg) first, which all OpenCode users have pre-installed.
|
- If you _still_ need to run `grep`, STOP. ALWAYS USE ripgrep at `rg` (or /usr/bin/rg) first, which all opencode users have pre-installed.
|
||||||
- When issuing multiple commands, use the ';' or '&&' operator to separate them. DO NOT use newlines (newlines are ok in quoted strings).
|
- When issuing multiple commands, use the ';' or '&&' operator to separate them. DO NOT use newlines (newlines are ok in quoted strings).
|
||||||
- Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of `cd`. You may use `cd` if the User explicitly requests it.
|
- Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of `cd`. You may use `cd` if the User explicitly requests it.
|
||||||
<good-example>
|
<good-example>
|
||||||
@@ -60,9 +60,9 @@ When the user asks you to create a new git commit, follow these steps carefully:
|
|||||||
3. You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance. ALWAYS run the following commands in parallel:
|
3. You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance. ALWAYS run the following commands in parallel:
|
||||||
- Add relevant untracked files to the staging area.
|
- Add relevant untracked files to the staging area.
|
||||||
- Create the commit with a message ending with:
|
- Create the commit with a message ending with:
|
||||||
🤖 Generated with [OpenCode](https://opencode.ai)
|
🤖 Generated with [opencode](https://opencode.ai)
|
||||||
|
|
||||||
Co-Authored-By: OpenCode <noreply@opencode.ai>
|
Co-Authored-By: opencode <noreply@opencode.ai>
|
||||||
- Run git status to make sure the commit succeeded.
|
- Run git status to make sure the commit succeeded.
|
||||||
|
|
||||||
4. If the commit fails due to pre-commit hook changes, retry the commit ONCE to include these automated changes. If it fails again, it usually means a pre-commit hook is preventing the commit. If the commit succeeds but you notice that files were modified by the pre-commit hook, you MUST amend your commit to include them.
|
4. If the commit fails due to pre-commit hook changes, retry the commit ONCE to include these automated changes. If it fails again, it usually means a pre-commit hook is preventing the commit. If the commit succeeds but you notice that files were modified by the pre-commit hook, you MUST amend your commit to include them.
|
||||||
@@ -81,9 +81,9 @@ Important notes:
|
|||||||
git commit -m "$(cat <<'EOF'
|
git commit -m "$(cat <<'EOF'
|
||||||
Commit message here.
|
Commit message here.
|
||||||
|
|
||||||
🤖 Generated with [OpenCode](https://opencode.ai)
|
🤖 Generated with [opencode](https://opencode.ai)
|
||||||
|
|
||||||
Co-Authored-By: OpenCode <noreply@opencode.ai>
|
Co-Authored-By: opencode <noreply@opencode.ai>
|
||||||
EOF
|
EOF
|
||||||
)"
|
)"
|
||||||
</example>
|
</example>
|
||||||
@@ -128,7 +128,7 @@ gh pr create --title "the pr title" --body "$(cat <<'EOF'
|
|||||||
## Test plan
|
## Test plan
|
||||||
[Checklist of TODOs for testing the pull request...]
|
[Checklist of TODOs for testing the pull request...]
|
||||||
|
|
||||||
🤖 Generated with [OpenCode](https://opencode.ai)
|
🤖 Generated with [opencode](https://opencode.ai)
|
||||||
EOF
|
EOF
|
||||||
)"
|
)"
|
||||||
</example>
|
</example>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export const GlobTool = Tool.define({
|
|||||||
const glob = new Bun.Glob(params.pattern)
|
const glob = new Bun.Glob(params.pattern)
|
||||||
const files = []
|
const files = []
|
||||||
let truncated = false
|
let truncated = false
|
||||||
for await (const file of glob.scan({ cwd: search })) {
|
for await (const file of glob.scan({ cwd: search, dot: true })) {
|
||||||
if (files.length >= limit) {
|
if (files.length >= limit) {
|
||||||
truncated = true
|
truncated = true
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export const ListTool = Tool.define({
|
|||||||
const glob = new Bun.Glob("**/*")
|
const glob = new Bun.Glob("**/*")
|
||||||
const files = []
|
const files = []
|
||||||
|
|
||||||
for await (const file of glob.scan({ cwd: searchPath })) {
|
for await (const file of glob.scan({ cwd: searchPath, dot: true })) {
|
||||||
if (file.startsWith(".") || IGNORE_PATTERNS.some((p) => file.includes(p)))
|
if (file.startsWith(".") || IGNORE_PATTERNS.some((p) => file.includes(p)))
|
||||||
continue
|
continue
|
||||||
if (params.ignore?.some((pattern) => new Bun.Glob(pattern).match(file)))
|
if (params.ignore?.some((pattern) => new Bun.Glob(pattern).match(file)))
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import path from "path"
|
|||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
export namespace Log {
|
export namespace Log {
|
||||||
export const Default = create()
|
export const Default = create({ service: "default" })
|
||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
print: boolean
|
print: boolean
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package completions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||||
|
"github.com/sst/opencode/internal/app"
|
||||||
|
"github.com/sst/opencode/internal/components/dialog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CommandCompletionProvider struct {
|
||||||
|
app *app.App
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCommandCompletionProvider(app *app.App) dialog.CompletionProvider {
|
||||||
|
return &CommandCompletionProvider{app: app}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CommandCompletionProvider) GetId() string {
|
||||||
|
return "commands"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CommandCompletionProvider) GetEntry() dialog.CompletionItemI {
|
||||||
|
return dialog.NewCompletionItem(dialog.CompletionItem{
|
||||||
|
Title: "Commands",
|
||||||
|
Value: "commands",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CommandCompletionProvider) GetChildEntries(query string) ([]dialog.CompletionItemI, error) {
|
||||||
|
if query == "" {
|
||||||
|
// If no query, return all commands
|
||||||
|
items := []dialog.CompletionItemI{}
|
||||||
|
for _, cmd := range c.app.Commands {
|
||||||
|
items = append(items, dialog.NewCompletionItem(dialog.CompletionItem{
|
||||||
|
Title: " /" + cmd.Name,
|
||||||
|
Value: "/" + cmd.Name,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use fuzzy matching for commands
|
||||||
|
var commandNames []string
|
||||||
|
commandMap := make(map[string]dialog.CompletionItemI)
|
||||||
|
|
||||||
|
for _, cmd := range c.app.Commands {
|
||||||
|
commandNames = append(commandNames, cmd.Name)
|
||||||
|
commandMap[cmd.Name] = dialog.NewCompletionItem(dialog.CompletionItem{
|
||||||
|
Title: " /" + cmd.Name,
|
||||||
|
Value: "/" + cmd.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find fuzzy matches
|
||||||
|
matches := fuzzy.RankFind(query, commandNames)
|
||||||
|
|
||||||
|
// Sort by score (best matches first)
|
||||||
|
sort.Sort(matches)
|
||||||
|
|
||||||
|
// Convert matches to completion items
|
||||||
|
items := []dialog.CompletionItemI{}
|
||||||
|
for _, match := range matches {
|
||||||
|
if item, ok := commandMap[match.Target]; ok {
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
package completions
|
package completions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/sst/opencode/internal/app"
|
||||||
"github.com/sst/opencode/internal/components/dialog"
|
"github.com/sst/opencode/internal/components/dialog"
|
||||||
|
"github.com/sst/opencode/pkg/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
type filesAndFoldersContextGroup struct {
|
type filesAndFoldersContextGroup struct {
|
||||||
|
app *app.App
|
||||||
prefix string
|
prefix string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,7 +25,17 @@ func (cg *filesAndFoldersContextGroup) GetEntry() dialog.CompletionItemI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (cg *filesAndFoldersContextGroup) getFiles(query string) ([]string, error) {
|
func (cg *filesAndFoldersContextGroup) getFiles(query string) ([]string, error) {
|
||||||
return []string{}, nil
|
response, err := cg.app.Client.PostFileSearchWithResponse(context.Background(), client.PostFileSearchJSONRequestBody{
|
||||||
|
Query: query,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return []string{}, err
|
||||||
|
}
|
||||||
|
if response.JSON200 == nil {
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return *response.JSON200, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cg *filesAndFoldersContextGroup) GetChildEntries(query string) ([]dialog.CompletionItemI, error) {
|
func (cg *filesAndFoldersContextGroup) GetChildEntries(query string) ([]dialog.CompletionItemI, error) {
|
||||||
@@ -41,8 +56,9 @@ func (cg *filesAndFoldersContextGroup) GetChildEntries(query string) ([]dialog.C
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFileAndFolderContextGroup() dialog.CompletionProvider {
|
func NewFileAndFolderContextGroup(app *app.App) dialog.CompletionProvider {
|
||||||
return &filesAndFoldersContextGroup{
|
return &filesAndFoldersContextGroup{
|
||||||
|
app: app,
|
||||||
prefix: "file",
|
prefix: "file",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package completions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sst/opencode/internal/app"
|
||||||
|
"github.com/sst/opencode/internal/components/dialog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CompletionManager struct {
|
||||||
|
providers map[string]dialog.CompletionProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCompletionManager(app *app.App) *CompletionManager {
|
||||||
|
return &CompletionManager{
|
||||||
|
providers: map[string]dialog.CompletionProvider{
|
||||||
|
"files": NewFileAndFolderContextGroup(app),
|
||||||
|
"commands": NewCommandCompletionProvider(app),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *CompletionManager) GetProvider(input string) dialog.CompletionProvider {
|
||||||
|
if strings.HasPrefix(input, "/") {
|
||||||
|
return m.providers["commands"]
|
||||||
|
}
|
||||||
|
return m.providers["files"]
|
||||||
|
}
|
||||||
|
|
||||||
@@ -44,11 +44,6 @@ type EditorKeyMaps struct {
|
|||||||
HistoryDown key.Binding
|
HistoryDown key.Binding
|
||||||
}
|
}
|
||||||
|
|
||||||
type bluredEditorKeyMaps struct {
|
|
||||||
Send key.Binding
|
|
||||||
Focus key.Binding
|
|
||||||
OpenEditor key.Binding
|
|
||||||
}
|
|
||||||
type DeleteAttachmentKeyMaps struct {
|
type DeleteAttachmentKeyMaps struct {
|
||||||
AttachmentDeleteMode key.Binding
|
AttachmentDeleteMode key.Binding
|
||||||
Escape key.Binding
|
Escape key.Binding
|
||||||
@@ -108,10 +103,18 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
case dialog.ThemeChangedMsg:
|
case dialog.ThemeChangedMsg:
|
||||||
m.textarea = createTextArea(&m.textarea)
|
m.textarea = createTextArea(&m.textarea)
|
||||||
case dialog.CompletionSelectedMsg:
|
case dialog.CompletionSelectedMsg:
|
||||||
existingValue := m.textarea.Value()
|
if msg.IsCommand {
|
||||||
modifiedValue := strings.Replace(existingValue, msg.SearchString, msg.CompletionValue, 1)
|
// Execute the command directly
|
||||||
m.textarea.SetValue(modifiedValue)
|
commandName := strings.TrimPrefix(msg.CompletionValue, "/")
|
||||||
return m, nil
|
m.textarea.Reset()
|
||||||
|
return m, util.CmdHandler(commands.ExecuteCommandMsg{Name: commandName})
|
||||||
|
} else {
|
||||||
|
// For files, replace the text in the editor
|
||||||
|
existingValue := m.textarea.Value()
|
||||||
|
modifiedValue := strings.Replace(existingValue, msg.SearchString, msg.CompletionValue, 1)
|
||||||
|
m.textarea.SetValue(modifiedValue)
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "ctrl+c":
|
case "ctrl+c":
|
||||||
@@ -378,12 +381,13 @@ func (m *editorComponent) send() tea.Cmd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for slash command
|
// Check for slash command
|
||||||
if strings.HasPrefix(value, "/") {
|
// if strings.HasPrefix(value, "/") {
|
||||||
commandName := strings.TrimPrefix(value, "/")
|
// commandName := strings.TrimPrefix(value, "/")
|
||||||
if _, ok := m.app.Commands[commandName]; ok {
|
// if _, ok := m.app.Commands[commandName]; ok {
|
||||||
return util.CmdHandler(commands.ExecuteCommandMsg{Name: commandName})
|
// return util.CmdHandler(commands.ExecuteCommandMsg{Name: commandName})
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
slog.Info("Send message", "value", value)
|
||||||
|
|
||||||
return tea.Batch(
|
return tea.Batch(
|
||||||
util.CmdHandler(SendMsg{
|
util.CmdHandler(SendMsg{
|
||||||
@@ -452,6 +456,10 @@ func createTextArea(existing *textarea.Model) textarea.Model {
|
|||||||
return ta
|
return ta
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *editorComponent) GetValue() string {
|
||||||
|
return m.textarea.Value()
|
||||||
|
}
|
||||||
|
|
||||||
func NewEditorComponent(app *app.App) layout.ModelWithView {
|
func NewEditorComponent(app *app.App) layout.ModelWithView {
|
||||||
s := spinner.New(spinner.WithSpinner(spinner.Ellipsis), spinner.WithStyle(styles.Muted().Width(3)))
|
s := spinner.New(spinner.WithSpinner(spinner.Ellipsis), spinner.WithStyle(styles.Muted().Width(3)))
|
||||||
ta := createTextArea(nil)
|
ta := createTextArea(nil)
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ func renderText(message client.MessageInfo, text string, author string) string {
|
|||||||
align = lipgloss.Left
|
align = lipgloss.Left
|
||||||
}
|
}
|
||||||
|
|
||||||
textWidth := lipgloss.Width(text)
|
textWidth := max(lipgloss.Width(text), lipgloss.Width(info))
|
||||||
markdownWidth := min(textWidth, width-padding-4) // -4 for the border and padding
|
markdownWidth := min(textWidth, width-padding-4) // -4 for the border and padding
|
||||||
content := toMarkdown(text, markdownWidth, t.BackgroundSubtle())
|
content := toMarkdown(text, markdownWidth, t.BackgroundSubtle())
|
||||||
content = lipgloss.JoinVertical(align, content, info)
|
content = lipgloss.JoinVertical(align, content, info)
|
||||||
@@ -299,13 +299,9 @@ func renderToolInvocation(
|
|||||||
body := ""
|
body := ""
|
||||||
error := ""
|
error := ""
|
||||||
finished := result != nil && *result != ""
|
finished := result != nil && *result != ""
|
||||||
if finished {
|
|
||||||
body = *result
|
|
||||||
}
|
|
||||||
|
|
||||||
if e, ok := metadata.Get("error"); ok && e.(bool) == true {
|
if e, ok := metadata.Get("error"); ok && e.(bool) == true {
|
||||||
if m, ok := metadata.Get("message"); ok {
|
if m, ok := metadata.Get("message"); ok {
|
||||||
body = "" // don't show the body if there's an error
|
|
||||||
style = style.BorderLeftForeground(t.Error())
|
style = style.BorderLeftForeground(t.Error())
|
||||||
error = styles.BaseStyle().
|
error = styles.BaseStyle().
|
||||||
Background(t.BackgroundSubtle()).
|
Background(t.BackgroundSubtle()).
|
||||||
@@ -336,58 +332,61 @@ func renderToolInvocation(
|
|||||||
case "opencode_read":
|
case "opencode_read":
|
||||||
toolArgs = renderArgs(&toolArgsMap, "filePath")
|
toolArgs = renderArgs(&toolArgsMap, "filePath")
|
||||||
title = fmt.Sprintf("Read: %s %s", toolArgs, elapsed)
|
title = fmt.Sprintf("Read: %s %s", toolArgs, elapsed)
|
||||||
body = ""
|
|
||||||
if preview, ok := metadata.Get("preview"); ok && toolArgsMap["filePath"] != nil {
|
if preview, ok := metadata.Get("preview"); ok && toolArgsMap["filePath"] != nil {
|
||||||
filename := toolArgsMap["filePath"].(string)
|
filename := toolArgsMap["filePath"].(string)
|
||||||
body = preview.(string)
|
body = preview.(string)
|
||||||
body = renderFile(filename, body, WithTruncate(6))
|
body = renderFile(filename, body, WithTruncate(6))
|
||||||
}
|
}
|
||||||
case "opencode_edit":
|
case "opencode_edit":
|
||||||
filename := toolArgsMap["filePath"].(string)
|
if filename, ok := toolArgsMap["filePath"].(string); ok {
|
||||||
title = fmt.Sprintf("Edit: %s %s", relative(filename), elapsed)
|
title = fmt.Sprintf("Edit: %s %s", relative(filename), elapsed)
|
||||||
if d, ok := metadata.Get("diff"); ok {
|
if d, ok := metadata.Get("diff"); ok {
|
||||||
patch := d.(string)
|
patch := d.(string)
|
||||||
var formattedDiff string
|
var formattedDiff string
|
||||||
if layout.Current.Viewport.Width < 80 {
|
if layout.Current.Viewport.Width < 80 {
|
||||||
formattedDiff, _ = diff.FormatUnifiedDiff(
|
formattedDiff, _ = diff.FormatUnifiedDiff(
|
||||||
filename,
|
filename,
|
||||||
patch,
|
patch,
|
||||||
diff.WithWidth(layout.Current.Container.Width-2),
|
diff.WithWidth(layout.Current.Container.Width-2),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
diffWidth := min(layout.Current.Viewport.Width-2, 120)
|
||||||
|
formattedDiff, _ = diff.FormatDiff(filename, patch, diff.WithTotalWidth(diffWidth))
|
||||||
|
}
|
||||||
|
formattedDiff = strings.TrimSpace(formattedDiff)
|
||||||
|
formattedDiff = lipgloss.NewStyle().
|
||||||
|
BorderStyle(lipgloss.ThickBorder()).
|
||||||
|
BorderForeground(t.BackgroundSubtle()).
|
||||||
|
BorderLeft(true).
|
||||||
|
BorderRight(true).
|
||||||
|
Render(formattedDiff)
|
||||||
|
|
||||||
|
if showResult {
|
||||||
|
style = style.Width(lipgloss.Width(formattedDiff))
|
||||||
|
title += "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
body = strings.TrimSpace(formattedDiff)
|
||||||
|
body = lipgloss.Place(
|
||||||
|
layout.Current.Viewport.Width,
|
||||||
|
lipgloss.Height(body)+1,
|
||||||
|
lipgloss.Center,
|
||||||
|
lipgloss.Top,
|
||||||
|
body,
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
diffWidth := min(layout.Current.Viewport.Width-2, 120)
|
|
||||||
formattedDiff, _ = diff.FormatDiff(filename, patch, diff.WithTotalWidth(diffWidth))
|
|
||||||
}
|
}
|
||||||
formattedDiff = strings.TrimSpace(formattedDiff)
|
|
||||||
formattedDiff = lipgloss.NewStyle().
|
|
||||||
BorderStyle(lipgloss.ThickBorder()).
|
|
||||||
BorderForeground(t.BackgroundSubtle()).
|
|
||||||
BorderLeft(true).
|
|
||||||
BorderRight(true).
|
|
||||||
Render(formattedDiff)
|
|
||||||
|
|
||||||
if showResult {
|
|
||||||
style = style.Width(lipgloss.Width(formattedDiff))
|
|
||||||
title += "\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
body = strings.TrimSpace(formattedDiff)
|
|
||||||
body = lipgloss.Place(
|
|
||||||
layout.Current.Viewport.Width,
|
|
||||||
lipgloss.Height(body)+1,
|
|
||||||
lipgloss.Center,
|
|
||||||
lipgloss.Top,
|
|
||||||
body,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
case "opencode_write":
|
case "opencode_write":
|
||||||
filename := toolArgsMap["filePath"].(string)
|
if filename, ok := toolArgsMap["filePath"].(string); ok {
|
||||||
title = fmt.Sprintf("Write: %s %s", relative(filename), elapsed)
|
title = fmt.Sprintf("Write: %s %s", relative(filename), elapsed)
|
||||||
content := toolArgsMap["content"].(string)
|
if content, ok := toolArgsMap["content"].(string); ok {
|
||||||
body = renderFile(filename, content)
|
body = renderFile(filename, content)
|
||||||
|
}
|
||||||
|
}
|
||||||
case "opencode_bash":
|
case "opencode_bash":
|
||||||
description := toolArgsMap["description"].(string)
|
if description, ok := toolArgsMap["description"].(string); ok {
|
||||||
title = fmt.Sprintf("Shell: %s %s", description, elapsed)
|
title = fmt.Sprintf("Shell: %s %s", description, elapsed)
|
||||||
|
}
|
||||||
if stdout, ok := metadata.Get("stdout"); ok {
|
if stdout, ok := metadata.Get("stdout"); ok {
|
||||||
command := toolArgsMap["command"].(string)
|
command := toolArgsMap["command"].(string)
|
||||||
stdout := stdout.(string)
|
stdout := stdout.(string)
|
||||||
@@ -396,18 +395,20 @@ func renderToolInvocation(
|
|||||||
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
|
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
|
||||||
}
|
}
|
||||||
case "opencode_webfetch":
|
case "opencode_webfetch":
|
||||||
|
toolArgs = renderArgs(&toolArgsMap, "url")
|
||||||
title = fmt.Sprintf("Fetching: %s %s", toolArgs, elapsed)
|
title = fmt.Sprintf("Fetching: %s %s", toolArgs, elapsed)
|
||||||
format := toolArgsMap["format"].(string)
|
if format, ok := toolArgsMap["format"].(string); ok {
|
||||||
body = truncateHeight(body, 10)
|
body = *result
|
||||||
if format == "html" || format == "markdown" {
|
body = truncateHeight(body, 10)
|
||||||
body = toMarkdown(body, innerWidth, t.BackgroundSubtle())
|
if format == "html" || format == "markdown" {
|
||||||
|
body = toMarkdown(body, innerWidth, t.BackgroundSubtle())
|
||||||
|
}
|
||||||
|
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
|
||||||
}
|
}
|
||||||
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
|
|
||||||
case "opencode_todowrite":
|
case "opencode_todowrite":
|
||||||
title = fmt.Sprintf("Planning %s", elapsed)
|
title = fmt.Sprintf("Planning %s", elapsed)
|
||||||
|
|
||||||
if to, ok := metadata.Get("todos"); ok && finished {
|
if to, ok := metadata.Get("todos"); ok && finished {
|
||||||
body = ""
|
|
||||||
todos := to.([]any)
|
todos := to.([]any)
|
||||||
for _, todo := range todos {
|
for _, todo := range todos {
|
||||||
t := todo.(map[string]any)
|
t := todo.(map[string]any)
|
||||||
@@ -416,7 +417,7 @@ func renderToolInvocation(
|
|||||||
case "completed":
|
case "completed":
|
||||||
body += fmt.Sprintf("- [x] %s\n", content)
|
body += fmt.Sprintf("- [x] %s\n", content)
|
||||||
// case "in-progress":
|
// case "in-progress":
|
||||||
// body += fmt.Sprintf("- [ ] _%s_\n", content)
|
// body += fmt.Sprintf("- [ ] %s\n", content)
|
||||||
default:
|
default:
|
||||||
body += fmt.Sprintf("- [ ] %s\n", content)
|
body += fmt.Sprintf("- [ ] %s\n", content)
|
||||||
}
|
}
|
||||||
@@ -427,6 +428,13 @@ func renderToolInvocation(
|
|||||||
default:
|
default:
|
||||||
toolName := renderToolName(toolCall.ToolName)
|
toolName := renderToolName(toolCall.ToolName)
|
||||||
title = fmt.Sprintf("%s: %s %s", toolName, toolArgs, elapsed)
|
title = fmt.Sprintf("%s: %s %s", toolName, toolArgs, elapsed)
|
||||||
|
body = *result
|
||||||
|
body = truncateHeight(body, 10)
|
||||||
|
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
if body == "" && error == "" {
|
||||||
|
body = *result
|
||||||
body = truncateHeight(body, 10)
|
body = truncateHeight(body, 10)
|
||||||
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
|
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ func (m *messagesComponent) header() string {
|
|||||||
base := styles.BaseStyle().Render
|
base := styles.BaseStyle().Render
|
||||||
muted := styles.Muted().Render
|
muted := styles.Muted().Render
|
||||||
headerLines := []string{}
|
headerLines := []string{}
|
||||||
headerLines = append(headerLines, toMarkdown("# "+m.app.Session.Title, width-4, t.Background()))
|
headerLines = append(headerLines, toMarkdown("# "+m.app.Session.Title, width-6, t.Background()))
|
||||||
if m.app.Session.Share != nil && m.app.Session.Share.Url != "" {
|
if m.app.Session.Share != nil && m.app.Session.Share.Url != "" {
|
||||||
headerLines = append(headerLines, muted(m.app.Session.Share.Url))
|
headerLines = append(headerLines, muted(m.app.Session.Share.Url))
|
||||||
} else {
|
} else {
|
||||||
@@ -256,6 +256,7 @@ func (m *messagesComponent) header() string {
|
|||||||
header = styles.BaseStyle().
|
header = styles.BaseStyle().
|
||||||
Width(width).
|
Width(width).
|
||||||
PaddingLeft(2).
|
PaddingLeft(2).
|
||||||
|
PaddingRight(2).
|
||||||
// Background(t.BackgroundElement()).
|
// Background(t.BackgroundElement()).
|
||||||
BorderLeft(true).
|
BorderLeft(true).
|
||||||
BorderRight(true).
|
BorderRight(true).
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"github.com/charmbracelet/bubbles/v2/textarea"
|
"github.com/charmbracelet/bubbles/v2/textarea"
|
||||||
tea "github.com/charmbracelet/bubbletea/v2"
|
tea "github.com/charmbracelet/bubbletea/v2"
|
||||||
"github.com/charmbracelet/lipgloss/v2"
|
"github.com/charmbracelet/lipgloss/v2"
|
||||||
utilComponents "github.com/sst/opencode/internal/components/util"
|
"github.com/sst/opencode/internal/components/list"
|
||||||
"github.com/sst/opencode/internal/layout"
|
"github.com/sst/opencode/internal/layout"
|
||||||
"github.com/sst/opencode/internal/status"
|
"github.com/sst/opencode/internal/status"
|
||||||
"github.com/sst/opencode/internal/styles"
|
"github.com/sst/opencode/internal/styles"
|
||||||
@@ -20,7 +20,7 @@ type CompletionItem struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CompletionItemI interface {
|
type CompletionItemI interface {
|
||||||
utilComponents.ListItem
|
list.ListItem
|
||||||
GetValue() string
|
GetValue() string
|
||||||
DisplayValue() string
|
DisplayValue() string
|
||||||
}
|
}
|
||||||
@@ -30,18 +30,18 @@ func (ci *CompletionItem) Render(selected bool, width int) string {
|
|||||||
baseStyle := styles.BaseStyle()
|
baseStyle := styles.BaseStyle()
|
||||||
|
|
||||||
itemStyle := baseStyle.
|
itemStyle := baseStyle.
|
||||||
|
Background(t.BackgroundElement()).
|
||||||
Width(width).
|
Width(width).
|
||||||
Padding(0, 1)
|
Padding(0, 1)
|
||||||
|
|
||||||
if selected {
|
if selected {
|
||||||
itemStyle = itemStyle.
|
itemStyle = itemStyle.
|
||||||
Background(t.Background()).
|
|
||||||
Foreground(t.Primary()).
|
Foreground(t.Primary()).
|
||||||
Bold(true)
|
Bold(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
title := itemStyle.Render(
|
title := itemStyle.Render(
|
||||||
ci.GetValue(),
|
ci.DisplayValue(),
|
||||||
)
|
)
|
||||||
|
|
||||||
return title
|
return title
|
||||||
@@ -68,6 +68,7 @@ type CompletionProvider interface {
|
|||||||
type CompletionSelectedMsg struct {
|
type CompletionSelectedMsg struct {
|
||||||
SearchString string
|
SearchString string
|
||||||
CompletionValue string
|
CompletionValue string
|
||||||
|
IsCommand bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type CompletionDialogCompleteItemMsg struct {
|
type CompletionDialogCompleteItemMsg struct {
|
||||||
@@ -80,6 +81,7 @@ type CompletionDialog interface {
|
|||||||
layout.ModelWithView
|
layout.ModelWithView
|
||||||
SetWidth(width int)
|
SetWidth(width int)
|
||||||
IsEmpty() bool
|
IsEmpty() bool
|
||||||
|
SetProvider(provider CompletionProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
type completionDialogComponent struct {
|
type completionDialogComponent struct {
|
||||||
@@ -88,7 +90,7 @@ type completionDialogComponent struct {
|
|||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
pseudoSearchTextArea textarea.Model
|
pseudoSearchTextArea textarea.Model
|
||||||
list utilComponents.List[CompletionItemI]
|
list list.List[CompletionItemI]
|
||||||
}
|
}
|
||||||
|
|
||||||
type completionDialogKeyMap struct {
|
type completionDialogKeyMap struct {
|
||||||
@@ -116,10 +118,14 @@ func (c *completionDialogComponent) complete(item CompletionItemI) tea.Cmd {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this is a command completion
|
||||||
|
isCommand := c.completionProvider.GetId() == "commands"
|
||||||
|
|
||||||
return tea.Batch(
|
return tea.Batch(
|
||||||
util.CmdHandler(CompletionSelectedMsg{
|
util.CmdHandler(CompletionSelectedMsg{
|
||||||
SearchString: value,
|
SearchString: value,
|
||||||
CompletionValue: item.GetValue(),
|
CompletionValue: item.GetValue(),
|
||||||
|
IsCommand: isCommand,
|
||||||
}),
|
}),
|
||||||
c.close(),
|
c.close(),
|
||||||
)
|
)
|
||||||
@@ -160,7 +166,7 @@ func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
u, cmd := c.list.Update(msg)
|
u, cmd := c.list.Update(msg)
|
||||||
c.list = u.(utilComponents.List[CompletionItemI])
|
c.list = u.(list.List[CompletionItemI])
|
||||||
|
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
}
|
}
|
||||||
@@ -171,8 +177,7 @@ func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
if i == -1 {
|
if i == -1 {
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
cmd := c.complete(item)
|
return c, c.complete(item)
|
||||||
return c, cmd
|
|
||||||
case key.Matches(msg, completionDialogKeys.Cancel):
|
case key.Matches(msg, completionDialogKeys.Cancel):
|
||||||
// Only close on backspace when there are no characters left
|
// Only close on backspace when there are no characters left
|
||||||
if msg.String() != "backspace" || len(c.pseudoSearchTextArea.Value()) <= 0 {
|
if msg.String() != "backspace" || len(c.pseudoSearchTextArea.Value()) <= 0 {
|
||||||
@@ -203,21 +208,20 @@ func (c *completionDialogComponent) View() string {
|
|||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
baseStyle := styles.BaseStyle()
|
baseStyle := styles.BaseStyle()
|
||||||
|
|
||||||
// maxWidth := 40
|
maxWidth := 40
|
||||||
//
|
completions := c.list.GetItems()
|
||||||
// completions := c.list.GetItems()
|
|
||||||
|
|
||||||
// for _, cmd := range completions {
|
for _, cmd := range completions {
|
||||||
// title := cmd.DisplayValue()
|
title := cmd.DisplayValue()
|
||||||
// if len(title) > maxWidth-4 {
|
if len(title) > maxWidth-4 {
|
||||||
// maxWidth = len(title) + 4
|
maxWidth = len(title) + 4
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
// c.list.SetMaxWidth(maxWidth)
|
c.list.SetMaxWidth(maxWidth)
|
||||||
|
|
||||||
return baseStyle.Padding(0, 0).
|
return baseStyle.Padding(0, 0).
|
||||||
Background(t.BackgroundSubtle()).
|
Background(t.BackgroundElement()).
|
||||||
Border(lipgloss.ThickBorder()).
|
Border(lipgloss.ThickBorder()).
|
||||||
BorderTop(false).
|
BorderTop(false).
|
||||||
BorderBottom(false).
|
BorderBottom(false).
|
||||||
@@ -236,6 +240,17 @@ func (c *completionDialogComponent) IsEmpty() bool {
|
|||||||
return c.list.IsEmpty()
|
return c.list.IsEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *completionDialogComponent) SetProvider(provider CompletionProvider) {
|
||||||
|
if c.completionProvider.GetId() != provider.GetId() {
|
||||||
|
c.completionProvider = provider
|
||||||
|
items, err := provider.GetChildEntries("")
|
||||||
|
if err != nil {
|
||||||
|
status.Error(err.Error())
|
||||||
|
}
|
||||||
|
c.list.SetItems(items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func NewCompletionDialogComponent(completionProvider CompletionProvider) CompletionDialog {
|
func NewCompletionDialogComponent(completionProvider CompletionProvider) CompletionDialog {
|
||||||
ti := textarea.New()
|
ti := textarea.New()
|
||||||
|
|
||||||
@@ -244,10 +259,10 @@ func NewCompletionDialogComponent(completionProvider CompletionProvider) Complet
|
|||||||
status.Error(err.Error())
|
status.Error(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
li := utilComponents.NewListComponent(
|
li := list.NewListComponent(
|
||||||
items,
|
items,
|
||||||
7,
|
7,
|
||||||
"No matching files",
|
"No matches",
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea/v2"
|
tea "github.com/charmbracelet/bubbletea/v2"
|
||||||
"github.com/sst/opencode/internal/app"
|
"github.com/sst/opencode/internal/app"
|
||||||
|
"github.com/sst/opencode/internal/components/list"
|
||||||
"github.com/sst/opencode/internal/components/modal"
|
"github.com/sst/opencode/internal/components/modal"
|
||||||
components "github.com/sst/opencode/internal/components/util"
|
|
||||||
"github.com/sst/opencode/internal/layout"
|
"github.com/sst/opencode/internal/layout"
|
||||||
"github.com/sst/opencode/internal/state"
|
"github.com/sst/opencode/internal/state"
|
||||||
"github.com/sst/opencode/internal/styles"
|
"github.com/sst/opencode/internal/styles"
|
||||||
@@ -48,7 +48,7 @@ type sessionDialog struct {
|
|||||||
height int
|
height int
|
||||||
modal *modal.Modal
|
modal *modal.Modal
|
||||||
selectedSessionID string
|
selectedSessionID string
|
||||||
list components.List[sessionItem]
|
list list.List[sessionItem]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sessionDialog) Init() tea.Cmd {
|
func (s *sessionDialog) Init() tea.Cmd {
|
||||||
@@ -77,7 +77,7 @@ func (s *sessionDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
|
|
||||||
var cmd tea.Cmd
|
var cmd tea.Cmd
|
||||||
listModel, cmd := s.list.Update(msg)
|
listModel, cmd := s.list.Update(msg)
|
||||||
s.list = listModel.(components.List[sessionItem])
|
s.list = listModel.(list.List[sessionItem])
|
||||||
return s, cmd
|
return s, cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ func NewSessionDialog(app *app.App) SessionDialog {
|
|||||||
sessionItems = append(sessionItems, sessionItem{session: sess})
|
sessionItems = append(sessionItems, sessionItem{session: sess})
|
||||||
}
|
}
|
||||||
|
|
||||||
list := components.NewListComponent(
|
list := list.NewListComponent(
|
||||||
sessionItems,
|
sessionItems,
|
||||||
10, // maxVisibleSessions
|
10, // maxVisibleSessions
|
||||||
"No sessions available",
|
"No sessions available",
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package dialog
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
tea "github.com/charmbracelet/bubbletea/v2"
|
tea "github.com/charmbracelet/bubbletea/v2"
|
||||||
|
list "github.com/sst/opencode/internal/components/list"
|
||||||
"github.com/sst/opencode/internal/components/modal"
|
"github.com/sst/opencode/internal/components/modal"
|
||||||
components "github.com/sst/opencode/internal/components/util"
|
|
||||||
"github.com/sst/opencode/internal/layout"
|
"github.com/sst/opencode/internal/layout"
|
||||||
"github.com/sst/opencode/internal/status"
|
"github.com/sst/opencode/internal/status"
|
||||||
"github.com/sst/opencode/internal/styles"
|
"github.com/sst/opencode/internal/styles"
|
||||||
@@ -49,7 +49,7 @@ type themeDialog struct {
|
|||||||
height int
|
height int
|
||||||
|
|
||||||
modal *modal.Modal
|
modal *modal.Modal
|
||||||
list components.List[themeItem]
|
list list.List[themeItem]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *themeDialog) Init() tea.Cmd {
|
func (t *themeDialog) Init() tea.Cmd {
|
||||||
@@ -84,7 +84,7 @@ func (t *themeDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
|
|
||||||
var cmd tea.Cmd
|
var cmd tea.Cmd
|
||||||
listModel, cmd := t.list.Update(msg)
|
listModel, cmd := t.list.Update(msg)
|
||||||
t.list = listModel.(components.List[themeItem])
|
t.list = listModel.(list.List[themeItem])
|
||||||
return t, cmd
|
return t, cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ func NewThemeDialog() ThemeDialog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
list := components.NewListComponent(
|
list := list.NewListComponent(
|
||||||
themeItems,
|
themeItems,
|
||||||
10, // maxVisibleThemes
|
10, // maxVisibleThemes
|
||||||
"No themes available",
|
"No themes available",
|
||||||
|
|||||||
+2
-8
@@ -1,11 +1,10 @@
|
|||||||
package utilComponents
|
package list
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/charmbracelet/bubbles/v2/key"
|
"github.com/charmbracelet/bubbles/v2/key"
|
||||||
tea "github.com/charmbracelet/bubbletea/v2"
|
tea "github.com/charmbracelet/bubbletea/v2"
|
||||||
"github.com/charmbracelet/lipgloss/v2"
|
"github.com/charmbracelet/lipgloss/v2"
|
||||||
"github.com/sst/opencode/internal/layout"
|
"github.com/sst/opencode/internal/layout"
|
||||||
"github.com/sst/opencode/internal/styles"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ListItem interface {
|
type ListItem interface {
|
||||||
@@ -116,18 +115,13 @@ func (c *listComponent[T]) SetSelectedIndex(idx int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *listComponent[T]) View() string {
|
func (c *listComponent[T]) View() string {
|
||||||
baseStyle := styles.BaseStyle()
|
|
||||||
|
|
||||||
items := c.items
|
items := c.items
|
||||||
maxWidth := c.maxWidth
|
maxWidth := c.maxWidth
|
||||||
maxVisibleItems := min(c.maxVisibleItems, len(items))
|
maxVisibleItems := min(c.maxVisibleItems, len(items))
|
||||||
startIdx := 0
|
startIdx := 0
|
||||||
|
|
||||||
if len(items) <= 0 {
|
if len(items) <= 0 {
|
||||||
return baseStyle.
|
return c.fallbackMsg
|
||||||
Padding(0, 1).
|
|
||||||
Width(maxWidth).
|
|
||||||
Render(c.fallbackMsg)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(items) > maxVisibleItems {
|
if len(items) > maxVisibleItems {
|
||||||
@@ -19,6 +19,7 @@ type Container interface {
|
|||||||
MaxWidth() int
|
MaxWidth() int
|
||||||
Alignment() lipgloss.Position
|
Alignment() lipgloss.Position
|
||||||
GetPosition() (x, y int)
|
GetPosition() (x, y int)
|
||||||
|
GetContent() ModelWithView
|
||||||
}
|
}
|
||||||
|
|
||||||
type container struct {
|
type container struct {
|
||||||
@@ -177,6 +178,11 @@ func (c *container) GetPosition() (x, y int) {
|
|||||||
return c.x, c.y
|
return c.x, c.y
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetContent returns the content of the container
|
||||||
|
func (c *container) GetContent() ModelWithView {
|
||||||
|
return c.content
|
||||||
|
}
|
||||||
|
|
||||||
type ContainerOption func(*container)
|
type ContainerOption func(*container)
|
||||||
|
|
||||||
func NewContainer(content ModelWithView, options ...ContainerOption) Container {
|
func NewContainer(content ModelWithView, options ...ContainerOption) Container {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ type chatPage struct {
|
|||||||
messages layout.Container
|
messages layout.Container
|
||||||
layout layout.FlexLayout
|
layout layout.FlexLayout
|
||||||
completionDialog dialog.CompletionDialog
|
completionDialog dialog.CompletionDialog
|
||||||
|
completionManager *completions.CompletionManager
|
||||||
showCompletionDialog bool
|
showCompletionDialog bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,13 +95,20 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if p.showCompletionDialog {
|
if p.showCompletionDialog {
|
||||||
|
// Get the current text from the editor to determine which provider to use
|
||||||
|
editorModel := p.editor.GetContent().(interface{ GetValue() string })
|
||||||
|
currentInput := editorModel.GetValue()
|
||||||
|
|
||||||
|
provider := p.completionManager.GetProvider(currentInput)
|
||||||
|
p.completionDialog.SetProvider(provider)
|
||||||
|
|
||||||
context, contextCmd := p.completionDialog.Update(msg)
|
context, contextCmd := p.completionDialog.Update(msg)
|
||||||
p.completionDialog = context.(dialog.CompletionDialog)
|
p.completionDialog = context.(dialog.CompletionDialog)
|
||||||
cmds = append(cmds, contextCmd)
|
cmds = append(cmds, contextCmd)
|
||||||
|
|
||||||
// Doesn't forward event if enter key is pressed
|
// Doesn't forward event if enter key is pressed and there are completions
|
||||||
if keyMsg, ok := msg.(tea.KeyMsg); ok {
|
if keyMsg, ok := msg.(tea.KeyMsg); ok {
|
||||||
if keyMsg.String() == "enter" && !p.completionDialog.IsEmpty() {
|
if keyMsg.String() == "enter" { // && !p.completionDialog.IsEmpty() {
|
||||||
return p, tea.Batch(cmds...)
|
return p, tea.Batch(cmds...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,8 +157,10 @@ func (p *chatPage) View() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewChatPage(app *app.App) layout.ModelWithView {
|
func NewChatPage(app *app.App) layout.ModelWithView {
|
||||||
cg := completions.NewFileAndFolderContextGroup()
|
completionManager := completions.NewCompletionManager(app)
|
||||||
completionDialog := dialog.NewCompletionDialogComponent(cg)
|
initialProvider := completionManager.GetProvider("")
|
||||||
|
completionDialog := dialog.NewCompletionDialogComponent(initialProvider)
|
||||||
|
|
||||||
messagesContainer := layout.NewContainer(
|
messagesContainer := layout.NewContainer(
|
||||||
chat.NewMessagesComponent(app),
|
chat.NewMessagesComponent(app),
|
||||||
)
|
)
|
||||||
@@ -159,11 +169,13 @@ func NewChatPage(app *app.App) layout.ModelWithView {
|
|||||||
layout.WithMaxWidth(layout.Current.Container.Width),
|
layout.WithMaxWidth(layout.Current.Container.Width),
|
||||||
layout.WithAlignCenter(),
|
layout.WithAlignCenter(),
|
||||||
)
|
)
|
||||||
|
|
||||||
return &chatPage{
|
return &chatPage{
|
||||||
app: app,
|
app: app,
|
||||||
editor: editorContainer,
|
editor: editorContainer,
|
||||||
messages: messagesContainer,
|
messages: messagesContainer,
|
||||||
completionDialog: completionDialog,
|
completionDialog: completionDialog,
|
||||||
|
completionManager: completionManager,
|
||||||
layout: layout.NewFlexLayout(
|
layout: layout.NewFlexLayout(
|
||||||
layout.WithPanes(messagesContainer, editorContainer),
|
layout.WithPanes(messagesContainer, editorContainer),
|
||||||
layout.WithDirection(layout.FlexDirectionVertical),
|
layout.WithDirection(layout.FlexDirectionVertical),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package tui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/v2/cursor"
|
"github.com/charmbracelet/bubbles/v2/cursor"
|
||||||
"github.com/charmbracelet/bubbles/v2/key"
|
"github.com/charmbracelet/bubbles/v2/key"
|
||||||
@@ -78,33 +79,52 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
var cmd tea.Cmd
|
var cmd tea.Cmd
|
||||||
|
|
||||||
if a.modal != nil {
|
if a.modal != nil {
|
||||||
isModalTrigger := false
|
bypassModal := false
|
||||||
|
|
||||||
if _, ok := msg.(modal.CloseModalMsg); ok {
|
if _, ok := msg.(modal.CloseModalMsg); ok {
|
||||||
a.modal = nil
|
a.modal = nil
|
||||||
return a, nil
|
return a, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if msg, ok := msg.(tea.KeyMsg); ok {
|
if msg, ok := msg.(tea.KeyMsg); ok {
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "esc":
|
case "esc":
|
||||||
a.modal = nil
|
a.modal = nil
|
||||||
return a, nil
|
return a, nil
|
||||||
case "ctrl+c":
|
case "ctrl+c":
|
||||||
if _, ok := a.modal.(dialog.QuitDialog); !ok {
|
if _, ok := a.modal.(dialog.QuitDialog); ok {
|
||||||
|
return a, tea.Quit
|
||||||
|
} else {
|
||||||
quitDialog := dialog.NewQuitDialog()
|
quitDialog := dialog.NewQuitDialog()
|
||||||
a.modal = quitDialog
|
a.modal = quitDialog
|
||||||
return a, nil
|
return a, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// don't send commands to the modal
|
||||||
for _, cmdDef := range a.app.Commands {
|
for _, cmdDef := range a.app.Commands {
|
||||||
if key.Matches(msg, cmdDef.KeyBinding) {
|
if key.Matches(msg, cmdDef.KeyBinding) {
|
||||||
isModalTrigger = true
|
bypassModal = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isModalTrigger {
|
// thanks i hate this
|
||||||
|
switch msg.(type) {
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
bypassModal = true
|
||||||
|
case client.EventSessionUpdated:
|
||||||
|
bypassModal = true
|
||||||
|
case client.EventMessageUpdated:
|
||||||
|
bypassModal = true
|
||||||
|
case cursor.BlinkMsg:
|
||||||
|
bypassModal = true
|
||||||
|
case spinner.TickMsg:
|
||||||
|
bypassModal = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bypassModal {
|
||||||
updatedModal, cmd := a.modal.Update(msg)
|
updatedModal, cmd := a.modal.Update(msg)
|
||||||
a.modal = updatedModal.(layout.Modal)
|
a.modal = updatedModal.(layout.Modal)
|
||||||
return a, cmd
|
return a, cmd
|
||||||
@@ -112,7 +132,6 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
|
|
||||||
case commands.ExecuteCommandMsg:
|
case commands.ExecuteCommandMsg:
|
||||||
switch msg.Name {
|
switch msg.Name {
|
||||||
case "quit":
|
case "quit":
|
||||||
@@ -143,6 +162,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
helpDialog := dialog.NewHelpDialog(helpBindings...)
|
helpDialog := dialog.NewHelpDialog(helpBindings...)
|
||||||
a.modal = helpDialog
|
a.modal = helpDialog
|
||||||
}
|
}
|
||||||
|
slog.Info("Execute command", "cmds", cmds)
|
||||||
return a, tea.Batch(cmds...)
|
return a, tea.Batch(cmds...)
|
||||||
|
|
||||||
case tea.BackgroundColorMsg:
|
case tea.BackgroundColorMsg:
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 75 KiB |
Reference in New Issue
Block a user