Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca48a4f0fb | ||
|
|
98ee5a3d87 | ||
|
|
67480e5a1c | ||
|
|
2581a9b54c | ||
|
|
14a293e124 | ||
|
|
780419ecae | ||
|
|
f0962e2d9c | ||
|
|
3a9584a419 | ||
|
|
196f42cbff | ||
|
|
322385f6b1 | ||
|
|
b7446cd7b9 | ||
|
|
f618e569ab | ||
|
|
7b394b91e2 | ||
|
|
6a7983a4ea | ||
|
|
737146fca1 | ||
|
|
688f3fd12f | ||
|
|
145df08444 | ||
|
|
8b400515ea | ||
|
|
289797f56d | ||
|
|
be0811ecc3 | ||
|
|
0676bcd4fd | ||
|
|
d076def561 | ||
|
|
e0807d7317 | ||
|
|
fa2723f2d0 | ||
|
|
87d62514db | ||
|
|
2f8cf9146b | ||
|
|
8e0ec6b037 | ||
|
|
6dc434cb83 | ||
|
|
d972c27f03 | ||
|
|
9e2bb63688 | ||
|
|
49053b66a9 | ||
|
|
47497aef07 | ||
|
|
8455029de1 | ||
|
|
9f07f89384 | ||
|
|
d840d43e8f | ||
|
|
9ead2f3dfb | ||
|
|
f3742ddbb8 | ||
|
|
b61a841aa8 | ||
|
|
ebcf11e574 | ||
|
|
065f0aaddf | ||
|
|
c0773dc7c5 | ||
|
|
1c3c74bd36 | ||
|
|
79bbf90b72 | ||
|
|
226a4a7f36 | ||
|
|
df3b424830 | ||
|
|
3cfd9d80bc | ||
|
|
e0553b8d2c | ||
|
|
391c837b37 | ||
|
|
5773d9d1a3 | ||
|
|
ce611963c3 | ||
|
|
f865cacfb8 | ||
|
|
2ec0611f42 | ||
|
|
334161a30e | ||
|
|
dbb6e55226 | ||
|
|
d0f9260559 | ||
|
|
d2176064e1 | ||
|
|
ed8d277e49 | ||
|
|
59b3268c64 | ||
|
|
d043f67761 | ||
|
|
51bf193889 | ||
|
|
f8b78f08b4 | ||
|
|
a4f32d602b | ||
|
|
dc3dd21cf3 | ||
|
|
b4c2fcccf5 | ||
|
|
e950ad5306 | ||
|
|
8ca713b737 | ||
|
|
5b54554fd5 | ||
|
|
4bc651f958 | ||
|
|
3b6976a9c8 | ||
|
|
863d5c1e8e | ||
|
|
97e19e9677 | ||
|
|
b27851461f | ||
|
|
209687377a | ||
|
|
90face1c09 | ||
|
|
936e2ce48b | ||
|
|
16ee8ee379 | ||
|
|
ac39308dad | ||
|
|
346b49219d | ||
|
|
d84c1f20c7 | ||
|
|
dfb8777555 | ||
|
|
008af18156 | ||
|
|
ab23167f80 | ||
|
|
b17ec46463 | ||
|
|
2e26b58d16 | ||
|
|
31b56e5a05 | ||
|
|
47c401cf25 | ||
|
|
fab8dc9e6f | ||
|
|
f39a2b1f16 | ||
|
|
66830ced4e | ||
|
|
9d3fad754d | ||
|
|
dcd3131f58 | ||
|
|
3d02e07161 | ||
|
|
4dbc6a43a6 | ||
|
|
5394b5188b | ||
|
|
8e680b3957 | ||
|
|
1b8cd796d6 | ||
|
|
35fba793d0 | ||
|
|
5358d43b74 | ||
|
|
f777347bac | ||
|
|
17c8b914df | ||
|
|
43b467dd12 | ||
|
|
0e0770921e | ||
|
|
8edbb74352 | ||
|
|
e6bfa95758 | ||
|
|
e4120b6287 | ||
|
|
ccbc9e00f2 | ||
|
|
7d13baadc8 | ||
|
|
9acc83697f | ||
|
|
db24bf87c0 | ||
|
|
f4c0d2d2fd | ||
|
|
d240f4c676 | ||
|
|
9c90cdbe08 | ||
|
|
fc7af31fe5 | ||
|
|
2f8d23ec66 | ||
|
|
77ae3fb9b9 | ||
|
|
4e7f6c47fd | ||
|
|
50469ed750 | ||
|
|
aaab785493 | ||
|
|
9751937894 | ||
|
|
0fc8dfc77e | ||
|
|
81b7df61ec | ||
|
|
8217b96d4a | ||
|
|
7dd0918d32 |
@@ -0,0 +1,32 @@
|
||||
name: stats
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 12 * * *" # Run daily at 12:00 UTC
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
|
||||
jobs:
|
||||
stats:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Run stats script
|
||||
run: bun scripts/stats.ts
|
||||
|
||||
- name: Commit stats
|
||||
run: |
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git add STATS.md
|
||||
git diff --staged --quiet || git commit -m "Update download stats $(date -I)"
|
||||
git push
|
||||
@@ -1,9 +1,9 @@
|
||||
<p align="center">
|
||||
<a href="https://opencode.ai">
|
||||
<picture>
|
||||
<source srcset="packages/web/src/assets/logo-dark.svg" media="(prefers-color-scheme: dark)">
|
||||
<source srcset="packages/web/src/assets/logo-light.svg" media="(prefers-color-scheme: light)">
|
||||
<img src="packages/web/src/assets/logo-light.svg" alt="opencode logo">
|
||||
<source srcset="packages/web/src/assets/logo-ornate-dark.svg" media="(prefers-color-scheme: dark)">
|
||||
<source srcset="packages/web/src/assets/logo-ornate-light.svg" media="(prefers-color-scheme: light)">
|
||||
<img src="packages/web/src/assets/logo-ornate-light.svg" alt="opencode logo">
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
@@ -14,7 +14,7 @@
|
||||
<a href="https://github.com/sst/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/sst/opencode/publish.yml?style=flat-square&branch=dev" /></a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
[](https://opencode.ai)
|
||||
|
||||
---
|
||||
|
||||
@@ -54,14 +54,7 @@ $ bun run packages/opencode/src/index.ts
|
||||
|
||||
#### Development Notes
|
||||
|
||||
**API Client Generation**: After making changes to the TypeScript API endpoints in `packages/opencode/src/server/server.ts`, you need to regenerate the Go client and OpenAPI specification:
|
||||
|
||||
```bash
|
||||
$ cd packages/tui
|
||||
$ go generate ./pkg/client/
|
||||
```
|
||||
|
||||
This updates the generated Go client code that the TUI uses to communicate with the backend server.
|
||||
**API Client**: After making changes to the TypeScript API endpoints in `packages/opencode/src/server/server.ts`, you will need the opencode team to generate a new stainless sdk for the clients.
|
||||
|
||||
### FAQ
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Download Stats
|
||||
|
||||
| Date | GitHub Downloads | npm Downloads | Total |
|
||||
| ---------- | ---------------- | ---------------- | ---------------- |
|
||||
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
|
||||
@@ -19,7 +19,10 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "0.0.0",
|
||||
"version": "0.0.5",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
"dependencies": {
|
||||
"@clack/prompts": "0.11.0",
|
||||
"@flystorage/file-storage": "1.1.0",
|
||||
|
||||
+17
-1
@@ -1,3 +1,19 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json"
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"experimental": {
|
||||
"hook": {
|
||||
"file_edited": {
|
||||
".json": [
|
||||
{
|
||||
"command": ["bun", "run", "prettier", "$FILE"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"session_completed": [
|
||||
{
|
||||
"command": ["touch", "./node_modules/foo"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+8
-8
@@ -6,20 +6,20 @@
|
||||
import "sst"
|
||||
declare module "sst" {
|
||||
export interface Resource {
|
||||
"Web": {
|
||||
"type": "sst.cloudflare.Astro"
|
||||
"url": string
|
||||
Web: {
|
||||
type: "sst.cloudflare.Astro"
|
||||
url: string
|
||||
}
|
||||
}
|
||||
}
|
||||
// cloudflare
|
||||
import * as cloudflare from "@cloudflare/workers-types";
|
||||
// cloudflare
|
||||
import * as cloudflare from "@cloudflare/workers-types"
|
||||
declare module "sst" {
|
||||
export interface Resource {
|
||||
"Api": cloudflare.Service
|
||||
"Bucket": cloudflare.R2Bucket
|
||||
Api: cloudflare.Service
|
||||
Bucket: cloudflare.R2Bucket
|
||||
}
|
||||
}
|
||||
|
||||
import "sst"
|
||||
export {}
|
||||
export {}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
- **Typecheck**: `bun run typecheck` (npm run typecheck)
|
||||
- **Test**: `bun test` (runs all tests)
|
||||
- **Single test**: `bun test test/tool/tool.test.ts` (specific test file)
|
||||
- **API Client Generation**: `cd packages/tui && go generate ./pkg/client/` (after changes to server endpoints)
|
||||
|
||||
## Code Style
|
||||
|
||||
@@ -38,4 +37,4 @@
|
||||
- **Validation**: All inputs validated with Zod schemas
|
||||
- **Logging**: Use `Log.create({ service: "name" })` pattern
|
||||
- **Storage**: Use `Storage` namespace for persistence
|
||||
- **API Client**: Go TUI communicates with TypeScript server via generated client. When adding/modifying server endpoints in `packages/opencode/src/server/server.ts`, run `cd packages/tui && go generate ./pkg/client/` to update the Go client code and OpenAPI spec.
|
||||
- **API Client**: Go TUI communicates with TypeScript server via stainless SDK. When adding/modifying server endpoints in `packages/opencode/src/server/server.ts`, ask the user to generate a new client SDK to proceed with client-side changes.
|
||||
|
||||
@@ -183,6 +183,9 @@
|
||||
"temperature": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"tool_call": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"cost": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -199,10 +202,7 @@
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"input",
|
||||
"output"
|
||||
],
|
||||
"required": ["input", "output"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"limit": {
|
||||
@@ -215,14 +215,15 @@
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"context",
|
||||
"output"
|
||||
],
|
||||
"required": ["context", "output"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"options": {
|
||||
"type": "object",
|
||||
"additionalProperties": {}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -233,9 +234,7 @@
|
||||
"additionalProperties": {}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"models"
|
||||
],
|
||||
"required": ["models"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"description": "Custom provider configurations and model overrides"
|
||||
@@ -265,12 +264,13 @@
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Environment variables to set when running the MCP server"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Enable or disable the MCP server on startup"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"command"
|
||||
],
|
||||
"required": ["type", "command"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
@@ -284,19 +284,79 @@
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "URL of the remote MCP server"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Enable or disable the MCP server on startup"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"url"
|
||||
],
|
||||
"required": ["type", "url"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "MCP (Model Context Protocol) server configurations"
|
||||
},
|
||||
"experimental": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"hook": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file_edited": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"environment": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["command"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"session_completed": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"environment": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["command"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "0.0.0",
|
||||
"version": "0.0.5",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
|
||||
@@ -142,7 +142,7 @@ if (!snapshot) {
|
||||
"# Maintainer: dax",
|
||||
"# Maintainer: adam",
|
||||
"",
|
||||
"pkgname='opencode-bin'",
|
||||
"pkgname='${pkg}'",
|
||||
`pkgver=${version.split("-")[0]}`,
|
||||
"options=('!debug' '!strip')",
|
||||
"pkgrel=1",
|
||||
@@ -166,14 +166,17 @@ if (!snapshot) {
|
||||
"",
|
||||
].join("\n")
|
||||
|
||||
await $`rm -rf ./dist/aur-opencode-bin`
|
||||
|
||||
await $`git clone ssh://aur@aur.archlinux.org/opencode-bin.git ./dist/aur-opencode-bin`
|
||||
await Bun.file("./dist/aur-opencode-bin/PKGBUILD").write(pkgbuild)
|
||||
await $`cd ./dist/aur-opencode-bin && makepkg --printsrcinfo > .SRCINFO`
|
||||
await $`cd ./dist/aur-opencode-bin && git add PKGBUILD .SRCINFO`
|
||||
await $`cd ./dist/aur-opencode-bin && git commit -m "Update to v${version}"`
|
||||
if (!dry) await $`cd ./dist/aur-opencode-bin && git push`
|
||||
for (const pkg of ["opencode", "opencode-bin"]) {
|
||||
await $`rm -rf ./dist/aur-${pkg}`
|
||||
await $`git clone ssh://aur@aur.archlinux.org/${pkg}.git ./dist/aur-${pkg}`
|
||||
await Bun.file(`./dist/aur-${pkg}/PKGBUILD`).write(
|
||||
pkgbuild.replace("${pkg}", pkg),
|
||||
)
|
||||
await $`cd ./dist/aur-${pkg} && makepkg --printsrcinfo > .SRCINFO`
|
||||
await $`cd ./dist/aur-${pkg} && git add PKGBUILD .SRCINFO`
|
||||
await $`cd ./dist/aur-${pkg} && git commit -m "Update to v${version}"`
|
||||
if (!dry) await $`cd ./dist/aur-${pkg} && git push`
|
||||
}
|
||||
|
||||
// Homebrew formula
|
||||
const homebrewFormula = [
|
||||
|
||||
@@ -13,6 +13,7 @@ export namespace App {
|
||||
export const Info = z
|
||||
.object({
|
||||
user: z.string(),
|
||||
hostname: z.string(),
|
||||
git: z.boolean(),
|
||||
path: z.object({
|
||||
config: z.string(),
|
||||
@@ -26,15 +27,25 @@ export namespace App {
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
ref: "App.Info",
|
||||
ref: "App",
|
||||
})
|
||||
export type Info = z.infer<typeof Info>
|
||||
|
||||
const ctx = Context.create<Awaited<ReturnType<typeof create>>>("app")
|
||||
const ctx = Context.create<{
|
||||
info: Info
|
||||
services: Map<any, { state: any; shutdown?: (input: any) => Promise<void> }>
|
||||
}>("app")
|
||||
|
||||
const APP_JSON = "app.json"
|
||||
|
||||
async function create(input: { cwd: string }) {
|
||||
export type Input = {
|
||||
cwd: string
|
||||
}
|
||||
|
||||
export async function provide<T>(
|
||||
input: Input,
|
||||
cb: (app: App.Info) => Promise<T>,
|
||||
) {
|
||||
log.info("creating", {
|
||||
cwd: input.cwd,
|
||||
})
|
||||
@@ -62,8 +73,11 @@ export namespace App {
|
||||
}
|
||||
>()
|
||||
|
||||
const root = git ?? input.cwd
|
||||
|
||||
const info: Info = {
|
||||
user: os.userInfo().username,
|
||||
hostname: os.hostname(),
|
||||
time: {
|
||||
initialized: state.initialized,
|
||||
},
|
||||
@@ -72,16 +86,24 @@ export namespace App {
|
||||
config: Global.Path.config,
|
||||
state: Global.Path.state,
|
||||
data,
|
||||
root: git ?? input.cwd,
|
||||
root,
|
||||
cwd: input.cwd,
|
||||
},
|
||||
}
|
||||
const result = {
|
||||
const app = {
|
||||
services,
|
||||
info,
|
||||
}
|
||||
|
||||
return result
|
||||
return ctx.provide(app, async () => {
|
||||
const result = await cb(app.info)
|
||||
for (const [key, entry] of app.services.entries()) {
|
||||
if (!entry.shutdown) continue
|
||||
log.info("shutdown", { name: key })
|
||||
await entry.shutdown?.(await entry.state)
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
export function state<State>(
|
||||
@@ -107,22 +129,6 @@ export namespace App {
|
||||
return ctx.use().info
|
||||
}
|
||||
|
||||
export async function provide<T>(
|
||||
input: { cwd: string },
|
||||
cb: (app: Info) => Promise<T>,
|
||||
) {
|
||||
const app = await create(input)
|
||||
return ctx.provide(app, async () => {
|
||||
const result = await cb(app.info)
|
||||
for (const [key, entry] of app.services.entries()) {
|
||||
if (!entry.shutdown) continue
|
||||
log.info("shutdown", { name: key })
|
||||
await entry.shutdown?.(await entry.state)
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
export async function initialize() {
|
||||
const { info } = ctx.use()
|
||||
info.time.initialized = Date.now()
|
||||
@@ -142,4 +148,3 @@ export namespace App {
|
||||
.replace(/[^A-Za-z0-9_]/g, "-")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Global } from "../global"
|
||||
import { Log } from "../util/log"
|
||||
import path from "path"
|
||||
import { NamedError } from "../util/error"
|
||||
import { readableStreamToText } from "bun"
|
||||
|
||||
export namespace BunProc {
|
||||
const log = Log.create({ service: "bun" })
|
||||
@@ -25,11 +26,9 @@ export namespace BunProc {
|
||||
BUN_BE_BUN: "1",
|
||||
},
|
||||
})
|
||||
const code = await result.exited
|
||||
// @ts-ignore
|
||||
const stdout = await result.stdout.text()
|
||||
// @ts-ignore
|
||||
const stderr = await result.stderr.text()
|
||||
const code = await result.exited;
|
||||
const stdout = result.stdout ? typeof result.stdout === "number" ? result.stdout : await readableStreamToText(result.stdout) : undefined
|
||||
const stderr = result.stderr ? typeof result.stderr === "number" ? result.stderr : await readableStreamToText(result.stderr) : undefined
|
||||
log.info("done", {
|
||||
code,
|
||||
stdout,
|
||||
@@ -65,7 +64,7 @@ export namespace BunProc {
|
||||
await BunProc.run(["install", "--registry=https://registry.npmjs.org"], {
|
||||
cwd: Global.Path.cache,
|
||||
}).catch((e) => {
|
||||
new InstallFailedError(
|
||||
throw new InstallFailedError(
|
||||
{ pkg, version },
|
||||
{
|
||||
cause: e,
|
||||
|
||||
@@ -49,7 +49,7 @@ export namespace Bus {
|
||||
)
|
||||
}
|
||||
|
||||
export function publish<Definition extends EventDefinition>(
|
||||
export async function publish<Definition extends EventDefinition>(
|
||||
def: Definition,
|
||||
properties: z.output<Definition["properties"]>,
|
||||
) {
|
||||
@@ -60,12 +60,14 @@ export namespace Bus {
|
||||
log.info("publishing", {
|
||||
type: def.type,
|
||||
})
|
||||
const pending = []
|
||||
for (const key of [def.type, "*"]) {
|
||||
const match = state().subscriptions.get(key)
|
||||
for (const sub of match ?? []) {
|
||||
sub(payload)
|
||||
pending.push(sub(payload))
|
||||
}
|
||||
}
|
||||
return Promise.all(pending)
|
||||
}
|
||||
|
||||
export function subscribe<Definition extends EventDefinition>(
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { App } from "../app/app"
|
||||
import { ConfigHooks } from "../config/hooks"
|
||||
import { Format } from "../format"
|
||||
import { Share } from "../share/share"
|
||||
|
||||
export async function bootstrap<T>(
|
||||
input: App.Input,
|
||||
cb: (app: App.Info) => Promise<T>,
|
||||
) {
|
||||
return App.provide(input, async (app) => {
|
||||
Share.init()
|
||||
Format.init()
|
||||
ConfigHooks.init()
|
||||
|
||||
return cb(app)
|
||||
})
|
||||
}
|
||||
@@ -7,6 +7,9 @@ import open from "open"
|
||||
import { UI } from "../ui"
|
||||
import { ModelsDev } from "../../provider/models"
|
||||
import { map, pipe, sortBy, values } from "remeda"
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
import { Global } from "../../global"
|
||||
|
||||
export const AuthCommand = cmd({
|
||||
command: "auth",
|
||||
@@ -26,16 +29,46 @@ export const AuthListCommand = cmd({
|
||||
describe: "list providers",
|
||||
async handler() {
|
||||
UI.empty()
|
||||
prompts.intro("Credentials")
|
||||
const authPath = path.join(Global.Path.data, "auth.json")
|
||||
const homedir = os.homedir()
|
||||
const displayPath = authPath.startsWith(homedir)
|
||||
? authPath.replace(homedir, "~")
|
||||
: authPath
|
||||
prompts.intro(`Credentials ${UI.Style.TEXT_DIM}${displayPath}`)
|
||||
const results = await Auth.all().then((x) => Object.entries(x))
|
||||
const database = await ModelsDev.get()
|
||||
|
||||
for (const [providerID, result] of results) {
|
||||
const name = database[providerID]?.name || providerID
|
||||
prompts.log.info(`${name} ${UI.Style.TEXT_DIM}(${result.type})`)
|
||||
prompts.log.info(`${name} ${UI.Style.TEXT_DIM}${result.type}`)
|
||||
}
|
||||
|
||||
prompts.outro(`${results.length} credentials`)
|
||||
|
||||
// Environment variables section
|
||||
const activeEnvVars: Array<{ provider: string; envVar: string }> = []
|
||||
|
||||
for (const [providerID, provider] of Object.entries(database)) {
|
||||
for (const envVar of provider.env) {
|
||||
if (process.env[envVar]) {
|
||||
activeEnvVars.push({
|
||||
provider: provider.name || providerID,
|
||||
envVar,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (activeEnvVars.length > 0) {
|
||||
UI.empty()
|
||||
prompts.intro("Environment")
|
||||
|
||||
for (const { provider, envVar } of activeEnvVars) {
|
||||
prompts.log.info(`${provider} ${UI.Style.TEXT_DIM}${envVar}`)
|
||||
}
|
||||
|
||||
prompts.outro(`${activeEnvVars.length} environment variables`)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { App } from "../../app/app"
|
||||
import { Provider } from "../../provider/provider"
|
||||
import { cmd } from "./cmd"
|
||||
|
||||
export const ModelsCommand = cmd({
|
||||
command: "models",
|
||||
describe: "list all available models",
|
||||
handler: async () => {
|
||||
await App.provide({ cwd: process.cwd() }, async () => {
|
||||
const providers = await Provider.list()
|
||||
|
||||
for (const [providerID, provider] of Object.entries(providers)) {
|
||||
for (const modelID of Object.keys(provider.info.models)) {
|
||||
console.log(`${providerID}/${modelID}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -1,14 +1,13 @@
|
||||
import type { Argv } from "yargs"
|
||||
import { App } from "../../app/app"
|
||||
import { Bus } from "../../bus"
|
||||
import { Provider } from "../../provider/provider"
|
||||
import { Session } from "../../session"
|
||||
import { Share } from "../../share/share"
|
||||
import { Message } from "../../session/message"
|
||||
import { UI } from "../ui"
|
||||
import { cmd } from "./cmd"
|
||||
import { Flag } from "../../flag/flag"
|
||||
import { Config } from "../../config/config"
|
||||
import { bootstrap } from "../bootstrap"
|
||||
|
||||
const TOOL: Record<string, [string, string]> = {
|
||||
todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
|
||||
@@ -56,110 +55,109 @@ export const RunCommand = cmd({
|
||||
},
|
||||
handler: async (args) => {
|
||||
const message = args.message.join(" ")
|
||||
await App.provide(
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
},
|
||||
async () => {
|
||||
await Share.init()
|
||||
const session = await (async () => {
|
||||
if (args.continue) {
|
||||
const first = await Session.list().next()
|
||||
if (first.done) return
|
||||
return first.value
|
||||
}
|
||||
|
||||
if (args.session) return Session.get(args.session)
|
||||
|
||||
return Session.create()
|
||||
})()
|
||||
|
||||
if (!session) {
|
||||
UI.error("Session not found")
|
||||
return
|
||||
await bootstrap({ cwd: process.cwd() }, async () => {
|
||||
const session = await (async () => {
|
||||
if (args.continue) {
|
||||
const first = await Session.list().next()
|
||||
if (first.done) return
|
||||
return first.value
|
||||
}
|
||||
|
||||
UI.empty()
|
||||
UI.println(UI.logo())
|
||||
UI.empty()
|
||||
UI.println(UI.Style.TEXT_NORMAL_BOLD + "> ", message)
|
||||
UI.empty()
|
||||
if (args.session) return Session.get(args.session)
|
||||
|
||||
const cfg = await Config.get()
|
||||
if (cfg.autoshare || Flag.OPENCODE_AUTO_SHARE || args.share) {
|
||||
await Session.share(session.id)
|
||||
UI.println(
|
||||
UI.Style.TEXT_INFO_BOLD +
|
||||
return Session.create()
|
||||
})()
|
||||
|
||||
if (!session) {
|
||||
UI.error("Session not found")
|
||||
return
|
||||
}
|
||||
|
||||
const isPiped = !process.stdout.isTTY
|
||||
|
||||
UI.empty()
|
||||
UI.println(UI.logo())
|
||||
UI.empty()
|
||||
UI.println(UI.Style.TEXT_NORMAL_BOLD + "> ", message)
|
||||
UI.empty()
|
||||
|
||||
const cfg = await Config.get()
|
||||
if (cfg.autoshare || Flag.OPENCODE_AUTO_SHARE || args.share) {
|
||||
await Session.share(session.id)
|
||||
UI.println(
|
||||
UI.Style.TEXT_INFO_BOLD +
|
||||
"~ https://opencode.ai/s/" +
|
||||
session.id.slice(-8),
|
||||
)
|
||||
}
|
||||
UI.empty()
|
||||
|
||||
const { providerID, modelID } = args.model
|
||||
? Provider.parseModel(args.model)
|
||||
: await Provider.defaultModel()
|
||||
UI.println(
|
||||
UI.Style.TEXT_NORMAL_BOLD + "@ ",
|
||||
UI.Style.TEXT_NORMAL + `${providerID}/${modelID}`,
|
||||
)
|
||||
UI.empty()
|
||||
}
|
||||
UI.empty()
|
||||
|
||||
function printEvent(color: string, type: string, title: string) {
|
||||
UI.println(
|
||||
color + `|`,
|
||||
UI.Style.TEXT_NORMAL +
|
||||
UI.Style.TEXT_DIM +
|
||||
` ${type.padEnd(7, " ")}`,
|
||||
"",
|
||||
UI.Style.TEXT_NORMAL + title,
|
||||
)
|
||||
const { providerID, modelID } = args.model
|
||||
? Provider.parseModel(args.model)
|
||||
: await Provider.defaultModel()
|
||||
UI.println(
|
||||
UI.Style.TEXT_NORMAL_BOLD + "@ ",
|
||||
UI.Style.TEXT_NORMAL + `${providerID}/${modelID}`,
|
||||
)
|
||||
UI.empty()
|
||||
|
||||
function printEvent(color: string, type: string, title: string) {
|
||||
UI.println(
|
||||
color + `|`,
|
||||
UI.Style.TEXT_NORMAL + UI.Style.TEXT_DIM + ` ${type.padEnd(7, " ")}`,
|
||||
"",
|
||||
UI.Style.TEXT_NORMAL + title,
|
||||
)
|
||||
}
|
||||
|
||||
Bus.subscribe(Message.Event.PartUpdated, async (evt) => {
|
||||
if (evt.properties.sessionID !== session.id) return
|
||||
const part = evt.properties.part
|
||||
const message = await Session.getMessage(
|
||||
evt.properties.sessionID,
|
||||
evt.properties.messageID,
|
||||
)
|
||||
|
||||
if (
|
||||
part.type === "tool-invocation" &&
|
||||
part.toolInvocation.state === "result"
|
||||
) {
|
||||
const metadata = message.metadata.tool[part.toolInvocation.toolCallId]
|
||||
const [tool, color] = TOOL[part.toolInvocation.toolName] ?? [
|
||||
part.toolInvocation.toolName,
|
||||
UI.Style.TEXT_INFO_BOLD,
|
||||
]
|
||||
printEvent(color, tool, metadata?.title || "Unknown")
|
||||
}
|
||||
|
||||
Bus.subscribe(Message.Event.PartUpdated, async (evt) => {
|
||||
if (evt.properties.sessionID !== session.id) return
|
||||
const part = evt.properties.part
|
||||
const message = await Session.getMessage(
|
||||
evt.properties.sessionID,
|
||||
evt.properties.messageID,
|
||||
)
|
||||
|
||||
if (
|
||||
part.type === "tool-invocation" &&
|
||||
part.toolInvocation.state === "result"
|
||||
) {
|
||||
const metadata =
|
||||
message.metadata.tool[part.toolInvocation.toolCallId]
|
||||
const [tool, color] = TOOL[part.toolInvocation.toolName] ?? [
|
||||
part.toolInvocation.toolName,
|
||||
UI.Style.TEXT_INFO_BOLD,
|
||||
]
|
||||
printEvent(color, tool, metadata.title)
|
||||
if (part.type === "text") {
|
||||
if (part.text.includes("\n")) {
|
||||
UI.empty()
|
||||
UI.println(part.text)
|
||||
UI.empty()
|
||||
return
|
||||
}
|
||||
printEvent(UI.Style.TEXT_NORMAL_BOLD, "Text", part.text)
|
||||
}
|
||||
})
|
||||
|
||||
if (part.type === "text") {
|
||||
if (part.text.includes("\n")) {
|
||||
UI.empty()
|
||||
UI.println(part.text)
|
||||
UI.empty()
|
||||
return
|
||||
}
|
||||
printEvent(UI.Style.TEXT_NORMAL_BOLD, "Text", part.text)
|
||||
}
|
||||
})
|
||||
await Session.chat({
|
||||
sessionID: session.id,
|
||||
providerID,
|
||||
modelID,
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: message,
|
||||
},
|
||||
],
|
||||
})
|
||||
UI.empty()
|
||||
},
|
||||
)
|
||||
const result = await Session.chat({
|
||||
sessionID: session.id,
|
||||
providerID,
|
||||
modelID,
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: message,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
if (isPiped) {
|
||||
const match = result.parts.findLast((x) => x.type === "text")
|
||||
if (match) process.stdout.write(match.text)
|
||||
}
|
||||
UI.empty()
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -7,12 +7,9 @@ export const ScrapCommand = cmd({
|
||||
builder: (yargs) =>
|
||||
yargs.positional("file", { type: "string", demandOption: true }),
|
||||
async handler(args) {
|
||||
await App.provide(
|
||||
{ cwd: process.cwd() },
|
||||
async () => {
|
||||
await LSP.touchFile(args.file, true)
|
||||
console.log(await LSP.diagnostics())
|
||||
},
|
||||
)
|
||||
await App.provide({ cwd: process.cwd() }, async () => {
|
||||
await LSP.touchFile(args.file, true)
|
||||
console.log(await LSP.diagnostics())
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import { Global } from "../../global"
|
||||
import { Provider } from "../../provider/provider"
|
||||
import { Server } from "../../server/server"
|
||||
import { bootstrap } from "../bootstrap"
|
||||
import { UI } from "../ui"
|
||||
import { cmd } from "./cmd"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { Installation } from "../../installation"
|
||||
import { Config } from "../../config/config"
|
||||
import { Bus } from "../../bus"
|
||||
|
||||
export const TuiCommand = cmd({
|
||||
command: "$0 [project]",
|
||||
describe: "start opencode tui",
|
||||
builder: (yargs) =>
|
||||
yargs.positional("project", {
|
||||
type: "string",
|
||||
describe: "path to start opencode in",
|
||||
}),
|
||||
handler: async (args) => {
|
||||
while (true) {
|
||||
const cwd = args.project ? path.resolve(args.project) : process.cwd()
|
||||
try {
|
||||
process.chdir(cwd)
|
||||
} catch (e) {
|
||||
UI.error("Failed to change directory to " + cwd)
|
||||
return
|
||||
}
|
||||
const result = await bootstrap({ cwd }, async (app) => {
|
||||
const providers = await Provider.list()
|
||||
if (Object.keys(providers).length === 0) {
|
||||
return "needs_provider"
|
||||
}
|
||||
|
||||
const server = Server.listen({
|
||||
port: 0,
|
||||
hostname: "127.0.0.1",
|
||||
})
|
||||
|
||||
let cmd = ["go", "run", "./main.go"]
|
||||
let cwd = Bun.fileURLToPath(
|
||||
new URL("../../../../tui/cmd/opencode", import.meta.url),
|
||||
)
|
||||
if (Bun.embeddedFiles.length > 0) {
|
||||
const blob = Bun.embeddedFiles[0] as File
|
||||
let binaryName = blob.name
|
||||
if (process.platform === "win32" && !binaryName.endsWith(".exe")) {
|
||||
binaryName += ".exe"
|
||||
}
|
||||
const binary = path.join(Global.Path.cache, "tui", binaryName)
|
||||
const file = Bun.file(binary)
|
||||
if (!(await file.exists())) {
|
||||
await Bun.write(file, blob, { mode: 0o755 })
|
||||
await fs.chmod(binary, 0o755)
|
||||
}
|
||||
cwd = process.cwd()
|
||||
cmd = [binary]
|
||||
}
|
||||
const proc = Bun.spawn({
|
||||
cmd: [...cmd, ...process.argv.slice(2)],
|
||||
cwd,
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
stdin: "inherit",
|
||||
env: {
|
||||
...process.env,
|
||||
OPENCODE_SERVER: server.url.toString(),
|
||||
OPENCODE_APP_INFO: JSON.stringify(app),
|
||||
},
|
||||
onExit: () => {
|
||||
server.stop()
|
||||
},
|
||||
})
|
||||
|
||||
;(async () => {
|
||||
if (Installation.VERSION === "dev") return
|
||||
if (Installation.isSnapshot()) return
|
||||
const config = await Config.global()
|
||||
if (config.autoupdate === false) return
|
||||
const latest = await Installation.latest().catch(() => {})
|
||||
if (!latest) return
|
||||
if (Installation.VERSION === latest) return
|
||||
const method = await Installation.method()
|
||||
if (method === "unknown") return
|
||||
await Installation.upgrade(method, latest)
|
||||
.then(() => {
|
||||
Bus.publish(Installation.Event.Updated, { version: latest })
|
||||
})
|
||||
.catch(() => {})
|
||||
})()
|
||||
|
||||
await proc.exited
|
||||
server.stop()
|
||||
|
||||
return "done"
|
||||
})
|
||||
if (result === "done") break
|
||||
if (result === "needs_provider") {
|
||||
UI.empty()
|
||||
UI.println(UI.logo(" "))
|
||||
const result = await Bun.spawn({
|
||||
cmd: [process.execPath, "auth", "login"],
|
||||
cwd: process.cwd(),
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
stdin: "inherit",
|
||||
}).exited
|
||||
if (result !== 0) return
|
||||
UI.empty()
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Config } from "../config/config"
|
||||
import { MCP } from "../mcp"
|
||||
import { UI } from "./ui"
|
||||
|
||||
export function FormatError(input: unknown) {
|
||||
if (MCP.Failed.isInstance(input))
|
||||
@@ -13,4 +14,6 @@ export function FormatError(input: unknown) {
|
||||
(issue) => "↳ " + issue.message + " " + issue.path.join("."),
|
||||
) ?? []),
|
||||
].join("\n")
|
||||
|
||||
if (UI.CancelledError.isInstance(input)) return ""
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ export namespace Config {
|
||||
}
|
||||
}
|
||||
log.info("loaded", result)
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
@@ -36,20 +37,28 @@ export namespace Config {
|
||||
.record(z.string(), z.string())
|
||||
.optional()
|
||||
.describe("Environment variables to set when running the MCP server"),
|
||||
enabled: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Enable or disable the MCP server on startup"),
|
||||
})
|
||||
.strict()
|
||||
.openapi({
|
||||
ref: "Config.McpLocal",
|
||||
ref: "McpLocalConfig",
|
||||
})
|
||||
|
||||
export const McpRemote = z
|
||||
.object({
|
||||
type: z.literal("remote").describe("Type of MCP server connection"),
|
||||
url: z.string().describe("URL of the remote MCP server"),
|
||||
enabled: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Enable or disable the MCP server on startup"),
|
||||
})
|
||||
.strict()
|
||||
.openapi({
|
||||
ref: "Config.McpRemote",
|
||||
ref: "McpRemoteConfig",
|
||||
})
|
||||
|
||||
export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote])
|
||||
@@ -123,7 +132,7 @@ export namespace Config {
|
||||
})
|
||||
.strict()
|
||||
.openapi({
|
||||
ref: "Config.Keybinds",
|
||||
ref: "KeybindsConfig",
|
||||
})
|
||||
export const Info = z
|
||||
.object({
|
||||
@@ -167,10 +176,36 @@ export namespace Config {
|
||||
.record(z.string(), Mcp)
|
||||
.optional()
|
||||
.describe("MCP (Model Context Protocol) server configurations"),
|
||||
experimental: z
|
||||
.object({
|
||||
hook: z
|
||||
.object({
|
||||
file_edited: z
|
||||
.record(
|
||||
z.string(),
|
||||
z
|
||||
.object({
|
||||
command: z.string().array(),
|
||||
environment: z.record(z.string(), z.string()).optional(),
|
||||
})
|
||||
.array(),
|
||||
)
|
||||
.optional(),
|
||||
session_completed: z
|
||||
.object({
|
||||
command: z.string().array(),
|
||||
environment: z.record(z.string(), z.string()).optional(),
|
||||
})
|
||||
.array()
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.openapi({
|
||||
ref: "Config.Info",
|
||||
ref: "Config",
|
||||
})
|
||||
|
||||
export type Info = z.output<typeof Info>
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { App } from "../app/app"
|
||||
import { Bus } from "../bus"
|
||||
import { File } from "../file"
|
||||
import { Session } from "../session"
|
||||
import { Log } from "../util/log"
|
||||
import { Config } from "./config"
|
||||
import path from "path"
|
||||
|
||||
export namespace ConfigHooks {
|
||||
const log = Log.create({ service: "config.hooks" })
|
||||
|
||||
export function init() {
|
||||
log.info("init")
|
||||
const app = App.info()
|
||||
|
||||
Bus.subscribe(File.Event.Edited, async (payload) => {
|
||||
const cfg = await Config.get()
|
||||
const ext = path.extname(payload.properties.file)
|
||||
for (const item of cfg.experimental?.hook?.file_edited?.[ext] ?? []) {
|
||||
log.info("file_edited", {
|
||||
file: payload.properties.file,
|
||||
command: item.command,
|
||||
})
|
||||
Bun.spawn({
|
||||
cmd: item.command.map((x) =>
|
||||
x.replace("$FILE", payload.properties.file),
|
||||
),
|
||||
env: item.environment,
|
||||
cwd: app.path.cwd,
|
||||
stdout: "ignore",
|
||||
stderr: "ignore",
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
Bus.subscribe(Session.Event.Idle, async () => {
|
||||
const cfg = await Config.get()
|
||||
if (cfg.experimental?.hook?.session_completed) {
|
||||
for (const item of cfg.experimental.hook.session_completed) {
|
||||
log.info("session_completed", {
|
||||
command: item.command,
|
||||
})
|
||||
Bun.spawn({
|
||||
cmd: item.command,
|
||||
cwd: App.info().path.cwd,
|
||||
env: item.environment,
|
||||
stdout: "ignore",
|
||||
stderr: "ignore",
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { z } from "zod"
|
||||
import { Bus } from "../bus"
|
||||
|
||||
export namespace File {
|
||||
export const Event = {
|
||||
Edited: Bus.event(
|
||||
"file.edited",
|
||||
z.object({
|
||||
file: z.string(),
|
||||
}),
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { App } from "../../app/app"
|
||||
import { App } from "../app/app"
|
||||
|
||||
export namespace FileTimes {
|
||||
export namespace FileTime {
|
||||
export const state = App.state("tool.filetimes", () => {
|
||||
const read: {
|
||||
[sessionID: string]: {
|
||||
@@ -0,0 +1,165 @@
|
||||
import { App } from "../app/app"
|
||||
import { BunProc } from "../bun"
|
||||
import { Bus } from "../bus"
|
||||
import { File } from "../file"
|
||||
import { Log } from "../util/log"
|
||||
import path from "path"
|
||||
|
||||
export namespace Format {
|
||||
const log = Log.create({ service: "format" })
|
||||
|
||||
const state = App.state("format", () => {
|
||||
const enabled: Record<string, boolean> = {}
|
||||
|
||||
return {
|
||||
enabled,
|
||||
}
|
||||
})
|
||||
|
||||
async function isEnabled(item: Definition) {
|
||||
const s = state()
|
||||
let status = s.enabled[item.name]
|
||||
if (status === undefined) {
|
||||
status = await item.enabled()
|
||||
s.enabled[item.name] = status
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
async function getFormatter(ext: string) {
|
||||
const result = []
|
||||
for (const item of FORMATTERS) {
|
||||
if (!item.extensions.includes(ext)) continue
|
||||
if (!isEnabled(item)) continue
|
||||
result.push(item)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function init() {
|
||||
log.info("init")
|
||||
Bus.subscribe(File.Event.Edited, async (payload) => {
|
||||
const file = payload.properties.file
|
||||
log.info("formatting", { file })
|
||||
const ext = path.extname(file)
|
||||
|
||||
for (const item of await getFormatter(ext)) {
|
||||
log.info("running", { command: item.command })
|
||||
const proc = Bun.spawn({
|
||||
cmd: item.command.map((x) => x.replace("$FILE", file)),
|
||||
cwd: App.info().path.cwd,
|
||||
env: item.environment,
|
||||
stdout: "ignore",
|
||||
stderr: "ignore",
|
||||
})
|
||||
const exit = await proc.exited
|
||||
if (exit !== 0)
|
||||
log.error("failed", {
|
||||
command: item.command,
|
||||
...item.environment,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
interface Definition {
|
||||
name: string
|
||||
command: string[]
|
||||
environment?: Record<string, string>
|
||||
extensions: string[]
|
||||
enabled(): Promise<boolean>
|
||||
}
|
||||
|
||||
const FORMATTERS: Definition[] = [
|
||||
{
|
||||
name: "prettier",
|
||||
command: [BunProc.which(), "run", "prettier", "--write", "$FILE"],
|
||||
environment: {
|
||||
BUN_BE_BUN: "1",
|
||||
},
|
||||
extensions: [
|
||||
".js",
|
||||
".jsx",
|
||||
".mjs",
|
||||
".cjs",
|
||||
".ts",
|
||||
".tsx",
|
||||
".mts",
|
||||
".cts",
|
||||
".html",
|
||||
".htm",
|
||||
".css",
|
||||
".scss",
|
||||
".sass",
|
||||
".less",
|
||||
".vue",
|
||||
".svelte",
|
||||
".json",
|
||||
".jsonc",
|
||||
".yaml",
|
||||
".yml",
|
||||
".toml",
|
||||
".xml",
|
||||
".md",
|
||||
".mdx",
|
||||
".graphql",
|
||||
".gql",
|
||||
],
|
||||
async enabled() {
|
||||
try {
|
||||
const proc = Bun.spawn({
|
||||
cmd: [BunProc.which(), "run", "prettier", "--version"],
|
||||
cwd: App.info().path.cwd,
|
||||
env: {
|
||||
BUN_BE_BUN: "1",
|
||||
},
|
||||
stdout: "ignore",
|
||||
stderr: "ignore",
|
||||
})
|
||||
const exit = await proc.exited
|
||||
return exit === 0
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mix",
|
||||
command: ["mix", "format", "$FILE"],
|
||||
extensions: [".ex", ".exs", ".eex", ".heex", ".leex", ".neex", ".sface"],
|
||||
async enabled() {
|
||||
try {
|
||||
const proc = Bun.spawn({
|
||||
cmd: ["mix", "--version"],
|
||||
cwd: App.info().path.cwd,
|
||||
stdout: "ignore",
|
||||
stderr: "ignore",
|
||||
})
|
||||
const exit = await proc.exited
|
||||
return exit === 0
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gofmt",
|
||||
command: ["gofmt", "-w", "$FILE"],
|
||||
extensions: [".go"],
|
||||
async enabled() {
|
||||
try {
|
||||
const proc = Bun.spawn({
|
||||
cmd: ["gofmt", "-h"],
|
||||
cwd: App.info().path.cwd,
|
||||
stdout: "ignore",
|
||||
stderr: "ignore",
|
||||
})
|
||||
const exit = await proc.exited
|
||||
return exit === 0
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
+33
-104
@@ -1,30 +1,34 @@
|
||||
import "zod-openapi/extend"
|
||||
import { App } from "./app/app"
|
||||
import { Server } from "./server/server"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { Share } from "./share/share"
|
||||
import url from "node:url"
|
||||
import { Global } from "./global"
|
||||
import yargs from "yargs"
|
||||
import { hideBin } from "yargs/helpers"
|
||||
import { RunCommand } from "./cli/cmd/run"
|
||||
import { GenerateCommand } from "./cli/cmd/generate"
|
||||
import { ScrapCommand } from "./cli/cmd/scrap"
|
||||
import { Log } from "./util/log"
|
||||
import { AuthCommand, AuthLoginCommand } from "./cli/cmd/auth"
|
||||
import { AuthCommand } from "./cli/cmd/auth"
|
||||
import { UpgradeCommand } from "./cli/cmd/upgrade"
|
||||
import { Provider } from "./provider/provider"
|
||||
import { ModelsCommand } from "./cli/cmd/models"
|
||||
import { UI } from "./cli/ui"
|
||||
import { Installation } from "./installation"
|
||||
import { Bus } from "./bus"
|
||||
import { Config } from "./config/config"
|
||||
import { NamedError } from "./util/error"
|
||||
import { FormatError } from "./cli/error"
|
||||
import { ServeCommand } from "./cli/cmd/serve"
|
||||
import { TuiCommand } from "./cli/cmd/tui"
|
||||
|
||||
const cancel = new AbortController()
|
||||
|
||||
process.on("unhandledRejection", (e) => {
|
||||
Log.Default.error("rejection", {
|
||||
e: e instanceof Error ? e.message : e,
|
||||
})
|
||||
})
|
||||
|
||||
process.on("uncaughtException", (e) => {
|
||||
Log.Default.error("exception", {
|
||||
e: e instanceof Error ? e.message : e,
|
||||
})
|
||||
})
|
||||
|
||||
const cli = yargs(hideBin(process.argv))
|
||||
.scriptName("opencode")
|
||||
.help("help", "show help")
|
||||
@@ -42,103 +46,14 @@ const cli = yargs(hideBin(process.argv))
|
||||
})
|
||||
})
|
||||
.usage("\n" + UI.logo())
|
||||
.command({
|
||||
command: "$0 [project]",
|
||||
describe: "start opencode tui",
|
||||
builder: (yargs) =>
|
||||
yargs.positional("project", {
|
||||
type: "string",
|
||||
describe: "path to start opencode in",
|
||||
}),
|
||||
handler: async (args) => {
|
||||
while (true) {
|
||||
const cwd = args.project ? path.resolve(args.project) : process.cwd()
|
||||
process.chdir(cwd)
|
||||
const result = await App.provide({ cwd }, async (app) => {
|
||||
const providers = await Provider.list()
|
||||
if (Object.keys(providers).length === 0) {
|
||||
return "needs_provider"
|
||||
}
|
||||
|
||||
await Share.init()
|
||||
const server = Server.listen({
|
||||
port: 0,
|
||||
})
|
||||
|
||||
let cmd = ["go", "run", "./main.go"]
|
||||
let cwd = url.fileURLToPath(
|
||||
new URL("../../tui/cmd/opencode", import.meta.url),
|
||||
)
|
||||
if (Bun.embeddedFiles.length > 0) {
|
||||
const blob = Bun.embeddedFiles[0] as File
|
||||
let binaryName = blob.name
|
||||
if (process.platform === "win32" && !binaryName.endsWith(".exe")) {
|
||||
binaryName += ".exe"
|
||||
}
|
||||
const binary = path.join(Global.Path.cache, "tui", binaryName)
|
||||
const file = Bun.file(binary)
|
||||
if (!(await file.exists())) {
|
||||
await Bun.write(file, blob, { mode: 0o755 })
|
||||
await fs.chmod(binary, 0o755)
|
||||
}
|
||||
cwd = process.cwd()
|
||||
cmd = [binary]
|
||||
}
|
||||
const proc = Bun.spawn({
|
||||
cmd: [...cmd, ...process.argv.slice(2)],
|
||||
signal: cancel.signal,
|
||||
cwd,
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
stdin: "inherit",
|
||||
env: {
|
||||
...process.env,
|
||||
OPENCODE_SERVER: server.url.toString(),
|
||||
OPENCODE_APP_INFO: JSON.stringify(app),
|
||||
},
|
||||
onExit: () => {
|
||||
server.stop()
|
||||
},
|
||||
})
|
||||
|
||||
;(async () => {
|
||||
if (Installation.VERSION === "dev") return
|
||||
if (Installation.isSnapshot()) return
|
||||
const config = await Config.global()
|
||||
if (config.autoupdate === false) return
|
||||
const latest = await Installation.latest().catch(() => {})
|
||||
if (!latest) return
|
||||
if (Installation.VERSION === latest) return
|
||||
const method = await Installation.method()
|
||||
if (method === "unknown") return
|
||||
await Installation.upgrade(method, latest)
|
||||
.then(() => {
|
||||
Bus.publish(Installation.Event.Updated, { version: latest })
|
||||
})
|
||||
.catch(() => {})
|
||||
})()
|
||||
|
||||
await proc.exited
|
||||
server.stop()
|
||||
|
||||
return "done"
|
||||
})
|
||||
if (result === "done") break
|
||||
if (result === "needs_provider") {
|
||||
UI.empty()
|
||||
UI.println(UI.logo(" "))
|
||||
UI.empty()
|
||||
await AuthLoginCommand.handler(args)
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
.command(TuiCommand)
|
||||
.command(RunCommand)
|
||||
.command(GenerateCommand)
|
||||
.command(ScrapCommand)
|
||||
.command(AuthCommand)
|
||||
.command(UpgradeCommand)
|
||||
.command(ServeCommand)
|
||||
.command(ModelsCommand)
|
||||
.fail((msg) => {
|
||||
if (
|
||||
msg.startsWith("Unknown argument") ||
|
||||
@@ -152,13 +67,14 @@ const cli = yargs(hideBin(process.argv))
|
||||
try {
|
||||
await cli.parse()
|
||||
} catch (e) {
|
||||
const data: Record<string, any> = {}
|
||||
let data: Record<string, any> = {}
|
||||
if (e instanceof NamedError) {
|
||||
const obj = e.toObject()
|
||||
Object.assign(data, {
|
||||
...obj.data,
|
||||
})
|
||||
}
|
||||
|
||||
if (e instanceof Error) {
|
||||
Object.assign(data, {
|
||||
name: e.name,
|
||||
@@ -166,13 +82,26 @@ try {
|
||||
cause: e.cause?.toString(),
|
||||
})
|
||||
}
|
||||
|
||||
if (e instanceof ResolveMessage) {
|
||||
Object.assign(data, {
|
||||
name: e.name,
|
||||
message: e.message,
|
||||
code: e.code,
|
||||
specifier: e.specifier,
|
||||
referrer: e.referrer,
|
||||
position: e.position,
|
||||
importKind: e.importKind,
|
||||
})
|
||||
}
|
||||
Log.Default.error("fatal", data)
|
||||
const formatted = FormatError(e)
|
||||
if (formatted) UI.error(formatted)
|
||||
if (!formatted)
|
||||
if (formatted === undefined)
|
||||
UI.error(
|
||||
"Unexpected error, check log file at " + Log.file() + " for more details",
|
||||
)
|
||||
process.exitCode = 1
|
||||
}
|
||||
|
||||
cancel.abort()
|
||||
|
||||
@@ -26,6 +26,10 @@ export namespace MCP {
|
||||
[name: string]: Awaited<ReturnType<typeof experimental_createMCPClient>>
|
||||
} = {}
|
||||
for (const [key, mcp] of Object.entries(cfg.mcp ?? {})) {
|
||||
if (mcp.enabled === false) {
|
||||
log.info("mcp server disabled", { key })
|
||||
continue
|
||||
}
|
||||
log.info("found", { key, type: mcp.type })
|
||||
if (mcp.type === "remote") {
|
||||
const client = await experimental_createMCPClient({
|
||||
|
||||
@@ -29,7 +29,7 @@ export namespace ModelsDev {
|
||||
options: z.record(z.any()),
|
||||
})
|
||||
.openapi({
|
||||
ref: "Model.Info",
|
||||
ref: "Model",
|
||||
})
|
||||
export type Model = z.infer<typeof Model>
|
||||
|
||||
@@ -43,7 +43,7 @@ export namespace ModelsDev {
|
||||
models: z.record(Model),
|
||||
})
|
||||
.openapi({
|
||||
ref: "Provider.Info",
|
||||
ref: "Provider",
|
||||
})
|
||||
|
||||
export type Provider = z.infer<typeof Provider>
|
||||
|
||||
@@ -23,25 +23,25 @@ import { AuthCopilot } from "../auth/copilot"
|
||||
import { ModelsDev } from "./models"
|
||||
import { NamedError } from "../util/error"
|
||||
import { Auth } from "../auth"
|
||||
import { TaskTool } from "../tool/task"
|
||||
|
||||
export namespace Provider {
|
||||
const log = Log.create({ service: "provider" })
|
||||
|
||||
type CustomLoader = (provider: ModelsDev.Provider) => Promise<
|
||||
| {
|
||||
getModel?: (sdk: any, modelID: string) => Promise<any>
|
||||
options: Record<string, any>
|
||||
}
|
||||
| false
|
||||
>
|
||||
type CustomLoader = (
|
||||
provider: ModelsDev.Provider,
|
||||
api?: string,
|
||||
) => Promise<{
|
||||
autoload: boolean
|
||||
getModel?: (sdk: any, modelID: string) => Promise<any>
|
||||
options?: Record<string, any>
|
||||
}>
|
||||
|
||||
type Source = "env" | "config" | "custom" | "api"
|
||||
|
||||
const CUSTOM_LOADERS: Record<string, CustomLoader> = {
|
||||
async anthropic(provider) {
|
||||
const access = await AuthAnthropic.access()
|
||||
if (!access) return false
|
||||
if (!access) return { autoload: false }
|
||||
for (const model of Object.values(provider.models)) {
|
||||
model.cost = {
|
||||
input: 0,
|
||||
@@ -49,6 +49,7 @@ export namespace Provider {
|
||||
}
|
||||
}
|
||||
return {
|
||||
autoload: true,
|
||||
options: {
|
||||
apiKey: "",
|
||||
async fetch(input: any, init: any) {
|
||||
@@ -69,9 +70,9 @@ export namespace Provider {
|
||||
},
|
||||
"github-copilot": async (provider) => {
|
||||
const copilot = await AuthCopilot()
|
||||
if (!copilot) return false
|
||||
if (!copilot) return { autoload: false }
|
||||
let info = await Auth.get("github-copilot")
|
||||
if (!info || info.type !== "oauth") return false
|
||||
if (!info || info.type !== "oauth") return { autoload: false }
|
||||
|
||||
if (provider && provider.models) {
|
||||
for (const model of Object.values(provider.models)) {
|
||||
@@ -83,6 +84,7 @@ export namespace Provider {
|
||||
}
|
||||
|
||||
return {
|
||||
autoload: true,
|
||||
options: {
|
||||
apiKey: "",
|
||||
async fetch(input: any, init: any) {
|
||||
@@ -115,6 +117,7 @@ export namespace Provider {
|
||||
},
|
||||
openai: async () => {
|
||||
return {
|
||||
autoload: false,
|
||||
async getModel(sdk: any, modelID: string) {
|
||||
return sdk.responses(modelID)
|
||||
},
|
||||
@@ -123,7 +126,7 @@ export namespace Provider {
|
||||
},
|
||||
"amazon-bedrock": async () => {
|
||||
if (!process.env["AWS_PROFILE"] && !process.env["AWS_ACCESS_KEY_ID"])
|
||||
return false
|
||||
return { autoload: false }
|
||||
|
||||
const region = process.env["AWS_REGION"] ?? "us-east-1"
|
||||
|
||||
@@ -131,6 +134,7 @@ export namespace Provider {
|
||||
await BunProc.install("@aws-sdk/credential-providers")
|
||||
)
|
||||
return {
|
||||
autoload: true,
|
||||
options: {
|
||||
region,
|
||||
credentialProvider: fromNodeProviderChain(),
|
||||
@@ -181,6 +185,7 @@ export namespace Provider {
|
||||
source,
|
||||
info,
|
||||
options,
|
||||
getModel,
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -256,8 +261,13 @@ export namespace Provider {
|
||||
for (const [providerID, fn] of Object.entries(CUSTOM_LOADERS)) {
|
||||
if (disabled.has(providerID)) continue
|
||||
const result = await fn(database[providerID])
|
||||
if (result) {
|
||||
mergeProvider(providerID, result.options, "custom", result.getModel)
|
||||
if (result && (result.autoload || providers[providerID])) {
|
||||
mergeProvider(
|
||||
providerID,
|
||||
result.options ?? {},
|
||||
"custom",
|
||||
result.getModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,7 +411,7 @@ export namespace Provider {
|
||||
// MultiEditTool,
|
||||
WriteTool,
|
||||
TodoWriteTool,
|
||||
TaskTool,
|
||||
// TaskTool,
|
||||
TodoReadTool,
|
||||
]
|
||||
|
||||
|
||||
@@ -20,6 +20,19 @@ export namespace ProviderTransform {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (providerID === "amazon-bedrock" || modelID.includes("anthropic")) {
|
||||
const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
|
||||
const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
|
||||
|
||||
for (const msg of unique([...system, ...final])) {
|
||||
msg.providerMetadata = {
|
||||
...msg.providerMetadata,
|
||||
bedrock: {
|
||||
cachePoint: { type: "ephemeral" },
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
return msgs
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,10 @@ import { z } from "zod"
|
||||
import { Message } from "../session/message"
|
||||
import { Provider } from "../provider/provider"
|
||||
import { App } from "../app/app"
|
||||
import { Global } from "../global"
|
||||
import { mapValues } from "remeda"
|
||||
import { NamedError } from "../util/error"
|
||||
import { ModelsDev } from "../provider/models"
|
||||
import { Ripgrep } from "../external/ripgrep"
|
||||
import { Installation } from "../installation"
|
||||
import { Config } from "../config/config"
|
||||
|
||||
const ERRORS = {
|
||||
@@ -70,12 +68,12 @@ export namespace Server {
|
||||
})
|
||||
})
|
||||
.get(
|
||||
"/openapi",
|
||||
"/doc",
|
||||
openAPISpecs(app, {
|
||||
documentation: {
|
||||
info: {
|
||||
title: "opencode",
|
||||
version: "1.0.0",
|
||||
version: "0.0.2",
|
||||
description: "opencode api",
|
||||
},
|
||||
openapi: "3.0.0",
|
||||
@@ -122,8 +120,8 @@ export namespace Server {
|
||||
})
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/app_info",
|
||||
.get(
|
||||
"/app",
|
||||
describeRoute({
|
||||
description: "Get app info",
|
||||
responses: {
|
||||
@@ -142,26 +140,7 @@ export namespace Server {
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/config_get",
|
||||
describeRoute({
|
||||
description: "Get config info",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Get config info",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Config.Info),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
return c.json(await Config.get())
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/app_initialize",
|
||||
"/app/init",
|
||||
describeRoute({
|
||||
description: "Initialize the app",
|
||||
responses: {
|
||||
@@ -180,172 +159,27 @@ export namespace Server {
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/session_initialize",
|
||||
.get(
|
||||
"/config",
|
||||
describeRoute({
|
||||
description: "Analyze the app and create an AGENTS.md file",
|
||||
description: "Get config info",
|
||||
responses: {
|
||||
200: {
|
||||
description: "200",
|
||||
description: "Get config info",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.boolean()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
zValidator(
|
||||
"json",
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
providerID: z.string(),
|
||||
modelID: z.string(),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const body = c.req.valid("json")
|
||||
await Session.initialize(body)
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/path_get",
|
||||
describeRoute({
|
||||
description: "Get paths",
|
||||
responses: {
|
||||
200: {
|
||||
description: "200",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(
|
||||
z.object({
|
||||
root: z.string(),
|
||||
data: z.string(),
|
||||
cwd: z.string(),
|
||||
config: z.string(),
|
||||
}),
|
||||
),
|
||||
schema: resolver(Config.Info),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const app = App.info()
|
||||
return c.json({
|
||||
root: app.path.root,
|
||||
data: app.path.data,
|
||||
cwd: app.path.cwd,
|
||||
config: Global.Path.data,
|
||||
})
|
||||
return c.json(await Config.get())
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/session_create",
|
||||
describeRoute({
|
||||
description: "Create a new session",
|
||||
responses: {
|
||||
...ERRORS,
|
||||
200: {
|
||||
description: "Successfully created session",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Session.Info),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const session = await Session.create()
|
||||
return c.json(session)
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/session_share",
|
||||
describeRoute({
|
||||
description: "Share the session",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successfully shared session",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Session.Info),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
zValidator(
|
||||
"json",
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const body = c.req.valid("json")
|
||||
await Session.share(body.sessionID)
|
||||
const session = await Session.get(body.sessionID)
|
||||
return c.json(session)
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/session_unshare",
|
||||
describeRoute({
|
||||
description: "Unshare the session",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successfully unshared session",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Session.Info),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
zValidator(
|
||||
"json",
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const body = c.req.valid("json")
|
||||
await Session.unshare(body.sessionID)
|
||||
const session = await Session.get(body.sessionID)
|
||||
return c.json(session)
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/session_messages",
|
||||
describeRoute({
|
||||
description: "Get messages for a session",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successfully created session",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Message.Info.array()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
zValidator(
|
||||
"json",
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const messages = await Session.messages(c.req.valid("json").sessionID)
|
||||
return c.json(messages)
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/session_list",
|
||||
.get(
|
||||
"/session",
|
||||
describeRoute({
|
||||
description: "List all sessions",
|
||||
responses: {
|
||||
@@ -365,33 +199,28 @@ export namespace Server {
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/session_abort",
|
||||
"/session",
|
||||
describeRoute({
|
||||
description: "Abort a session",
|
||||
description: "Create a new session",
|
||||
responses: {
|
||||
...ERRORS,
|
||||
200: {
|
||||
description: "Aborted session",
|
||||
description: "Successfully created session",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.boolean()),
|
||||
schema: resolver(Session.Info),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
zValidator(
|
||||
"json",
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const body = c.req.valid("json")
|
||||
return c.json(Session.abort(body.sessionID))
|
||||
const session = await Session.create()
|
||||
return c.json(session)
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/session_delete",
|
||||
.delete(
|
||||
"/session/:id",
|
||||
describeRoute({
|
||||
description: "Delete a session and all its data",
|
||||
responses: {
|
||||
@@ -406,24 +235,23 @@ export namespace Server {
|
||||
},
|
||||
}),
|
||||
zValidator(
|
||||
"json",
|
||||
"param",
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
id: z.string(),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const body = c.req.valid("json")
|
||||
await Session.remove(body.sessionID)
|
||||
await Session.remove(c.req.valid("param").id)
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/session_summarize",
|
||||
"/session/:id/init",
|
||||
describeRoute({
|
||||
description: "Summarize the session",
|
||||
description: "Analyze the app and create an AGENTS.md file",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Summarize the session",
|
||||
description: "200",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.boolean()),
|
||||
@@ -432,27 +260,175 @@ export namespace Server {
|
||||
},
|
||||
},
|
||||
}),
|
||||
zValidator(
|
||||
"param",
|
||||
z.object({
|
||||
id: z.string().openapi({ description: "Session ID" }),
|
||||
}),
|
||||
),
|
||||
zValidator(
|
||||
"json",
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
providerID: z.string(),
|
||||
modelID: z.string(),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const sessionID = c.req.valid("param").id
|
||||
const body = c.req.valid("json")
|
||||
await Session.summarize(body)
|
||||
await Session.initialize({ ...body, sessionID })
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/session_chat",
|
||||
"/session/:id/abort",
|
||||
describeRoute({
|
||||
description: "Chat with a model",
|
||||
description: "Abort a session",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Chat with a model",
|
||||
description: "Aborted session",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.boolean()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
zValidator(
|
||||
"param",
|
||||
z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
return c.json(Session.abort(c.req.valid("param").id))
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/session/:id/share",
|
||||
describeRoute({
|
||||
description: "Share a session",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successfully shared session",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Session.Info),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
zValidator(
|
||||
"param",
|
||||
z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const id = c.req.valid("param").id
|
||||
await Session.share(id)
|
||||
const session = await Session.get(id)
|
||||
return c.json(session)
|
||||
},
|
||||
)
|
||||
.delete(
|
||||
"/session/:id/share",
|
||||
describeRoute({
|
||||
description: "Unshare the session",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successfully unshared session",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Session.Info),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
zValidator(
|
||||
"param",
|
||||
z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const id = c.req.valid("param").id
|
||||
await Session.unshare(id)
|
||||
const session = await Session.get(id)
|
||||
return c.json(session)
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/session/:id/summarize",
|
||||
describeRoute({
|
||||
description: "Summarize the session",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Summarized session",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.boolean()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
zValidator(
|
||||
"param",
|
||||
z.object({
|
||||
id: z.string().openapi({ description: "Session ID" }),
|
||||
}),
|
||||
),
|
||||
zValidator(
|
||||
"json",
|
||||
z.object({
|
||||
providerID: z.string(),
|
||||
modelID: z.string(),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const id = c.req.valid("param").id
|
||||
const body = c.req.valid("json")
|
||||
await Session.summarize({ ...body, sessionID: id })
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/session/:id/message",
|
||||
describeRoute({
|
||||
description: "List messages for a session",
|
||||
responses: {
|
||||
200: {
|
||||
description: "List of messages",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Message.Info.array()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
zValidator(
|
||||
"param",
|
||||
z.object({
|
||||
id: z.string().openapi({ description: "Session ID" }),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const messages = await Session.messages(c.req.valid("param").id)
|
||||
return c.json(messages)
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/session/:id/message",
|
||||
describeRoute({
|
||||
description: "Create and send a new message to a session",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Created message",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Message.Info),
|
||||
@@ -461,23 +437,29 @@ export namespace Server {
|
||||
},
|
||||
},
|
||||
}),
|
||||
zValidator(
|
||||
"param",
|
||||
z.object({
|
||||
id: z.string().openapi({ description: "Session ID" }),
|
||||
}),
|
||||
),
|
||||
zValidator(
|
||||
"json",
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
providerID: z.string(),
|
||||
modelID: z.string(),
|
||||
parts: Message.Part.array(),
|
||||
parts: Message.MessagePart.array(),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const sessionID = c.req.valid("param").id
|
||||
const body = c.req.valid("json")
|
||||
const msg = await Session.chat(body)
|
||||
const msg = await Session.chat({ ...body, sessionID })
|
||||
return c.json(msg)
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/provider_list",
|
||||
.get(
|
||||
"/config/providers",
|
||||
describeRoute({
|
||||
description: "List all providers",
|
||||
responses: {
|
||||
@@ -509,8 +491,8 @@ export namespace Server {
|
||||
})
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/file_search",
|
||||
.get(
|
||||
"/file",
|
||||
describeRoute({
|
||||
description: "Search for files",
|
||||
responses: {
|
||||
@@ -525,41 +507,22 @@ export namespace Server {
|
||||
},
|
||||
}),
|
||||
zValidator(
|
||||
"json",
|
||||
"query",
|
||||
z.object({
|
||||
query: z.string(),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const body = c.req.valid("json")
|
||||
const query = c.req.valid("query").query
|
||||
const app = App.info()
|
||||
const result = await Ripgrep.files({
|
||||
cwd: app.path.cwd,
|
||||
query: body.query,
|
||||
query,
|
||||
limit: 10,
|
||||
})
|
||||
return c.json(result)
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"installation_info",
|
||||
describeRoute({
|
||||
description: "Get installation info",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Get installation info",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Installation.Info),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
return c.json(Installation.info())
|
||||
},
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -55,14 +55,18 @@ export namespace Session {
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
ref: "session.info",
|
||||
ref: "Session",
|
||||
})
|
||||
export type Info = z.output<typeof Info>
|
||||
|
||||
export const ShareInfo = z.object({
|
||||
secret: z.string(),
|
||||
url: z.string(),
|
||||
})
|
||||
export const ShareInfo = z
|
||||
.object({
|
||||
secret: z.string(),
|
||||
url: z.string(),
|
||||
})
|
||||
.openapi({
|
||||
ref: "SessionShare",
|
||||
})
|
||||
export type ShareInfo = z.output<typeof ShareInfo>
|
||||
|
||||
export const Event = {
|
||||
@@ -78,6 +82,12 @@ export namespace Session {
|
||||
info: Info,
|
||||
}),
|
||||
),
|
||||
Idle: Bus.event(
|
||||
"session.idle",
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
}),
|
||||
),
|
||||
Error: Bus.event(
|
||||
"session.error",
|
||||
z.object({
|
||||
@@ -267,7 +277,7 @@ export namespace Session {
|
||||
sessionID: string
|
||||
providerID: string
|
||||
modelID: string
|
||||
parts: Message.Part[]
|
||||
parts: Message.MessagePart[]
|
||||
system?: string[]
|
||||
tools?: Tool.Info[]
|
||||
}) {
|
||||
@@ -287,7 +297,10 @@ export namespace Session {
|
||||
if (
|
||||
model.info.limit.context &&
|
||||
tokens >
|
||||
(model.info.limit.context - (model.info.limit.output ?? 0)) * 0.9
|
||||
Math.max(
|
||||
(model.info.limit.context - (model.info.limit.output ?? 0)) * 0.9,
|
||||
0,
|
||||
)
|
||||
) {
|
||||
await summarize({
|
||||
sessionID: input.sessionID,
|
||||
@@ -534,6 +547,7 @@ export namespace Session {
|
||||
// return step
|
||||
// },
|
||||
toolCallStreaming: true,
|
||||
maxTokens: Math.max(0, model.info.limit.output) || undefined,
|
||||
abortSignal: abort.signal,
|
||||
maxSteps: 1000,
|
||||
providerOptions: model.info.options,
|
||||
@@ -657,6 +671,21 @@ export namespace Session {
|
||||
}
|
||||
break
|
||||
|
||||
case "finish":
|
||||
log.info("message finish", {
|
||||
reason: value.finishReason,
|
||||
})
|
||||
const assistant = next.metadata!.assistant!
|
||||
const usage = getUsage(
|
||||
model.info,
|
||||
value.usage,
|
||||
value.providerMetadata,
|
||||
)
|
||||
assistant.cost = usage.cost
|
||||
await updateMessage(next)
|
||||
if (value.finishReason === "length")
|
||||
throw new Message.OutputLengthError({})
|
||||
break
|
||||
default:
|
||||
l.info("unhandled", {
|
||||
type: value.type,
|
||||
@@ -670,6 +699,9 @@ export namespace Session {
|
||||
error: e,
|
||||
})
|
||||
switch (true) {
|
||||
case Message.OutputLengthError.isInstance(e):
|
||||
next.metadata.error = e
|
||||
break
|
||||
case LoadAPIKeyError.isInstance(e):
|
||||
next.metadata.error = new Provider.AuthError(
|
||||
{
|
||||
@@ -832,6 +864,9 @@ export namespace Session {
|
||||
[Symbol.dispose]() {
|
||||
log.info("unlocking", { sessionID })
|
||||
state().pending.delete(sessionID)
|
||||
Bus.publish(Event.Idle, {
|
||||
sessionID,
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -847,8 +882,12 @@ export namespace Session {
|
||||
reasoning: 0,
|
||||
cache: {
|
||||
write: (metadata?.["anthropic"]?.["cacheCreationInputTokens"] ??
|
||||
// @ts-expect-error
|
||||
metadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ??
|
||||
0) as number,
|
||||
read: (metadata?.["anthropic"]?.["cacheReadInputTokens"] ??
|
||||
// @ts-expect-error
|
||||
metadata?.["bedrock"]?.["usage"]?.["cacheReadInputTokens"] ??
|
||||
0) as number,
|
||||
},
|
||||
}
|
||||
@@ -920,7 +959,7 @@ function toUIMessage(msg: Message.Info): UIMessage {
|
||||
throw new Error("not implemented")
|
||||
}
|
||||
|
||||
function toParts(parts: Message.Part[]): UIMessage["parts"] {
|
||||
function toParts(parts: Message.MessagePart[]): UIMessage["parts"] {
|
||||
const result: UIMessage["parts"] = []
|
||||
for (const part of parts) {
|
||||
switch (part.type) {
|
||||
|
||||
@@ -4,6 +4,11 @@ import { Provider } from "../provider/provider"
|
||||
import { NamedError } from "../util/error"
|
||||
|
||||
export namespace Message {
|
||||
export const OutputLengthError = NamedError.create(
|
||||
"MessageOutputLengthError",
|
||||
z.object({}),
|
||||
)
|
||||
|
||||
export const ToolCall = z
|
||||
.object({
|
||||
state: z.literal("call"),
|
||||
@@ -13,7 +18,7 @@ export namespace Message {
|
||||
args: z.custom<Required<unknown>>(),
|
||||
})
|
||||
.openapi({
|
||||
ref: "Message.ToolInvocation.ToolCall",
|
||||
ref: "ToolCall",
|
||||
})
|
||||
export type ToolCall = z.infer<typeof ToolCall>
|
||||
|
||||
@@ -26,7 +31,7 @@ export namespace Message {
|
||||
args: z.custom<Required<unknown>>(),
|
||||
})
|
||||
.openapi({
|
||||
ref: "Message.ToolInvocation.ToolPartialCall",
|
||||
ref: "ToolPartialCall",
|
||||
})
|
||||
export type ToolPartialCall = z.infer<typeof ToolPartialCall>
|
||||
|
||||
@@ -40,14 +45,14 @@ export namespace Message {
|
||||
result: z.string(),
|
||||
})
|
||||
.openapi({
|
||||
ref: "Message.ToolInvocation.ToolResult",
|
||||
ref: "ToolResult",
|
||||
})
|
||||
export type ToolResult = z.infer<typeof ToolResult>
|
||||
|
||||
export const ToolInvocation = z
|
||||
.discriminatedUnion("state", [ToolCall, ToolPartialCall, ToolResult])
|
||||
.openapi({
|
||||
ref: "Message.ToolInvocation",
|
||||
ref: "ToolInvocation",
|
||||
})
|
||||
export type ToolInvocation = z.infer<typeof ToolInvocation>
|
||||
|
||||
@@ -57,7 +62,7 @@ export namespace Message {
|
||||
text: z.string(),
|
||||
})
|
||||
.openapi({
|
||||
ref: "Message.Part.Text",
|
||||
ref: "TextPart",
|
||||
})
|
||||
export type TextPart = z.infer<typeof TextPart>
|
||||
|
||||
@@ -68,7 +73,7 @@ export namespace Message {
|
||||
providerMetadata: z.record(z.any()).optional(),
|
||||
})
|
||||
.openapi({
|
||||
ref: "Message.Part.Reasoning",
|
||||
ref: "ReasoningPart",
|
||||
})
|
||||
export type ReasoningPart = z.infer<typeof ReasoningPart>
|
||||
|
||||
@@ -78,7 +83,7 @@ export namespace Message {
|
||||
toolInvocation: ToolInvocation,
|
||||
})
|
||||
.openapi({
|
||||
ref: "Message.Part.ToolInvocation",
|
||||
ref: "ToolInvocationPart",
|
||||
})
|
||||
export type ToolInvocationPart = z.infer<typeof ToolInvocationPart>
|
||||
|
||||
@@ -91,7 +96,7 @@ export namespace Message {
|
||||
providerMetadata: z.record(z.any()).optional(),
|
||||
})
|
||||
.openapi({
|
||||
ref: "Message.Part.SourceUrl",
|
||||
ref: "SourceUrlPart",
|
||||
})
|
||||
export type SourceUrlPart = z.infer<typeof SourceUrlPart>
|
||||
|
||||
@@ -103,7 +108,7 @@ export namespace Message {
|
||||
url: z.string(),
|
||||
})
|
||||
.openapi({
|
||||
ref: "Message.Part.File",
|
||||
ref: "FilePart",
|
||||
})
|
||||
export type FilePart = z.infer<typeof FilePart>
|
||||
|
||||
@@ -112,11 +117,11 @@ export namespace Message {
|
||||
type: z.literal("step-start"),
|
||||
})
|
||||
.openapi({
|
||||
ref: "Message.Part.StepStart",
|
||||
ref: "StepStartPart",
|
||||
})
|
||||
export type StepStartPart = z.infer<typeof StepStartPart>
|
||||
|
||||
export const Part = z
|
||||
export const MessagePart = z
|
||||
.discriminatedUnion("type", [
|
||||
TextPart,
|
||||
ReasoningPart,
|
||||
@@ -126,15 +131,15 @@ export namespace Message {
|
||||
StepStartPart,
|
||||
])
|
||||
.openapi({
|
||||
ref: "Message.Part",
|
||||
ref: "MessagePart",
|
||||
})
|
||||
export type Part = z.infer<typeof Part>
|
||||
export type MessagePart = z.infer<typeof MessagePart>
|
||||
|
||||
export const Info = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
role: z.enum(["user", "assistant"]),
|
||||
parts: z.array(Part),
|
||||
parts: z.array(MessagePart),
|
||||
metadata: z
|
||||
.object({
|
||||
time: z.object({
|
||||
@@ -145,6 +150,7 @@ export namespace Message {
|
||||
.discriminatedUnion("name", [
|
||||
Provider.AuthError.Schema,
|
||||
NamedError.Unknown.Schema,
|
||||
OutputLengthError.Schema,
|
||||
])
|
||||
.optional(),
|
||||
sessionID: z.string(),
|
||||
@@ -183,10 +189,10 @@ export namespace Message {
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.openapi({ ref: "Message.Metadata" }),
|
||||
.openapi({ ref: "MessageMetadata" }),
|
||||
})
|
||||
.openapi({
|
||||
ref: "Message.Info",
|
||||
ref: "Message",
|
||||
})
|
||||
export type Info = z.infer<typeof Info>
|
||||
|
||||
@@ -199,7 +205,11 @@ export namespace Message {
|
||||
),
|
||||
PartUpdated: Bus.event(
|
||||
"message.part.updated",
|
||||
z.object({ part: Part, sessionID: z.string(), messageID: z.string() }),
|
||||
z.object({
|
||||
part: MessagePart,
|
||||
sessionID: z.string(),
|
||||
messageID: z.string(),
|
||||
}),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { App } from "../app/app"
|
||||
import { Bus } from "../bus"
|
||||
import { Installation } from "../installation"
|
||||
import { Session } from "../session"
|
||||
@@ -11,12 +10,6 @@ export namespace Share {
|
||||
let queue: Promise<void> = Promise.resolve()
|
||||
const pending = new Map<string, any>()
|
||||
|
||||
const state = App.state("share", async () => {
|
||||
Bus.subscribe(Storage.Event.Write, async (payload) => {
|
||||
await sync(payload.properties.key, payload.properties.content)
|
||||
})
|
||||
})
|
||||
|
||||
export async function sync(key: string, content: any) {
|
||||
const [root, ...splits] = key.split("/")
|
||||
if (root !== "session") return
|
||||
@@ -52,8 +45,10 @@ export namespace Share {
|
||||
})
|
||||
}
|
||||
|
||||
export async function init() {
|
||||
await state()
|
||||
export function init() {
|
||||
Bus.subscribe(Storage.Event.Write, async (payload) => {
|
||||
await sync(payload.properties.key, payload.properties.content)
|
||||
})
|
||||
}
|
||||
|
||||
export const URL =
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
// the approaches in this edit tool are sourced from
|
||||
// https://github.com/cline/cline/blob/main/evals/diff-edits/diff-apply/diff-06-23-25.ts
|
||||
// https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/utils/editCorrector.ts
|
||||
|
||||
import { z } from "zod"
|
||||
import * as path from "path"
|
||||
import { Tool } from "./tool"
|
||||
import { FileTimes } from "./util/file-times"
|
||||
import { LSP } from "../lsp"
|
||||
import { createTwoFilesPatch } from "diff"
|
||||
import { Permission } from "../permission"
|
||||
import DESCRIPTION from "./edit.txt"
|
||||
import { App } from "../app/app"
|
||||
import { File } from "../file"
|
||||
import { Bus } from "../bus"
|
||||
import { FileTime } from "../file/time"
|
||||
|
||||
export const EditTool = Tool.define({
|
||||
id: "edit",
|
||||
@@ -29,6 +35,10 @@ export const EditTool = Tool.define({
|
||||
throw new Error("filePath is required")
|
||||
}
|
||||
|
||||
if (params.oldString === params.newString) {
|
||||
throw new Error("oldString and newString must be different")
|
||||
}
|
||||
|
||||
const app = App.info()
|
||||
const filepath = path.isAbsolute(params.filePath)
|
||||
? params.filePath
|
||||
@@ -51,47 +61,38 @@ export const EditTool = Tool.define({
|
||||
if (params.oldString === "") {
|
||||
contentNew = params.newString
|
||||
await Bun.write(filepath, params.newString)
|
||||
await Bus.publish(File.Event.Edited, {
|
||||
file: filepath,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const file = Bun.file(filepath)
|
||||
if (!(await file.exists())) throw new Error(`File ${filepath} not found`)
|
||||
const stats = await file.stat()
|
||||
const stats = await file.stat().catch(() => {})
|
||||
if (!stats) throw new Error(`File ${filepath} not found`)
|
||||
if (stats.isDirectory())
|
||||
throw new Error(`Path is a directory, not a file: ${filepath}`)
|
||||
await FileTimes.assert(ctx.sessionID, filepath)
|
||||
await FileTime.assert(ctx.sessionID, filepath)
|
||||
contentOld = await file.text()
|
||||
const index = contentOld.indexOf(params.oldString)
|
||||
if (index === -1)
|
||||
throw new Error(
|
||||
`oldString not found in file. Make sure it matches exactly, including whitespace and line breaks`,
|
||||
)
|
||||
|
||||
if (params.replaceAll) {
|
||||
contentNew = contentOld.replaceAll(params.oldString, params.newString)
|
||||
}
|
||||
|
||||
if (!params.replaceAll) {
|
||||
const lastIndex = contentOld.lastIndexOf(params.oldString)
|
||||
if (index !== lastIndex)
|
||||
throw new Error(
|
||||
`oldString appears multiple times in the file. Please provide more context to ensure a unique match`,
|
||||
)
|
||||
|
||||
contentNew =
|
||||
contentOld.substring(0, index) +
|
||||
params.newString +
|
||||
contentOld.substring(index + params.oldString.length)
|
||||
}
|
||||
|
||||
contentNew = replace(
|
||||
contentOld,
|
||||
params.oldString,
|
||||
params.newString,
|
||||
params.replaceAll,
|
||||
)
|
||||
await file.write(contentNew)
|
||||
await Bus.publish(File.Event.Edited, {
|
||||
file: filepath,
|
||||
})
|
||||
contentNew = await file.text()
|
||||
})()
|
||||
|
||||
const diff = trimDiff(
|
||||
createTwoFilesPatch(filepath, filepath, contentOld, contentNew),
|
||||
)
|
||||
|
||||
FileTimes.read(ctx.sessionID, filepath)
|
||||
FileTime.read(ctx.sessionID, filepath)
|
||||
|
||||
let output = ""
|
||||
await LSP.touchFile(filepath, true)
|
||||
@@ -116,6 +117,326 @@ export const EditTool = Tool.define({
|
||||
},
|
||||
})
|
||||
|
||||
export type Replacer = (
|
||||
content: string,
|
||||
find: string,
|
||||
) => Generator<string, void, unknown>
|
||||
|
||||
export const SimpleReplacer: Replacer = function* (_content, find) {
|
||||
yield find
|
||||
}
|
||||
|
||||
export const LineTrimmedReplacer: Replacer = function* (content, find) {
|
||||
const originalLines = content.split("\n")
|
||||
const searchLines = find.split("\n")
|
||||
|
||||
if (searchLines[searchLines.length - 1] === "") {
|
||||
searchLines.pop()
|
||||
}
|
||||
|
||||
for (let i = 0; i <= originalLines.length - searchLines.length; i++) {
|
||||
let matches = true
|
||||
|
||||
for (let j = 0; j < searchLines.length; j++) {
|
||||
const originalTrimmed = originalLines[i + j].trim()
|
||||
const searchTrimmed = searchLines[j].trim()
|
||||
|
||||
if (originalTrimmed !== searchTrimmed) {
|
||||
matches = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (matches) {
|
||||
let matchStartIndex = 0
|
||||
for (let k = 0; k < i; k++) {
|
||||
matchStartIndex += originalLines[k].length + 1
|
||||
}
|
||||
|
||||
let matchEndIndex = matchStartIndex
|
||||
for (let k = 0; k < searchLines.length; k++) {
|
||||
matchEndIndex += originalLines[i + k].length + 1
|
||||
}
|
||||
|
||||
yield content.substring(matchStartIndex, matchEndIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const BlockAnchorReplacer: Replacer = function* (content, find) {
|
||||
const originalLines = content.split("\n")
|
||||
const searchLines = find.split("\n")
|
||||
|
||||
if (searchLines.length < 3) {
|
||||
return
|
||||
}
|
||||
|
||||
if (searchLines[searchLines.length - 1] === "") {
|
||||
searchLines.pop()
|
||||
}
|
||||
|
||||
const firstLineSearch = searchLines[0].trim()
|
||||
const lastLineSearch = searchLines[searchLines.length - 1].trim()
|
||||
|
||||
// Find blocks where first line matches the search first line
|
||||
for (let i = 0; i < originalLines.length; i++) {
|
||||
if (originalLines[i].trim() !== firstLineSearch) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Look for the matching last line after this first line
|
||||
for (let j = i + 2; j < originalLines.length; j++) {
|
||||
if (originalLines[j].trim() === lastLineSearch) {
|
||||
// Found a potential block from i to j
|
||||
let matchStartIndex = 0
|
||||
for (let k = 0; k < i; k++) {
|
||||
matchStartIndex += originalLines[k].length + 1
|
||||
}
|
||||
|
||||
let matchEndIndex = matchStartIndex
|
||||
for (let k = 0; k <= j - i; k++) {
|
||||
matchEndIndex += originalLines[i + k].length
|
||||
if (k < j - i) {
|
||||
matchEndIndex += 1 // Add newline character except for the last line
|
||||
}
|
||||
}
|
||||
|
||||
yield content.substring(matchStartIndex, matchEndIndex)
|
||||
break // Only match the first occurrence of the last line
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const WhitespaceNormalizedReplacer: Replacer = function* (
|
||||
content,
|
||||
find,
|
||||
) {
|
||||
const normalizeWhitespace = (text: string) => text.replace(/\s+/g, " ").trim()
|
||||
const normalizedFind = normalizeWhitespace(find)
|
||||
|
||||
// Handle single line matches
|
||||
const lines = content.split("\n")
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]
|
||||
if (normalizeWhitespace(line) === normalizedFind) {
|
||||
yield line
|
||||
}
|
||||
|
||||
// Also check for substring matches within lines
|
||||
const normalizedLine = normalizeWhitespace(line)
|
||||
if (normalizedLine.includes(normalizedFind)) {
|
||||
// Find the actual substring in the original line that matches
|
||||
const words = find.trim().split(/\s+/)
|
||||
if (words.length > 0) {
|
||||
const pattern = words
|
||||
.map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
||||
.join("\\s+")
|
||||
try {
|
||||
const regex = new RegExp(pattern)
|
||||
const match = line.match(regex)
|
||||
if (match) {
|
||||
yield match[0]
|
||||
}
|
||||
} catch (e) {
|
||||
// Invalid regex pattern, skip
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle multi-line matches
|
||||
const findLines = find.split("\n")
|
||||
if (findLines.length > 1) {
|
||||
for (let i = 0; i <= lines.length - findLines.length; i++) {
|
||||
const block = lines.slice(i, i + findLines.length)
|
||||
if (normalizeWhitespace(block.join("\n")) === normalizedFind) {
|
||||
yield block.join("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const IndentationFlexibleReplacer: Replacer = function* (content, find) {
|
||||
const removeIndentation = (text: string) => {
|
||||
const lines = text.split("\n")
|
||||
const nonEmptyLines = lines.filter((line) => line.trim().length > 0)
|
||||
if (nonEmptyLines.length === 0) return text
|
||||
|
||||
const minIndent = Math.min(
|
||||
...nonEmptyLines.map((line) => {
|
||||
const match = line.match(/^(\s*)/)
|
||||
return match ? match[1].length : 0
|
||||
}),
|
||||
)
|
||||
|
||||
return lines
|
||||
.map((line) => (line.trim().length === 0 ? line : line.slice(minIndent)))
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
const normalizedFind = removeIndentation(find)
|
||||
const contentLines = content.split("\n")
|
||||
const findLines = find.split("\n")
|
||||
|
||||
for (let i = 0; i <= contentLines.length - findLines.length; i++) {
|
||||
const block = contentLines.slice(i, i + findLines.length).join("\n")
|
||||
if (removeIndentation(block) === normalizedFind) {
|
||||
yield block
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const EscapeNormalizedReplacer: Replacer = function* (content, find) {
|
||||
const unescapeString = (str: string): string => {
|
||||
return str.replace(/\\(n|t|r|'|"|`|\\|\n|\$)/g, (match, capturedChar) => {
|
||||
switch (capturedChar) {
|
||||
case "n":
|
||||
return "\n"
|
||||
case "t":
|
||||
return "\t"
|
||||
case "r":
|
||||
return "\r"
|
||||
case "'":
|
||||
return "'"
|
||||
case '"':
|
||||
return '"'
|
||||
case "`":
|
||||
return "`"
|
||||
case "\\":
|
||||
return "\\"
|
||||
case "\n":
|
||||
return "\n"
|
||||
case "$":
|
||||
return "$"
|
||||
default:
|
||||
return match
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const unescapedFind = unescapeString(find)
|
||||
|
||||
// Try direct match with unescaped find string
|
||||
if (content.includes(unescapedFind)) {
|
||||
yield unescapedFind
|
||||
}
|
||||
|
||||
// Also try finding escaped versions in content that match unescaped find
|
||||
const lines = content.split("\n")
|
||||
const findLines = unescapedFind.split("\n")
|
||||
|
||||
for (let i = 0; i <= lines.length - findLines.length; i++) {
|
||||
const block = lines.slice(i, i + findLines.length).join("\n")
|
||||
const unescapedBlock = unescapeString(block)
|
||||
|
||||
if (unescapedBlock === unescapedFind) {
|
||||
yield block
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const MultiOccurrenceReplacer: Replacer = function* (content, find) {
|
||||
// This replacer yields all exact matches, allowing the replace function
|
||||
// to handle multiple occurrences based on replaceAll parameter
|
||||
let startIndex = 0
|
||||
|
||||
while (true) {
|
||||
const index = content.indexOf(find, startIndex)
|
||||
if (index === -1) break
|
||||
|
||||
yield find
|
||||
startIndex = index + find.length
|
||||
}
|
||||
}
|
||||
|
||||
export const TrimmedBoundaryReplacer: Replacer = function* (content, find) {
|
||||
const trimmedFind = find.trim()
|
||||
|
||||
if (trimmedFind === find) {
|
||||
// Already trimmed, no point in trying
|
||||
return
|
||||
}
|
||||
|
||||
// Try to find the trimmed version
|
||||
if (content.includes(trimmedFind)) {
|
||||
yield trimmedFind
|
||||
}
|
||||
|
||||
// Also try finding blocks where trimmed content matches
|
||||
const lines = content.split("\n")
|
||||
const findLines = find.split("\n")
|
||||
|
||||
for (let i = 0; i <= lines.length - findLines.length; i++) {
|
||||
const block = lines.slice(i, i + findLines.length).join("\n")
|
||||
|
||||
if (block.trim() === trimmedFind) {
|
||||
yield block
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const ContextAwareReplacer: Replacer = function* (content, find) {
|
||||
const findLines = find.split("\n")
|
||||
if (findLines.length < 3) {
|
||||
// Need at least 3 lines to have meaningful context
|
||||
return
|
||||
}
|
||||
|
||||
// Remove trailing empty line if present
|
||||
if (findLines[findLines.length - 1] === "") {
|
||||
findLines.pop()
|
||||
}
|
||||
|
||||
const contentLines = content.split("\n")
|
||||
|
||||
// Extract first and last lines as context anchors
|
||||
const firstLine = findLines[0].trim()
|
||||
const lastLine = findLines[findLines.length - 1].trim()
|
||||
|
||||
// Find blocks that start and end with the context anchors
|
||||
for (let i = 0; i < contentLines.length; i++) {
|
||||
if (contentLines[i].trim() !== firstLine) continue
|
||||
|
||||
// Look for the matching last line
|
||||
for (let j = i + 2; j < contentLines.length; j++) {
|
||||
if (contentLines[j].trim() === lastLine) {
|
||||
// Found a potential context block
|
||||
const blockLines = contentLines.slice(i, j + 1)
|
||||
const block = blockLines.join("\n")
|
||||
|
||||
// Check if the middle content has reasonable similarity
|
||||
// (simple heuristic: at least 50% of non-empty lines should match when trimmed)
|
||||
if (blockLines.length === findLines.length) {
|
||||
let matchingLines = 0
|
||||
let totalNonEmptyLines = 0
|
||||
|
||||
for (let k = 1; k < blockLines.length - 1; k++) {
|
||||
const blockLine = blockLines[k].trim()
|
||||
const findLine = findLines[k].trim()
|
||||
|
||||
if (blockLine.length > 0 || findLine.length > 0) {
|
||||
totalNonEmptyLines++
|
||||
if (blockLine === findLine) {
|
||||
matchingLines++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
totalNonEmptyLines === 0 ||
|
||||
matchingLines / totalNonEmptyLines >= 0.5
|
||||
) {
|
||||
yield block
|
||||
break // Only match the first occurrence
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function trimDiff(diff: string): string {
|
||||
const lines = diff.split("\n")
|
||||
const contentLines = lines.filter(
|
||||
@@ -151,3 +472,42 @@ function trimDiff(diff: string): string {
|
||||
|
||||
return trimmedLines.join("\n")
|
||||
}
|
||||
|
||||
export function replace(
|
||||
content: string,
|
||||
oldString: string,
|
||||
newString: string,
|
||||
replaceAll = false,
|
||||
): string {
|
||||
if (oldString === newString) {
|
||||
throw new Error("oldString and newString must be different")
|
||||
}
|
||||
|
||||
for (const replacer of [
|
||||
SimpleReplacer,
|
||||
LineTrimmedReplacer,
|
||||
BlockAnchorReplacer,
|
||||
WhitespaceNormalizedReplacer,
|
||||
IndentationFlexibleReplacer,
|
||||
EscapeNormalizedReplacer,
|
||||
TrimmedBoundaryReplacer,
|
||||
ContextAwareReplacer,
|
||||
MultiOccurrenceReplacer,
|
||||
]) {
|
||||
for (const search of replacer(content, oldString)) {
|
||||
const index = content.indexOf(search)
|
||||
if (index === -1) continue
|
||||
if (replaceAll) {
|
||||
return content.replaceAll(search, newString)
|
||||
}
|
||||
const lastIndex = content.lastIndexOf(search)
|
||||
if (index !== lastIndex) continue
|
||||
return (
|
||||
content.substring(0, index) +
|
||||
newString +
|
||||
content.substring(index + search.length)
|
||||
)
|
||||
}
|
||||
}
|
||||
throw new Error("oldString not found in content or was found multiple times")
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { z } from "zod"
|
||||
import * as path from "path"
|
||||
import * as fs from "fs/promises"
|
||||
import { Tool } from "./tool"
|
||||
import { FileTimes } from "./util/file-times"
|
||||
import { FileTime } from "../file/time"
|
||||
import DESCRIPTION from "./patch.txt"
|
||||
|
||||
const PatchParams = z.object({
|
||||
@@ -244,7 +244,7 @@ export const PatchTool = Tool.define({
|
||||
absPath = path.resolve(process.cwd(), absPath)
|
||||
}
|
||||
|
||||
await FileTimes.assert(ctx.sessionID, absPath)
|
||||
await FileTime.assert(ctx.sessionID, absPath)
|
||||
|
||||
try {
|
||||
const stats = await fs.stat(absPath)
|
||||
@@ -351,7 +351,7 @@ export const PatchTool = Tool.define({
|
||||
totalAdditions += additions
|
||||
totalRemovals += removals
|
||||
|
||||
FileTimes.read(ctx.sessionID, absPath)
|
||||
FileTime.read(ctx.sessionID, absPath)
|
||||
}
|
||||
|
||||
const result = `Patch applied successfully. ${changedFiles.length} files changed, ${totalAdditions} additions, ${totalRemovals} removals`
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import { Tool } from "./tool"
|
||||
import { LSP } from "../lsp"
|
||||
import { FileTimes } from "./util/file-times"
|
||||
import { FileTime } from "../file/time"
|
||||
import DESCRIPTION from "./read.txt"
|
||||
import { App } from "../app/app"
|
||||
|
||||
@@ -90,7 +90,7 @@ export const ReadTool = Tool.define({
|
||||
|
||||
// just warms the lsp client
|
||||
await LSP.touchFile(filePath, true)
|
||||
FileTimes.read(ctx.sessionID, filePath)
|
||||
FileTime.read(ctx.sessionID, filePath)
|
||||
|
||||
return {
|
||||
output,
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { z } from "zod"
|
||||
import * as path from "path"
|
||||
import { Tool } from "./tool"
|
||||
import { FileTimes } from "./util/file-times"
|
||||
import { LSP } from "../lsp"
|
||||
import { Permission } from "../permission"
|
||||
import DESCRIPTION from "./write.txt"
|
||||
import { App } from "../app/app"
|
||||
import { Bus } from "../bus"
|
||||
import { File } from "../file"
|
||||
import { FileTime } from "../file/time"
|
||||
|
||||
export const WriteTool = Tool.define({
|
||||
id: "write",
|
||||
@@ -26,7 +28,7 @@ export const WriteTool = Tool.define({
|
||||
|
||||
const file = Bun.file(filepath)
|
||||
const exists = await file.exists()
|
||||
if (exists) await FileTimes.assert(ctx.sessionID, filepath)
|
||||
if (exists) await FileTime.assert(ctx.sessionID, filepath)
|
||||
|
||||
await Permission.ask({
|
||||
id: "write",
|
||||
@@ -42,7 +44,10 @@ export const WriteTool = Tool.define({
|
||||
})
|
||||
|
||||
await Bun.write(filepath, params.content)
|
||||
FileTimes.read(ctx.sessionID, filepath)
|
||||
await Bus.publish(File.Event.Edited, {
|
||||
file: filepath,
|
||||
})
|
||||
FileTime.read(ctx.sessionID, filepath)
|
||||
|
||||
let output = ""
|
||||
await LSP.touchFile(filepath, true)
|
||||
|
||||
@@ -8,4 +8,3 @@ export function lazy<T>(fn: () => T) {
|
||||
return value as T
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,10 @@ export namespace Log {
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
cleanup(dir)
|
||||
if (options.print) return
|
||||
logpath = path.join(dir, new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log")
|
||||
logpath = path.join(
|
||||
dir,
|
||||
new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log",
|
||||
)
|
||||
const logfile = Bun.file(logpath)
|
||||
await fs.truncate(logpath).catch(() => {})
|
||||
const writer = logfile.writer()
|
||||
|
||||
Vendored
+1
-1
@@ -6,4 +6,4 @@
|
||||
/// <reference path="../../sst-env.d.ts" />
|
||||
|
||||
import "sst"
|
||||
export {}
|
||||
export {}
|
||||
|
||||
@@ -0,0 +1,413 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { replace } from "../../src/tool/edit"
|
||||
|
||||
interface TestCase {
|
||||
content: string
|
||||
find: string
|
||||
replace: string
|
||||
all?: boolean
|
||||
fail?: boolean
|
||||
}
|
||||
|
||||
const testCases: TestCase[] = [
|
||||
// SimpleReplacer cases
|
||||
{
|
||||
content: ["function hello() {", ' console.log("world");', "}"].join("\n"),
|
||||
find: 'console.log("world");',
|
||||
replace: 'console.log("universe");',
|
||||
},
|
||||
{
|
||||
content: [
|
||||
"if (condition) {",
|
||||
" doSomething();",
|
||||
" doSomethingElse();",
|
||||
"}",
|
||||
].join("\n"),
|
||||
find: [" doSomething();", " doSomethingElse();"].join("\n"),
|
||||
replace: [" doNewThing();", " doAnotherThing();"].join("\n"),
|
||||
},
|
||||
|
||||
// LineTrimmedReplacer cases
|
||||
{
|
||||
content: ["function test() {", ' console.log("hello");', "}"].join("\n"),
|
||||
find: 'console.log("hello");',
|
||||
replace: 'console.log("goodbye");',
|
||||
},
|
||||
{
|
||||
content: ["const x = 5; ", "const y = 10;"].join("\n"),
|
||||
find: "const x = 5;",
|
||||
replace: "const x = 15;",
|
||||
},
|
||||
{
|
||||
content: [" if (true) {", " return false;", " }"].join("\n"),
|
||||
find: ["if (true) {", "return false;", "}"].join("\n"),
|
||||
replace: ["if (false) {", "return true;", "}"].join("\n"),
|
||||
},
|
||||
|
||||
// BlockAnchorReplacer cases
|
||||
{
|
||||
content: [
|
||||
"function calculate(a, b) {",
|
||||
" const temp = a + b;",
|
||||
" const result = temp * 2;",
|
||||
" return result;",
|
||||
"}",
|
||||
].join("\n"),
|
||||
find: [
|
||||
"function calculate(a, b) {",
|
||||
" // different middle content",
|
||||
" return result;",
|
||||
"}",
|
||||
].join("\n"),
|
||||
replace: ["function calculate(a, b) {", " return a * b * 2;", "}"].join(
|
||||
"\n",
|
||||
),
|
||||
},
|
||||
{
|
||||
content: [
|
||||
"class MyClass {",
|
||||
" constructor() {",
|
||||
" this.value = 0;",
|
||||
" }",
|
||||
" ",
|
||||
" getValue() {",
|
||||
" return this.value;",
|
||||
" }",
|
||||
"}",
|
||||
].join("\n"),
|
||||
find: ["class MyClass {", " // different implementation", "}"].join("\n"),
|
||||
replace: [
|
||||
"class MyClass {",
|
||||
" constructor() {",
|
||||
" this.value = 42;",
|
||||
" }",
|
||||
"}",
|
||||
].join("\n"),
|
||||
},
|
||||
|
||||
// WhitespaceNormalizedReplacer cases
|
||||
{
|
||||
content: ["function test() {", '\tconsole.log("hello");', "}"].join("\n"),
|
||||
find: ' console.log("hello");',
|
||||
replace: ' console.log("world");',
|
||||
},
|
||||
{
|
||||
content: "const x = 5;",
|
||||
find: "const x = 5;",
|
||||
replace: "const x = 10;",
|
||||
},
|
||||
{
|
||||
content: "if\t( condition\t) {",
|
||||
find: "if ( condition ) {",
|
||||
replace: "if (newCondition) {",
|
||||
},
|
||||
|
||||
// IndentationFlexibleReplacer cases
|
||||
{
|
||||
content: [
|
||||
" function nested() {",
|
||||
' console.log("deeply nested");',
|
||||
" return true;",
|
||||
" }",
|
||||
].join("\n"),
|
||||
find: [
|
||||
"function nested() {",
|
||||
' console.log("deeply nested");',
|
||||
" return true;",
|
||||
"}",
|
||||
].join("\n"),
|
||||
replace: [
|
||||
"function nested() {",
|
||||
' console.log("updated");',
|
||||
" return false;",
|
||||
"}",
|
||||
].join("\n"),
|
||||
},
|
||||
{
|
||||
content: [
|
||||
" if (true) {",
|
||||
' console.log("level 1");',
|
||||
' console.log("level 2");',
|
||||
" }",
|
||||
].join("\n"),
|
||||
find: [
|
||||
"if (true) {",
|
||||
'console.log("level 1");',
|
||||
' console.log("level 2");',
|
||||
"}",
|
||||
].join("\n"),
|
||||
replace: ["if (true) {", 'console.log("updated");', "}"].join("\n"),
|
||||
},
|
||||
|
||||
// replaceAll option cases
|
||||
{
|
||||
content: [
|
||||
'console.log("test");',
|
||||
'console.log("test");',
|
||||
'console.log("test");',
|
||||
].join("\n"),
|
||||
find: 'console.log("test");',
|
||||
replace: 'console.log("updated");',
|
||||
all: true,
|
||||
},
|
||||
{
|
||||
content: ['console.log("test");', 'console.log("test");'].join("\n"),
|
||||
find: 'console.log("test");',
|
||||
replace: 'console.log("updated");',
|
||||
all: false,
|
||||
},
|
||||
|
||||
// Error cases
|
||||
{
|
||||
content: 'console.log("hello");',
|
||||
find: "nonexistent string",
|
||||
replace: "updated",
|
||||
fail: true,
|
||||
},
|
||||
{
|
||||
content: ["test", "test", "different content", "test"].join("\n"),
|
||||
find: "test",
|
||||
replace: "updated",
|
||||
all: false,
|
||||
fail: true,
|
||||
},
|
||||
|
||||
// Edge cases
|
||||
{
|
||||
content: "",
|
||||
find: "",
|
||||
replace: "new content",
|
||||
},
|
||||
{
|
||||
content: "const regex = /[.*+?^${}()|[\\\\]\\\\\\\\]/g;",
|
||||
find: "/[.*+?^${}()|[\\\\]\\\\\\\\]/g",
|
||||
replace: "/\\\\w+/g",
|
||||
},
|
||||
{
|
||||
content: 'const message = "Hello 世界! 🌍";',
|
||||
find: "Hello 世界! 🌍",
|
||||
replace: "Hello World! 🌎",
|
||||
},
|
||||
|
||||
// EscapeNormalizedReplacer cases
|
||||
{
|
||||
content: 'console.log("Hello\nWorld");',
|
||||
find: 'console.log("Hello\\nWorld");',
|
||||
replace: 'console.log("Hello\nUniverse");',
|
||||
},
|
||||
{
|
||||
content: "const str = 'It's working';",
|
||||
find: "const str = 'It\\'s working';",
|
||||
replace: "const str = 'It's fixed';",
|
||||
},
|
||||
{
|
||||
content: "const template = `Hello ${name}`;",
|
||||
find: "const template = `Hello \\${name}`;",
|
||||
replace: "const template = `Hi ${name}`;",
|
||||
},
|
||||
{
|
||||
content: "const path = 'C:\\Users\\test';",
|
||||
find: "const path = 'C:\\\\Users\\\\test';",
|
||||
replace: "const path = 'C:\\Users\\admin';",
|
||||
},
|
||||
|
||||
// MultiOccurrenceReplacer cases (with replaceAll)
|
||||
{
|
||||
content: ["debug('start');", "debug('middle');", "debug('end');"].join(
|
||||
"\n",
|
||||
),
|
||||
find: "debug",
|
||||
replace: "log",
|
||||
all: true,
|
||||
},
|
||||
{
|
||||
content: "const x = 1; const y = 1; const z = 1;",
|
||||
find: "1",
|
||||
replace: "2",
|
||||
all: true,
|
||||
},
|
||||
|
||||
// TrimmedBoundaryReplacer cases
|
||||
{
|
||||
content: [" function test() {", " return true;", " }"].join("\n"),
|
||||
find: ["function test() {", " return true;", "}"].join("\n"),
|
||||
replace: ["function test() {", " return false;", "}"].join("\n"),
|
||||
},
|
||||
{
|
||||
content: "\n const value = 42; \n",
|
||||
find: "const value = 42;",
|
||||
replace: "const value = 24;",
|
||||
},
|
||||
{
|
||||
content: ["", " if (condition) {", " doSomething();", " }", ""].join(
|
||||
"\n",
|
||||
),
|
||||
find: ["if (condition) {", " doSomething();", "}"].join("\n"),
|
||||
replace: ["if (condition) {", " doNothing();", "}"].join("\n"),
|
||||
},
|
||||
|
||||
// ContextAwareReplacer cases
|
||||
{
|
||||
content: [
|
||||
"function calculate(a, b) {",
|
||||
" const temp = a + b;",
|
||||
" const result = temp * 2;",
|
||||
" return result;",
|
||||
"}",
|
||||
].join("\n"),
|
||||
find: [
|
||||
"function calculate(a, b) {",
|
||||
" // some different content here",
|
||||
" // more different content",
|
||||
" return result;",
|
||||
"}",
|
||||
].join("\n"),
|
||||
replace: ["function calculate(a, b) {", " return (a + b) * 2;", "}"].join(
|
||||
"\n",
|
||||
),
|
||||
},
|
||||
{
|
||||
content: [
|
||||
"class TestClass {",
|
||||
" constructor() {",
|
||||
" this.value = 0;",
|
||||
" }",
|
||||
" ",
|
||||
" method() {",
|
||||
" return this.value;",
|
||||
" }",
|
||||
"}",
|
||||
].join("\n"),
|
||||
find: [
|
||||
"class TestClass {",
|
||||
" // different implementation",
|
||||
" // with multiple lines",
|
||||
"}",
|
||||
].join("\n"),
|
||||
replace: ["class TestClass {", " getValue() { return 42; }", "}"].join(
|
||||
"\n",
|
||||
),
|
||||
},
|
||||
|
||||
// Combined edge cases for new replacers
|
||||
{
|
||||
content: '\tconsole.log("test");\t',
|
||||
find: 'console.log("test");',
|
||||
replace: 'console.log("updated");',
|
||||
},
|
||||
{
|
||||
content: [" ", "function test() {", " return 'value';", "}", " "].join(
|
||||
"\n",
|
||||
),
|
||||
find: ["function test() {", "return 'value';", "}"].join("\n"),
|
||||
replace: ["function test() {", "return 'new value';", "}"].join("\n"),
|
||||
},
|
||||
|
||||
// Test for same oldString and newString (should fail)
|
||||
{
|
||||
content: 'console.log("test");',
|
||||
find: 'console.log("test");',
|
||||
replace: 'console.log("test");',
|
||||
fail: true,
|
||||
},
|
||||
|
||||
// Additional tests for fixes made
|
||||
|
||||
// WhitespaceNormalizedReplacer - test regex special characters that could cause errors
|
||||
{
|
||||
content: 'const pattern = "test[123]";',
|
||||
find: "test[123]",
|
||||
replace: "test[456]",
|
||||
},
|
||||
{
|
||||
content: 'const regex = "^start.*end$";',
|
||||
find: "^start.*end$",
|
||||
replace: "^begin.*finish$",
|
||||
},
|
||||
|
||||
// EscapeNormalizedReplacer - test single backslash vs double backslash
|
||||
{
|
||||
content: 'const path = "C:\\Users";',
|
||||
find: 'const path = "C:\\Users";',
|
||||
replace: 'const path = "D:\\Users";',
|
||||
},
|
||||
{
|
||||
content: 'console.log("Line1\\nLine2");',
|
||||
find: 'console.log("Line1\\nLine2");',
|
||||
replace: 'console.log("First\\nSecond");',
|
||||
},
|
||||
|
||||
// BlockAnchorReplacer - test edge case with exact newline boundaries
|
||||
{
|
||||
content: ["function test() {", " return true;", "}"].join("\n"),
|
||||
find: ["function test() {", " // middle", "}"].join("\n"),
|
||||
replace: ["function test() {", " return false;", "}"].join("\n"),
|
||||
},
|
||||
|
||||
// ContextAwareReplacer - test with trailing newline in find string
|
||||
{
|
||||
content: [
|
||||
"class Test {",
|
||||
" method1() {",
|
||||
" return 1;",
|
||||
" }",
|
||||
"}",
|
||||
].join("\n"),
|
||||
find: [
|
||||
"class Test {",
|
||||
" // different content",
|
||||
"}",
|
||||
"", // trailing empty line
|
||||
].join("\n"),
|
||||
replace: ["class Test {", " method2() { return 2; }", "}"].join("\n"),
|
||||
},
|
||||
|
||||
// Test validation for empty strings with same oldString and newString
|
||||
{
|
||||
content: "",
|
||||
find: "",
|
||||
replace: "",
|
||||
fail: true,
|
||||
},
|
||||
|
||||
// Test multiple occurrences with replaceAll=false (should fail)
|
||||
{
|
||||
content: ["const a = 1;", "const b = 1;", "const c = 1;"].join("\n"),
|
||||
find: "= 1",
|
||||
replace: "= 2",
|
||||
all: false,
|
||||
fail: true,
|
||||
},
|
||||
|
||||
// Test whitespace normalization with multiple spaces and tabs mixed
|
||||
{
|
||||
content: "if\t \t( \tcondition\t )\t{",
|
||||
find: "if ( condition ) {",
|
||||
replace: "if (newCondition) {",
|
||||
},
|
||||
|
||||
// Test escape sequences in template literals
|
||||
{
|
||||
content: "const msg = `Hello\\tWorld`;",
|
||||
find: "const msg = `Hello\\tWorld`;",
|
||||
replace: "const msg = `Hi\\tWorld`;",
|
||||
},
|
||||
]
|
||||
|
||||
describe("EditTool Replacers", () => {
|
||||
test.each(testCases)("case %#", (testCase) => {
|
||||
if (testCase.fail) {
|
||||
expect(() => {
|
||||
replace(testCase.content, testCase.find, testCase.replace, testCase.all)
|
||||
}).toThrow()
|
||||
} else {
|
||||
const result = replace(
|
||||
testCase.content,
|
||||
testCase.find,
|
||||
testCase.replace,
|
||||
testCase.all,
|
||||
)
|
||||
expect(result).toContain(testCase.replace)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
# TUI Agent Guidelines
|
||||
|
||||
## Build/Test Commands
|
||||
|
||||
- **Build**: `go build ./cmd/opencode` (builds main binary)
|
||||
- **Test**: `go test ./...` (runs all tests)
|
||||
- **Single test**: `go test ./internal/theme -run TestLoadThemesFromJSON` (specific test)
|
||||
- **Release build**: Uses `.goreleaser.yml` configuration
|
||||
|
||||
## Code Style
|
||||
|
||||
- **Language**: Go 1.24+ with standard formatting (`gofmt`)
|
||||
- **Imports**: Group standard, third-party, local packages with blank lines
|
||||
- **Naming**: Go conventions - PascalCase exports, camelCase private, ALL_CAPS constants
|
||||
- **Error handling**: Return errors explicitly, use `fmt.Errorf` for wrapping
|
||||
- **Structs**: Define clear interfaces, embed when appropriate
|
||||
- **Testing**: Use table-driven tests, `t.TempDir()` for file operations
|
||||
|
||||
## Architecture
|
||||
|
||||
- **TUI Framework**: Bubble Tea v2 with Lipgloss v2 for styling
|
||||
- **Client**: Generated OpenAPI client communicates with TypeScript server
|
||||
- **Components**: Reusable UI components in `internal/components/`
|
||||
- **Themes**: JSON-based theming system with override hierarchy
|
||||
- **State**: Centralized app state with message passing
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/sst/opencode-sdk-go"
|
||||
"github.com/sst/opencode-sdk-go/option"
|
||||
"github.com/sst/opencode/internal/app"
|
||||
"github.com/sst/opencode/internal/tui"
|
||||
"github.com/sst/opencode/pkg/client"
|
||||
@@ -25,7 +27,7 @@ func main() {
|
||||
url := os.Getenv("OPENCODE_SERVER")
|
||||
|
||||
appInfoStr := os.Getenv("OPENCODE_APP_INFO")
|
||||
var appInfo client.AppInfo
|
||||
var appInfo opencode.App
|
||||
err := json.Unmarshal([]byte(appInfoStr), &appInfo)
|
||||
if err != nil {
|
||||
slog.Error("Failed to unmarshal app info", "error", err)
|
||||
@@ -49,7 +51,12 @@ func main() {
|
||||
logger := slog.New(slog.NewTextHandler(file, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
slog.SetDefault(logger)
|
||||
|
||||
httpClient, err := client.NewClientWithResponses(url)
|
||||
slog.Debug("TUI launched", "app", appInfo)
|
||||
|
||||
httpClient := opencode.NewClient(
|
||||
option.WithBaseURL(url),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
slog.Error("Failed to create client", "error", err)
|
||||
os.Exit(1)
|
||||
@@ -71,13 +78,7 @@ func main() {
|
||||
tea.WithMouseCellMotion(),
|
||||
)
|
||||
|
||||
eventClient, err := client.NewClient(url)
|
||||
if err != nil {
|
||||
slog.Error("Failed to create event client", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
evts, err := eventClient.Event(ctx)
|
||||
evts, err := client.Event(httpClient, url, ctx)
|
||||
if err != nil {
|
||||
slog.Error("Failed to subscribe to events", "error", err)
|
||||
os.Exit(1)
|
||||
|
||||
+7
-2
@@ -16,6 +16,8 @@ require (
|
||||
github.com/muesli/termenv v0.16.0
|
||||
github.com/oapi-codegen/runtime v1.1.1
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3
|
||||
github.com/sst/opencode-sdk-go v0.1.0-alpha.5
|
||||
github.com/tidwall/gjson v1.14.4
|
||||
rsc.io/qr v0.2.0
|
||||
)
|
||||
|
||||
@@ -48,6 +50,9 @@ require (
|
||||
github.com/sosodev/duration v1.3.1 // indirect
|
||||
github.com/speakeasy-api/openapi-overlay v0.9.0 // indirect
|
||||
github.com/spf13/cobra v1.9.1 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/tools v0.31.0 // indirect
|
||||
@@ -68,10 +73,10 @@ require (
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rivo/uniseg v0.4.7
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
|
||||
@@ -191,6 +191,8 @@ github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wx
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
|
||||
github.com/sst/opencode-sdk-go v0.1.0-alpha.5 h1:iZjdSHLo6jOMjUbDH5JWi+44v76yNbEktsRqG/Qxrco=
|
||||
github.com/sst/opencode-sdk-go v0.1.0-alpha.5/go.mod h1:uagorfAHZsVy6vf0xY6TlQraM4uCILdZ5tKKhl1oToM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
@@ -198,6 +200,16 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
|
||||
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk=
|
||||
|
||||
@@ -11,34 +11,35 @@ import (
|
||||
"log/slog"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/sst/opencode-sdk-go"
|
||||
"github.com/sst/opencode/internal/commands"
|
||||
"github.com/sst/opencode/internal/components/toast"
|
||||
"github.com/sst/opencode/internal/config"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
"github.com/sst/opencode/internal/util"
|
||||
"github.com/sst/opencode/pkg/client"
|
||||
)
|
||||
|
||||
var RootPath string
|
||||
|
||||
type App struct {
|
||||
Info client.AppInfo
|
||||
Info opencode.App
|
||||
Version string
|
||||
StatePath string
|
||||
Config *client.ConfigInfo
|
||||
Client *client.ClientWithResponses
|
||||
Config *opencode.Config
|
||||
Client *opencode.Client
|
||||
State *config.State
|
||||
Provider *client.ProviderInfo
|
||||
Model *client.ModelInfo
|
||||
Session *client.SessionInfo
|
||||
Messages []client.MessageInfo
|
||||
Provider *opencode.Provider
|
||||
Model *opencode.Model
|
||||
Session *opencode.Session
|
||||
Messages []opencode.Message
|
||||
Commands commands.CommandRegistry
|
||||
}
|
||||
|
||||
type SessionSelectedMsg = *client.SessionInfo
|
||||
type SessionSelectedMsg = *opencode.Session
|
||||
type ModelSelectedMsg struct {
|
||||
Provider client.ProviderInfo
|
||||
Model client.ModelInfo
|
||||
Provider opencode.Provider
|
||||
Model opencode.Model
|
||||
}
|
||||
type SessionClearedMsg struct{}
|
||||
type CompactSessionMsg struct{}
|
||||
@@ -50,31 +51,24 @@ type CompletionDialogTriggeredMsg struct {
|
||||
InitialValue string
|
||||
}
|
||||
type OptimisticMessageAddedMsg struct {
|
||||
Message client.MessageInfo
|
||||
Message opencode.Message
|
||||
}
|
||||
|
||||
func New(
|
||||
ctx context.Context,
|
||||
version string,
|
||||
appInfo client.AppInfo,
|
||||
httpClient *client.ClientWithResponses,
|
||||
appInfo opencode.App,
|
||||
httpClient *opencode.Client,
|
||||
) (*App, error) {
|
||||
RootPath = appInfo.Path.Root
|
||||
|
||||
configResponse, err := httpClient.PostConfigGetWithResponse(ctx)
|
||||
configInfo, err := httpClient.Config.Get(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if configResponse.StatusCode() != 200 || configResponse.JSON200 == nil {
|
||||
return nil, fmt.Errorf("failed to get config: %d", configResponse.StatusCode())
|
||||
}
|
||||
configInfo := configResponse.JSON200
|
||||
if configInfo.Keybinds == nil {
|
||||
leader := "ctrl+x"
|
||||
keybinds := client.ConfigKeybinds{
|
||||
Leader: &leader,
|
||||
}
|
||||
configInfo.Keybinds = &keybinds
|
||||
|
||||
if configInfo.Keybinds.Leader == "" {
|
||||
configInfo.Keybinds.Leader = "ctrl+x"
|
||||
}
|
||||
|
||||
appStatePath := filepath.Join(appInfo.Path.State, "tui")
|
||||
@@ -84,16 +78,16 @@ func New(
|
||||
config.SaveState(appStatePath, appState)
|
||||
}
|
||||
|
||||
if configInfo.Theme != nil {
|
||||
appState.Theme = *configInfo.Theme
|
||||
if configInfo.Theme != "" {
|
||||
appState.Theme = configInfo.Theme
|
||||
}
|
||||
if configInfo.Model != nil {
|
||||
splits := strings.Split(*configInfo.Model, "/")
|
||||
|
||||
if configInfo.Model != "" {
|
||||
splits := strings.Split(configInfo.Model, "/")
|
||||
appState.Provider = splits[0]
|
||||
appState.Model = strings.Join(splits[1:], "/")
|
||||
}
|
||||
|
||||
// Load themes from all directories
|
||||
if err := theme.LoadThemesFromDirectories(
|
||||
appInfo.Path.Config,
|
||||
appInfo.Path.Root,
|
||||
@@ -103,6 +97,12 @@ func New(
|
||||
}
|
||||
|
||||
if appState.Theme != "" {
|
||||
if appState.Theme == "system" && styles.Terminal != nil {
|
||||
theme.UpdateSystemTheme(
|
||||
styles.Terminal.Background,
|
||||
styles.Terminal.BackgroundIsDark,
|
||||
)
|
||||
}
|
||||
theme.SetTheme(appState.Theme)
|
||||
}
|
||||
|
||||
@@ -115,8 +115,8 @@ func New(
|
||||
Config: configInfo,
|
||||
State: appState,
|
||||
Client: httpClient,
|
||||
Session: &client.SessionInfo{},
|
||||
Messages: []client.MessageInfo{},
|
||||
Session: &opencode.Session{},
|
||||
Messages: []opencode.Message{},
|
||||
Commands: commands.LoadFromConfig(configInfo),
|
||||
}
|
||||
|
||||
@@ -125,23 +125,19 @@ func New(
|
||||
|
||||
func (a *App) InitializeProvider() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
providersResponse, err := a.Client.PostProviderListWithResponse(context.Background())
|
||||
providersResponse, err := a.Client.Config.Providers(context.Background())
|
||||
if err != nil {
|
||||
slog.Error("Failed to list providers", "error", err)
|
||||
// TODO: notify user
|
||||
return nil
|
||||
}
|
||||
if providersResponse != nil && providersResponse.StatusCode() != 200 {
|
||||
slog.Error("failed to retrieve providers", "status", providersResponse.StatusCode(), "message", string(providersResponse.Body))
|
||||
return nil
|
||||
}
|
||||
providers := []client.ProviderInfo{}
|
||||
var defaultProvider *client.ProviderInfo
|
||||
var defaultModel *client.ModelInfo
|
||||
providers := providersResponse.Providers
|
||||
var defaultProvider *opencode.Provider
|
||||
var defaultModel *opencode.Model
|
||||
|
||||
var anthropic *client.ProviderInfo
|
||||
for _, provider := range providersResponse.JSON200.Providers {
|
||||
if provider.Id == "anthropic" {
|
||||
var anthropic *opencode.Provider
|
||||
for _, provider := range providers {
|
||||
if provider.ID == "anthropic" {
|
||||
anthropic = &provider
|
||||
}
|
||||
}
|
||||
@@ -152,7 +148,7 @@ func (a *App) InitializeProvider() tea.Cmd {
|
||||
defaultModel = getDefaultModel(providersResponse, *anthropic)
|
||||
}
|
||||
|
||||
for _, provider := range providersResponse.JSON200.Providers {
|
||||
for _, provider := range providers {
|
||||
if defaultProvider == nil || defaultModel == nil {
|
||||
defaultProvider = &provider
|
||||
defaultModel = getDefaultModel(providersResponse, provider)
|
||||
@@ -164,14 +160,14 @@ func (a *App) InitializeProvider() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
var currentProvider *client.ProviderInfo
|
||||
var currentModel *client.ModelInfo
|
||||
var currentProvider *opencode.Provider
|
||||
var currentModel *opencode.Model
|
||||
for _, provider := range providers {
|
||||
if provider.Id == a.State.Provider {
|
||||
if provider.ID == a.State.Provider {
|
||||
currentProvider = &provider
|
||||
|
||||
for _, model := range provider.Models {
|
||||
if model.Id == a.State.Model {
|
||||
if model.ID == a.State.Model {
|
||||
currentModel = &model
|
||||
}
|
||||
}
|
||||
@@ -182,7 +178,6 @@ func (a *App) InitializeProvider() tea.Cmd {
|
||||
currentModel = defaultModel
|
||||
}
|
||||
|
||||
// TODO: handle no provider or model setup, yet
|
||||
return ModelSelectedMsg{
|
||||
Provider: *currentProvider,
|
||||
Model: *currentModel,
|
||||
@@ -190,8 +185,8 @@ func (a *App) InitializeProvider() tea.Cmd {
|
||||
}
|
||||
}
|
||||
|
||||
func getDefaultModel(response *client.PostProviderListResponse, provider client.ProviderInfo) *client.ModelInfo {
|
||||
if match, ok := response.JSON200.Default[provider.Id]; ok {
|
||||
func getDefaultModel(response *opencode.ConfigProvidersResponse, provider opencode.Provider) *opencode.Model {
|
||||
if match, ok := response.Default[provider.ID]; ok {
|
||||
model := provider.Models[match]
|
||||
return &model
|
||||
} else {
|
||||
@@ -215,7 +210,7 @@ func (a *App) IsBusy() bool {
|
||||
}
|
||||
|
||||
lastMessage := a.Messages[len(a.Messages)-1]
|
||||
return lastMessage.Metadata.Time.Completed == nil
|
||||
return lastMessage.Metadata.Time.Completed == 0
|
||||
}
|
||||
|
||||
func (a *App) SaveState() {
|
||||
@@ -238,19 +233,14 @@ func (a *App) InitializeProject(ctx context.Context) tea.Cmd {
|
||||
cmds = append(cmds, util.CmdHandler(SessionSelectedMsg(session)))
|
||||
|
||||
go func() {
|
||||
response, err := a.Client.PostSessionInitialize(ctx, client.PostSessionInitializeJSONRequestBody{
|
||||
SessionID: a.Session.Id,
|
||||
ProviderID: a.Provider.Id,
|
||||
ModelID: a.Model.Id,
|
||||
_, err := a.Client.Session.Init(ctx, a.Session.ID, opencode.SessionInitParams{
|
||||
ProviderID: opencode.F(a.Provider.ID),
|
||||
ModelID: opencode.F(a.Model.ID),
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("Failed to initialize project", "error", err)
|
||||
// status.Error(err.Error())
|
||||
}
|
||||
if response != nil && response.StatusCode != 200 {
|
||||
slog.Error("Failed to initialize project", "error", response.StatusCode)
|
||||
// status.Error(fmt.Sprintf("failed to initialize project: %d", response.StatusCode))
|
||||
}
|
||||
}()
|
||||
|
||||
return tea.Batch(cmds...)
|
||||
@@ -258,48 +248,37 @@ func (a *App) InitializeProject(ctx context.Context) tea.Cmd {
|
||||
|
||||
func (a *App) CompactSession(ctx context.Context) tea.Cmd {
|
||||
go func() {
|
||||
response, err := a.Client.PostSessionSummarizeWithResponse(ctx, client.PostSessionSummarizeJSONRequestBody{
|
||||
SessionID: a.Session.Id,
|
||||
ProviderID: a.Provider.Id,
|
||||
ModelID: a.Model.Id,
|
||||
_, err := a.Client.Session.Summarize(ctx, a.Session.ID, opencode.SessionSummarizeParams{
|
||||
ProviderID: opencode.F(a.Provider.ID),
|
||||
ModelID: opencode.F(a.Model.ID),
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("Failed to compact session", "error", err)
|
||||
}
|
||||
if response != nil && response.StatusCode() != 200 {
|
||||
slog.Error("Failed to compact session", "error", response.StatusCode)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) MarkProjectInitialized(ctx context.Context) error {
|
||||
response, err := a.Client.PostAppInitialize(ctx)
|
||||
_, err := a.Client.App.Init(ctx)
|
||||
if err != nil {
|
||||
slog.Error("Failed to mark project as initialized", "error", err)
|
||||
return err
|
||||
}
|
||||
if response != nil && response.StatusCode != 200 {
|
||||
return fmt.Errorf("failed to initialize project: %d", response.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) CreateSession(ctx context.Context) (*client.SessionInfo, error) {
|
||||
resp, err := a.Client.PostSessionCreateWithResponse(ctx)
|
||||
func (a *App) CreateSession(ctx context.Context) (*opencode.Session, error) {
|
||||
session, err := a.Client.Session.New(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp != nil && resp.StatusCode() != 200 {
|
||||
return nil, fmt.Errorf("failed to create session: %d", resp.StatusCode())
|
||||
}
|
||||
session := resp.JSON200
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (a *App) SendChatMessage(ctx context.Context, text string, attachments []Attachment) tea.Cmd {
|
||||
var cmds []tea.Cmd
|
||||
if a.Session.Id == "" {
|
||||
if a.Session.ID == "" {
|
||||
session, err := a.CreateSession(ctx)
|
||||
if err != nil {
|
||||
return toast.NewErrorToast(err.Error())
|
||||
@@ -308,26 +287,18 @@ func (a *App) SendChatMessage(ctx context.Context, text string, attachments []At
|
||||
cmds = append(cmds, util.CmdHandler(SessionSelectedMsg(session)))
|
||||
}
|
||||
|
||||
part := client.MessagePart{}
|
||||
part.FromMessagePartText(client.MessagePartText{
|
||||
Type: "text",
|
||||
Text: text,
|
||||
})
|
||||
parts := []client.MessagePart{part}
|
||||
|
||||
optimisticMessage := client.MessageInfo{
|
||||
Id: fmt.Sprintf("optimistic-%d", time.Now().UnixNano()),
|
||||
Role: client.User,
|
||||
Parts: parts,
|
||||
Metadata: client.MessageMetadata{
|
||||
SessionID: a.Session.Id,
|
||||
Time: struct {
|
||||
Completed *float32 `json:"completed,omitempty"`
|
||||
Created float32 `json:"created"`
|
||||
}{
|
||||
Created: float32(time.Now().Unix()),
|
||||
optimisticMessage := opencode.Message{
|
||||
ID: fmt.Sprintf("optimistic-%d", time.Now().UnixNano()),
|
||||
Role: opencode.MessageRoleUser,
|
||||
Parts: []opencode.MessagePart{{
|
||||
Type: opencode.MessagePartTypeText,
|
||||
Text: text,
|
||||
}},
|
||||
Metadata: opencode.MessageMetadata{
|
||||
SessionID: a.Session.ID,
|
||||
Time: opencode.MessageMetadataTime{
|
||||
Created: float64(time.Now().Unix()),
|
||||
},
|
||||
Tool: make(map[string]client.MessageMetadata_Tool_AdditionalProperties),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -335,22 +306,21 @@ func (a *App) SendChatMessage(ctx context.Context, text string, attachments []At
|
||||
cmds = append(cmds, util.CmdHandler(OptimisticMessageAddedMsg{Message: optimisticMessage}))
|
||||
|
||||
cmds = append(cmds, func() tea.Msg {
|
||||
response, err := a.Client.PostSessionChat(ctx, client.PostSessionChatJSONRequestBody{
|
||||
SessionID: a.Session.Id,
|
||||
Parts: parts,
|
||||
ProviderID: a.Provider.Id,
|
||||
ModelID: a.Model.Id,
|
||||
_, err := a.Client.Session.Chat(ctx, a.Session.ID, opencode.SessionChatParams{
|
||||
Parts: opencode.F([]opencode.MessagePartUnionParam{
|
||||
opencode.TextPartParam{
|
||||
Type: opencode.F(opencode.TextPartTypeText),
|
||||
Text: opencode.F(text),
|
||||
},
|
||||
}),
|
||||
ProviderID: opencode.F(a.Provider.ID),
|
||||
ModelID: opencode.F(a.Model.ID),
|
||||
})
|
||||
if err != nil {
|
||||
errormsg := fmt.Sprintf("failed to send message: %v", err)
|
||||
slog.Error(errormsg)
|
||||
return toast.NewErrorToast(errormsg)()
|
||||
}
|
||||
if response != nil && response.StatusCode != 200 {
|
||||
errormsg := fmt.Sprintf("failed to send message: %d", response.StatusCode)
|
||||
slog.Error(errormsg)
|
||||
return toast.NewErrorToast(errormsg)()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -360,83 +330,61 @@ func (a *App) SendChatMessage(ctx context.Context, text string, attachments []At
|
||||
}
|
||||
|
||||
func (a *App) Cancel(ctx context.Context, sessionID string) error {
|
||||
response, err := a.Client.PostSessionAbort(ctx, client.PostSessionAbortJSONRequestBody{
|
||||
SessionID: sessionID,
|
||||
})
|
||||
_, err := a.Client.Session.Abort(ctx, sessionID)
|
||||
if err != nil {
|
||||
slog.Error("Failed to cancel session", "error", err)
|
||||
// status.Error(err.Error())
|
||||
return err
|
||||
}
|
||||
if response != nil && response.StatusCode != 200 {
|
||||
slog.Error("Failed to cancel session", "error", fmt.Sprintf("failed to cancel session: %d", response.StatusCode))
|
||||
// status.Error(fmt.Sprintf("failed to cancel session: %d", response.StatusCode))
|
||||
return fmt.Errorf("failed to cancel session: %d", response.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) ListSessions(ctx context.Context) ([]client.SessionInfo, error) {
|
||||
resp, err := a.Client.PostSessionListWithResponse(ctx)
|
||||
func (a *App) ListSessions(ctx context.Context) ([]opencode.Session, error) {
|
||||
response, err := a.Client.Session.List(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode() != 200 {
|
||||
return nil, fmt.Errorf("failed to list sessions: %d", resp.StatusCode())
|
||||
if response == nil {
|
||||
return []opencode.Session{}, nil
|
||||
}
|
||||
if resp.JSON200 == nil {
|
||||
return []client.SessionInfo{}, nil
|
||||
}
|
||||
sessions := *resp.JSON200
|
||||
|
||||
sessions := *response
|
||||
sort.Slice(sessions, func(i, j int) bool {
|
||||
return sessions[i].Time.Created-sessions[j].Time.Created > 0
|
||||
})
|
||||
|
||||
return sessions, nil
|
||||
}
|
||||
|
||||
func (a *App) DeleteSession(ctx context.Context, sessionID string) error {
|
||||
resp, err := a.Client.PostSessionDeleteWithResponse(ctx, client.PostSessionDeleteJSONRequestBody{
|
||||
SessionID: sessionID,
|
||||
})
|
||||
_, err := a.Client.Session.Delete(ctx, sessionID)
|
||||
if err != nil {
|
||||
slog.Error("Failed to delete session", "error", err)
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode() != 200 {
|
||||
return fmt.Errorf("failed to delete session: %d", resp.StatusCode())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) ListMessages(ctx context.Context, sessionId string) ([]client.MessageInfo, error) {
|
||||
resp, err := a.Client.PostSessionMessagesWithResponse(ctx, client.PostSessionMessagesJSONRequestBody{SessionID: sessionId})
|
||||
func (a *App) ListMessages(ctx context.Context, sessionId string) ([]opencode.Message, error) {
|
||||
response, err := a.Client.Session.Messages(ctx, sessionId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode() != 200 {
|
||||
return nil, fmt.Errorf("failed to list messages: %d", resp.StatusCode())
|
||||
if response == nil {
|
||||
return []opencode.Message{}, nil
|
||||
}
|
||||
if resp.JSON200 == nil {
|
||||
return []client.MessageInfo{}, nil
|
||||
}
|
||||
messages := *resp.JSON200
|
||||
messages := *response
|
||||
return messages, nil
|
||||
}
|
||||
|
||||
func (a *App) ListProviders(ctx context.Context) ([]client.ProviderInfo, error) {
|
||||
resp, err := a.Client.PostProviderListWithResponse(ctx)
|
||||
func (a *App) ListProviders(ctx context.Context) ([]opencode.Provider, error) {
|
||||
response, err := a.Client.Config.Providers(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode() != 200 {
|
||||
return nil, fmt.Errorf("failed to list sessions: %d", resp.StatusCode())
|
||||
}
|
||||
if resp.JSON200 == nil {
|
||||
return []client.ProviderInfo{}, nil
|
||||
if response == nil {
|
||||
return []opencode.Provider{}, nil
|
||||
}
|
||||
|
||||
providers := *resp.JSON200
|
||||
providers := *response
|
||||
return providers.Providers, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/sst/opencode/pkg/client"
|
||||
"github.com/sst/opencode-sdk-go"
|
||||
)
|
||||
|
||||
type ExecuteCommandMsg Command
|
||||
@@ -123,7 +123,7 @@ func parseBindings(bindings ...string) []Keybinding {
|
||||
return parsedBindings
|
||||
}
|
||||
|
||||
func LoadFromConfig(config *client.ConfigInfo) CommandRegistry {
|
||||
func LoadFromConfig(config *opencode.Config) CommandRegistry {
|
||||
defaults := []Command{
|
||||
{
|
||||
Name: AppHelpCommand,
|
||||
@@ -269,10 +269,10 @@ func LoadFromConfig(config *client.ConfigInfo) CommandRegistry {
|
||||
}
|
||||
registry := make(CommandRegistry)
|
||||
keybinds := map[string]string{}
|
||||
marshalled, _ := json.Marshal(*config.Keybinds)
|
||||
marshalled, _ := json.Marshal(config.Keybinds)
|
||||
json.Unmarshal(marshalled, &keybinds)
|
||||
for _, command := range defaults {
|
||||
if keybind, ok := keybinds[string(command.Name)]; ok {
|
||||
if keybind, ok := keybinds[string(command.Name)]; ok && keybind != "" {
|
||||
command.Keybindings = parseBindings(keybind)
|
||||
}
|
||||
registry[command.Name] = command
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/sst/opencode/internal/app"
|
||||
"github.com/sst/opencode/internal/commands"
|
||||
"github.com/sst/opencode/internal/components/dialog"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
)
|
||||
|
||||
@@ -37,7 +38,7 @@ func (c *CommandCompletionProvider) GetEmptyMessage() string {
|
||||
|
||||
func getCommandCompletionItem(cmd commands.Command, space int, t theme.Theme) dialog.CompletionItemI {
|
||||
spacer := strings.Repeat(" ", space)
|
||||
title := " /" + cmd.Trigger + lipgloss.NewStyle().Foreground(t.TextMuted()).Render(spacer+cmd.Description)
|
||||
title := " /" + cmd.Trigger + styles.NewStyle().Foreground(t.TextMuted()).Render(spacer+cmd.Description)
|
||||
value := string(cmd.Name)
|
||||
return dialog.NewCompletionItem(dialog.CompletionItem{
|
||||
Title: title,
|
||||
|
||||
@@ -3,9 +3,9 @@ package completions
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sst/opencode-sdk-go"
|
||||
"github.com/sst/opencode/internal/app"
|
||||
"github.com/sst/opencode/internal/components/dialog"
|
||||
"github.com/sst/opencode/pkg/client"
|
||||
)
|
||||
|
||||
type filesAndFoldersContextGroup struct {
|
||||
@@ -29,17 +29,14 @@ func (cg *filesAndFoldersContextGroup) GetEmptyMessage() string {
|
||||
}
|
||||
|
||||
func (cg *filesAndFoldersContextGroup) getFiles(query string) ([]string, error) {
|
||||
response, err := cg.app.Client.PostFileSearchWithResponse(context.Background(), client.PostFileSearchJSONRequestBody{
|
||||
Query: query,
|
||||
})
|
||||
files, err := cg.app.Client.File.Search(
|
||||
context.Background(),
|
||||
opencode.FileSearchParams{Query: opencode.F(query)},
|
||||
)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
if response.JSON200 == nil {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
return *response.JSON200, nil
|
||||
return *files, nil
|
||||
}
|
||||
|
||||
func (cg *filesAndFoldersContextGroup) GetChildEntries(query string) ([]dialog.CompletionItemI, error) {
|
||||
|
||||
@@ -26,6 +26,9 @@ type EditorComponent interface {
|
||||
Content() string
|
||||
Lines() int
|
||||
Value() string
|
||||
Focused() bool
|
||||
Focus() (tea.Model, tea.Cmd)
|
||||
Blur()
|
||||
Submit() (tea.Model, tea.Cmd)
|
||||
Clear() (tea.Model, tea.Cmd)
|
||||
Paste() (tea.Model, tea.Cmd)
|
||||
@@ -48,7 +51,7 @@ type editorComponent struct {
|
||||
}
|
||||
|
||||
func (m *editorComponent) Init() tea.Cmd {
|
||||
return tea.Batch(textarea.Blink, m.spinner.Tick, tea.EnableReportFocus)
|
||||
return tea.Batch(m.textarea.Focus(), m.spinner.Tick, tea.EnableReportFocus)
|
||||
}
|
||||
|
||||
func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
@@ -69,7 +72,7 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case dialog.ThemeSelectedMsg:
|
||||
m.textarea = createTextArea(&m.textarea)
|
||||
m.spinner = createSpinner()
|
||||
return m, tea.Batch(m.spinner.Tick, textarea.Blink)
|
||||
return m, tea.Batch(m.spinner.Tick, m.textarea.Focus())
|
||||
case dialog.CompletionSelectedMsg:
|
||||
if msg.IsCommand {
|
||||
commandName := strings.TrimPrefix(msg.CompletionValue, "/")
|
||||
@@ -80,7 +83,7 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, tea.Batch(cmds...)
|
||||
} else {
|
||||
existingValue := m.textarea.Value()
|
||||
|
||||
|
||||
// Replace the current token (after last space)
|
||||
lastSpaceIndex := strings.LastIndex(existingValue, " ")
|
||||
if lastSpaceIndex == -1 {
|
||||
@@ -104,12 +107,11 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
func (m *editorComponent) Content() string {
|
||||
t := theme.CurrentTheme()
|
||||
base := styles.BaseStyle().Background(t.Background()).Render
|
||||
muted := styles.Muted().Background(t.Background()).Render
|
||||
promptStyle := lipgloss.NewStyle().
|
||||
base := styles.NewStyle().Foreground(t.Text()).Background(t.Background()).Render
|
||||
muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
|
||||
promptStyle := styles.NewStyle().Foreground(t.Primary()).
|
||||
Padding(0, 0, 0, 1).
|
||||
Bold(true).
|
||||
Foreground(t.Primary())
|
||||
Bold(true)
|
||||
prompt := promptStyle.Render(">")
|
||||
|
||||
textarea := lipgloss.JoinHorizontal(
|
||||
@@ -117,11 +119,16 @@ func (m *editorComponent) Content() string {
|
||||
prompt,
|
||||
m.textarea.View(),
|
||||
)
|
||||
textarea = styles.BaseStyle().
|
||||
textarea = styles.NewStyle().
|
||||
Background(t.BackgroundElement()).
|
||||
Width(m.width).
|
||||
PaddingTop(1).
|
||||
PaddingBottom(1).
|
||||
Background(t.BackgroundElement()).
|
||||
BorderStyle(lipgloss.ThickBorder()).
|
||||
BorderForeground(t.Border()).
|
||||
BorderBackground(t.Background()).
|
||||
BorderLeft(true).
|
||||
BorderRight(true).
|
||||
Render(textarea)
|
||||
|
||||
hint := base(m.getSubmitKeyText()) + muted(" send ")
|
||||
@@ -140,10 +147,10 @@ func (m *editorComponent) Content() string {
|
||||
}
|
||||
|
||||
space := m.width - 2 - lipgloss.Width(model) - lipgloss.Width(hint)
|
||||
spacer := lipgloss.NewStyle().Background(t.Background()).Width(space).Render("")
|
||||
spacer := styles.NewStyle().Background(t.Background()).Width(space).Render("")
|
||||
|
||||
info := hint + spacer + model
|
||||
info = styles.Padded().Background(t.Background()).Render(info)
|
||||
info = styles.NewStyle().Background(t.Background()).Padding(0, 1).Render(info)
|
||||
|
||||
content := strings.Join([]string{"", textarea, info}, "\n")
|
||||
return content
|
||||
@@ -156,6 +163,18 @@ func (m *editorComponent) View() string {
|
||||
return m.Content()
|
||||
}
|
||||
|
||||
func (m *editorComponent) Focused() bool {
|
||||
return m.textarea.Focused()
|
||||
}
|
||||
|
||||
func (m *editorComponent) Focus() (tea.Model, tea.Cmd) {
|
||||
return m, m.textarea.Focus()
|
||||
}
|
||||
|
||||
func (m *editorComponent) Blur() {
|
||||
m.textarea.Blur()
|
||||
}
|
||||
|
||||
func (m *editorComponent) GetSize() (width, height int) {
|
||||
return m.width, m.height
|
||||
}
|
||||
@@ -163,8 +182,6 @@ func (m *editorComponent) GetSize() (width, height int) {
|
||||
func (m *editorComponent) SetSize(width, height int) tea.Cmd {
|
||||
m.width = width
|
||||
m.height = height
|
||||
m.textarea.SetWidth(width - 5) // account for the prompt and padding right
|
||||
// m.textarea.SetHeight(height - 4)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -297,38 +314,42 @@ func createTextArea(existing *textarea.Model) textarea.Model {
|
||||
|
||||
ta := textarea.New()
|
||||
|
||||
ta.Styles.Blurred.Base = lipgloss.NewStyle().Background(bgColor).Foreground(textColor)
|
||||
ta.Styles.Blurred.CursorLine = lipgloss.NewStyle().Background(bgColor)
|
||||
ta.Styles.Blurred.Placeholder = lipgloss.NewStyle().Background(bgColor).Foreground(textMutedColor)
|
||||
ta.Styles.Blurred.Text = lipgloss.NewStyle().Background(bgColor).Foreground(textColor)
|
||||
ta.Styles.Focused.Base = lipgloss.NewStyle().Background(bgColor).Foreground(textColor)
|
||||
ta.Styles.Focused.CursorLine = lipgloss.NewStyle().Background(bgColor)
|
||||
ta.Styles.Focused.Placeholder = lipgloss.NewStyle().Background(bgColor).Foreground(textMutedColor)
|
||||
ta.Styles.Focused.Text = lipgloss.NewStyle().Background(bgColor).Foreground(textColor)
|
||||
ta.Styles.Blurred.Base = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss()
|
||||
ta.Styles.Blurred.CursorLine = styles.NewStyle().Background(bgColor).Lipgloss()
|
||||
ta.Styles.Blurred.Placeholder = styles.NewStyle().Foreground(textMutedColor).Background(bgColor).Lipgloss()
|
||||
ta.Styles.Blurred.Text = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss()
|
||||
ta.Styles.Focused.Base = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss()
|
||||
ta.Styles.Focused.CursorLine = styles.NewStyle().Background(bgColor).Lipgloss()
|
||||
ta.Styles.Focused.Placeholder = styles.NewStyle().Foreground(textMutedColor).Background(bgColor).Lipgloss()
|
||||
ta.Styles.Focused.Text = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss()
|
||||
ta.Styles.Cursor.Color = t.Primary()
|
||||
|
||||
ta.Prompt = " "
|
||||
ta.ShowLineNumbers = false
|
||||
ta.CharLimit = -1
|
||||
ta.SetWidth(layout.Current.Container.Width - 6)
|
||||
|
||||
if existing != nil {
|
||||
ta.SetValue(existing.Value())
|
||||
ta.SetWidth(existing.Width())
|
||||
// ta.SetWidth(existing.Width())
|
||||
ta.SetHeight(existing.Height())
|
||||
}
|
||||
|
||||
ta.Focus()
|
||||
// ta.Focus()
|
||||
return ta
|
||||
}
|
||||
|
||||
func createSpinner() spinner.Model {
|
||||
t := theme.CurrentTheme()
|
||||
return spinner.New(
|
||||
spinner.WithSpinner(spinner.Ellipsis),
|
||||
spinner.WithStyle(
|
||||
styles.
|
||||
Muted().
|
||||
Background(theme.CurrentTheme().Background()).
|
||||
Width(3)),
|
||||
styles.NewStyle().
|
||||
Background(t.Background()).
|
||||
Foreground(t.TextMuted()).
|
||||
Width(3).
|
||||
Lipgloss(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,12 +12,13 @@ import (
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2/compat"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
"github.com/sst/opencode-sdk-go"
|
||||
"github.com/sst/opencode/internal/app"
|
||||
"github.com/sst/opencode/internal/components/diff"
|
||||
"github.com/sst/opencode/internal/layout"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
"github.com/sst/opencode/pkg/client"
|
||||
"github.com/tidwall/gjson"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
@@ -129,15 +130,13 @@ func renderContentBlock(content string, options ...renderingOption) string {
|
||||
option(renderer)
|
||||
}
|
||||
|
||||
style := styles.BaseStyle().
|
||||
style := styles.NewStyle().Foreground(t.TextMuted()).Background(t.BackgroundPanel()).
|
||||
// MarginTop(renderer.marginTop).
|
||||
// MarginBottom(renderer.marginBottom).
|
||||
PaddingTop(renderer.paddingTop).
|
||||
PaddingBottom(renderer.paddingBottom).
|
||||
PaddingLeft(renderer.paddingLeft).
|
||||
PaddingRight(renderer.paddingRight).
|
||||
Background(t.BackgroundPanel()).
|
||||
Foreground(t.TextMuted()).
|
||||
BorderStyle(lipgloss.ThickBorder())
|
||||
|
||||
align := lipgloss.Left
|
||||
@@ -179,13 +178,13 @@ func renderContentBlock(content string, options ...renderingOption) string {
|
||||
layout.Current.Container.Width,
|
||||
align,
|
||||
content,
|
||||
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||
styles.WhitespaceStyle(t.Background()),
|
||||
)
|
||||
content = lipgloss.PlaceHorizontal(
|
||||
layout.Current.Viewport.Width,
|
||||
lipgloss.Center,
|
||||
content,
|
||||
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||
styles.WhitespaceStyle(t.Background()),
|
||||
)
|
||||
if renderer.marginTop > 0 {
|
||||
for range renderer.marginTop {
|
||||
@@ -211,7 +210,7 @@ func calculatePadding() int {
|
||||
}
|
||||
}
|
||||
|
||||
func renderText(message client.MessageInfo, text string, author string) string {
|
||||
func renderText(message opencode.Message, text string, author string) string {
|
||||
t := theme.CurrentTheme()
|
||||
width := layout.Current.Container.Width
|
||||
padding := calculatePadding()
|
||||
@@ -225,23 +224,30 @@ func renderText(message client.MessageInfo, text string, author string) string {
|
||||
|
||||
textWidth := max(lipgloss.Width(text), lipgloss.Width(info))
|
||||
markdownWidth := min(textWidth, width-padding-4) // -4 for the border and padding
|
||||
if message.Role == client.Assistant {
|
||||
markdownWidth = width - padding - 4 - 2
|
||||
if message.Role == opencode.MessageRoleAssistant {
|
||||
markdownWidth = width - padding - 4 - 3
|
||||
}
|
||||
if message.Role == client.User {
|
||||
text = strings.ReplaceAll(text, "<", "\\<")
|
||||
text = strings.ReplaceAll(text, ">", "\\>")
|
||||
minWidth := max(markdownWidth, (width-4)/2)
|
||||
messageStyle := styles.NewStyle().
|
||||
Width(minWidth).
|
||||
Background(t.BackgroundPanel()).
|
||||
Foreground(t.Text())
|
||||
if textWidth < minWidth {
|
||||
messageStyle = messageStyle.AlignHorizontal(lipgloss.Right)
|
||||
}
|
||||
content := messageStyle.Render(text)
|
||||
if message.Role == opencode.MessageRoleAssistant {
|
||||
content = toMarkdown(text, markdownWidth, t.BackgroundPanel())
|
||||
}
|
||||
content := toMarkdown(text, markdownWidth, t.BackgroundPanel())
|
||||
content = strings.Join([]string{content, info}, "\n")
|
||||
|
||||
switch message.Role {
|
||||
case client.User:
|
||||
case opencode.MessageRoleUser:
|
||||
return renderContentBlock(content,
|
||||
WithAlign(lipgloss.Right),
|
||||
WithBorderColor(t.Secondary()),
|
||||
)
|
||||
case client.Assistant:
|
||||
case opencode.MessageRoleAssistant:
|
||||
return renderContentBlock(content,
|
||||
WithAlign(lipgloss.Left),
|
||||
WithBorderColor(t.Accent()),
|
||||
@@ -251,15 +257,16 @@ func renderText(message client.MessageInfo, text string, author string) string {
|
||||
}
|
||||
|
||||
func renderToolInvocation(
|
||||
toolCall client.MessageToolInvocationToolCall,
|
||||
toolCall opencode.ToolInvocationPart,
|
||||
result *string,
|
||||
metadata client.MessageMetadata_Tool_AdditionalProperties,
|
||||
metadata opencode.MessageMetadataTool,
|
||||
showDetails bool,
|
||||
isLast bool,
|
||||
contentOnly bool,
|
||||
messageMetadata opencode.MessageMetadata,
|
||||
) string {
|
||||
ignoredTools := []string{"todoread"}
|
||||
if slices.Contains(ignoredTools, toolCall.ToolName) {
|
||||
if slices.Contains(ignoredTools, toolCall.ToolInvocation.ToolName) {
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -275,9 +282,10 @@ func renderToolInvocation(
|
||||
}
|
||||
|
||||
t := theme.CurrentTheme()
|
||||
style := styles.Muted().
|
||||
Width(outerWidth).
|
||||
style := styles.NewStyle().
|
||||
Foreground(t.TextMuted()).
|
||||
Background(t.BackgroundPanel()).
|
||||
Width(outerWidth).
|
||||
PaddingTop(paddingTop).
|
||||
PaddingBottom(paddingBottom).
|
||||
PaddingLeft(2).
|
||||
@@ -288,12 +296,14 @@ func renderToolInvocation(
|
||||
BorderForeground(t.BackgroundPanel()).
|
||||
BorderStyle(lipgloss.ThickBorder())
|
||||
|
||||
if toolCall.State == "partial-call" {
|
||||
title := renderToolAction(toolCall.ToolName)
|
||||
if toolCall.ToolInvocation.State == "partial-call" {
|
||||
title := renderToolAction(toolCall.ToolInvocation.ToolName)
|
||||
if !showDetails {
|
||||
title = "∟ " + title
|
||||
padding := calculatePadding()
|
||||
style := lipgloss.NewStyle().Width(outerWidth - padding - 4).Background(t.BackgroundPanel())
|
||||
style := styles.NewStyle().
|
||||
Background(t.BackgroundPanel()).
|
||||
Width(outerWidth - padding - 4 - 3)
|
||||
return renderContentBlock(style.Render(title),
|
||||
WithAlign(lipgloss.Left),
|
||||
WithBorderColor(t.Accent()),
|
||||
@@ -308,8 +318,8 @@ func renderToolInvocation(
|
||||
|
||||
toolArgs := ""
|
||||
toolArgsMap := make(map[string]any)
|
||||
if toolCall.Args != nil {
|
||||
value := *toolCall.Args
|
||||
if toolCall.ToolInvocation.Args != nil {
|
||||
value := toolCall.ToolInvocation.Args
|
||||
if m, ok := value.(map[string]any); ok {
|
||||
toolArgsMap = m
|
||||
|
||||
@@ -331,28 +341,35 @@ func renderToolInvocation(
|
||||
error := ""
|
||||
finished := result != nil && *result != ""
|
||||
|
||||
if e, ok := metadata.Get("error"); ok && e.(bool) == true {
|
||||
if m, ok := metadata.Get("message"); ok {
|
||||
style = style.BorderLeftForeground(t.Error())
|
||||
error = styles.BaseStyle().
|
||||
Background(t.BackgroundPanel()).
|
||||
Foreground(t.Error()).
|
||||
Render(m.(string))
|
||||
error = renderContentBlock(
|
||||
error,
|
||||
WithFullWidth(),
|
||||
WithBorderColor(t.Error()),
|
||||
WithMarginBottom(1),
|
||||
)
|
||||
}
|
||||
er := messageMetadata.Error.AsUnion()
|
||||
switch er.(type) {
|
||||
case nil:
|
||||
default:
|
||||
clientError := er.(opencode.UnknownError)
|
||||
error = clientError.Data.Message
|
||||
}
|
||||
|
||||
if error != "" {
|
||||
style = style.BorderLeftForeground(t.Error())
|
||||
error = styles.NewStyle().
|
||||
Foreground(t.Error()).
|
||||
Background(t.BackgroundPanel()).
|
||||
Render(error)
|
||||
error = renderContentBlock(
|
||||
error,
|
||||
WithFullWidth(),
|
||||
WithBorderColor(t.Error()),
|
||||
WithMarginBottom(1),
|
||||
)
|
||||
}
|
||||
|
||||
title := ""
|
||||
switch toolCall.ToolName {
|
||||
switch toolCall.ToolInvocation.ToolName {
|
||||
case "read":
|
||||
toolArgs = renderArgs(&toolArgsMap, "filePath")
|
||||
title = fmt.Sprintf("READ %s", toolArgs)
|
||||
if preview, ok := metadata.Get("preview"); ok && toolArgsMap["filePath"] != nil {
|
||||
preview := metadata.ExtraFields["preview"]
|
||||
if preview != nil && toolArgsMap["filePath"] != nil {
|
||||
filename := toolArgsMap["filePath"].(string)
|
||||
body = preview.(string)
|
||||
body = renderFile(filename, body, WithTruncate(6))
|
||||
@@ -360,8 +377,9 @@ func renderToolInvocation(
|
||||
case "edit":
|
||||
if filename, ok := toolArgsMap["filePath"].(string); ok {
|
||||
title = fmt.Sprintf("EDIT %s", relative(filename))
|
||||
if d, ok := metadata.Get("diff"); ok {
|
||||
patch := d.(string)
|
||||
diffField := metadata.ExtraFields["diff"]
|
||||
if diffField != nil {
|
||||
patch := diffField.(string)
|
||||
var formattedDiff string
|
||||
if layout.Current.Viewport.Width < 80 {
|
||||
formattedDiff, _ = diff.FormatUnifiedDiff(
|
||||
@@ -374,7 +392,7 @@ func renderToolInvocation(
|
||||
formattedDiff, _ = diff.FormatDiff(filename, patch, diff.WithTotalWidth(diffWidth))
|
||||
}
|
||||
formattedDiff = strings.TrimSpace(formattedDiff)
|
||||
formattedDiff = lipgloss.NewStyle().
|
||||
formattedDiff = styles.NewStyle().
|
||||
BorderStyle(lipgloss.ThickBorder()).
|
||||
BorderBackground(t.Background()).
|
||||
BorderForeground(t.BackgroundPanel()).
|
||||
@@ -394,8 +412,13 @@ func renderToolInvocation(
|
||||
lipgloss.Center,
|
||||
lipgloss.Top,
|
||||
body,
|
||||
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||
styles.WhitespaceStyle(t.Background()),
|
||||
)
|
||||
|
||||
// Add diagnostics at the bottom if they exist
|
||||
if diagnostics := renderDiagnostics(messageMetadata, filename); diagnostics != "" {
|
||||
body += "\n" + renderContentBlock(diagnostics, WithFullWidth(), WithBorderColor(t.Error()))
|
||||
}
|
||||
}
|
||||
}
|
||||
case "write":
|
||||
@@ -403,15 +426,21 @@ func renderToolInvocation(
|
||||
title = fmt.Sprintf("WRITE %s", relative(filename))
|
||||
if content, ok := toolArgsMap["content"].(string); ok {
|
||||
body = renderFile(filename, content)
|
||||
|
||||
// Add diagnostics at the bottom if they exist
|
||||
if diagnostics := renderDiagnostics(messageMetadata, filename); diagnostics != "" {
|
||||
body += "\n" + renderContentBlock(diagnostics, WithFullWidth(), WithBorderColor(t.Error()))
|
||||
}
|
||||
}
|
||||
}
|
||||
case "bash":
|
||||
if description, ok := toolArgsMap["description"].(string); ok {
|
||||
title = fmt.Sprintf("SHELL %s", description)
|
||||
}
|
||||
if stdout, ok := metadata.Get("stdout"); ok {
|
||||
stdout := metadata.JSON.ExtraFields["stdout"]
|
||||
if !stdout.IsNull() {
|
||||
command := toolArgsMap["command"].(string)
|
||||
stdout := stdout.(string)
|
||||
stdout := stdout.Raw()
|
||||
body = fmt.Sprintf("```console\n> %s\n%s```", command, stdout)
|
||||
body = toMarkdown(body, innerWidth, t.BackgroundPanel())
|
||||
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
|
||||
@@ -432,12 +461,13 @@ func renderToolInvocation(
|
||||
case "todowrite":
|
||||
title = fmt.Sprintf("PLAN")
|
||||
|
||||
if to, ok := metadata.Get("todos"); ok && finished {
|
||||
todos := to.([]any)
|
||||
for _, todo := range todos {
|
||||
t := todo.(map[string]any)
|
||||
content := t["content"].(string)
|
||||
switch t["status"].(string) {
|
||||
todos := metadata.JSON.ExtraFields["todos"]
|
||||
if !todos.IsNull() && finished {
|
||||
strTodos := todos.Raw()
|
||||
todos := gjson.Parse(strTodos)
|
||||
for _, todo := range todos.Array() {
|
||||
content := todo.Get("content").String()
|
||||
switch todo.Get("status").String() {
|
||||
case "completed":
|
||||
body += fmt.Sprintf("- [x] %s\n", content)
|
||||
// case "in-progress":
|
||||
@@ -452,21 +482,22 @@ func renderToolInvocation(
|
||||
case "task":
|
||||
if description, ok := toolArgsMap["description"].(string); ok {
|
||||
title = fmt.Sprintf("TASK %s", description)
|
||||
if summary, ok := metadata.Get("summary"); ok {
|
||||
toolcalls := summary.([]any)
|
||||
// toolcalls :=
|
||||
summary := metadata.JSON.ExtraFields["summary"]
|
||||
if !summary.IsNull() {
|
||||
strValue := summary.Raw()
|
||||
toolcalls := gjson.Parse(strValue).Array()
|
||||
|
||||
steps := []string{}
|
||||
for _, toolcall := range toolcalls {
|
||||
call := toolcall.(map[string]any)
|
||||
call := toolcall.Value().(map[string]any)
|
||||
if toolInvocation, ok := call["toolInvocation"].(map[string]any); ok {
|
||||
data, _ := json.Marshal(toolInvocation)
|
||||
var toolCall client.MessageToolInvocationToolCall
|
||||
var toolCall opencode.ToolInvocationPart
|
||||
_ = json.Unmarshal(data, &toolCall)
|
||||
|
||||
if metadata, ok := call["metadata"].(map[string]any); ok {
|
||||
data, _ = json.Marshal(metadata)
|
||||
var toolMetadata client.MessageMetadata_Tool_AdditionalProperties
|
||||
var toolMetadata opencode.MessageMetadataTool
|
||||
_ = json.Unmarshal(data, &toolMetadata)
|
||||
|
||||
step := renderToolInvocation(
|
||||
@@ -476,6 +507,7 @@ func renderToolInvocation(
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
messageMetadata,
|
||||
)
|
||||
steps = append(steps, step)
|
||||
}
|
||||
@@ -487,7 +519,7 @@ func renderToolInvocation(
|
||||
}
|
||||
|
||||
default:
|
||||
toolName := renderToolName(toolCall.ToolName)
|
||||
toolName := renderToolName(toolCall.ToolInvocation.ToolName)
|
||||
title = fmt.Sprintf("%s %s", toolName, toolArgs)
|
||||
if result == nil {
|
||||
empty := ""
|
||||
@@ -506,7 +538,7 @@ func renderToolInvocation(
|
||||
if !showDetails {
|
||||
title = "∟ " + title
|
||||
padding := calculatePadding()
|
||||
style := lipgloss.NewStyle().Width(outerWidth - padding - 4).Background(t.BackgroundPanel())
|
||||
style := styles.NewStyle().Background(t.BackgroundPanel()).Width(outerWidth - padding - 4 - 3)
|
||||
paddingBottom := 0
|
||||
if isLast {
|
||||
paddingBottom = 1
|
||||
@@ -519,7 +551,7 @@ func renderToolInvocation(
|
||||
)
|
||||
}
|
||||
|
||||
if body == "" && error == "" {
|
||||
if body == "" && error == "" && result != nil {
|
||||
body = *result
|
||||
body = truncateHeight(body, 10)
|
||||
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
|
||||
@@ -530,7 +562,7 @@ func renderToolInvocation(
|
||||
layout.Current.Viewport.Width,
|
||||
lipgloss.Center,
|
||||
content,
|
||||
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||
styles.WhitespaceStyle(t.Background()),
|
||||
)
|
||||
if showDetails && body != "" && error == "" {
|
||||
content += "\n" + body
|
||||
@@ -684,3 +716,78 @@ func extension(path string) string {
|
||||
}
|
||||
return ext
|
||||
}
|
||||
|
||||
// Diagnostic represents an LSP diagnostic
|
||||
type Diagnostic struct {
|
||||
Range struct {
|
||||
Start struct {
|
||||
Line int `json:"line"`
|
||||
Character int `json:"character"`
|
||||
} `json:"start"`
|
||||
} `json:"range"`
|
||||
Severity int `json:"severity"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// renderDiagnostics formats LSP diagnostics for display in the TUI
|
||||
func renderDiagnostics(metadata opencode.MessageMetadata, filePath string) string {
|
||||
diagnosticsData := metadata.JSON.ExtraFields["diagnostics"]
|
||||
if diagnosticsData.IsNull() {
|
||||
return ""
|
||||
}
|
||||
|
||||
// diagnosticsData should be a map[string][]Diagnostic
|
||||
strDiagnosticsData := diagnosticsData.Raw()
|
||||
diagnosticsMap := gjson.Parse(strDiagnosticsData).Value().(map[string]any)
|
||||
fileDiagnostics, ok := diagnosticsMap[filePath]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
diagnosticsList, ok := fileDiagnostics.([]any)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
var errorDiagnostics []string
|
||||
for _, diagInterface := range diagnosticsList {
|
||||
diagMap, ok := diagInterface.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse the diagnostic
|
||||
var diag Diagnostic
|
||||
diagBytes, err := json.Marshal(diagMap)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if err := json.Unmarshal(diagBytes, &diag); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Only show error diagnostics (severity === 1)
|
||||
if diag.Severity != 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
line := diag.Range.Start.Line + 1 // 1-based
|
||||
column := diag.Range.Start.Character + 1 // 1-based
|
||||
errorDiagnostics = append(errorDiagnostics, fmt.Sprintf("Error [%d:%d] %s", line, column, diag.Message))
|
||||
}
|
||||
|
||||
if len(errorDiagnostics) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
t := theme.CurrentTheme()
|
||||
var result strings.Builder
|
||||
for _, diagnostic := range errorDiagnostics {
|
||||
if result.Len() > 0 {
|
||||
result.WriteString("\n")
|
||||
}
|
||||
result.WriteString(styles.NewStyle().Foreground(t.Error()).Render(diagnostic))
|
||||
}
|
||||
|
||||
return result.String()
|
||||
}
|
||||
|
||||
@@ -9,13 +9,14 @@ import (
|
||||
"github.com/charmbracelet/bubbles/v2/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/sst/opencode-sdk-go"
|
||||
"github.com/sst/opencode/internal/app"
|
||||
"github.com/sst/opencode/internal/components/commands"
|
||||
"github.com/sst/opencode/internal/components/dialog"
|
||||
"github.com/sst/opencode/internal/layout"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
"github.com/sst/opencode/pkg/client"
|
||||
"github.com/sst/opencode/internal/util"
|
||||
)
|
||||
|
||||
type MessagesComponent interface {
|
||||
@@ -83,7 +84,7 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if m.tail {
|
||||
m.viewport.GotoBottom()
|
||||
}
|
||||
case client.EventSessionUpdated, client.EventMessageUpdated:
|
||||
case opencode.EventListResponseEventSessionUpdated, opencode.EventListResponseEventMessageUpdated:
|
||||
m.renderView()
|
||||
if m.tail {
|
||||
m.viewport.GotoBottom()
|
||||
@@ -121,99 +122,110 @@ func (m *messagesComponent) renderView() {
|
||||
return
|
||||
}
|
||||
|
||||
measure := util.Measure("messages.renderView")
|
||||
defer measure("messageCount", len(m.app.Messages))
|
||||
|
||||
t := theme.CurrentTheme()
|
||||
blocks := make([]string, 0)
|
||||
previousBlockType := none
|
||||
|
||||
for _, message := range m.app.Messages {
|
||||
var content string
|
||||
var cached bool
|
||||
lastToolIndex := 0
|
||||
lastToolIndices := []int{}
|
||||
for i, p := range message.Parts {
|
||||
part, _ := p.ValueByDiscriminator()
|
||||
switch part.(type) {
|
||||
case client.MessagePartText:
|
||||
switch p.Type {
|
||||
case opencode.MessagePartTypeText:
|
||||
lastToolIndices = append(lastToolIndices, lastToolIndex)
|
||||
case client.MessagePartToolInvocation:
|
||||
case opencode.MessagePartTypeToolInvocation:
|
||||
lastToolIndex = i
|
||||
}
|
||||
}
|
||||
|
||||
author := ""
|
||||
switch message.Role {
|
||||
case client.User:
|
||||
case opencode.MessageRoleUser:
|
||||
author = m.app.Info.User
|
||||
case client.Assistant:
|
||||
case opencode.MessageRoleAssistant:
|
||||
author = message.Metadata.Assistant.ModelID
|
||||
}
|
||||
|
||||
for i, p := range message.Parts {
|
||||
part, err := p.ValueByDiscriminator()
|
||||
if err != nil {
|
||||
continue //TODO: handle error?
|
||||
}
|
||||
|
||||
switch part.(type) {
|
||||
switch part := p.AsUnion().(type) {
|
||||
// case client.MessagePartStepStart:
|
||||
// messages = append(messages, "")
|
||||
case client.MessagePartText:
|
||||
text := part.(client.MessagePartText)
|
||||
key := m.cache.GenerateKey(message.Id, text.Text, layout.Current.Viewport.Width)
|
||||
case opencode.TextPart:
|
||||
key := m.cache.GenerateKey(message.ID, p.Text, layout.Current.Viewport.Width)
|
||||
content, cached = m.cache.Get(key)
|
||||
if !cached {
|
||||
content = renderText(message, text.Text, author)
|
||||
content = renderText(message, p.Text, author)
|
||||
m.cache.Set(key, content)
|
||||
}
|
||||
if previousBlockType != none {
|
||||
blocks = append(blocks, "")
|
||||
}
|
||||
blocks = append(blocks, content)
|
||||
if message.Role == client.User {
|
||||
if message.Role == opencode.MessageRoleUser {
|
||||
previousBlockType = userTextBlock
|
||||
} else if message.Role == client.Assistant {
|
||||
} else if message.Role == opencode.MessageRoleAssistant {
|
||||
previousBlockType = assistantTextBlock
|
||||
}
|
||||
case client.MessagePartToolInvocation:
|
||||
case opencode.ToolInvocationPart:
|
||||
isLastToolInvocation := slices.Contains(lastToolIndices, i)
|
||||
toolInvocationPart := part.(client.MessagePartToolInvocation)
|
||||
toolCall, _ := toolInvocationPart.ToolInvocation.AsMessageToolInvocationToolCall()
|
||||
metadata := client.MessageMetadata_Tool_AdditionalProperties{}
|
||||
if _, ok := message.Metadata.Tool[toolCall.ToolCallId]; ok {
|
||||
metadata = message.Metadata.Tool[toolCall.ToolCallId]
|
||||
}
|
||||
var result *string
|
||||
resultPart, resultError := toolInvocationPart.ToolInvocation.AsMessageToolInvocationToolResult()
|
||||
if resultError == nil {
|
||||
result = &resultPart.Result
|
||||
metadata := opencode.MessageMetadataTool{}
|
||||
|
||||
toolCallID := part.ToolInvocation.ToolCallID
|
||||
// var toolCallID string
|
||||
// var result *string
|
||||
// switch toolCall := part.ToolInvocation.AsUnion().(type) {
|
||||
// case opencode.ToolCall:
|
||||
// toolCallID = toolCall.ToolCallID
|
||||
// case opencode.ToolPartialCall:
|
||||
// toolCallID = toolCall.ToolCallID
|
||||
// case opencode.ToolResult:
|
||||
// toolCallID = toolCall.ToolCallID
|
||||
// result = &toolCall.Result
|
||||
// }
|
||||
|
||||
if _, ok := message.Metadata.Tool[toolCallID]; ok {
|
||||
metadata = message.Metadata.Tool[toolCallID]
|
||||
}
|
||||
|
||||
if toolCall.State == "result" {
|
||||
key := m.cache.GenerateKey(message.Id,
|
||||
toolCall.ToolCallId,
|
||||
var result *string
|
||||
if part.ToolInvocation.Result != "" {
|
||||
result = &part.ToolInvocation.Result
|
||||
}
|
||||
|
||||
if part.ToolInvocation.State == "result" {
|
||||
key := m.cache.GenerateKey(message.ID,
|
||||
part.ToolInvocation.ToolCallID,
|
||||
m.showToolDetails,
|
||||
layout.Current.Viewport.Width,
|
||||
)
|
||||
content, cached = m.cache.Get(key)
|
||||
if !cached {
|
||||
content = renderToolInvocation(
|
||||
toolCall,
|
||||
part,
|
||||
result,
|
||||
metadata,
|
||||
m.showToolDetails,
|
||||
isLastToolInvocation,
|
||||
false,
|
||||
message.Metadata,
|
||||
)
|
||||
m.cache.Set(key, content)
|
||||
}
|
||||
} else {
|
||||
// if the tool call isn't finished, don't cache
|
||||
content = renderToolInvocation(
|
||||
toolCall,
|
||||
part,
|
||||
result,
|
||||
metadata,
|
||||
m.showToolDetails,
|
||||
isLastToolInvocation,
|
||||
false,
|
||||
message.Metadata,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -226,16 +238,17 @@ func (m *messagesComponent) renderView() {
|
||||
}
|
||||
|
||||
error := ""
|
||||
if message.Metadata.Error != nil {
|
||||
errorValue, _ := message.Metadata.Error.ValueByDiscriminator()
|
||||
switch errorValue.(type) {
|
||||
case client.UnknownError:
|
||||
clientError := errorValue.(client.UnknownError)
|
||||
error = clientError.Data.Message
|
||||
error = renderContentBlock(error, WithBorderColor(t.Error()), WithFullWidth(), WithMarginTop(1), WithMarginBottom(1))
|
||||
blocks = append(blocks, error)
|
||||
previousBlockType = errorBlock
|
||||
}
|
||||
switch err := message.Metadata.Error.AsUnion().(type) {
|
||||
case nil:
|
||||
default:
|
||||
clientError := err.(opencode.UnknownError)
|
||||
error = clientError.Data.Message
|
||||
}
|
||||
|
||||
if error != "" {
|
||||
error = renderContentBlock(error, WithBorderColor(t.Error()), WithFullWidth(), WithMarginTop(1), WithMarginBottom(1))
|
||||
blocks = append(blocks, error)
|
||||
previousBlockType = errorBlock
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,7 +258,7 @@ func (m *messagesComponent) renderView() {
|
||||
m.width,
|
||||
lipgloss.Center,
|
||||
block,
|
||||
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||
styles.WhitespaceStyle(t.Background()),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -254,28 +267,28 @@ func (m *messagesComponent) renderView() {
|
||||
}
|
||||
|
||||
func (m *messagesComponent) header() string {
|
||||
if m.app.Session.Id == "" {
|
||||
if m.app.Session.ID == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
t := theme.CurrentTheme()
|
||||
width := layout.Current.Container.Width
|
||||
base := styles.BaseStyle().Background(t.Background()).Render
|
||||
muted := styles.Muted().Background(t.Background()).Render
|
||||
base := styles.NewStyle().Foreground(t.Text()).Background(t.Background()).Render
|
||||
muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
|
||||
headerLines := []string{}
|
||||
headerLines = append(headerLines, toMarkdown("# "+m.app.Session.Title, width-6, t.Background()))
|
||||
if m.app.Session.Share != nil && m.app.Session.Share.Url != "" {
|
||||
headerLines = append(headerLines, muted(m.app.Session.Share.Url))
|
||||
if m.app.Session.Share.URL != "" {
|
||||
headerLines = append(headerLines, muted(m.app.Session.Share.URL))
|
||||
} else {
|
||||
headerLines = append(headerLines, base("/share")+muted(" to create a shareable link"))
|
||||
}
|
||||
header := strings.Join(headerLines, "\n")
|
||||
|
||||
header = styles.BaseStyle().
|
||||
header = styles.NewStyle().
|
||||
Background(t.Background()).
|
||||
Width(width).
|
||||
PaddingLeft(2).
|
||||
PaddingRight(2).
|
||||
Background(t.Background()).
|
||||
BorderLeft(true).
|
||||
BorderRight(true).
|
||||
BorderBackground(t.Background()).
|
||||
@@ -306,7 +319,7 @@ func (m *messagesComponent) View() string {
|
||||
m.width,
|
||||
lipgloss.Center,
|
||||
m.header(),
|
||||
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||
styles.WhitespaceStyle(t.Background()),
|
||||
),
|
||||
m.viewport.View(),
|
||||
)
|
||||
@@ -314,9 +327,9 @@ func (m *messagesComponent) View() string {
|
||||
|
||||
func (m *messagesComponent) home() string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle().Background(t.Background())
|
||||
baseStyle := styles.NewStyle().Background(t.Background())
|
||||
base := baseStyle.Render
|
||||
muted := styles.Muted().Background(t.Background()).Render
|
||||
muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
|
||||
|
||||
open := `
|
||||
█▀▀█ █▀▀█ █▀▀ █▀▀▄
|
||||
@@ -335,9 +348,9 @@ func (m *messagesComponent) home() string {
|
||||
// cwd := app.Info.Path.Cwd
|
||||
// config := app.Info.Path.Config
|
||||
|
||||
versionStyle := lipgloss.NewStyle().
|
||||
Background(t.Background()).
|
||||
versionStyle := styles.NewStyle().
|
||||
Foreground(t.TextMuted()).
|
||||
Background(t.Background()).
|
||||
Width(lipgloss.Width(logo)).
|
||||
Align(lipgloss.Right)
|
||||
version := versionStyle.Render(m.app.Version)
|
||||
@@ -347,14 +360,14 @@ func (m *messagesComponent) home() string {
|
||||
m.width,
|
||||
lipgloss.Center,
|
||||
logoAndVersion,
|
||||
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||
styles.WhitespaceStyle(t.Background()),
|
||||
)
|
||||
m.commands.SetBackgroundColor(t.Background())
|
||||
commands := lipgloss.PlaceHorizontal(
|
||||
m.width,
|
||||
lipgloss.Center,
|
||||
m.commands.View(),
|
||||
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||
styles.WhitespaceStyle(t.Background()),
|
||||
)
|
||||
|
||||
lines := []string{}
|
||||
@@ -372,7 +385,7 @@ func (m *messagesComponent) home() string {
|
||||
lipgloss.Center,
|
||||
lipgloss.Center,
|
||||
baseStyle.Render(strings.Join(lines, "\n")),
|
||||
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||
styles.WhitespaceStyle(t.Background()),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -60,15 +60,9 @@ func (c *commandsComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
func (c *commandsComponent) View() string {
|
||||
t := theme.CurrentTheme()
|
||||
|
||||
triggerStyle := lipgloss.NewStyle().
|
||||
Foreground(t.Primary()).
|
||||
Bold(true)
|
||||
|
||||
descriptionStyle := lipgloss.NewStyle().
|
||||
Foreground(t.Text())
|
||||
|
||||
keybindStyle := lipgloss.NewStyle().
|
||||
Foreground(t.TextMuted())
|
||||
triggerStyle := styles.NewStyle().Foreground(t.Primary()).Bold(true)
|
||||
descriptionStyle := styles.NewStyle().Foreground(t.Text())
|
||||
keybindStyle := styles.NewStyle().Foreground(t.TextMuted())
|
||||
|
||||
if c.background != nil {
|
||||
triggerStyle = triggerStyle.Background(*c.background)
|
||||
@@ -99,10 +93,11 @@ func (c *commandsComponent) View() string {
|
||||
}
|
||||
|
||||
if len(commandsToShow) == 0 {
|
||||
muted := styles.NewStyle().Foreground(theme.CurrentTheme().TextMuted())
|
||||
if c.showAll {
|
||||
return styles.Muted().Render("No commands available")
|
||||
return muted.Render("No commands available")
|
||||
}
|
||||
return styles.Muted().Render("No commands with triggers available")
|
||||
return muted.Render("No commands with triggers available")
|
||||
}
|
||||
|
||||
// Calculate column widths
|
||||
@@ -133,7 +128,7 @@ func (c *commandsComponent) View() string {
|
||||
if c.showKeybinds {
|
||||
for _, kb := range cmd.Keybindings {
|
||||
if kb.RequiresLeader {
|
||||
keybindStrs = append(keybindStrs, *c.app.Config.Keybinds.Leader+" "+kb.Key)
|
||||
keybindStrs = append(keybindStrs, c.app.Config.Keybinds.Leader+" "+kb.Key)
|
||||
} else {
|
||||
keybindStrs = append(keybindStrs, kb.Key)
|
||||
}
|
||||
@@ -188,7 +183,7 @@ func (c *commandsComponent) View() string {
|
||||
// Remove trailing newline
|
||||
result := strings.TrimSuffix(output.String(), "\n")
|
||||
if c.background != nil {
|
||||
result = lipgloss.NewStyle().Background(c.background).Width(maxWidth).Render(result)
|
||||
result = styles.NewStyle().Background(*c.background).Width(maxWidth).Render(result)
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/charmbracelet/bubbles/v2/key"
|
||||
"github.com/charmbracelet/bubbles/v2/textarea"
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/sst/opencode/internal/app"
|
||||
"github.com/sst/opencode/internal/components/list"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
@@ -26,7 +27,7 @@ type CompletionItemI interface {
|
||||
|
||||
func (ci *CompletionItem) Render(selected bool, width int) string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
baseStyle := styles.NewStyle().Foreground(t.Text())
|
||||
|
||||
itemStyle := baseStyle.
|
||||
Background(t.BackgroundElement()).
|
||||
@@ -34,8 +35,7 @@ func (ci *CompletionItem) Render(selected bool, width int) string {
|
||||
Padding(0, 1)
|
||||
|
||||
if selected {
|
||||
itemStyle = itemStyle.
|
||||
Foreground(t.Primary())
|
||||
itemStyle = itemStyle.Foreground(t.Primary())
|
||||
}
|
||||
|
||||
title := itemStyle.Render(
|
||||
@@ -185,7 +185,7 @@ func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
func (c *completionDialogComponent) View() string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
baseStyle := styles.NewStyle().Foreground(t.Text())
|
||||
|
||||
maxWidth := 40
|
||||
completions := c.list.GetItems()
|
||||
@@ -199,8 +199,14 @@ func (c *completionDialogComponent) View() string {
|
||||
|
||||
c.list.SetMaxWidth(maxWidth)
|
||||
|
||||
return baseStyle.Padding(0, 0).
|
||||
return baseStyle.
|
||||
Padding(0, 0).
|
||||
Background(t.BackgroundElement()).
|
||||
BorderStyle(lipgloss.ThickBorder()).
|
||||
BorderLeft(true).
|
||||
BorderRight(true).
|
||||
BorderForeground(t.Border()).
|
||||
BorderBackground(t.Background()).
|
||||
Width(c.width).
|
||||
Render(c.list.View())
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ func (m InitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
// View implements tea.Model.
|
||||
func (m InitDialogCmp) View() string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
baseStyle := styles.NewStyle().Foreground(t.Text())
|
||||
|
||||
// Calculate width needed for content
|
||||
maxWidth := 60 // Width for explanation text
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/charmbracelet/bubbles/v2/key"
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/sst/opencode-sdk-go"
|
||||
"github.com/sst/opencode/internal/app"
|
||||
"github.com/sst/opencode/internal/components/list"
|
||||
"github.com/sst/opencode/internal/components/modal"
|
||||
@@ -17,7 +18,6 @@ import (
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
"github.com/sst/opencode/internal/util"
|
||||
"github.com/sst/opencode/pkg/client"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -32,8 +32,8 @@ type ModelDialog interface {
|
||||
|
||||
type modelDialog struct {
|
||||
app *app.App
|
||||
availableProviders []client.ProviderInfo
|
||||
provider client.ProviderInfo
|
||||
availableProviders []opencode.Provider
|
||||
provider opencode.Provider
|
||||
width int
|
||||
height int
|
||||
hScrollOffset int
|
||||
@@ -69,7 +69,7 @@ var modelKeys = modelKeyMap{
|
||||
}
|
||||
|
||||
func (m *modelDialog) Init() tea.Cmd {
|
||||
m.setupModelsForProvider(m.provider.Id)
|
||||
m.setupModelsForProvider(m.provider.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ func (m *modelDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case key.Matches(msg, modelKeys.Enter):
|
||||
selectedItem, _ := m.modelList.GetSelectedItem()
|
||||
models := m.models()
|
||||
var selectedModel client.ModelInfo
|
||||
var selectedModel opencode.Model
|
||||
for _, model := range models {
|
||||
if model.Name == string(selectedItem) {
|
||||
selectedModel = model
|
||||
@@ -119,8 +119,8 @@ func (m *modelDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m *modelDialog) models() []client.ModelInfo {
|
||||
models := slices.SortedFunc(maps.Values(m.provider.Models), func(a, b client.ModelInfo) int {
|
||||
func (m *modelDialog) models() []opencode.Model {
|
||||
models := slices.SortedFunc(maps.Values(m.provider.Models), func(a, b opencode.Model) int {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
return models
|
||||
@@ -139,7 +139,7 @@ func (m *modelDialog) switchProvider(offset int) {
|
||||
m.hScrollOffset = newOffset
|
||||
m.provider = m.availableProviders[m.hScrollOffset]
|
||||
m.modal.SetTitle(fmt.Sprintf("Select %s Model", m.provider.Name))
|
||||
m.setupModelsForProvider(m.provider.Id)
|
||||
m.setupModelsForProvider(m.provider.ID)
|
||||
}
|
||||
|
||||
func (m *modelDialog) View() string {
|
||||
@@ -158,7 +158,7 @@ func (m *modelDialog) getScrollIndicators(maxWidth int) string {
|
||||
}
|
||||
|
||||
t := theme.CurrentTheme()
|
||||
return styles.BaseStyle().
|
||||
return styles.NewStyle().
|
||||
Foreground(t.TextMuted()).
|
||||
Width(maxWidth).
|
||||
Align(lipgloss.Right).
|
||||
@@ -175,9 +175,9 @@ func (m *modelDialog) setupModelsForProvider(providerId string) {
|
||||
m.modelList = list.NewStringList(modelNames, numVisibleModels, "No models available", true)
|
||||
m.modelList.SetMaxWidth(maxDialogWidth)
|
||||
|
||||
if m.app.Provider != nil && m.app.Model != nil && m.app.Provider.Id == providerId {
|
||||
if m.app.Provider != nil && m.app.Model != nil && m.app.Provider.ID == providerId {
|
||||
for i, model := range models {
|
||||
if model.Id == m.app.Model.Id {
|
||||
if model.ID == m.app.Model.ID {
|
||||
m.modelList.SetSelectedIndex(i)
|
||||
break
|
||||
}
|
||||
@@ -200,7 +200,7 @@ func NewModelDialog(app *app.App) ModelDialog {
|
||||
hScrollOffset := 0
|
||||
if app.Provider != nil {
|
||||
for i, provider := range availableProviders {
|
||||
if provider.Id == app.Provider.Id {
|
||||
if provider.ID == app.Provider.ID {
|
||||
currentProvider = provider
|
||||
hScrollOffset = i
|
||||
break
|
||||
@@ -220,6 +220,6 @@ func NewModelDialog(app *app.App) ModelDialog {
|
||||
),
|
||||
}
|
||||
|
||||
dialog.setupModelsForProvider(currentProvider.Id)
|
||||
dialog.setupModelsForProvider(currentProvider.ID)
|
||||
return dialog
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ func (p *permissionDialogComponent) selectCurrentOption() tea.Cmd {
|
||||
|
||||
func (p *permissionDialogComponent) renderButtons() string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
baseStyle := styles.NewStyle().Foreground(t.Text())
|
||||
|
||||
allowStyle := baseStyle
|
||||
allowSessionStyle := baseStyle
|
||||
@@ -355,8 +355,7 @@ func (p *permissionDialogComponent) renderDefaultContent() string {
|
||||
|
||||
func (p *permissionDialogComponent) styleViewport() string {
|
||||
t := theme.CurrentTheme()
|
||||
contentStyle := lipgloss.NewStyle().
|
||||
Background(t.Background())
|
||||
contentStyle := styles.NewStyle().Background(t.Background())
|
||||
|
||||
return contentStyle.Render(p.contentViewPort.View())
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"slices"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/muesli/reflow/truncate"
|
||||
"github.com/sst/opencode-sdk-go"
|
||||
"github.com/sst/opencode/internal/app"
|
||||
"github.com/sst/opencode/internal/components/list"
|
||||
"github.com/sst/opencode/internal/components/modal"
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
"github.com/sst/opencode/internal/util"
|
||||
"github.com/sst/opencode/pkg/client"
|
||||
)
|
||||
|
||||
// SessionDialog interface for the session switching dialog
|
||||
@@ -33,7 +32,7 @@ type sessionItem struct {
|
||||
|
||||
func (s sessionItem) Render(selected bool, width int) string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
baseStyle := styles.NewStyle()
|
||||
|
||||
var text string
|
||||
if s.isDeleteConfirming {
|
||||
@@ -44,20 +43,20 @@ func (s sessionItem) Render(selected bool, width int) string {
|
||||
|
||||
truncatedStr := truncate.StringWithTail(text, uint(width-1), "...")
|
||||
|
||||
var itemStyle lipgloss.Style
|
||||
var itemStyle styles.Style
|
||||
if selected {
|
||||
if s.isDeleteConfirming {
|
||||
// Red background for delete confirmation
|
||||
itemStyle = baseStyle.
|
||||
Background(t.Error()).
|
||||
Foreground(t.Background()).
|
||||
Foreground(t.BackgroundElement()).
|
||||
Width(width).
|
||||
PaddingLeft(1)
|
||||
} else {
|
||||
// Normal selection
|
||||
itemStyle = baseStyle.
|
||||
Background(t.Primary()).
|
||||
Foreground(t.Background()).
|
||||
Foreground(t.BackgroundElement()).
|
||||
Width(width).
|
||||
PaddingLeft(1)
|
||||
}
|
||||
@@ -80,7 +79,7 @@ type sessionDialog struct {
|
||||
width int
|
||||
height int
|
||||
modal *modal.Modal
|
||||
sessions []client.SessionInfo
|
||||
sessions []opencode.Session
|
||||
list list.List[sessionItem]
|
||||
app *app.App
|
||||
deleteConfirmation int // -1 means no confirmation, >= 0 means confirming deletion of session at this index
|
||||
@@ -123,7 +122,7 @@ func (s *sessionDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
s.updateListItems()
|
||||
return nil
|
||||
},
|
||||
s.deleteSession(sessionToDelete.Id),
|
||||
s.deleteSession(sessionToDelete.ID),
|
||||
)
|
||||
} else {
|
||||
// First press - enter delete confirmation mode
|
||||
@@ -151,9 +150,9 @@ func (s *sessionDialog) Render(background string) string {
|
||||
listView := s.list.View()
|
||||
|
||||
t := theme.CurrentTheme()
|
||||
helpStyle := styles.BaseStyle().PaddingLeft(1).PaddingTop(1)
|
||||
helpText := styles.BaseStyle().Foreground(t.Text()).Render("x/del")
|
||||
helpText = helpText + styles.BaseStyle().Background(t.BackgroundElement()).Foreground(t.TextMuted()).Render(" delete session")
|
||||
helpStyle := styles.NewStyle().PaddingLeft(1).PaddingTop(1)
|
||||
helpText := styles.NewStyle().Foreground(t.Text()).Render("x/del")
|
||||
helpText = helpText + styles.NewStyle().Background(t.BackgroundElement()).Foreground(t.TextMuted()).Render(" delete session")
|
||||
helpText = helpStyle.Render(helpText)
|
||||
|
||||
content := strings.Join([]string{listView, helpText}, "\n")
|
||||
@@ -194,10 +193,10 @@ func (s *sessionDialog) Close() tea.Cmd {
|
||||
func NewSessionDialog(app *app.App) SessionDialog {
|
||||
sessions, _ := app.ListSessions(context.Background())
|
||||
|
||||
var filteredSessions []client.SessionInfo
|
||||
var filteredSessions []opencode.Session
|
||||
var items []sessionItem
|
||||
for _, sess := range sessions {
|
||||
if sess.ParentID != nil {
|
||||
if sess.ParentID != "" {
|
||||
continue
|
||||
}
|
||||
filteredSessions = append(filteredSessions, sess)
|
||||
|
||||
@@ -103,7 +103,7 @@ func NewThemeDialog() ThemeDialog {
|
||||
|
||||
// Set the initial selection to the current theme
|
||||
list.SetSelectedIndex(selectedIdx)
|
||||
|
||||
|
||||
// Set the max width for the list to match the modal width
|
||||
list.SetMaxWidth(36) // 40 (modal max width) - 4 (modal padding)
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
"github.com/alecthomas/chroma/v2/formatters"
|
||||
@@ -19,6 +20,7 @@ import (
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
stylesi "github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
"github.com/sst/opencode/internal/util"
|
||||
)
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -441,84 +443,84 @@ func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg color.C
|
||||
<entry type="TextWhitespace" style="%s"/>
|
||||
</style>
|
||||
`,
|
||||
getColor(t.BackgroundPanel()), // Background
|
||||
getColor(t.Text()), // Text
|
||||
getColor(t.Text()), // Other
|
||||
getColor(t.Error()), // Error
|
||||
getChromaColor(t.BackgroundPanel()), // Background
|
||||
getChromaColor(t.Text()), // Text
|
||||
getChromaColor(t.Text()), // Other
|
||||
getChromaColor(t.Error()), // Error
|
||||
|
||||
getColor(t.SyntaxKeyword()), // Keyword
|
||||
getColor(t.SyntaxKeyword()), // KeywordConstant
|
||||
getColor(t.SyntaxKeyword()), // KeywordDeclaration
|
||||
getColor(t.SyntaxKeyword()), // KeywordNamespace
|
||||
getColor(t.SyntaxKeyword()), // KeywordPseudo
|
||||
getColor(t.SyntaxKeyword()), // KeywordReserved
|
||||
getColor(t.SyntaxType()), // KeywordType
|
||||
getChromaColor(t.SyntaxKeyword()), // Keyword
|
||||
getChromaColor(t.SyntaxKeyword()), // KeywordConstant
|
||||
getChromaColor(t.SyntaxKeyword()), // KeywordDeclaration
|
||||
getChromaColor(t.SyntaxKeyword()), // KeywordNamespace
|
||||
getChromaColor(t.SyntaxKeyword()), // KeywordPseudo
|
||||
getChromaColor(t.SyntaxKeyword()), // KeywordReserved
|
||||
getChromaColor(t.SyntaxType()), // KeywordType
|
||||
|
||||
getColor(t.Text()), // Name
|
||||
getColor(t.SyntaxVariable()), // NameAttribute
|
||||
getColor(t.SyntaxType()), // NameBuiltin
|
||||
getColor(t.SyntaxVariable()), // NameBuiltinPseudo
|
||||
getColor(t.SyntaxType()), // NameClass
|
||||
getColor(t.SyntaxVariable()), // NameConstant
|
||||
getColor(t.SyntaxFunction()), // NameDecorator
|
||||
getColor(t.SyntaxVariable()), // NameEntity
|
||||
getColor(t.SyntaxType()), // NameException
|
||||
getColor(t.SyntaxFunction()), // NameFunction
|
||||
getColor(t.Text()), // NameLabel
|
||||
getColor(t.SyntaxType()), // NameNamespace
|
||||
getColor(t.SyntaxVariable()), // NameOther
|
||||
getColor(t.SyntaxKeyword()), // NameTag
|
||||
getColor(t.SyntaxVariable()), // NameVariable
|
||||
getColor(t.SyntaxVariable()), // NameVariableClass
|
||||
getColor(t.SyntaxVariable()), // NameVariableGlobal
|
||||
getColor(t.SyntaxVariable()), // NameVariableInstance
|
||||
getChromaColor(t.Text()), // Name
|
||||
getChromaColor(t.SyntaxVariable()), // NameAttribute
|
||||
getChromaColor(t.SyntaxType()), // NameBuiltin
|
||||
getChromaColor(t.SyntaxVariable()), // NameBuiltinPseudo
|
||||
getChromaColor(t.SyntaxType()), // NameClass
|
||||
getChromaColor(t.SyntaxVariable()), // NameConstant
|
||||
getChromaColor(t.SyntaxFunction()), // NameDecorator
|
||||
getChromaColor(t.SyntaxVariable()), // NameEntity
|
||||
getChromaColor(t.SyntaxType()), // NameException
|
||||
getChromaColor(t.SyntaxFunction()), // NameFunction
|
||||
getChromaColor(t.Text()), // NameLabel
|
||||
getChromaColor(t.SyntaxType()), // NameNamespace
|
||||
getChromaColor(t.SyntaxVariable()), // NameOther
|
||||
getChromaColor(t.SyntaxKeyword()), // NameTag
|
||||
getChromaColor(t.SyntaxVariable()), // NameVariable
|
||||
getChromaColor(t.SyntaxVariable()), // NameVariableClass
|
||||
getChromaColor(t.SyntaxVariable()), // NameVariableGlobal
|
||||
getChromaColor(t.SyntaxVariable()), // NameVariableInstance
|
||||
|
||||
getColor(t.SyntaxString()), // Literal
|
||||
getColor(t.SyntaxString()), // LiteralDate
|
||||
getColor(t.SyntaxString()), // LiteralString
|
||||
getColor(t.SyntaxString()), // LiteralStringBacktick
|
||||
getColor(t.SyntaxString()), // LiteralStringChar
|
||||
getColor(t.SyntaxString()), // LiteralStringDoc
|
||||
getColor(t.SyntaxString()), // LiteralStringDouble
|
||||
getColor(t.SyntaxString()), // LiteralStringEscape
|
||||
getColor(t.SyntaxString()), // LiteralStringHeredoc
|
||||
getColor(t.SyntaxString()), // LiteralStringInterpol
|
||||
getColor(t.SyntaxString()), // LiteralStringOther
|
||||
getColor(t.SyntaxString()), // LiteralStringRegex
|
||||
getColor(t.SyntaxString()), // LiteralStringSingle
|
||||
getColor(t.SyntaxString()), // LiteralStringSymbol
|
||||
getChromaColor(t.SyntaxString()), // Literal
|
||||
getChromaColor(t.SyntaxString()), // LiteralDate
|
||||
getChromaColor(t.SyntaxString()), // LiteralString
|
||||
getChromaColor(t.SyntaxString()), // LiteralStringBacktick
|
||||
getChromaColor(t.SyntaxString()), // LiteralStringChar
|
||||
getChromaColor(t.SyntaxString()), // LiteralStringDoc
|
||||
getChromaColor(t.SyntaxString()), // LiteralStringDouble
|
||||
getChromaColor(t.SyntaxString()), // LiteralStringEscape
|
||||
getChromaColor(t.SyntaxString()), // LiteralStringHeredoc
|
||||
getChromaColor(t.SyntaxString()), // LiteralStringInterpol
|
||||
getChromaColor(t.SyntaxString()), // LiteralStringOther
|
||||
getChromaColor(t.SyntaxString()), // LiteralStringRegex
|
||||
getChromaColor(t.SyntaxString()), // LiteralStringSingle
|
||||
getChromaColor(t.SyntaxString()), // LiteralStringSymbol
|
||||
|
||||
getColor(t.SyntaxNumber()), // LiteralNumber
|
||||
getColor(t.SyntaxNumber()), // LiteralNumberBin
|
||||
getColor(t.SyntaxNumber()), // LiteralNumberFloat
|
||||
getColor(t.SyntaxNumber()), // LiteralNumberHex
|
||||
getColor(t.SyntaxNumber()), // LiteralNumberInteger
|
||||
getColor(t.SyntaxNumber()), // LiteralNumberIntegerLong
|
||||
getColor(t.SyntaxNumber()), // LiteralNumberOct
|
||||
getChromaColor(t.SyntaxNumber()), // LiteralNumber
|
||||
getChromaColor(t.SyntaxNumber()), // LiteralNumberBin
|
||||
getChromaColor(t.SyntaxNumber()), // LiteralNumberFloat
|
||||
getChromaColor(t.SyntaxNumber()), // LiteralNumberHex
|
||||
getChromaColor(t.SyntaxNumber()), // LiteralNumberInteger
|
||||
getChromaColor(t.SyntaxNumber()), // LiteralNumberIntegerLong
|
||||
getChromaColor(t.SyntaxNumber()), // LiteralNumberOct
|
||||
|
||||
getColor(t.SyntaxOperator()), // Operator
|
||||
getColor(t.SyntaxKeyword()), // OperatorWord
|
||||
getColor(t.SyntaxPunctuation()), // Punctuation
|
||||
getChromaColor(t.SyntaxOperator()), // Operator
|
||||
getChromaColor(t.SyntaxKeyword()), // OperatorWord
|
||||
getChromaColor(t.SyntaxPunctuation()), // Punctuation
|
||||
|
||||
getColor(t.SyntaxComment()), // Comment
|
||||
getColor(t.SyntaxComment()), // CommentHashbang
|
||||
getColor(t.SyntaxComment()), // CommentMultiline
|
||||
getColor(t.SyntaxComment()), // CommentSingle
|
||||
getColor(t.SyntaxComment()), // CommentSpecial
|
||||
getColor(t.SyntaxKeyword()), // CommentPreproc
|
||||
getChromaColor(t.SyntaxComment()), // Comment
|
||||
getChromaColor(t.SyntaxComment()), // CommentHashbang
|
||||
getChromaColor(t.SyntaxComment()), // CommentMultiline
|
||||
getChromaColor(t.SyntaxComment()), // CommentSingle
|
||||
getChromaColor(t.SyntaxComment()), // CommentSpecial
|
||||
getChromaColor(t.SyntaxKeyword()), // CommentPreproc
|
||||
|
||||
getColor(t.Text()), // Generic
|
||||
getColor(t.Error()), // GenericDeleted
|
||||
getColor(t.Text()), // GenericEmph
|
||||
getColor(t.Error()), // GenericError
|
||||
getColor(t.Text()), // GenericHeading
|
||||
getColor(t.Success()), // GenericInserted
|
||||
getColor(t.TextMuted()), // GenericOutput
|
||||
getColor(t.Text()), // GenericPrompt
|
||||
getColor(t.Text()), // GenericStrong
|
||||
getColor(t.Text()), // GenericSubheading
|
||||
getColor(t.Error()), // GenericTraceback
|
||||
getColor(t.Text()), // TextWhitespace
|
||||
getChromaColor(t.Text()), // Generic
|
||||
getChromaColor(t.Error()), // GenericDeleted
|
||||
getChromaColor(t.Text()), // GenericEmph
|
||||
getChromaColor(t.Error()), // GenericError
|
||||
getChromaColor(t.Text()), // GenericHeading
|
||||
getChromaColor(t.Success()), // GenericInserted
|
||||
getChromaColor(t.TextMuted()), // GenericOutput
|
||||
getChromaColor(t.Text()), // GenericPrompt
|
||||
getChromaColor(t.Text()), // GenericStrong
|
||||
getChromaColor(t.Text()), // GenericSubheading
|
||||
getChromaColor(t.Error()), // GenericTraceback
|
||||
getChromaColor(t.Text()), // TextWhitespace
|
||||
)
|
||||
|
||||
r := strings.NewReader(syntaxThemeXml)
|
||||
@@ -527,6 +529,9 @@ func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg color.C
|
||||
// Modify the style to use the provided background
|
||||
s, err := style.Builder().Transform(
|
||||
func(t chroma.StyleEntry) chroma.StyleEntry {
|
||||
if _, ok := bg.(lipgloss.NoColor); ok {
|
||||
return t
|
||||
}
|
||||
r, g, b, _ := bg.RGBA()
|
||||
t.Background = chroma.NewColour(uint8(r>>8), uint8(g>>8), uint8(b>>8))
|
||||
return t
|
||||
@@ -546,10 +551,18 @@ func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg color.C
|
||||
}
|
||||
|
||||
// getColor returns the appropriate hex color string based on terminal background
|
||||
func getColor(adaptiveColor compat.AdaptiveColor) string {
|
||||
func getColor(adaptiveColor compat.AdaptiveColor) *string {
|
||||
return stylesi.AdaptiveColorToString(adaptiveColor)
|
||||
}
|
||||
|
||||
func getChromaColor(adaptiveColor compat.AdaptiveColor) string {
|
||||
color := stylesi.AdaptiveColorToString(adaptiveColor)
|
||||
if color == nil {
|
||||
return ""
|
||||
}
|
||||
return *color
|
||||
}
|
||||
|
||||
// highlightLine applies syntax highlighting to a single line
|
||||
func highlightLine(fileName string, line string, bg color.Color) string {
|
||||
var buf bytes.Buffer
|
||||
@@ -561,11 +574,11 @@ func highlightLine(fileName string, line string, bg color.Color) string {
|
||||
}
|
||||
|
||||
// createStyles generates the lipgloss styles needed for rendering diffs
|
||||
func createStyles(t theme.Theme) (removedLineStyle, addedLineStyle, contextLineStyle, lineNumberStyle lipgloss.Style) {
|
||||
removedLineStyle = lipgloss.NewStyle().Background(t.DiffRemovedBg())
|
||||
addedLineStyle = lipgloss.NewStyle().Background(t.DiffAddedBg())
|
||||
contextLineStyle = lipgloss.NewStyle().Background(t.DiffContextBg())
|
||||
lineNumberStyle = lipgloss.NewStyle().Background(t.DiffLineNumber()).Foreground(t.TextMuted())
|
||||
func createStyles(t theme.Theme) (removedLineStyle, addedLineStyle, contextLineStyle, lineNumberStyle stylesi.Style) {
|
||||
removedLineStyle = stylesi.NewStyle().Background(t.DiffRemovedBg())
|
||||
addedLineStyle = stylesi.NewStyle().Background(t.DiffAddedBg())
|
||||
contextLineStyle = stylesi.NewStyle().Background(t.DiffContextBg())
|
||||
lineNumberStyle = stylesi.NewStyle().Foreground(t.TextMuted()).Background(t.DiffLineNumber())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -613,9 +626,17 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
|
||||
currentPos := 0
|
||||
|
||||
// Get the appropriate color based on terminal background
|
||||
bgColor := lipgloss.Color(getColor(highlightBg))
|
||||
fgColor := lipgloss.Color(getColor(theme.CurrentTheme().BackgroundPanel()))
|
||||
bg := getColor(highlightBg)
|
||||
fg := getColor(theme.CurrentTheme().BackgroundPanel())
|
||||
var bgColor color.Color
|
||||
var fgColor color.Color
|
||||
|
||||
if bg != nil {
|
||||
bgColor = lipgloss.Color(*bg)
|
||||
}
|
||||
if fg != nil {
|
||||
fgColor = lipgloss.Color(*fg)
|
||||
}
|
||||
for i := 0; i < len(content); {
|
||||
// Check if we're at an ANSI sequence
|
||||
isAnsi := false
|
||||
@@ -651,12 +672,20 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
|
||||
currentStyle := ansiSequences[currentPos]
|
||||
|
||||
// Apply foreground and background highlight
|
||||
sb.WriteString("\x1b[38;2;")
|
||||
r, g, b, _ := fgColor.RGBA()
|
||||
sb.WriteString(fmt.Sprintf("%d;%d;%dm", r>>8, g>>8, b>>8))
|
||||
sb.WriteString("\x1b[48;2;")
|
||||
r, g, b, _ = bgColor.RGBA()
|
||||
sb.WriteString(fmt.Sprintf("%d;%d;%dm", r>>8, g>>8, b>>8))
|
||||
if fgColor != nil {
|
||||
sb.WriteString("\x1b[38;2;")
|
||||
r, g, b, _ := fgColor.RGBA()
|
||||
sb.WriteString(fmt.Sprintf("%d;%d;%dm", r>>8, g>>8, b>>8))
|
||||
} else {
|
||||
sb.WriteString("\x1b[49m")
|
||||
}
|
||||
if bgColor != nil {
|
||||
sb.WriteString("\x1b[48;2;")
|
||||
r, g, b, _ := bgColor.RGBA()
|
||||
sb.WriteString(fmt.Sprintf("%d;%d;%dm", r>>8, g>>8, b>>8))
|
||||
} else {
|
||||
sb.WriteString("\x1b[39m")
|
||||
}
|
||||
sb.WriteString(char)
|
||||
|
||||
// Full reset of all attributes to ensure clean state
|
||||
@@ -677,16 +706,16 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
|
||||
}
|
||||
|
||||
// renderLinePrefix renders the line number and marker prefix for a diff line
|
||||
func renderLinePrefix(dl DiffLine, lineNum string, marker string, lineNumberStyle lipgloss.Style, t theme.Theme) string {
|
||||
func renderLinePrefix(dl DiffLine, lineNum string, marker string, lineNumberStyle stylesi.Style, t theme.Theme) string {
|
||||
// Style the marker based on line type
|
||||
var styledMarker string
|
||||
switch dl.Kind {
|
||||
case LineRemoved:
|
||||
styledMarker = lipgloss.NewStyle().Background(t.DiffRemovedBg()).Foreground(t.DiffRemoved()).Render(marker)
|
||||
styledMarker = stylesi.NewStyle().Foreground(t.DiffRemoved()).Background(t.DiffRemovedBg()).Render(marker)
|
||||
case LineAdded:
|
||||
styledMarker = lipgloss.NewStyle().Background(t.DiffAddedBg()).Foreground(t.DiffAdded()).Render(marker)
|
||||
styledMarker = stylesi.NewStyle().Foreground(t.DiffAdded()).Background(t.DiffAddedBg()).Render(marker)
|
||||
case LineContext:
|
||||
styledMarker = lipgloss.NewStyle().Background(t.DiffContextBg()).Foreground(t.TextMuted()).Render(marker)
|
||||
styledMarker = stylesi.NewStyle().Foreground(t.TextMuted()).Background(t.DiffContextBg()).Render(marker)
|
||||
default:
|
||||
styledMarker = marker
|
||||
}
|
||||
@@ -695,7 +724,7 @@ func renderLinePrefix(dl DiffLine, lineNum string, marker string, lineNumberStyl
|
||||
}
|
||||
|
||||
// renderLineContent renders the content of a diff line with syntax and intra-line highlighting
|
||||
func renderLineContent(fileName string, dl DiffLine, bgStyle lipgloss.Style, highlightColor compat.AdaptiveColor, width int, t theme.Theme) string {
|
||||
func renderLineContent(fileName string, dl DiffLine, bgStyle stylesi.Style, highlightColor compat.AdaptiveColor, width int) string {
|
||||
// Apply syntax highlighting
|
||||
content := highlightLine(fileName, dl.Content, bgStyle.GetBackground())
|
||||
|
||||
@@ -714,7 +743,9 @@ func renderLineContent(fileName string, dl DiffLine, bgStyle lipgloss.Style, hig
|
||||
ansi.Truncate(
|
||||
content,
|
||||
width,
|
||||
lipgloss.NewStyle().Background(bgStyle.GetBackground()).Foreground(t.TextMuted()).Render("..."),
|
||||
"...",
|
||||
// stylesi.NewStyleWithColors(t.TextMuted(), bgStyle.GetBackground()).Render("..."),
|
||||
// stylesi.WithForeground(stylesi.NewStyle().Background(bgStyle.GetBackground()), t.TextMuted()).Render("..."),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -725,7 +756,7 @@ func renderUnifiedLine(fileName string, dl DiffLine, width int, t theme.Theme) s
|
||||
|
||||
// Determine line style and marker based on line type
|
||||
var marker string
|
||||
var bgStyle lipgloss.Style
|
||||
var bgStyle stylesi.Style
|
||||
var lineNum string
|
||||
var highlightColor compat.AdaptiveColor
|
||||
|
||||
@@ -733,8 +764,8 @@ func renderUnifiedLine(fileName string, dl DiffLine, width int, t theme.Theme) s
|
||||
case LineRemoved:
|
||||
marker = "-"
|
||||
bgStyle = removedLineStyle
|
||||
lineNumberStyle = lineNumberStyle.Foreground(t.DiffRemoved()).Background(t.DiffRemovedLineNumberBg())
|
||||
highlightColor = t.DiffHighlightRemoved()
|
||||
lineNumberStyle = lineNumberStyle.Background(t.DiffRemovedLineNumberBg()).Foreground(t.DiffRemoved())
|
||||
highlightColor = t.DiffHighlightRemoved() // TODO: handle "none"
|
||||
if dl.OldLineNo > 0 {
|
||||
lineNum = fmt.Sprintf("%6d ", dl.OldLineNo)
|
||||
} else {
|
||||
@@ -743,8 +774,8 @@ func renderUnifiedLine(fileName string, dl DiffLine, width int, t theme.Theme) s
|
||||
case LineAdded:
|
||||
marker = "+"
|
||||
bgStyle = addedLineStyle
|
||||
lineNumberStyle = lineNumberStyle.Foreground(t.DiffAdded()).Background(t.DiffAddedLineNumberBg())
|
||||
highlightColor = t.DiffHighlightAdded()
|
||||
lineNumberStyle = lineNumberStyle.Background(t.DiffAddedLineNumberBg()).Foreground(t.DiffAdded())
|
||||
highlightColor = t.DiffHighlightAdded() // TODO: handle "none"
|
||||
if dl.NewLineNo > 0 {
|
||||
lineNum = fmt.Sprintf(" %7d", dl.NewLineNo)
|
||||
} else {
|
||||
@@ -766,7 +797,7 @@ func renderUnifiedLine(fileName string, dl DiffLine, width int, t theme.Theme) s
|
||||
// Render the content
|
||||
prefixWidth := ansi.StringWidth(prefix)
|
||||
contentWidth := width - prefixWidth
|
||||
content := renderLineContent(fileName, dl, bgStyle, highlightColor, contentWidth, t)
|
||||
content := renderLineContent(fileName, dl, bgStyle, highlightColor, contentWidth)
|
||||
|
||||
return prefix + content
|
||||
}
|
||||
@@ -780,7 +811,7 @@ func renderDiffColumnLine(
|
||||
t theme.Theme,
|
||||
) string {
|
||||
if dl == nil {
|
||||
contextLineStyle := lipgloss.NewStyle().Background(t.DiffContextBg())
|
||||
contextLineStyle := stylesi.NewStyle().Background(t.DiffContextBg())
|
||||
return contextLineStyle.Width(colWidth).Render("")
|
||||
}
|
||||
|
||||
@@ -788,7 +819,7 @@ func renderDiffColumnLine(
|
||||
|
||||
// Determine line style based on line type and column
|
||||
var marker string
|
||||
var bgStyle lipgloss.Style
|
||||
var bgStyle stylesi.Style
|
||||
var lineNum string
|
||||
var highlightColor compat.AdaptiveColor
|
||||
|
||||
@@ -798,8 +829,8 @@ func renderDiffColumnLine(
|
||||
case LineRemoved:
|
||||
marker = "-"
|
||||
bgStyle = removedLineStyle
|
||||
lineNumberStyle = lineNumberStyle.Foreground(t.DiffRemoved()).Background(t.DiffRemovedLineNumberBg())
|
||||
highlightColor = t.DiffHighlightRemoved()
|
||||
lineNumberStyle = lineNumberStyle.Background(t.DiffRemovedLineNumberBg()).Foreground(t.DiffRemoved())
|
||||
highlightColor = t.DiffHighlightRemoved() // TODO: handle "none"
|
||||
case LineAdded:
|
||||
marker = "?"
|
||||
bgStyle = contextLineStyle
|
||||
@@ -818,7 +849,7 @@ func renderDiffColumnLine(
|
||||
case LineAdded:
|
||||
marker = "+"
|
||||
bgStyle = addedLineStyle
|
||||
lineNumberStyle = lineNumberStyle.Foreground(t.DiffAdded()).Background(t.DiffAddedLineNumberBg())
|
||||
lineNumberStyle = lineNumberStyle.Background(t.DiffAddedLineNumberBg()).Foreground(t.DiffAdded())
|
||||
highlightColor = t.DiffHighlightAdded()
|
||||
case LineRemoved:
|
||||
marker = "?"
|
||||
@@ -849,7 +880,7 @@ func renderDiffColumnLine(
|
||||
// Render the content
|
||||
prefixWidth := ansi.StringWidth(prefix)
|
||||
contentWidth := colWidth - prefixWidth
|
||||
content := renderLineContent(fileName, *dl, bgStyle, highlightColor, contentWidth, t)
|
||||
content := renderLineContent(fileName, *dl, bgStyle, highlightColor, contentWidth)
|
||||
|
||||
return prefix + content
|
||||
}
|
||||
@@ -910,11 +941,22 @@ func RenderSideBySideHunk(fileName string, h Hunk, opts ...SideBySideOption) str
|
||||
leftWidth := colWidth
|
||||
rightWidth := config.TotalWidth - colWidth
|
||||
var sb strings.Builder
|
||||
for _, p := range pairs {
|
||||
leftStr := renderLeftColumn(fileName, p.left, leftWidth)
|
||||
rightStr := renderRightColumn(fileName, p.right, rightWidth)
|
||||
sb.WriteString(leftStr + rightStr + "\n")
|
||||
}
|
||||
|
||||
util.WriteStringsPar(&sb, pairs, func(p linePair) string {
|
||||
wg := &sync.WaitGroup{}
|
||||
var leftStr, rightStr string
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
leftStr = renderLeftColumn(fileName, p.left, leftWidth)
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
rightStr = renderRightColumn(fileName, p.right, rightWidth)
|
||||
}()
|
||||
wg.Wait()
|
||||
return leftStr + rightStr + "\n"
|
||||
})
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
@@ -928,7 +970,8 @@ func FormatUnifiedDiff(filename string, diffText string, opts ...UnifiedOption)
|
||||
|
||||
var sb strings.Builder
|
||||
for _, h := range diffResult.Hunks {
|
||||
sb.WriteString(RenderUnifiedHunk(filename, h, opts...))
|
||||
unifiedDiff := RenderUnifiedHunk(filename, h, opts...)
|
||||
sb.WriteString(unifiedDiff)
|
||||
}
|
||||
|
||||
return sb.String(), nil
|
||||
@@ -944,7 +987,7 @@ func FormatDiff(filename string, diffText string, opts ...SideBySideOption) (str
|
||||
|
||||
var sb strings.Builder
|
||||
// config := NewSideBySideConfig(opts...)
|
||||
for _, h := range diffResult.Hunks {
|
||||
util.WriteStringsPar(&sb, diffResult.Hunks, func(h Hunk) string {
|
||||
// sb.WriteString(
|
||||
// lipgloss.NewStyle().
|
||||
// Background(t.DiffHunkHeader()).
|
||||
@@ -952,8 +995,8 @@ func FormatDiff(filename string, diffText string, opts ...SideBySideOption) (str
|
||||
// Width(config.TotalWidth).
|
||||
// Render(h.Header) + "\n",
|
||||
// )
|
||||
sb.WriteString(RenderSideBySideHunk(filename, h, opts...))
|
||||
}
|
||||
return RenderSideBySideHunk(filename, h, opts...)
|
||||
})
|
||||
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
|
||||
"github.com/charmbracelet/bubbles/v2/key"
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/muesli/reflow/truncate"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
@@ -174,19 +173,20 @@ type StringItem string
|
||||
|
||||
func (s StringItem) Render(selected bool, width int) string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
baseStyle := styles.NewStyle()
|
||||
|
||||
truncatedStr := truncate.StringWithTail(string(s), uint(width-1), "...")
|
||||
|
||||
var itemStyle lipgloss.Style
|
||||
var itemStyle styles.Style
|
||||
if selected {
|
||||
itemStyle = baseStyle.
|
||||
Background(t.Primary()).
|
||||
Foreground(t.Background()).
|
||||
Foreground(t.BackgroundElement()).
|
||||
Width(width).
|
||||
PaddingLeft(1)
|
||||
} else {
|
||||
itemStyle = baseStyle.
|
||||
Foreground(t.TextMuted()).
|
||||
PaddingLeft(1)
|
||||
}
|
||||
|
||||
|
||||
@@ -90,12 +90,8 @@ func (m *Modal) Render(contentView string, background string) string {
|
||||
|
||||
innerWidth := outerWidth - 4
|
||||
|
||||
// Base style for the modal
|
||||
baseStyle := styles.BaseStyle().
|
||||
Background(t.BackgroundElement()).
|
||||
Foreground(t.TextMuted())
|
||||
baseStyle := styles.NewStyle().Foreground(t.TextMuted()).Background(t.BackgroundElement())
|
||||
|
||||
// Add title if provided
|
||||
var finalContent string
|
||||
if m.title != "" {
|
||||
titleStyle := baseStyle.
|
||||
|
||||
@@ -3,7 +3,7 @@ package qr
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
"rsc.io/qr"
|
||||
)
|
||||
@@ -23,9 +23,7 @@ func Generate(text string) (string, int, error) {
|
||||
}
|
||||
|
||||
// Create lipgloss style for QR code with theme colors
|
||||
qrStyle := lipgloss.NewStyle().
|
||||
Foreground(t.Text()).
|
||||
Background(t.Background())
|
||||
qrStyle := styles.NewStyleWithColors(t.Text(), t.Background())
|
||||
|
||||
var result strings.Builder
|
||||
|
||||
|
||||
@@ -36,18 +36,19 @@ func (m statusComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
func (m statusComponent) logo() string {
|
||||
t := theme.CurrentTheme()
|
||||
base := lipgloss.NewStyle().Background(t.BackgroundElement()).Foreground(t.TextMuted()).Render
|
||||
emphasis := lipgloss.NewStyle().Bold(true).Background(t.BackgroundElement()).Foreground(t.Text()).Render
|
||||
base := styles.NewStyle().Foreground(t.TextMuted()).Background(t.BackgroundElement()).Render
|
||||
emphasis := styles.NewStyle().Foreground(t.Text()).Background(t.BackgroundElement()).Bold(true).Render
|
||||
|
||||
open := base("open")
|
||||
code := emphasis("code ")
|
||||
version := base(m.app.Version)
|
||||
return styles.Padded().
|
||||
return styles.NewStyle().
|
||||
Background(t.BackgroundElement()).
|
||||
Padding(0, 1).
|
||||
Render(open + code + version)
|
||||
}
|
||||
|
||||
func formatTokensAndCost(tokens float32, contextWindow float32, cost float32) string {
|
||||
func formatTokensAndCost(tokens float64, contextWindow float64, cost float64) string {
|
||||
// Format tokens in human-readable format (e.g., 110K, 1.2M)
|
||||
var formattedTokens string
|
||||
switch {
|
||||
@@ -76,8 +77,8 @@ func formatTokensAndCost(tokens float32, contextWindow float32, cost float32) st
|
||||
|
||||
func (m statusComponent) View() string {
|
||||
t := theme.CurrentTheme()
|
||||
if m.app.Session.Id == "" {
|
||||
return styles.BaseStyle().
|
||||
if m.app.Session.ID == "" {
|
||||
return styles.NewStyle().
|
||||
Background(t.Background()).
|
||||
Width(m.width).
|
||||
Height(2).
|
||||
@@ -86,34 +87,38 @@ func (m statusComponent) View() string {
|
||||
|
||||
logo := m.logo()
|
||||
|
||||
cwd := styles.Padded().
|
||||
cwd := styles.NewStyle().
|
||||
Foreground(t.TextMuted()).
|
||||
Background(t.BackgroundPanel()).
|
||||
Padding(0, 1).
|
||||
Render(m.app.Info.Path.Cwd)
|
||||
|
||||
sessionInfo := ""
|
||||
if m.app.Session.Id != "" {
|
||||
tokens := float32(0)
|
||||
cost := float32(0)
|
||||
if m.app.Session.ID != "" {
|
||||
tokens := float64(0)
|
||||
cost := float64(0)
|
||||
contextWindow := m.app.Model.Limit.Context
|
||||
|
||||
for _, message := range m.app.Messages {
|
||||
if message.Metadata.Assistant != nil {
|
||||
cost += message.Metadata.Assistant.Cost
|
||||
usage := message.Metadata.Assistant.Tokens
|
||||
if usage.Output > 0 {
|
||||
tokens = (usage.Input +
|
||||
usage.Cache.Write +
|
||||
usage.Cache.Read +
|
||||
usage.Output +
|
||||
usage.Reasoning)
|
||||
cost += message.Metadata.Assistant.Cost
|
||||
usage := message.Metadata.Assistant.Tokens
|
||||
if usage.Output > 0 {
|
||||
if message.Metadata.Assistant.Summary {
|
||||
tokens = usage.Output
|
||||
continue
|
||||
}
|
||||
tokens = (usage.Input +
|
||||
usage.Cache.Write +
|
||||
usage.Cache.Read +
|
||||
usage.Output +
|
||||
usage.Reasoning)
|
||||
}
|
||||
}
|
||||
|
||||
sessionInfo = styles.Padded().
|
||||
Background(t.BackgroundElement()).
|
||||
sessionInfo = styles.NewStyle().
|
||||
Foreground(t.TextMuted()).
|
||||
Background(t.BackgroundElement()).
|
||||
Padding(0, 1).
|
||||
Render(formatTokensAndCost(tokens, contextWindow, cost))
|
||||
}
|
||||
|
||||
@@ -123,11 +128,11 @@ func (m statusComponent) View() string {
|
||||
0,
|
||||
m.width-lipgloss.Width(logo)-lipgloss.Width(cwd)-lipgloss.Width(sessionInfo),
|
||||
)
|
||||
spacer := lipgloss.NewStyle().Background(t.BackgroundPanel()).Width(space).Render("")
|
||||
spacer := styles.NewStyle().Background(t.BackgroundPanel()).Width(space).Render("")
|
||||
|
||||
status := logo + cwd + spacer + sessionInfo
|
||||
|
||||
blank := styles.BaseStyle().Background(t.Background()).Width(m.width).Render("")
|
||||
blank := styles.NewStyle().Background(t.Background()).Width(m.width).Render("")
|
||||
return blank + "\n" + status
|
||||
}
|
||||
|
||||
|
||||
@@ -90,9 +90,9 @@ func (tm *ToastManager) Update(msg tea.Msg) (*ToastManager, tea.Cmd) {
|
||||
func (tm *ToastManager) renderSingleToast(toast Toast) string {
|
||||
t := theme.CurrentTheme()
|
||||
|
||||
baseStyle := styles.BaseStyle().
|
||||
Background(t.BackgroundElement()).
|
||||
baseStyle := styles.NewStyle().
|
||||
Foreground(t.Text()).
|
||||
Background(t.BackgroundElement()).
|
||||
Padding(1, 2)
|
||||
|
||||
maxWidth := max(40, layout.Current.Viewport.Width/3)
|
||||
@@ -101,15 +101,14 @@ func (tm *ToastManager) renderSingleToast(toast Toast) string {
|
||||
// Build content with wrapping
|
||||
var content strings.Builder
|
||||
if toast.Title != nil {
|
||||
titleStyle := lipgloss.NewStyle().
|
||||
Foreground(toast.Color).
|
||||
titleStyle := styles.NewStyle().Foreground(toast.Color).
|
||||
Bold(true)
|
||||
content.WriteString(titleStyle.Render(*toast.Title))
|
||||
content.WriteString("\n")
|
||||
}
|
||||
|
||||
// Wrap message text
|
||||
messageStyle := lipgloss.NewStyle()
|
||||
messageStyle := styles.NewStyle()
|
||||
contentWidth := lipgloss.Width(toast.Message)
|
||||
if contentWidth > contentMaxWidth {
|
||||
messageStyle = messageStyle.Width(contentMaxWidth)
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/sst/opencode/pkg/client"
|
||||
)
|
||||
|
||||
type State struct {
|
||||
@@ -22,13 +21,6 @@ func NewState() *State {
|
||||
}
|
||||
}
|
||||
|
||||
func MergeState(state *State, config *client.ConfigInfo) *client.ConfigInfo {
|
||||
if config.Theme == nil {
|
||||
config.Theme = &state.Theme
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
// SaveState writes the provided Config struct to the specified TOML file.
|
||||
// It will create the file if it doesn't exist, or overwrite it if it does.
|
||||
func SaveState(filePath string, state *State) error {
|
||||
|
||||
@@ -3,6 +3,7 @@ package layout
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
)
|
||||
|
||||
@@ -57,7 +58,7 @@ func (c *container) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
func (c *container) View() string {
|
||||
t := theme.CurrentTheme()
|
||||
style := lipgloss.NewStyle()
|
||||
style := styles.NewStyle().Background(t.Background())
|
||||
width := c.width
|
||||
height := c.height
|
||||
|
||||
@@ -66,8 +67,6 @@ func (c *container) View() string {
|
||||
width = c.maxWidth
|
||||
}
|
||||
|
||||
style = style.Background(t.Background())
|
||||
|
||||
// Apply border if any side is enabled
|
||||
if c.borderTop || c.borderRight || c.borderBottom || c.borderLeft {
|
||||
// Adjust width and height for borders
|
||||
|
||||
@@ -3,6 +3,7 @@ package layout
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
)
|
||||
|
||||
@@ -66,7 +67,7 @@ func (f *flexLayout) View() string {
|
||||
alignment,
|
||||
child.View(),
|
||||
// TODO: make configurable WithBackgroundStyle
|
||||
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||
lipgloss.WithWhitespaceStyle(styles.NewStyle().Background(t.Background()).Lipgloss()),
|
||||
)
|
||||
views = append(views, view)
|
||||
} else {
|
||||
@@ -78,7 +79,7 @@ func (f *flexLayout) View() string {
|
||||
alignment,
|
||||
child.View(),
|
||||
// TODO: make configurable WithBackgroundStyle
|
||||
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||
lipgloss.WithWhitespaceStyle(styles.NewStyle().Background(t.Background()).Lipgloss()),
|
||||
)
|
||||
views = append(views, view)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package styles
|
||||
|
||||
import "image/color"
|
||||
|
||||
type TerminalInfo struct {
|
||||
Background color.Color
|
||||
BackgroundIsDark bool
|
||||
}
|
||||
|
||||
@@ -8,6 +11,7 @@ var Terminal *TerminalInfo
|
||||
|
||||
func init() {
|
||||
Terminal = &TerminalInfo{
|
||||
Background: color.Black,
|
||||
BackgroundIsDark: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package styles
|
||||
import (
|
||||
"github.com/charmbracelet/glamour"
|
||||
"github.com/charmbracelet/glamour/ansi"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2/compat"
|
||||
"github.com/lucasb-eyer/go-colorful"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
@@ -29,7 +30,7 @@ func GetMarkdownRenderer(width int, backgroundColor compat.AdaptiveColor) *glamo
|
||||
// using adaptive colors from the provided theme.
|
||||
func generateMarkdownStyleConfig(backgroundColor compat.AdaptiveColor) ansi.StyleConfig {
|
||||
t := theme.CurrentTheme()
|
||||
background := stringPtr(AdaptiveColorToString(backgroundColor))
|
||||
background := AdaptiveColorToString(backgroundColor)
|
||||
|
||||
return ansi.StyleConfig{
|
||||
Document: ansi.StyleBlock{
|
||||
@@ -37,12 +38,12 @@ func generateMarkdownStyleConfig(backgroundColor compat.AdaptiveColor) ansi.Styl
|
||||
BlockPrefix: "",
|
||||
BlockSuffix: "",
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownText())),
|
||||
Color: AdaptiveColorToString(t.MarkdownText()),
|
||||
},
|
||||
},
|
||||
BlockQuote: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownBlockQuote())),
|
||||
Color: AdaptiveColorToString(t.MarkdownBlockQuote()),
|
||||
Italic: boolPtr(true),
|
||||
Prefix: "┃ ",
|
||||
},
|
||||
@@ -54,108 +55,108 @@ func generateMarkdownStyleConfig(backgroundColor compat.AdaptiveColor) ansi.Styl
|
||||
StyleBlock: ansi.StyleBlock{
|
||||
IndentToken: stringPtr(" "),
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownText())),
|
||||
Color: AdaptiveColorToString(t.MarkdownText()),
|
||||
},
|
||||
},
|
||||
},
|
||||
Heading: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
BlockSuffix: "\n",
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
|
||||
Color: AdaptiveColorToString(t.MarkdownHeading()),
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
},
|
||||
H1: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "# ",
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
|
||||
Color: AdaptiveColorToString(t.MarkdownHeading()),
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
},
|
||||
H2: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "## ",
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
|
||||
Color: AdaptiveColorToString(t.MarkdownHeading()),
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
},
|
||||
H3: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "### ",
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
|
||||
Color: AdaptiveColorToString(t.MarkdownHeading()),
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
},
|
||||
H4: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "#### ",
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
|
||||
Color: AdaptiveColorToString(t.MarkdownHeading()),
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
},
|
||||
H5: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "##### ",
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
|
||||
Color: AdaptiveColorToString(t.MarkdownHeading()),
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
},
|
||||
H6: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "###### ",
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
|
||||
Color: AdaptiveColorToString(t.MarkdownHeading()),
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
},
|
||||
Strikethrough: ansi.StylePrimitive{
|
||||
CrossedOut: boolPtr(true),
|
||||
Color: stringPtr(AdaptiveColorToString(t.TextMuted())),
|
||||
Color: AdaptiveColorToString(t.TextMuted()),
|
||||
},
|
||||
Emph: ansi.StylePrimitive{
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownEmph())),
|
||||
Color: AdaptiveColorToString(t.MarkdownEmph()),
|
||||
Italic: boolPtr(true),
|
||||
},
|
||||
Strong: ansi.StylePrimitive{
|
||||
Bold: boolPtr(true),
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownStrong())),
|
||||
Color: AdaptiveColorToString(t.MarkdownStrong()),
|
||||
},
|
||||
HorizontalRule: ansi.StylePrimitive{
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownHorizontalRule())),
|
||||
Color: AdaptiveColorToString(t.MarkdownHorizontalRule()),
|
||||
Format: "\n─────────────────────────────────────────\n",
|
||||
},
|
||||
Item: ansi.StylePrimitive{
|
||||
BlockPrefix: "• ",
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownListItem())),
|
||||
Color: AdaptiveColorToString(t.MarkdownListItem()),
|
||||
},
|
||||
Enumeration: ansi.StylePrimitive{
|
||||
BlockPrefix: ". ",
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownListEnumeration())),
|
||||
Color: AdaptiveColorToString(t.MarkdownListEnumeration()),
|
||||
},
|
||||
Task: ansi.StyleTask{
|
||||
Ticked: "[✓] ",
|
||||
Unticked: "[ ] ",
|
||||
},
|
||||
Link: ansi.StylePrimitive{
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownLink())),
|
||||
Color: AdaptiveColorToString(t.MarkdownLink()),
|
||||
Underline: boolPtr(true),
|
||||
},
|
||||
LinkText: ansi.StylePrimitive{
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownLinkText())),
|
||||
Color: AdaptiveColorToString(t.MarkdownLinkText()),
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
Image: ansi.StylePrimitive{
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownImage())),
|
||||
Color: AdaptiveColorToString(t.MarkdownImage()),
|
||||
Underline: boolPtr(true),
|
||||
Format: "🖼 {{.text}}",
|
||||
},
|
||||
ImageText: ansi.StylePrimitive{
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownImageText())),
|
||||
Color: AdaptiveColorToString(t.MarkdownImageText()),
|
||||
Format: "{{.text}}",
|
||||
},
|
||||
Code: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownCode())),
|
||||
Color: AdaptiveColorToString(t.MarkdownCode()),
|
||||
Prefix: "",
|
||||
Suffix: "",
|
||||
},
|
||||
@@ -165,7 +166,7 @@ func generateMarkdownStyleConfig(backgroundColor compat.AdaptiveColor) ansi.Styl
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Prefix: " ",
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownCodeBlock())),
|
||||
Color: AdaptiveColorToString(t.MarkdownCodeBlock()),
|
||||
},
|
||||
},
|
||||
Chroma: &ansi.Chroma{
|
||||
@@ -174,109 +175,109 @@ func generateMarkdownStyleConfig(backgroundColor compat.AdaptiveColor) ansi.Styl
|
||||
},
|
||||
Text: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownText())),
|
||||
Color: AdaptiveColorToString(t.MarkdownText()),
|
||||
},
|
||||
Error: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.Error())),
|
||||
Color: AdaptiveColorToString(t.Error()),
|
||||
},
|
||||
Comment: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.SyntaxComment())),
|
||||
Color: AdaptiveColorToString(t.SyntaxComment()),
|
||||
},
|
||||
CommentPreproc: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.SyntaxKeyword())),
|
||||
Color: AdaptiveColorToString(t.SyntaxKeyword()),
|
||||
},
|
||||
Keyword: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.SyntaxKeyword())),
|
||||
Color: AdaptiveColorToString(t.SyntaxKeyword()),
|
||||
},
|
||||
KeywordReserved: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.SyntaxKeyword())),
|
||||
Color: AdaptiveColorToString(t.SyntaxKeyword()),
|
||||
},
|
||||
KeywordNamespace: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.SyntaxKeyword())),
|
||||
Color: AdaptiveColorToString(t.SyntaxKeyword()),
|
||||
},
|
||||
KeywordType: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.SyntaxType())),
|
||||
Color: AdaptiveColorToString(t.SyntaxType()),
|
||||
},
|
||||
Operator: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.SyntaxOperator())),
|
||||
Color: AdaptiveColorToString(t.SyntaxOperator()),
|
||||
},
|
||||
Punctuation: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.SyntaxPunctuation())),
|
||||
Color: AdaptiveColorToString(t.SyntaxPunctuation()),
|
||||
},
|
||||
Name: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.SyntaxVariable())),
|
||||
Color: AdaptiveColorToString(t.SyntaxVariable()),
|
||||
},
|
||||
NameBuiltin: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.SyntaxVariable())),
|
||||
Color: AdaptiveColorToString(t.SyntaxVariable()),
|
||||
},
|
||||
NameTag: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.SyntaxKeyword())),
|
||||
Color: AdaptiveColorToString(t.SyntaxKeyword()),
|
||||
},
|
||||
NameAttribute: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.SyntaxFunction())),
|
||||
Color: AdaptiveColorToString(t.SyntaxFunction()),
|
||||
},
|
||||
NameClass: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.SyntaxType())),
|
||||
Color: AdaptiveColorToString(t.SyntaxType()),
|
||||
},
|
||||
NameConstant: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.SyntaxVariable())),
|
||||
Color: AdaptiveColorToString(t.SyntaxVariable()),
|
||||
},
|
||||
NameDecorator: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.SyntaxFunction())),
|
||||
Color: AdaptiveColorToString(t.SyntaxFunction()),
|
||||
},
|
||||
NameFunction: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.SyntaxFunction())),
|
||||
Color: AdaptiveColorToString(t.SyntaxFunction()),
|
||||
},
|
||||
LiteralNumber: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.SyntaxNumber())),
|
||||
Color: AdaptiveColorToString(t.SyntaxNumber()),
|
||||
},
|
||||
LiteralString: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.SyntaxString())),
|
||||
Color: AdaptiveColorToString(t.SyntaxString()),
|
||||
},
|
||||
LiteralStringEscape: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.SyntaxKeyword())),
|
||||
Color: AdaptiveColorToString(t.SyntaxKeyword()),
|
||||
},
|
||||
GenericDeleted: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.DiffRemoved())),
|
||||
Color: AdaptiveColorToString(t.DiffRemoved()),
|
||||
},
|
||||
GenericEmph: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownEmph())),
|
||||
Color: AdaptiveColorToString(t.MarkdownEmph()),
|
||||
Italic: boolPtr(true),
|
||||
},
|
||||
GenericInserted: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.DiffAdded())),
|
||||
Color: AdaptiveColorToString(t.DiffAdded()),
|
||||
},
|
||||
GenericStrong: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownStrong())),
|
||||
Color: AdaptiveColorToString(t.MarkdownStrong()),
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
GenericSubheading: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
|
||||
Color: AdaptiveColorToString(t.MarkdownHeading()),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -293,14 +294,14 @@ func generateMarkdownStyleConfig(backgroundColor compat.AdaptiveColor) ansi.Styl
|
||||
},
|
||||
DefinitionDescription: ansi.StylePrimitive{
|
||||
BlockPrefix: "\n ❯ ",
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownLinkText())),
|
||||
Color: AdaptiveColorToString(t.MarkdownLinkText()),
|
||||
},
|
||||
Text: ansi.StylePrimitive{
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownText())),
|
||||
Color: AdaptiveColorToString(t.MarkdownText()),
|
||||
},
|
||||
Paragraph: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownText())),
|
||||
Color: AdaptiveColorToString(t.MarkdownText()),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -308,11 +309,17 @@ func generateMarkdownStyleConfig(backgroundColor compat.AdaptiveColor) ansi.Styl
|
||||
|
||||
// AdaptiveColorToString converts a compat.AdaptiveColor to the appropriate
|
||||
// hex color string based on the current terminal background
|
||||
func AdaptiveColorToString(color compat.AdaptiveColor) string {
|
||||
func AdaptiveColorToString(color compat.AdaptiveColor) *string {
|
||||
if Terminal.BackgroundIsDark {
|
||||
if _, ok := color.Dark.(lipgloss.NoColor); ok {
|
||||
return nil
|
||||
}
|
||||
c1, _ := colorful.MakeColor(color.Dark)
|
||||
return c1.Hex()
|
||||
return stringPtr(c1.Hex())
|
||||
}
|
||||
if _, ok := color.Light.(lipgloss.NoColor); ok {
|
||||
return nil
|
||||
}
|
||||
c1, _ := colorful.MakeColor(color.Light)
|
||||
return c1.Hex()
|
||||
return stringPtr(c1.Hex())
|
||||
}
|
||||
|
||||
@@ -3,155 +3,8 @@ package styles
|
||||
import (
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2/compat"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
)
|
||||
|
||||
// BaseStyle returns the base style with background and foreground colors
|
||||
func BaseStyle() lipgloss.Style {
|
||||
t := theme.CurrentTheme()
|
||||
return lipgloss.NewStyle().Foreground(t.Text())
|
||||
}
|
||||
|
||||
func Panel() lipgloss.Style {
|
||||
t := theme.CurrentTheme()
|
||||
return lipgloss.NewStyle().
|
||||
Background(t.BackgroundPanel()).
|
||||
Border(lipgloss.NormalBorder(), true, false, true, false).
|
||||
BorderForeground(t.BorderSubtle()).
|
||||
Foreground(t.Text())
|
||||
}
|
||||
|
||||
// Regular returns a basic unstyled lipgloss.Style
|
||||
func Regular() lipgloss.Style {
|
||||
return lipgloss.NewStyle()
|
||||
}
|
||||
|
||||
func Muted() lipgloss.Style {
|
||||
t := theme.CurrentTheme()
|
||||
return lipgloss.NewStyle().Foreground(t.TextMuted())
|
||||
}
|
||||
|
||||
// Bold returns a bold style
|
||||
func Bold() lipgloss.Style {
|
||||
return BaseStyle().Bold(true)
|
||||
}
|
||||
|
||||
// Padded returns a style with horizontal padding
|
||||
func Padded() lipgloss.Style {
|
||||
return BaseStyle().Padding(0, 1)
|
||||
}
|
||||
|
||||
// Border returns a style with a normal border
|
||||
func Border() lipgloss.Style {
|
||||
t := theme.CurrentTheme()
|
||||
return Regular().
|
||||
Border(lipgloss.NormalBorder()).
|
||||
BorderForeground(t.Border())
|
||||
}
|
||||
|
||||
// ThickBorder returns a style with a thick border
|
||||
func ThickBorder() lipgloss.Style {
|
||||
t := theme.CurrentTheme()
|
||||
return Regular().
|
||||
Border(lipgloss.ThickBorder()).
|
||||
BorderForeground(t.Border())
|
||||
}
|
||||
|
||||
// DoubleBorder returns a style with a double border
|
||||
func DoubleBorder() lipgloss.Style {
|
||||
t := theme.CurrentTheme()
|
||||
return Regular().
|
||||
Border(lipgloss.DoubleBorder()).
|
||||
BorderForeground(t.Border())
|
||||
}
|
||||
|
||||
// FocusedBorder returns a style with a border using the focused border color
|
||||
func FocusedBorder() lipgloss.Style {
|
||||
t := theme.CurrentTheme()
|
||||
return Regular().
|
||||
Border(lipgloss.NormalBorder()).
|
||||
BorderForeground(t.BorderActive())
|
||||
}
|
||||
|
||||
// DimBorder returns a style with a border using the dim border color
|
||||
func DimBorder() lipgloss.Style {
|
||||
t := theme.CurrentTheme()
|
||||
return Regular().
|
||||
Border(lipgloss.NormalBorder()).
|
||||
BorderForeground(t.BorderSubtle())
|
||||
}
|
||||
|
||||
// PrimaryColor returns the primary color from the current theme
|
||||
func PrimaryColor() compat.AdaptiveColor {
|
||||
return theme.CurrentTheme().Primary()
|
||||
}
|
||||
|
||||
// SecondaryColor returns the secondary color from the current theme
|
||||
func SecondaryColor() compat.AdaptiveColor {
|
||||
return theme.CurrentTheme().Secondary()
|
||||
}
|
||||
|
||||
// AccentColor returns the accent color from the current theme
|
||||
func AccentColor() compat.AdaptiveColor {
|
||||
return theme.CurrentTheme().Accent()
|
||||
}
|
||||
|
||||
// ErrorColor returns the error color from the current theme
|
||||
func ErrorColor() compat.AdaptiveColor {
|
||||
return theme.CurrentTheme().Error()
|
||||
}
|
||||
|
||||
// WarningColor returns the warning color from the current theme
|
||||
func WarningColor() compat.AdaptiveColor {
|
||||
return theme.CurrentTheme().Warning()
|
||||
}
|
||||
|
||||
// SuccessColor returns the success color from the current theme
|
||||
func SuccessColor() compat.AdaptiveColor {
|
||||
return theme.CurrentTheme().Success()
|
||||
}
|
||||
|
||||
// InfoColor returns the info color from the current theme
|
||||
func InfoColor() compat.AdaptiveColor {
|
||||
return theme.CurrentTheme().Info()
|
||||
}
|
||||
|
||||
// TextColor returns the text color from the current theme
|
||||
func TextColor() compat.AdaptiveColor {
|
||||
return theme.CurrentTheme().Text()
|
||||
}
|
||||
|
||||
// TextMutedColor returns the muted text color from the current theme
|
||||
func TextMutedColor() compat.AdaptiveColor {
|
||||
return theme.CurrentTheme().TextMuted()
|
||||
}
|
||||
|
||||
// BackgroundColor returns the background color from the current theme
|
||||
func BackgroundColor() compat.AdaptiveColor {
|
||||
return theme.CurrentTheme().Background()
|
||||
}
|
||||
|
||||
// BackgroundPanelColor returns the subtle background color from the current theme
|
||||
func BackgroundPanelColor() compat.AdaptiveColor {
|
||||
return theme.CurrentTheme().BackgroundPanel()
|
||||
}
|
||||
|
||||
// BackgroundElementColor returns the darker background color from the current theme
|
||||
func BackgroundElementColor() compat.AdaptiveColor {
|
||||
return theme.CurrentTheme().BackgroundElement()
|
||||
}
|
||||
|
||||
// BorderColor returns the border color from the current theme
|
||||
func BorderColor() compat.AdaptiveColor {
|
||||
return theme.CurrentTheme().Border()
|
||||
}
|
||||
|
||||
// BorderActiveColor returns the active border color from the current theme
|
||||
func BorderActiveColor() compat.AdaptiveColor {
|
||||
return theme.CurrentTheme().BorderActive()
|
||||
}
|
||||
|
||||
// BorderSubtleColor returns the subtle border color from the current theme
|
||||
func BorderSubtleColor() compat.AdaptiveColor {
|
||||
return theme.CurrentTheme().BorderSubtle()
|
||||
func WhitespaceStyle(bg compat.AdaptiveColor) lipgloss.WhitespaceOption {
|
||||
return lipgloss.WithWhitespaceStyle(NewStyle().Background(bg).Lipgloss())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
package styles
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2/compat"
|
||||
)
|
||||
|
||||
// IsNoColor checks if a color is the special NoColor type
|
||||
func IsNoColor(c color.Color) bool {
|
||||
_, ok := c.(lipgloss.NoColor)
|
||||
return ok
|
||||
}
|
||||
|
||||
// Style wraps lipgloss.Style to provide a fluent API for handling "none" colors
|
||||
type Style struct {
|
||||
lipgloss.Style
|
||||
}
|
||||
|
||||
// NewStyle creates a new Style with proper handling of "none" colors
|
||||
func NewStyle() Style {
|
||||
return Style{lipgloss.NewStyle()}
|
||||
}
|
||||
|
||||
func (s Style) Lipgloss() lipgloss.Style {
|
||||
return s.Style
|
||||
}
|
||||
|
||||
// Foreground sets the foreground color, handling "none" appropriately
|
||||
func (s Style) Foreground(c compat.AdaptiveColor) Style {
|
||||
if IsNoColor(c.Dark) && IsNoColor(c.Light) {
|
||||
return Style{s.Style.UnsetForeground()}
|
||||
}
|
||||
return Style{s.Style.Foreground(c)}
|
||||
}
|
||||
|
||||
// Background sets the background color, handling "none" appropriately
|
||||
func (s Style) Background(c compat.AdaptiveColor) Style {
|
||||
if IsNoColor(c.Dark) && IsNoColor(c.Light) {
|
||||
return Style{s.Style.UnsetBackground()}
|
||||
}
|
||||
return Style{s.Style.Background(c)}
|
||||
}
|
||||
|
||||
// BorderForeground sets the border foreground color, handling "none" appropriately
|
||||
func (s Style) BorderForeground(c compat.AdaptiveColor) Style {
|
||||
if IsNoColor(c.Dark) && IsNoColor(c.Light) {
|
||||
return Style{s.Style.UnsetBorderForeground()}
|
||||
}
|
||||
return Style{s.Style.BorderForeground(c)}
|
||||
}
|
||||
|
||||
// BorderBackground sets the border background color, handling "none" appropriately
|
||||
func (s Style) BorderBackground(c compat.AdaptiveColor) Style {
|
||||
if IsNoColor(c.Dark) && IsNoColor(c.Light) {
|
||||
return Style{s.Style.UnsetBorderBackground()}
|
||||
}
|
||||
return Style{s.Style.BorderBackground(c)}
|
||||
}
|
||||
|
||||
// BorderTopForeground sets the border top foreground color, handling "none" appropriately
|
||||
func (s Style) BorderTopForeground(c compat.AdaptiveColor) Style {
|
||||
if IsNoColor(c.Dark) && IsNoColor(c.Light) {
|
||||
return Style{s.Style.UnsetBorderTopForeground()}
|
||||
}
|
||||
return Style{s.Style.BorderTopForeground(c)}
|
||||
}
|
||||
|
||||
// BorderTopBackground sets the border top background color, handling "none" appropriately
|
||||
func (s Style) BorderTopBackground(c compat.AdaptiveColor) Style {
|
||||
if IsNoColor(c.Dark) && IsNoColor(c.Light) {
|
||||
return Style{s.Style.UnsetBorderTopBackground()}
|
||||
}
|
||||
return Style{s.Style.BorderTopBackground(c)}
|
||||
}
|
||||
|
||||
// BorderBottomForeground sets the border bottom foreground color, handling "none" appropriately
|
||||
func (s Style) BorderBottomForeground(c compat.AdaptiveColor) Style {
|
||||
if IsNoColor(c.Dark) && IsNoColor(c.Light) {
|
||||
return Style{s.Style.UnsetBorderBottomForeground()}
|
||||
}
|
||||
return Style{s.Style.BorderBottomForeground(c)}
|
||||
}
|
||||
|
||||
// BorderBottomBackground sets the border bottom background color, handling "none" appropriately
|
||||
func (s Style) BorderBottomBackground(c compat.AdaptiveColor) Style {
|
||||
if IsNoColor(c.Dark) && IsNoColor(c.Light) {
|
||||
return Style{s.Style.UnsetBorderBottomBackground()}
|
||||
}
|
||||
return Style{s.Style.BorderBottomBackground(c)}
|
||||
}
|
||||
|
||||
// BorderLeftForeground sets the border left foreground color, handling "none" appropriately
|
||||
func (s Style) BorderLeftForeground(c compat.AdaptiveColor) Style {
|
||||
if IsNoColor(c.Dark) && IsNoColor(c.Light) {
|
||||
return Style{s.Style.UnsetBorderLeftForeground()}
|
||||
}
|
||||
return Style{s.Style.BorderLeftForeground(c)}
|
||||
}
|
||||
|
||||
// BorderLeftBackground sets the border left background color, handling "none" appropriately
|
||||
func (s Style) BorderLeftBackground(c compat.AdaptiveColor) Style {
|
||||
if IsNoColor(c.Dark) && IsNoColor(c.Light) {
|
||||
return Style{s.Style.UnsetBorderLeftBackground()}
|
||||
}
|
||||
return Style{s.Style.BorderLeftBackground(c)}
|
||||
}
|
||||
|
||||
// BorderRightForeground sets the border right foreground color, handling "none" appropriately
|
||||
func (s Style) BorderRightForeground(c compat.AdaptiveColor) Style {
|
||||
if IsNoColor(c.Dark) && IsNoColor(c.Light) {
|
||||
return Style{s.Style.UnsetBorderRightForeground()}
|
||||
}
|
||||
return Style{s.Style.BorderRightForeground(c)}
|
||||
}
|
||||
|
||||
// BorderRightBackground sets the border right background color, handling "none" appropriately
|
||||
func (s Style) BorderRightBackground(c compat.AdaptiveColor) Style {
|
||||
if IsNoColor(c.Dark) && IsNoColor(c.Light) {
|
||||
return Style{s.Style.UnsetBorderRightBackground()}
|
||||
}
|
||||
return Style{s.Style.BorderRightBackground(c)}
|
||||
}
|
||||
|
||||
// Render applies the style to a string
|
||||
func (s Style) Render(str string) string {
|
||||
return s.Style.Render(str)
|
||||
}
|
||||
|
||||
// Common lipgloss.Style method delegations for seamless usage
|
||||
|
||||
func (s Style) Bold(v bool) Style {
|
||||
return Style{s.Style.Bold(v)}
|
||||
}
|
||||
|
||||
func (s Style) Italic(v bool) Style {
|
||||
return Style{s.Style.Italic(v)}
|
||||
}
|
||||
|
||||
func (s Style) Underline(v bool) Style {
|
||||
return Style{s.Style.Underline(v)}
|
||||
}
|
||||
|
||||
func (s Style) Strikethrough(v bool) Style {
|
||||
return Style{s.Style.Strikethrough(v)}
|
||||
}
|
||||
|
||||
func (s Style) Blink(v bool) Style {
|
||||
return Style{s.Style.Blink(v)}
|
||||
}
|
||||
|
||||
func (s Style) Faint(v bool) Style {
|
||||
return Style{s.Style.Faint(v)}
|
||||
}
|
||||
|
||||
func (s Style) Reverse(v bool) Style {
|
||||
return Style{s.Style.Reverse(v)}
|
||||
}
|
||||
|
||||
func (s Style) Width(i int) Style {
|
||||
return Style{s.Style.Width(i)}
|
||||
}
|
||||
|
||||
func (s Style) Height(i int) Style {
|
||||
return Style{s.Style.Height(i)}
|
||||
}
|
||||
|
||||
func (s Style) Padding(i ...int) Style {
|
||||
return Style{s.Style.Padding(i...)}
|
||||
}
|
||||
|
||||
func (s Style) PaddingTop(i int) Style {
|
||||
return Style{s.Style.PaddingTop(i)}
|
||||
}
|
||||
|
||||
func (s Style) PaddingBottom(i int) Style {
|
||||
return Style{s.Style.PaddingBottom(i)}
|
||||
}
|
||||
|
||||
func (s Style) PaddingLeft(i int) Style {
|
||||
return Style{s.Style.PaddingLeft(i)}
|
||||
}
|
||||
|
||||
func (s Style) PaddingRight(i int) Style {
|
||||
return Style{s.Style.PaddingRight(i)}
|
||||
}
|
||||
|
||||
func (s Style) Margin(i ...int) Style {
|
||||
return Style{s.Style.Margin(i...)}
|
||||
}
|
||||
|
||||
func (s Style) MarginTop(i int) Style {
|
||||
return Style{s.Style.MarginTop(i)}
|
||||
}
|
||||
|
||||
func (s Style) MarginBottom(i int) Style {
|
||||
return Style{s.Style.MarginBottom(i)}
|
||||
}
|
||||
|
||||
func (s Style) MarginLeft(i int) Style {
|
||||
return Style{s.Style.MarginLeft(i)}
|
||||
}
|
||||
|
||||
func (s Style) MarginRight(i int) Style {
|
||||
return Style{s.Style.MarginRight(i)}
|
||||
}
|
||||
|
||||
func (s Style) Border(b lipgloss.Border, sides ...bool) Style {
|
||||
return Style{s.Style.Border(b, sides...)}
|
||||
}
|
||||
|
||||
func (s Style) BorderStyle(b lipgloss.Border) Style {
|
||||
return Style{s.Style.BorderStyle(b)}
|
||||
}
|
||||
|
||||
func (s Style) BorderTop(v bool) Style {
|
||||
return Style{s.Style.BorderTop(v)}
|
||||
}
|
||||
|
||||
func (s Style) BorderBottom(v bool) Style {
|
||||
return Style{s.Style.BorderBottom(v)}
|
||||
}
|
||||
|
||||
func (s Style) BorderLeft(v bool) Style {
|
||||
return Style{s.Style.BorderLeft(v)}
|
||||
}
|
||||
|
||||
func (s Style) BorderRight(v bool) Style {
|
||||
return Style{s.Style.BorderRight(v)}
|
||||
}
|
||||
|
||||
func (s Style) Align(p ...lipgloss.Position) Style {
|
||||
return Style{s.Style.Align(p...)}
|
||||
}
|
||||
|
||||
func (s Style) AlignHorizontal(p lipgloss.Position) Style {
|
||||
return Style{s.Style.AlignHorizontal(p)}
|
||||
}
|
||||
|
||||
func (s Style) AlignVertical(p lipgloss.Position) Style {
|
||||
return Style{s.Style.AlignVertical(p)}
|
||||
}
|
||||
|
||||
func (s Style) Inline(v bool) Style {
|
||||
return Style{s.Style.Inline(v)}
|
||||
}
|
||||
|
||||
func (s Style) MaxWidth(n int) Style {
|
||||
return Style{s.Style.MaxWidth(n)}
|
||||
}
|
||||
|
||||
func (s Style) MaxHeight(n int) Style {
|
||||
return Style{s.Style.MaxHeight(n)}
|
||||
}
|
||||
|
||||
func (s Style) TabWidth(n int) Style {
|
||||
return Style{s.Style.TabWidth(n)}
|
||||
}
|
||||
|
||||
func (s Style) UnsetBold() Style {
|
||||
return Style{s.Style.UnsetBold()}
|
||||
}
|
||||
|
||||
func (s Style) UnsetItalic() Style {
|
||||
return Style{s.Style.UnsetItalic()}
|
||||
}
|
||||
|
||||
func (s Style) UnsetUnderline() Style {
|
||||
return Style{s.Style.UnsetUnderline()}
|
||||
}
|
||||
|
||||
func (s Style) UnsetStrikethrough() Style {
|
||||
return Style{s.Style.UnsetStrikethrough()}
|
||||
}
|
||||
|
||||
func (s Style) UnsetBlink() Style {
|
||||
return Style{s.Style.UnsetBlink()}
|
||||
}
|
||||
|
||||
func (s Style) UnsetFaint() Style {
|
||||
return Style{s.Style.UnsetFaint()}
|
||||
}
|
||||
|
||||
func (s Style) UnsetReverse() Style {
|
||||
return Style{s.Style.UnsetReverse()}
|
||||
}
|
||||
|
||||
func (s Style) Copy() Style {
|
||||
return Style{s.Style}
|
||||
}
|
||||
|
||||
func (s Style) Inherit(i Style) Style {
|
||||
return Style{s.Style.Inherit(i.Style)}
|
||||
}
|
||||
@@ -171,7 +171,7 @@ func (r *colorResolver) resolveColor(key string, value any) (any, error) {
|
||||
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
if strings.HasPrefix(v, "#") {
|
||||
if strings.HasPrefix(v, "#") || v == "none" {
|
||||
return v, nil
|
||||
}
|
||||
return r.resolveReference(v)
|
||||
@@ -205,7 +205,7 @@ func (r *colorResolver) resolveColor(key string, value any) (any, error) {
|
||||
func (r *colorResolver) resolveColorValue(value any) (any, error) {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
if strings.HasPrefix(v, "#") {
|
||||
if strings.HasPrefix(v, "#") || v == "none" {
|
||||
return v, nil
|
||||
}
|
||||
return r.resolveReference(v)
|
||||
@@ -240,6 +240,12 @@ func (r *colorResolver) resolveReference(ref string) (any, error) {
|
||||
func parseResolvedColor(value any) (compat.AdaptiveColor, error) {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
if v == "none" {
|
||||
return compat.AdaptiveColor{
|
||||
Dark: lipgloss.NoColor{},
|
||||
Light: lipgloss.NoColor{},
|
||||
}, nil
|
||||
}
|
||||
return compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(v),
|
||||
Light: lipgloss.Color(v),
|
||||
@@ -277,6 +283,9 @@ func parseResolvedColor(value any) (compat.AdaptiveColor, error) {
|
||||
func parseColorValue(value any) (color.Color, error) {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
if v == "none" {
|
||||
return lipgloss.NoColor{}, nil
|
||||
}
|
||||
return lipgloss.Color(v), nil
|
||||
case float64:
|
||||
return lipgloss.Color(fmt.Sprintf("%d", int(v))), nil
|
||||
|
||||
@@ -21,7 +21,7 @@ func TestLoadThemesFromJSON(t *testing.T) {
|
||||
}
|
||||
|
||||
// Check for expected themes
|
||||
expectedThemes := []string{"tokyonight", "opencode", "everforest", "ayu", "example"}
|
||||
expectedThemes := []string{"tokyonight", "opencode", "everforest", "ayu"}
|
||||
for _, expected := range expectedThemes {
|
||||
found := slices.Contains(themes, expected)
|
||||
if !found {
|
||||
@@ -43,22 +43,28 @@ func TestLoadThemesFromJSON(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestColorReferenceResolution(t *testing.T) {
|
||||
// Test the example theme which uses references
|
||||
example := GetTheme("example")
|
||||
if example == nil {
|
||||
t.Fatal("Failed to get example theme")
|
||||
// Load themes first
|
||||
err := LoadThemesFromJSON()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load themes: %v", err)
|
||||
}
|
||||
|
||||
// Check that brandBlue reference was resolved
|
||||
primary := example.Primary()
|
||||
// Test a theme that uses references (e.g., solarized uses color definitions)
|
||||
solarized := GetTheme("solarized")
|
||||
if solarized == nil {
|
||||
t.Fatal("Failed to get solarized theme")
|
||||
}
|
||||
|
||||
// Check that color references were resolved
|
||||
primary := solarized.Primary()
|
||||
if primary.Dark == nil || primary.Light == nil {
|
||||
t.Error("Primary color (brandBlue reference) not resolved")
|
||||
t.Error("Primary color reference not resolved")
|
||||
}
|
||||
|
||||
// Check that nested reference (borderActive -> primary -> brandBlue) works
|
||||
borderActive := example.BorderActive()
|
||||
if borderActive.Dark == nil || borderActive.Light == nil {
|
||||
t.Error("BorderActive color (nested reference) not resolved")
|
||||
// Check that all colors are properly resolved
|
||||
text := solarized.Text()
|
||||
if text.Dark == nil || text.Light == nil {
|
||||
t.Error("Text color reference not resolved")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,19 +2,25 @@ package theme
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/alecthomas/chroma/v2/styles"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2/compat"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
)
|
||||
|
||||
// Manager handles theme registration, selection, and retrieval.
|
||||
// It maintains a registry of available themes and tracks the currently active theme.
|
||||
type Manager struct {
|
||||
themes map[string]Theme
|
||||
currentName string
|
||||
mu sync.RWMutex
|
||||
themes map[string]Theme
|
||||
currentName string
|
||||
currentUsesAnsiCache bool // Cache whether current theme uses ANSI colors
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// Global instance of the theme manager
|
||||
@@ -34,6 +40,7 @@ func RegisterTheme(name string, theme Theme) {
|
||||
// If this is the first theme, make it the default
|
||||
if globalManager.currentName == "" {
|
||||
globalManager.currentName = name
|
||||
globalManager.currentUsesAnsiCache = themeUsesAnsiColors(theme)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,11 +51,13 @@ func SetTheme(name string) error {
|
||||
defer globalManager.mu.Unlock()
|
||||
delete(styles.Registry, "charm")
|
||||
|
||||
if _, exists := globalManager.themes[name]; !exists {
|
||||
theme, exists := globalManager.themes[name]
|
||||
if !exists {
|
||||
return fmt.Errorf("theme '%s' not found", name)
|
||||
}
|
||||
|
||||
globalManager.currentName = name
|
||||
globalManager.currentUsesAnsiCache = themeUsesAnsiColors(theme)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -84,12 +93,16 @@ func AvailableThemes() []string {
|
||||
names = append(names, name)
|
||||
}
|
||||
slices.SortFunc(names, func(a, b string) int {
|
||||
// list system theme first
|
||||
if a == "opencode" {
|
||||
return -1
|
||||
} else if b == "opencode" {
|
||||
return 1
|
||||
}
|
||||
if a == "system" {
|
||||
return -1
|
||||
} else if b == "system" {
|
||||
return 1
|
||||
}
|
||||
return strings.Compare(a, b)
|
||||
})
|
||||
return names
|
||||
@@ -103,3 +116,114 @@ func GetTheme(name string) Theme {
|
||||
|
||||
return globalManager.themes[name]
|
||||
}
|
||||
|
||||
// UpdateSystemTheme updates the system theme with terminal background info
|
||||
func UpdateSystemTheme(terminalBg color.Color, isDark bool) {
|
||||
globalManager.mu.Lock()
|
||||
defer globalManager.mu.Unlock()
|
||||
|
||||
dynamicTheme := NewSystemTheme(terminalBg, isDark)
|
||||
globalManager.themes["system"] = dynamicTheme
|
||||
if globalManager.currentName == "system" {
|
||||
globalManager.currentUsesAnsiCache = themeUsesAnsiColors(dynamicTheme)
|
||||
}
|
||||
}
|
||||
|
||||
// CurrentThemeUsesAnsiColors returns true if the current theme uses ANSI 0-16 colors
|
||||
func CurrentThemeUsesAnsiColors() bool {
|
||||
// globalManager.mu.RLock()
|
||||
// defer globalManager.mu.RUnlock()
|
||||
|
||||
return globalManager.currentUsesAnsiCache
|
||||
}
|
||||
|
||||
// isAnsiColor checks if a color represents an ANSI 0-16 color
|
||||
func isAnsiColor(c color.Color) bool {
|
||||
if _, ok := c.(lipgloss.NoColor); ok {
|
||||
return false
|
||||
}
|
||||
if _, ok := c.(ansi.BasicColor); ok {
|
||||
return true
|
||||
}
|
||||
|
||||
// For other color types, check if they represent ANSI colors
|
||||
// by examining their string representation
|
||||
if stringer, ok := c.(fmt.Stringer); ok {
|
||||
str := stringer.String()
|
||||
// Check if it's a numeric ANSI color (0-15)
|
||||
if num, err := strconv.Atoi(str); err == nil && num >= 0 && num <= 15 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// adaptiveColorUsesAnsi checks if an AdaptiveColor uses ANSI colors
|
||||
func adaptiveColorUsesAnsi(ac compat.AdaptiveColor) bool {
|
||||
if isAnsiColor(ac.Dark) {
|
||||
return true
|
||||
}
|
||||
if isAnsiColor(ac.Light) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// themeUsesAnsiColors checks if a theme uses any ANSI 0-16 colors
|
||||
func themeUsesAnsiColors(theme Theme) bool {
|
||||
if theme == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return adaptiveColorUsesAnsi(theme.Primary()) ||
|
||||
adaptiveColorUsesAnsi(theme.Secondary()) ||
|
||||
adaptiveColorUsesAnsi(theme.Accent()) ||
|
||||
adaptiveColorUsesAnsi(theme.Error()) ||
|
||||
adaptiveColorUsesAnsi(theme.Warning()) ||
|
||||
adaptiveColorUsesAnsi(theme.Success()) ||
|
||||
adaptiveColorUsesAnsi(theme.Info()) ||
|
||||
adaptiveColorUsesAnsi(theme.Text()) ||
|
||||
adaptiveColorUsesAnsi(theme.TextMuted()) ||
|
||||
adaptiveColorUsesAnsi(theme.Background()) ||
|
||||
adaptiveColorUsesAnsi(theme.BackgroundPanel()) ||
|
||||
adaptiveColorUsesAnsi(theme.BackgroundElement()) ||
|
||||
adaptiveColorUsesAnsi(theme.Border()) ||
|
||||
adaptiveColorUsesAnsi(theme.BorderActive()) ||
|
||||
adaptiveColorUsesAnsi(theme.BorderSubtle()) ||
|
||||
adaptiveColorUsesAnsi(theme.DiffAdded()) ||
|
||||
adaptiveColorUsesAnsi(theme.DiffRemoved()) ||
|
||||
adaptiveColorUsesAnsi(theme.DiffContext()) ||
|
||||
adaptiveColorUsesAnsi(theme.DiffHunkHeader()) ||
|
||||
adaptiveColorUsesAnsi(theme.DiffHighlightAdded()) ||
|
||||
adaptiveColorUsesAnsi(theme.DiffHighlightRemoved()) ||
|
||||
adaptiveColorUsesAnsi(theme.DiffAddedBg()) ||
|
||||
adaptiveColorUsesAnsi(theme.DiffRemovedBg()) ||
|
||||
adaptiveColorUsesAnsi(theme.DiffContextBg()) ||
|
||||
adaptiveColorUsesAnsi(theme.DiffLineNumber()) ||
|
||||
adaptiveColorUsesAnsi(theme.DiffAddedLineNumberBg()) ||
|
||||
adaptiveColorUsesAnsi(theme.DiffRemovedLineNumberBg()) ||
|
||||
adaptiveColorUsesAnsi(theme.MarkdownText()) ||
|
||||
adaptiveColorUsesAnsi(theme.MarkdownHeading()) ||
|
||||
adaptiveColorUsesAnsi(theme.MarkdownLink()) ||
|
||||
adaptiveColorUsesAnsi(theme.MarkdownLinkText()) ||
|
||||
adaptiveColorUsesAnsi(theme.MarkdownCode()) ||
|
||||
adaptiveColorUsesAnsi(theme.MarkdownBlockQuote()) ||
|
||||
adaptiveColorUsesAnsi(theme.MarkdownEmph()) ||
|
||||
adaptiveColorUsesAnsi(theme.MarkdownStrong()) ||
|
||||
adaptiveColorUsesAnsi(theme.MarkdownHorizontalRule()) ||
|
||||
adaptiveColorUsesAnsi(theme.MarkdownListItem()) ||
|
||||
adaptiveColorUsesAnsi(theme.MarkdownListEnumeration()) ||
|
||||
adaptiveColorUsesAnsi(theme.MarkdownImage()) ||
|
||||
adaptiveColorUsesAnsi(theme.MarkdownImageText()) ||
|
||||
adaptiveColorUsesAnsi(theme.MarkdownCodeBlock()) ||
|
||||
adaptiveColorUsesAnsi(theme.SyntaxComment()) ||
|
||||
adaptiveColorUsesAnsi(theme.SyntaxKeyword()) ||
|
||||
adaptiveColorUsesAnsi(theme.SyntaxFunction()) ||
|
||||
adaptiveColorUsesAnsi(theme.SyntaxVariable()) ||
|
||||
adaptiveColorUsesAnsi(theme.SyntaxString()) ||
|
||||
adaptiveColorUsesAnsi(theme.SyntaxNumber()) ||
|
||||
adaptiveColorUsesAnsi(theme.SyntaxType()) ||
|
||||
adaptiveColorUsesAnsi(theme.SyntaxOperator()) ||
|
||||
adaptiveColorUsesAnsi(theme.SyntaxPunctuation())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"math"
|
||||
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2/compat"
|
||||
)
|
||||
|
||||
// SystemTheme is a dynamic theme that derives its gray scale colors
|
||||
// from the terminal's background color at runtime
|
||||
type SystemTheme struct {
|
||||
BaseTheme
|
||||
terminalBg color.Color
|
||||
terminalBgIsDark bool
|
||||
}
|
||||
|
||||
// NewSystemTheme creates a new instance of the dynamic system theme
|
||||
func NewSystemTheme(terminalBg color.Color, isDark bool) *SystemTheme {
|
||||
theme := &SystemTheme{
|
||||
terminalBg: terminalBg,
|
||||
terminalBgIsDark: isDark,
|
||||
}
|
||||
theme.initializeColors()
|
||||
return theme
|
||||
}
|
||||
|
||||
// initializeColors sets up all theme colors
|
||||
func (t *SystemTheme) initializeColors() {
|
||||
// Generate gray scale based on terminal background
|
||||
grays := t.generateGrayScale()
|
||||
|
||||
// Set ANSI colors for primary colors
|
||||
t.PrimaryColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Cyan,
|
||||
Light: lipgloss.Cyan,
|
||||
}
|
||||
t.SecondaryColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Magenta,
|
||||
Light: lipgloss.Magenta,
|
||||
}
|
||||
t.AccentColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Cyan,
|
||||
Light: lipgloss.Cyan,
|
||||
}
|
||||
|
||||
// Status colors using ANSI
|
||||
t.ErrorColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Red,
|
||||
Light: lipgloss.Red,
|
||||
}
|
||||
t.WarningColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Yellow,
|
||||
Light: lipgloss.Yellow,
|
||||
}
|
||||
t.SuccessColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Green,
|
||||
Light: lipgloss.Green,
|
||||
}
|
||||
t.InfoColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Cyan,
|
||||
Light: lipgloss.Cyan,
|
||||
}
|
||||
|
||||
// Text colors
|
||||
t.TextColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.NoColor{},
|
||||
Light: lipgloss.NoColor{},
|
||||
}
|
||||
// Derive muted text color from terminal foreground
|
||||
t.TextMutedColor = t.generateMutedTextColor()
|
||||
|
||||
// Background colors
|
||||
t.BackgroundColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.NoColor{},
|
||||
Light: lipgloss.NoColor{},
|
||||
}
|
||||
t.BackgroundPanelColor = grays[2]
|
||||
t.BackgroundElementColor = grays[3]
|
||||
|
||||
// Border colors
|
||||
t.BorderSubtleColor = grays[6]
|
||||
t.BorderColor = grays[7]
|
||||
t.BorderActiveColor = grays[8]
|
||||
|
||||
// Diff colors using ANSI colors
|
||||
t.DiffAddedColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("2"), // green
|
||||
Light: lipgloss.Color("2"),
|
||||
}
|
||||
t.DiffRemovedColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("1"), // red
|
||||
Light: lipgloss.Color("1"),
|
||||
}
|
||||
t.DiffContextColor = grays[7] // Use gray for context
|
||||
t.DiffHunkHeaderColor = grays[7]
|
||||
t.DiffHighlightAddedColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("2"), // green
|
||||
Light: lipgloss.Color("2"),
|
||||
}
|
||||
t.DiffHighlightRemovedColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("1"), // red
|
||||
Light: lipgloss.Color("1"),
|
||||
}
|
||||
// Use subtle gray backgrounds for diff
|
||||
t.DiffAddedBgColor = grays[2]
|
||||
t.DiffRemovedBgColor = grays[2]
|
||||
t.DiffContextBgColor = grays[1]
|
||||
t.DiffLineNumberColor = grays[6]
|
||||
t.DiffAddedLineNumberBgColor = grays[3]
|
||||
t.DiffRemovedLineNumberBgColor = grays[3]
|
||||
|
||||
// Markdown colors using ANSI
|
||||
t.MarkdownTextColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.NoColor{},
|
||||
Light: lipgloss.NoColor{},
|
||||
}
|
||||
t.MarkdownHeadingColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.NoColor{},
|
||||
Light: lipgloss.NoColor{},
|
||||
}
|
||||
t.MarkdownLinkColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("4"), // blue
|
||||
Light: lipgloss.Color("4"),
|
||||
}
|
||||
t.MarkdownLinkTextColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("6"), // cyan
|
||||
Light: lipgloss.Color("6"),
|
||||
}
|
||||
t.MarkdownCodeColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("2"), // green
|
||||
Light: lipgloss.Color("2"),
|
||||
}
|
||||
t.MarkdownBlockQuoteColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("3"), // yellow
|
||||
Light: lipgloss.Color("3"),
|
||||
}
|
||||
t.MarkdownEmphColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("3"), // yellow
|
||||
Light: lipgloss.Color("3"),
|
||||
}
|
||||
t.MarkdownStrongColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.NoColor{},
|
||||
Light: lipgloss.NoColor{},
|
||||
}
|
||||
t.MarkdownHorizontalRuleColor = t.BorderColor
|
||||
t.MarkdownListItemColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("4"), // blue
|
||||
Light: lipgloss.Color("4"),
|
||||
}
|
||||
t.MarkdownListEnumerationColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("6"), // cyan
|
||||
Light: lipgloss.Color("6"),
|
||||
}
|
||||
t.MarkdownImageColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("4"), // blue
|
||||
Light: lipgloss.Color("4"),
|
||||
}
|
||||
t.MarkdownImageTextColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("6"), // cyan
|
||||
Light: lipgloss.Color("6"),
|
||||
}
|
||||
t.MarkdownCodeBlockColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.NoColor{},
|
||||
Light: lipgloss.NoColor{},
|
||||
}
|
||||
|
||||
// Syntax colors
|
||||
t.SyntaxCommentColor = t.TextMutedColor // Use same as muted text
|
||||
t.SyntaxKeywordColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("5"), // magenta
|
||||
Light: lipgloss.Color("5"),
|
||||
}
|
||||
t.SyntaxFunctionColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("4"), // blue
|
||||
Light: lipgloss.Color("4"),
|
||||
}
|
||||
t.SyntaxVariableColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.NoColor{},
|
||||
Light: lipgloss.NoColor{},
|
||||
}
|
||||
t.SyntaxStringColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("2"), // green
|
||||
Light: lipgloss.Color("2"),
|
||||
}
|
||||
t.SyntaxNumberColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("3"), // yellow
|
||||
Light: lipgloss.Color("3"),
|
||||
}
|
||||
t.SyntaxTypeColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("6"), // cyan
|
||||
Light: lipgloss.Color("6"),
|
||||
}
|
||||
t.SyntaxOperatorColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("6"), // cyan
|
||||
Light: lipgloss.Color("6"),
|
||||
}
|
||||
t.SyntaxPunctuationColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.NoColor{},
|
||||
Light: lipgloss.NoColor{},
|
||||
}
|
||||
}
|
||||
|
||||
// generateGrayScale creates a gray scale based on the terminal background
|
||||
func (t *SystemTheme) generateGrayScale() map[int]compat.AdaptiveColor {
|
||||
grays := make(map[int]compat.AdaptiveColor)
|
||||
|
||||
r, g, b, _ := t.terminalBg.RGBA()
|
||||
bgR := float64(r >> 8)
|
||||
bgG := float64(g >> 8)
|
||||
bgB := float64(b >> 8)
|
||||
|
||||
luminance := 0.299*bgR + 0.587*bgG + 0.114*bgB
|
||||
|
||||
for i := 1; i <= 12; i++ {
|
||||
var stepColor string
|
||||
factor := float64(i) / 12.0
|
||||
|
||||
if t.terminalBgIsDark {
|
||||
if luminance < 10 {
|
||||
grayValue := int(factor * 0.4 * 255)
|
||||
stepColor = fmt.Sprintf("#%02x%02x%02x", grayValue, grayValue, grayValue)
|
||||
} else {
|
||||
newLum := luminance + (255-luminance)*factor*0.4
|
||||
|
||||
ratio := newLum / luminance
|
||||
newR := math.Min(bgR*ratio, 255)
|
||||
newG := math.Min(bgG*ratio, 255)
|
||||
newB := math.Min(bgB*ratio, 255)
|
||||
|
||||
stepColor = fmt.Sprintf("#%02x%02x%02x", int(newR), int(newG), int(newB))
|
||||
}
|
||||
} else {
|
||||
if luminance > 245 {
|
||||
grayValue := int(255 - factor*0.4*255)
|
||||
stepColor = fmt.Sprintf("#%02x%02x%02x", grayValue, grayValue, grayValue)
|
||||
} else {
|
||||
newLum := luminance * (1 - factor*0.4)
|
||||
|
||||
ratio := newLum / luminance
|
||||
newR := math.Max(bgR*ratio, 0)
|
||||
newG := math.Max(bgG*ratio, 0)
|
||||
newB := math.Max(bgB*ratio, 0)
|
||||
|
||||
stepColor = fmt.Sprintf("#%02x%02x%02x", int(newR), int(newG), int(newB))
|
||||
}
|
||||
}
|
||||
|
||||
grays[i] = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(stepColor),
|
||||
Light: lipgloss.Color(stepColor),
|
||||
}
|
||||
}
|
||||
|
||||
return grays
|
||||
}
|
||||
|
||||
// generateMutedTextColor creates a muted gray color based on the terminal background
|
||||
func (t *SystemTheme) generateMutedTextColor() compat.AdaptiveColor {
|
||||
bgR, bgG, bgB, _ := t.terminalBg.RGBA()
|
||||
|
||||
bgRf := float64(bgR >> 8)
|
||||
bgGf := float64(bgG >> 8)
|
||||
bgBf := float64(bgB >> 8)
|
||||
|
||||
bgLum := 0.299*bgRf + 0.587*bgGf + 0.114*bgBf
|
||||
|
||||
var grayValue int
|
||||
if t.terminalBgIsDark {
|
||||
if bgLum < 10 {
|
||||
// Very dark/black background
|
||||
// grays[3] would be around #2e (46), so we need much lighter
|
||||
grayValue = 180 // #b4b4b4
|
||||
} else {
|
||||
// Scale up for lighter dark backgrounds
|
||||
// Ensure we're always significantly brighter than BackgroundElement
|
||||
grayValue = min(int(160+(bgLum*0.3)), 200)
|
||||
}
|
||||
} else {
|
||||
if bgLum > 245 {
|
||||
// Very light/white background
|
||||
// grays[3] would be around #f5 (245), so we need much darker
|
||||
grayValue = 75 // #4b4b4b
|
||||
} else {
|
||||
// Scale down for darker light backgrounds
|
||||
// Ensure we're always significantly darker than BackgroundElement
|
||||
grayValue = max(int(100-((255-bgLum)*0.2)), 60)
|
||||
}
|
||||
}
|
||||
|
||||
mutedColor := fmt.Sprintf("#%02x%02x%02x", grayValue, grayValue, grayValue)
|
||||
|
||||
return compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(mutedColor),
|
||||
Light: lipgloss.Color(mutedColor),
|
||||
}
|
||||
}
|
||||
@@ -78,4 +78,3 @@
|
||||
"syntaxPunctuation": "darkFg"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -110,4 +110,3 @@
|
||||
"syntaxPunctuation": { "dark": "darkText", "light": "lightText" }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"background": "#193549",
|
||||
"backgroundAlt": "#122738",
|
||||
"backgroundPanel": "#1f4662",
|
||||
"foreground": "#ffffff",
|
||||
"foregroundMuted": "#adb7c9",
|
||||
"yellow": "#ffc600",
|
||||
"yellowBright": "#ffe14c",
|
||||
"orange": "#ff9d00",
|
||||
"orangeBright": "#ffb454",
|
||||
"mint": "#2affdf",
|
||||
"mintBright": "#7efff5",
|
||||
"blue": "#0088ff",
|
||||
"blueBright": "#5cb7ff",
|
||||
"pink": "#ff628c",
|
||||
"pinkBright": "#ff86a5",
|
||||
"green": "#9eff80",
|
||||
"greenBright": "#b9ff9f",
|
||||
"purple": "#9a5feb",
|
||||
"purpleBright": "#b88cfd",
|
||||
"red": "#ff0088",
|
||||
"redBright": "#ff5fb3"
|
||||
},
|
||||
"theme": {
|
||||
"primary": {
|
||||
"dark": "blue",
|
||||
"light": "#0066cc"
|
||||
},
|
||||
"secondary": {
|
||||
"dark": "purple",
|
||||
"light": "#7c4dff"
|
||||
},
|
||||
"accent": {
|
||||
"dark": "mint",
|
||||
"light": "#00acc1"
|
||||
},
|
||||
"error": {
|
||||
"dark": "red",
|
||||
"light": "#e91e63"
|
||||
},
|
||||
"warning": {
|
||||
"dark": "yellow",
|
||||
"light": "#ff9800"
|
||||
},
|
||||
"success": {
|
||||
"dark": "green",
|
||||
"light": "#4caf50"
|
||||
},
|
||||
"info": {
|
||||
"dark": "orange",
|
||||
"light": "#ff5722"
|
||||
},
|
||||
"text": {
|
||||
"dark": "foreground",
|
||||
"light": "#193549"
|
||||
},
|
||||
"textMuted": {
|
||||
"dark": "foregroundMuted",
|
||||
"light": "#5c6b7d"
|
||||
},
|
||||
"background": {
|
||||
"dark": "#193549",
|
||||
"light": "#ffffff"
|
||||
},
|
||||
"backgroundPanel": {
|
||||
"dark": "#122738",
|
||||
"light": "#f5f7fa"
|
||||
},
|
||||
"backgroundElement": {
|
||||
"dark": "#1f4662",
|
||||
"light": "#e8ecf1"
|
||||
},
|
||||
"border": {
|
||||
"dark": "#1f4662",
|
||||
"light": "#d3dae3"
|
||||
},
|
||||
"borderActive": {
|
||||
"dark": "blue",
|
||||
"light": "#0066cc"
|
||||
},
|
||||
"borderSubtle": {
|
||||
"dark": "#0e1e2e",
|
||||
"light": "#e8ecf1"
|
||||
},
|
||||
"diffAdded": {
|
||||
"dark": "green",
|
||||
"light": "#4caf50"
|
||||
},
|
||||
"diffRemoved": {
|
||||
"dark": "red",
|
||||
"light": "#e91e63"
|
||||
},
|
||||
"diffContext": {
|
||||
"dark": "foregroundMuted",
|
||||
"light": "#5c6b7d"
|
||||
},
|
||||
"diffHunkHeader": {
|
||||
"dark": "mint",
|
||||
"light": "#00acc1"
|
||||
},
|
||||
"diffHighlightAdded": {
|
||||
"dark": "greenBright",
|
||||
"light": "#4caf50"
|
||||
},
|
||||
"diffHighlightRemoved": {
|
||||
"dark": "redBright",
|
||||
"light": "#e91e63"
|
||||
},
|
||||
"diffAddedBg": {
|
||||
"dark": "#1a3a2a",
|
||||
"light": "#e8f5e9"
|
||||
},
|
||||
"diffRemovedBg": {
|
||||
"dark": "#3a1a2a",
|
||||
"light": "#ffebee"
|
||||
},
|
||||
"diffContextBg": {
|
||||
"dark": "#122738",
|
||||
"light": "#f5f7fa"
|
||||
},
|
||||
"diffLineNumber": {
|
||||
"dark": "#2d5a7b",
|
||||
"light": "#b0bec5"
|
||||
},
|
||||
"diffAddedLineNumberBg": {
|
||||
"dark": "#1a3a2a",
|
||||
"light": "#e8f5e9"
|
||||
},
|
||||
"diffRemovedLineNumberBg": {
|
||||
"dark": "#3a1a2a",
|
||||
"light": "#ffebee"
|
||||
},
|
||||
"markdownText": {
|
||||
"dark": "foreground",
|
||||
"light": "#193549"
|
||||
},
|
||||
"markdownHeading": {
|
||||
"dark": "yellow",
|
||||
"light": "#ff9800"
|
||||
},
|
||||
"markdownLink": {
|
||||
"dark": "blue",
|
||||
"light": "#0066cc"
|
||||
},
|
||||
"markdownLinkText": {
|
||||
"dark": "mint",
|
||||
"light": "#00acc1"
|
||||
},
|
||||
"markdownCode": {
|
||||
"dark": "green",
|
||||
"light": "#4caf50"
|
||||
},
|
||||
"markdownBlockQuote": {
|
||||
"dark": "foregroundMuted",
|
||||
"light": "#5c6b7d"
|
||||
},
|
||||
"markdownEmph": {
|
||||
"dark": "orange",
|
||||
"light": "#ff5722"
|
||||
},
|
||||
"markdownStrong": {
|
||||
"dark": "pink",
|
||||
"light": "#e91e63"
|
||||
},
|
||||
"markdownHorizontalRule": {
|
||||
"dark": "#2d5a7b",
|
||||
"light": "#d3dae3"
|
||||
},
|
||||
"markdownListItem": {
|
||||
"dark": "blue",
|
||||
"light": "#0066cc"
|
||||
},
|
||||
"markdownListEnumeration": {
|
||||
"dark": "mint",
|
||||
"light": "#00acc1"
|
||||
},
|
||||
"markdownImage": {
|
||||
"dark": "blue",
|
||||
"light": "#0066cc"
|
||||
},
|
||||
"markdownImageText": {
|
||||
"dark": "mint",
|
||||
"light": "#00acc1"
|
||||
},
|
||||
"markdownCodeBlock": {
|
||||
"dark": "foreground",
|
||||
"light": "#193549"
|
||||
},
|
||||
"syntaxComment": {
|
||||
"dark": "#0088ff",
|
||||
"light": "#5c6b7d"
|
||||
},
|
||||
"syntaxKeyword": {
|
||||
"dark": "orange",
|
||||
"light": "#ff5722"
|
||||
},
|
||||
"syntaxFunction": {
|
||||
"dark": "yellow",
|
||||
"light": "#ff9800"
|
||||
},
|
||||
"syntaxVariable": {
|
||||
"dark": "foreground",
|
||||
"light": "#193549"
|
||||
},
|
||||
"syntaxString": {
|
||||
"dark": "green",
|
||||
"light": "#4caf50"
|
||||
},
|
||||
"syntaxNumber": {
|
||||
"dark": "pink",
|
||||
"light": "#e91e63"
|
||||
},
|
||||
"syntaxType": {
|
||||
"dark": "mint",
|
||||
"light": "#00acc1"
|
||||
},
|
||||
"syntaxOperator": {
|
||||
"dark": "orange",
|
||||
"light": "#ff5722"
|
||||
},
|
||||
"syntaxPunctuation": {
|
||||
"dark": "foreground",
|
||||
"light": "#193549"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"background": "#282a36",
|
||||
"currentLine": "#44475a",
|
||||
"selection": "#44475a",
|
||||
"foreground": "#f8f8f2",
|
||||
"comment": "#6272a4",
|
||||
"cyan": "#8be9fd",
|
||||
"green": "#50fa7b",
|
||||
"orange": "#ffb86c",
|
||||
"pink": "#ff79c6",
|
||||
"purple": "#bd93f9",
|
||||
"red": "#ff5555",
|
||||
"yellow": "#f1fa8c"
|
||||
},
|
||||
"theme": {
|
||||
"primary": {
|
||||
"dark": "purple",
|
||||
"light": "purple"
|
||||
},
|
||||
"secondary": {
|
||||
"dark": "pink",
|
||||
"light": "pink"
|
||||
},
|
||||
"accent": {
|
||||
"dark": "cyan",
|
||||
"light": "cyan"
|
||||
},
|
||||
"error": {
|
||||
"dark": "red",
|
||||
"light": "red"
|
||||
},
|
||||
"warning": {
|
||||
"dark": "yellow",
|
||||
"light": "yellow"
|
||||
},
|
||||
"success": {
|
||||
"dark": "green",
|
||||
"light": "green"
|
||||
},
|
||||
"info": {
|
||||
"dark": "orange",
|
||||
"light": "orange"
|
||||
},
|
||||
"text": {
|
||||
"dark": "foreground",
|
||||
"light": "#282a36"
|
||||
},
|
||||
"textMuted": {
|
||||
"dark": "comment",
|
||||
"light": "#6272a4"
|
||||
},
|
||||
"background": {
|
||||
"dark": "#282a36",
|
||||
"light": "#f8f8f2"
|
||||
},
|
||||
"backgroundPanel": {
|
||||
"dark": "#21222c",
|
||||
"light": "#e8e8e2"
|
||||
},
|
||||
"backgroundElement": {
|
||||
"dark": "currentLine",
|
||||
"light": "#d8d8d2"
|
||||
},
|
||||
"border": {
|
||||
"dark": "currentLine",
|
||||
"light": "#c8c8c2"
|
||||
},
|
||||
"borderActive": {
|
||||
"dark": "purple",
|
||||
"light": "purple"
|
||||
},
|
||||
"borderSubtle": {
|
||||
"dark": "#191a21",
|
||||
"light": "#e0e0e0"
|
||||
},
|
||||
"diffAdded": {
|
||||
"dark": "green",
|
||||
"light": "green"
|
||||
},
|
||||
"diffRemoved": {
|
||||
"dark": "red",
|
||||
"light": "red"
|
||||
},
|
||||
"diffContext": {
|
||||
"dark": "comment",
|
||||
"light": "#6272a4"
|
||||
},
|
||||
"diffHunkHeader": {
|
||||
"dark": "comment",
|
||||
"light": "#6272a4"
|
||||
},
|
||||
"diffHighlightAdded": {
|
||||
"dark": "green",
|
||||
"light": "green"
|
||||
},
|
||||
"diffHighlightRemoved": {
|
||||
"dark": "red",
|
||||
"light": "red"
|
||||
},
|
||||
"diffAddedBg": {
|
||||
"dark": "#1a3a1a",
|
||||
"light": "#e0ffe0"
|
||||
},
|
||||
"diffRemovedBg": {
|
||||
"dark": "#3a1a1a",
|
||||
"light": "#ffe0e0"
|
||||
},
|
||||
"diffContextBg": {
|
||||
"dark": "#21222c",
|
||||
"light": "#e8e8e2"
|
||||
},
|
||||
"diffLineNumber": {
|
||||
"dark": "currentLine",
|
||||
"light": "#c8c8c2"
|
||||
},
|
||||
"diffAddedLineNumberBg": {
|
||||
"dark": "#1a3a1a",
|
||||
"light": "#e0ffe0"
|
||||
},
|
||||
"diffRemovedLineNumberBg": {
|
||||
"dark": "#3a1a1a",
|
||||
"light": "#ffe0e0"
|
||||
},
|
||||
"markdownText": {
|
||||
"dark": "foreground",
|
||||
"light": "#282a36"
|
||||
},
|
||||
"markdownHeading": {
|
||||
"dark": "purple",
|
||||
"light": "purple"
|
||||
},
|
||||
"markdownLink": {
|
||||
"dark": "cyan",
|
||||
"light": "cyan"
|
||||
},
|
||||
"markdownLinkText": {
|
||||
"dark": "pink",
|
||||
"light": "pink"
|
||||
},
|
||||
"markdownCode": {
|
||||
"dark": "green",
|
||||
"light": "green"
|
||||
},
|
||||
"markdownBlockQuote": {
|
||||
"dark": "comment",
|
||||
"light": "#6272a4"
|
||||
},
|
||||
"markdownEmph": {
|
||||
"dark": "yellow",
|
||||
"light": "yellow"
|
||||
},
|
||||
"markdownStrong": {
|
||||
"dark": "orange",
|
||||
"light": "orange"
|
||||
},
|
||||
"markdownHorizontalRule": {
|
||||
"dark": "comment",
|
||||
"light": "#6272a4"
|
||||
},
|
||||
"markdownListItem": {
|
||||
"dark": "purple",
|
||||
"light": "purple"
|
||||
},
|
||||
"markdownListEnumeration": {
|
||||
"dark": "cyan",
|
||||
"light": "cyan"
|
||||
},
|
||||
"markdownImage": {
|
||||
"dark": "cyan",
|
||||
"light": "cyan"
|
||||
},
|
||||
"markdownImageText": {
|
||||
"dark": "pink",
|
||||
"light": "pink"
|
||||
},
|
||||
"markdownCodeBlock": {
|
||||
"dark": "foreground",
|
||||
"light": "#282a36"
|
||||
},
|
||||
"syntaxComment": {
|
||||
"dark": "comment",
|
||||
"light": "#6272a4"
|
||||
},
|
||||
"syntaxKeyword": {
|
||||
"dark": "pink",
|
||||
"light": "pink"
|
||||
},
|
||||
"syntaxFunction": {
|
||||
"dark": "green",
|
||||
"light": "green"
|
||||
},
|
||||
"syntaxVariable": {
|
||||
"dark": "foreground",
|
||||
"light": "#282a36"
|
||||
},
|
||||
"syntaxString": {
|
||||
"dark": "yellow",
|
||||
"light": "yellow"
|
||||
},
|
||||
"syntaxNumber": {
|
||||
"dark": "purple",
|
||||
"light": "purple"
|
||||
},
|
||||
"syntaxType": {
|
||||
"dark": "cyan",
|
||||
"light": "cyan"
|
||||
},
|
||||
"syntaxOperator": {
|
||||
"dark": "pink",
|
||||
"light": "pink"
|
||||
},
|
||||
"syntaxPunctuation": {
|
||||
"dark": "foreground",
|
||||
"light": "#282a36"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -239,4 +239,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"darkBg": "#0d1117",
|
||||
"darkBgAlt": "#010409",
|
||||
"darkBgPanel": "#161b22",
|
||||
"darkFg": "#c9d1d9",
|
||||
"darkFgMuted": "#8b949e",
|
||||
"darkBlue": "#58a6ff",
|
||||
"darkGreen": "#3fb950",
|
||||
"darkRed": "#f85149",
|
||||
"darkOrange": "#d29922",
|
||||
"darkPurple": "#bc8cff",
|
||||
"darkPink": "#ff7b72",
|
||||
"darkYellow": "#e3b341",
|
||||
"darkCyan": "#39c5cf",
|
||||
"lightBg": "#ffffff",
|
||||
"lightBgAlt": "#f6f8fa",
|
||||
"lightBgPanel": "#f0f3f6",
|
||||
"lightFg": "#24292f",
|
||||
"lightFgMuted": "#57606a",
|
||||
"lightBlue": "#0969da",
|
||||
"lightGreen": "#1a7f37",
|
||||
"lightRed": "#cf222e",
|
||||
"lightOrange": "#bc4c00",
|
||||
"lightPurple": "#8250df",
|
||||
"lightPink": "#bf3989",
|
||||
"lightYellow": "#9a6700",
|
||||
"lightCyan": "#1b7c83"
|
||||
},
|
||||
"theme": {
|
||||
"primary": {
|
||||
"dark": "darkBlue",
|
||||
"light": "lightBlue"
|
||||
},
|
||||
"secondary": {
|
||||
"dark": "darkPurple",
|
||||
"light": "lightPurple"
|
||||
},
|
||||
"accent": {
|
||||
"dark": "darkCyan",
|
||||
"light": "lightCyan"
|
||||
},
|
||||
"error": {
|
||||
"dark": "darkRed",
|
||||
"light": "lightRed"
|
||||
},
|
||||
"warning": {
|
||||
"dark": "darkYellow",
|
||||
"light": "lightYellow"
|
||||
},
|
||||
"success": {
|
||||
"dark": "darkGreen",
|
||||
"light": "lightGreen"
|
||||
},
|
||||
"info": {
|
||||
"dark": "darkOrange",
|
||||
"light": "lightOrange"
|
||||
},
|
||||
"text": {
|
||||
"dark": "darkFg",
|
||||
"light": "lightFg"
|
||||
},
|
||||
"textMuted": {
|
||||
"dark": "darkFgMuted",
|
||||
"light": "lightFgMuted"
|
||||
},
|
||||
"background": {
|
||||
"dark": "darkBg",
|
||||
"light": "lightBg"
|
||||
},
|
||||
"backgroundPanel": {
|
||||
"dark": "darkBgAlt",
|
||||
"light": "lightBgAlt"
|
||||
},
|
||||
"backgroundElement": {
|
||||
"dark": "darkBgPanel",
|
||||
"light": "lightBgPanel"
|
||||
},
|
||||
"border": {
|
||||
"dark": "#30363d",
|
||||
"light": "#d0d7de"
|
||||
},
|
||||
"borderActive": {
|
||||
"dark": "darkBlue",
|
||||
"light": "lightBlue"
|
||||
},
|
||||
"borderSubtle": {
|
||||
"dark": "#21262d",
|
||||
"light": "#d8dee4"
|
||||
},
|
||||
"diffAdded": {
|
||||
"dark": "darkGreen",
|
||||
"light": "lightGreen"
|
||||
},
|
||||
"diffRemoved": {
|
||||
"dark": "darkRed",
|
||||
"light": "lightRed"
|
||||
},
|
||||
"diffContext": {
|
||||
"dark": "darkFgMuted",
|
||||
"light": "lightFgMuted"
|
||||
},
|
||||
"diffHunkHeader": {
|
||||
"dark": "darkBlue",
|
||||
"light": "lightBlue"
|
||||
},
|
||||
"diffHighlightAdded": {
|
||||
"dark": "#3fb950",
|
||||
"light": "#1a7f37"
|
||||
},
|
||||
"diffHighlightRemoved": {
|
||||
"dark": "#f85149",
|
||||
"light": "#cf222e"
|
||||
},
|
||||
"diffAddedBg": {
|
||||
"dark": "#033a16",
|
||||
"light": "#dafbe1"
|
||||
},
|
||||
"diffRemovedBg": {
|
||||
"dark": "#67060c",
|
||||
"light": "#ffebe9"
|
||||
},
|
||||
"diffContextBg": {
|
||||
"dark": "darkBgAlt",
|
||||
"light": "lightBgAlt"
|
||||
},
|
||||
"diffLineNumber": {
|
||||
"dark": "#484f58",
|
||||
"light": "#afb8c1"
|
||||
},
|
||||
"diffAddedLineNumberBg": {
|
||||
"dark": "#033a16",
|
||||
"light": "#dafbe1"
|
||||
},
|
||||
"diffRemovedLineNumberBg": {
|
||||
"dark": "#67060c",
|
||||
"light": "#ffebe9"
|
||||
},
|
||||
"markdownText": {
|
||||
"dark": "darkFg",
|
||||
"light": "lightFg"
|
||||
},
|
||||
"markdownHeading": {
|
||||
"dark": "darkBlue",
|
||||
"light": "lightBlue"
|
||||
},
|
||||
"markdownLink": {
|
||||
"dark": "darkBlue",
|
||||
"light": "lightBlue"
|
||||
},
|
||||
"markdownLinkText": {
|
||||
"dark": "darkCyan",
|
||||
"light": "lightCyan"
|
||||
},
|
||||
"markdownCode": {
|
||||
"dark": "darkPink",
|
||||
"light": "lightPink"
|
||||
},
|
||||
"markdownBlockQuote": {
|
||||
"dark": "darkFgMuted",
|
||||
"light": "lightFgMuted"
|
||||
},
|
||||
"markdownEmph": {
|
||||
"dark": "darkYellow",
|
||||
"light": "lightYellow"
|
||||
},
|
||||
"markdownStrong": {
|
||||
"dark": "darkOrange",
|
||||
"light": "lightOrange"
|
||||
},
|
||||
"markdownHorizontalRule": {
|
||||
"dark": "#30363d",
|
||||
"light": "#d0d7de"
|
||||
},
|
||||
"markdownListItem": {
|
||||
"dark": "darkBlue",
|
||||
"light": "lightBlue"
|
||||
},
|
||||
"markdownListEnumeration": {
|
||||
"dark": "darkCyan",
|
||||
"light": "lightCyan"
|
||||
},
|
||||
"markdownImage": {
|
||||
"dark": "darkBlue",
|
||||
"light": "lightBlue"
|
||||
},
|
||||
"markdownImageText": {
|
||||
"dark": "darkCyan",
|
||||
"light": "lightCyan"
|
||||
},
|
||||
"markdownCodeBlock": {
|
||||
"dark": "darkFg",
|
||||
"light": "lightFg"
|
||||
},
|
||||
"syntaxComment": {
|
||||
"dark": "darkFgMuted",
|
||||
"light": "lightFgMuted"
|
||||
},
|
||||
"syntaxKeyword": {
|
||||
"dark": "darkPink",
|
||||
"light": "lightRed"
|
||||
},
|
||||
"syntaxFunction": {
|
||||
"dark": "darkPurple",
|
||||
"light": "lightPurple"
|
||||
},
|
||||
"syntaxVariable": {
|
||||
"dark": "darkOrange",
|
||||
"light": "lightOrange"
|
||||
},
|
||||
"syntaxString": {
|
||||
"dark": "darkCyan",
|
||||
"light": "lightBlue"
|
||||
},
|
||||
"syntaxNumber": {
|
||||
"dark": "darkBlue",
|
||||
"light": "lightCyan"
|
||||
},
|
||||
"syntaxType": {
|
||||
"dark": "darkOrange",
|
||||
"light": "lightOrange"
|
||||
},
|
||||
"syntaxOperator": {
|
||||
"dark": "darkPink",
|
||||
"light": "lightRed"
|
||||
},
|
||||
"syntaxPunctuation": {
|
||||
"dark": "darkFg",
|
||||
"light": "lightFg"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"darkBg": "#263238",
|
||||
"darkBgAlt": "#1e272c",
|
||||
"darkBgPanel": "#37474f",
|
||||
"darkFg": "#eeffff",
|
||||
"darkFgMuted": "#546e7a",
|
||||
"darkRed": "#f07178",
|
||||
"darkPink": "#f78c6c",
|
||||
"darkOrange": "#ffcb6b",
|
||||
"darkYellow": "#ffcb6b",
|
||||
"darkGreen": "#c3e88d",
|
||||
"darkCyan": "#89ddff",
|
||||
"darkBlue": "#82aaff",
|
||||
"darkPurple": "#c792ea",
|
||||
"darkViolet": "#bb80b3",
|
||||
"lightBg": "#fafafa",
|
||||
"lightBgAlt": "#f5f5f5",
|
||||
"lightBgPanel": "#e7e7e8",
|
||||
"lightFg": "#263238",
|
||||
"lightFgMuted": "#90a4ae",
|
||||
"lightRed": "#e53935",
|
||||
"lightPink": "#ec407a",
|
||||
"lightOrange": "#f4511e",
|
||||
"lightYellow": "#ffb300",
|
||||
"lightGreen": "#91b859",
|
||||
"lightCyan": "#39adb5",
|
||||
"lightBlue": "#6182b8",
|
||||
"lightPurple": "#7c4dff",
|
||||
"lightViolet": "#945eb8"
|
||||
},
|
||||
"theme": {
|
||||
"primary": {
|
||||
"dark": "darkBlue",
|
||||
"light": "lightBlue"
|
||||
},
|
||||
"secondary": {
|
||||
"dark": "darkPurple",
|
||||
"light": "lightPurple"
|
||||
},
|
||||
"accent": {
|
||||
"dark": "darkCyan",
|
||||
"light": "lightCyan"
|
||||
},
|
||||
"error": {
|
||||
"dark": "darkRed",
|
||||
"light": "lightRed"
|
||||
},
|
||||
"warning": {
|
||||
"dark": "darkYellow",
|
||||
"light": "lightYellow"
|
||||
},
|
||||
"success": {
|
||||
"dark": "darkGreen",
|
||||
"light": "lightGreen"
|
||||
},
|
||||
"info": {
|
||||
"dark": "darkOrange",
|
||||
"light": "lightOrange"
|
||||
},
|
||||
"text": {
|
||||
"dark": "darkFg",
|
||||
"light": "lightFg"
|
||||
},
|
||||
"textMuted": {
|
||||
"dark": "darkFgMuted",
|
||||
"light": "lightFgMuted"
|
||||
},
|
||||
"background": {
|
||||
"dark": "darkBg",
|
||||
"light": "lightBg"
|
||||
},
|
||||
"backgroundPanel": {
|
||||
"dark": "darkBgAlt",
|
||||
"light": "lightBgAlt"
|
||||
},
|
||||
"backgroundElement": {
|
||||
"dark": "darkBgPanel",
|
||||
"light": "lightBgPanel"
|
||||
},
|
||||
"border": {
|
||||
"dark": "#37474f",
|
||||
"light": "#e0e0e0"
|
||||
},
|
||||
"borderActive": {
|
||||
"dark": "darkBlue",
|
||||
"light": "lightBlue"
|
||||
},
|
||||
"borderSubtle": {
|
||||
"dark": "#1e272c",
|
||||
"light": "#eeeeee"
|
||||
},
|
||||
"diffAdded": {
|
||||
"dark": "darkGreen",
|
||||
"light": "lightGreen"
|
||||
},
|
||||
"diffRemoved": {
|
||||
"dark": "darkRed",
|
||||
"light": "lightRed"
|
||||
},
|
||||
"diffContext": {
|
||||
"dark": "darkFgMuted",
|
||||
"light": "lightFgMuted"
|
||||
},
|
||||
"diffHunkHeader": {
|
||||
"dark": "darkCyan",
|
||||
"light": "lightCyan"
|
||||
},
|
||||
"diffHighlightAdded": {
|
||||
"dark": "darkGreen",
|
||||
"light": "lightGreen"
|
||||
},
|
||||
"diffHighlightRemoved": {
|
||||
"dark": "darkRed",
|
||||
"light": "lightRed"
|
||||
},
|
||||
"diffAddedBg": {
|
||||
"dark": "#2e3c2b",
|
||||
"light": "#e8f5e9"
|
||||
},
|
||||
"diffRemovedBg": {
|
||||
"dark": "#3c2b2b",
|
||||
"light": "#ffebee"
|
||||
},
|
||||
"diffContextBg": {
|
||||
"dark": "darkBgAlt",
|
||||
"light": "lightBgAlt"
|
||||
},
|
||||
"diffLineNumber": {
|
||||
"dark": "#37474f",
|
||||
"light": "#cfd8dc"
|
||||
},
|
||||
"diffAddedLineNumberBg": {
|
||||
"dark": "#2e3c2b",
|
||||
"light": "#e8f5e9"
|
||||
},
|
||||
"diffRemovedLineNumberBg": {
|
||||
"dark": "#3c2b2b",
|
||||
"light": "#ffebee"
|
||||
},
|
||||
"markdownText": {
|
||||
"dark": "darkFg",
|
||||
"light": "lightFg"
|
||||
},
|
||||
"markdownHeading": {
|
||||
"dark": "darkBlue",
|
||||
"light": "lightBlue"
|
||||
},
|
||||
"markdownLink": {
|
||||
"dark": "darkCyan",
|
||||
"light": "lightCyan"
|
||||
},
|
||||
"markdownLinkText": {
|
||||
"dark": "darkPurple",
|
||||
"light": "lightPurple"
|
||||
},
|
||||
"markdownCode": {
|
||||
"dark": "darkGreen",
|
||||
"light": "lightGreen"
|
||||
},
|
||||
"markdownBlockQuote": {
|
||||
"dark": "darkFgMuted",
|
||||
"light": "lightFgMuted"
|
||||
},
|
||||
"markdownEmph": {
|
||||
"dark": "darkYellow",
|
||||
"light": "lightYellow"
|
||||
},
|
||||
"markdownStrong": {
|
||||
"dark": "darkOrange",
|
||||
"light": "lightOrange"
|
||||
},
|
||||
"markdownHorizontalRule": {
|
||||
"dark": "#37474f",
|
||||
"light": "#e0e0e0"
|
||||
},
|
||||
"markdownListItem": {
|
||||
"dark": "darkBlue",
|
||||
"light": "lightBlue"
|
||||
},
|
||||
"markdownListEnumeration": {
|
||||
"dark": "darkCyan",
|
||||
"light": "lightCyan"
|
||||
},
|
||||
"markdownImage": {
|
||||
"dark": "darkCyan",
|
||||
"light": "lightCyan"
|
||||
},
|
||||
"markdownImageText": {
|
||||
"dark": "darkPurple",
|
||||
"light": "lightPurple"
|
||||
},
|
||||
"markdownCodeBlock": {
|
||||
"dark": "darkFg",
|
||||
"light": "lightFg"
|
||||
},
|
||||
"syntaxComment": {
|
||||
"dark": "darkFgMuted",
|
||||
"light": "lightFgMuted"
|
||||
},
|
||||
"syntaxKeyword": {
|
||||
"dark": "darkPurple",
|
||||
"light": "lightPurple"
|
||||
},
|
||||
"syntaxFunction": {
|
||||
"dark": "darkBlue",
|
||||
"light": "lightBlue"
|
||||
},
|
||||
"syntaxVariable": {
|
||||
"dark": "darkFg",
|
||||
"light": "lightFg"
|
||||
},
|
||||
"syntaxString": {
|
||||
"dark": "darkGreen",
|
||||
"light": "lightGreen"
|
||||
},
|
||||
"syntaxNumber": {
|
||||
"dark": "darkOrange",
|
||||
"light": "lightOrange"
|
||||
},
|
||||
"syntaxType": {
|
||||
"dark": "darkYellow",
|
||||
"light": "lightYellow"
|
||||
},
|
||||
"syntaxOperator": {
|
||||
"dark": "darkCyan",
|
||||
"light": "lightCyan"
|
||||
},
|
||||
"syntaxPunctuation": {
|
||||
"dark": "darkFg",
|
||||
"light": "lightFg"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"background": "#272822",
|
||||
"backgroundAlt": "#1e1f1c",
|
||||
"backgroundPanel": "#3e3d32",
|
||||
"foreground": "#f8f8f2",
|
||||
"comment": "#75715e",
|
||||
"red": "#f92672",
|
||||
"orange": "#fd971f",
|
||||
"lightOrange": "#e69f66",
|
||||
"yellow": "#e6db74",
|
||||
"green": "#a6e22e",
|
||||
"cyan": "#66d9ef",
|
||||
"blue": "#66d9ef",
|
||||
"purple": "#ae81ff",
|
||||
"pink": "#f92672"
|
||||
},
|
||||
"theme": {
|
||||
"primary": {
|
||||
"dark": "cyan",
|
||||
"light": "blue"
|
||||
},
|
||||
"secondary": {
|
||||
"dark": "purple",
|
||||
"light": "purple"
|
||||
},
|
||||
"accent": {
|
||||
"dark": "green",
|
||||
"light": "green"
|
||||
},
|
||||
"error": {
|
||||
"dark": "red",
|
||||
"light": "red"
|
||||
},
|
||||
"warning": {
|
||||
"dark": "yellow",
|
||||
"light": "orange"
|
||||
},
|
||||
"success": {
|
||||
"dark": "green",
|
||||
"light": "green"
|
||||
},
|
||||
"info": {
|
||||
"dark": "orange",
|
||||
"light": "orange"
|
||||
},
|
||||
"text": {
|
||||
"dark": "foreground",
|
||||
"light": "#272822"
|
||||
},
|
||||
"textMuted": {
|
||||
"dark": "comment",
|
||||
"light": "#75715e"
|
||||
},
|
||||
"background": {
|
||||
"dark": "#272822",
|
||||
"light": "#fafafa"
|
||||
},
|
||||
"backgroundPanel": {
|
||||
"dark": "#1e1f1c",
|
||||
"light": "#f0f0f0"
|
||||
},
|
||||
"backgroundElement": {
|
||||
"dark": "#3e3d32",
|
||||
"light": "#e0e0e0"
|
||||
},
|
||||
"border": {
|
||||
"dark": "#3e3d32",
|
||||
"light": "#d0d0d0"
|
||||
},
|
||||
"borderActive": {
|
||||
"dark": "cyan",
|
||||
"light": "blue"
|
||||
},
|
||||
"borderSubtle": {
|
||||
"dark": "#1e1f1c",
|
||||
"light": "#e8e8e8"
|
||||
},
|
||||
"diffAdded": {
|
||||
"dark": "green",
|
||||
"light": "green"
|
||||
},
|
||||
"diffRemoved": {
|
||||
"dark": "red",
|
||||
"light": "red"
|
||||
},
|
||||
"diffContext": {
|
||||
"dark": "comment",
|
||||
"light": "#75715e"
|
||||
},
|
||||
"diffHunkHeader": {
|
||||
"dark": "comment",
|
||||
"light": "#75715e"
|
||||
},
|
||||
"diffHighlightAdded": {
|
||||
"dark": "green",
|
||||
"light": "green"
|
||||
},
|
||||
"diffHighlightRemoved": {
|
||||
"dark": "red",
|
||||
"light": "red"
|
||||
},
|
||||
"diffAddedBg": {
|
||||
"dark": "#1a3a1a",
|
||||
"light": "#e0ffe0"
|
||||
},
|
||||
"diffRemovedBg": {
|
||||
"dark": "#3a1a1a",
|
||||
"light": "#ffe0e0"
|
||||
},
|
||||
"diffContextBg": {
|
||||
"dark": "#1e1f1c",
|
||||
"light": "#f0f0f0"
|
||||
},
|
||||
"diffLineNumber": {
|
||||
"dark": "#3e3d32",
|
||||
"light": "#d0d0d0"
|
||||
},
|
||||
"diffAddedLineNumberBg": {
|
||||
"dark": "#1a3a1a",
|
||||
"light": "#e0ffe0"
|
||||
},
|
||||
"diffRemovedLineNumberBg": {
|
||||
"dark": "#3a1a1a",
|
||||
"light": "#ffe0e0"
|
||||
},
|
||||
"markdownText": {
|
||||
"dark": "foreground",
|
||||
"light": "#272822"
|
||||
},
|
||||
"markdownHeading": {
|
||||
"dark": "pink",
|
||||
"light": "pink"
|
||||
},
|
||||
"markdownLink": {
|
||||
"dark": "cyan",
|
||||
"light": "blue"
|
||||
},
|
||||
"markdownLinkText": {
|
||||
"dark": "purple",
|
||||
"light": "purple"
|
||||
},
|
||||
"markdownCode": {
|
||||
"dark": "green",
|
||||
"light": "green"
|
||||
},
|
||||
"markdownBlockQuote": {
|
||||
"dark": "comment",
|
||||
"light": "#75715e"
|
||||
},
|
||||
"markdownEmph": {
|
||||
"dark": "yellow",
|
||||
"light": "orange"
|
||||
},
|
||||
"markdownStrong": {
|
||||
"dark": "orange",
|
||||
"light": "orange"
|
||||
},
|
||||
"markdownHorizontalRule": {
|
||||
"dark": "comment",
|
||||
"light": "#75715e"
|
||||
},
|
||||
"markdownListItem": {
|
||||
"dark": "cyan",
|
||||
"light": "blue"
|
||||
},
|
||||
"markdownListEnumeration": {
|
||||
"dark": "purple",
|
||||
"light": "purple"
|
||||
},
|
||||
"markdownImage": {
|
||||
"dark": "cyan",
|
||||
"light": "blue"
|
||||
},
|
||||
"markdownImageText": {
|
||||
"dark": "purple",
|
||||
"light": "purple"
|
||||
},
|
||||
"markdownCodeBlock": {
|
||||
"dark": "foreground",
|
||||
"light": "#272822"
|
||||
},
|
||||
"syntaxComment": {
|
||||
"dark": "comment",
|
||||
"light": "#75715e"
|
||||
},
|
||||
"syntaxKeyword": {
|
||||
"dark": "pink",
|
||||
"light": "pink"
|
||||
},
|
||||
"syntaxFunction": {
|
||||
"dark": "green",
|
||||
"light": "green"
|
||||
},
|
||||
"syntaxVariable": {
|
||||
"dark": "foreground",
|
||||
"light": "#272822"
|
||||
},
|
||||
"syntaxString": {
|
||||
"dark": "yellow",
|
||||
"light": "orange"
|
||||
},
|
||||
"syntaxNumber": {
|
||||
"dark": "purple",
|
||||
"light": "purple"
|
||||
},
|
||||
"syntaxType": {
|
||||
"dark": "cyan",
|
||||
"light": "blue"
|
||||
},
|
||||
"syntaxOperator": {
|
||||
"dark": "pink",
|
||||
"light": "pink"
|
||||
},
|
||||
"syntaxPunctuation": {
|
||||
"dark": "foreground",
|
||||
"light": "#272822"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -243,4 +243,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"background": "#292d3e",
|
||||
"backgroundAlt": "#1e2132",
|
||||
"backgroundPanel": "#32364a",
|
||||
"foreground": "#a6accd",
|
||||
"foregroundBright": "#bfc7d5",
|
||||
"comment": "#676e95",
|
||||
"red": "#f07178",
|
||||
"orange": "#f78c6c",
|
||||
"yellow": "#ffcb6b",
|
||||
"green": "#c3e88d",
|
||||
"cyan": "#89ddff",
|
||||
"blue": "#82aaff",
|
||||
"purple": "#c792ea",
|
||||
"magenta": "#ff5370",
|
||||
"pink": "#f07178"
|
||||
},
|
||||
"theme": {
|
||||
"primary": {
|
||||
"dark": "blue",
|
||||
"light": "#4976eb"
|
||||
},
|
||||
"secondary": {
|
||||
"dark": "purple",
|
||||
"light": "#a854f2"
|
||||
},
|
||||
"accent": {
|
||||
"dark": "cyan",
|
||||
"light": "#00acc1"
|
||||
},
|
||||
"error": {
|
||||
"dark": "red",
|
||||
"light": "#e53935"
|
||||
},
|
||||
"warning": {
|
||||
"dark": "yellow",
|
||||
"light": "#ffb300"
|
||||
},
|
||||
"success": {
|
||||
"dark": "green",
|
||||
"light": "#91b859"
|
||||
},
|
||||
"info": {
|
||||
"dark": "orange",
|
||||
"light": "#f4511e"
|
||||
},
|
||||
"text": {
|
||||
"dark": "foreground",
|
||||
"light": "#292d3e"
|
||||
},
|
||||
"textMuted": {
|
||||
"dark": "comment",
|
||||
"light": "#8796b0"
|
||||
},
|
||||
"background": {
|
||||
"dark": "#292d3e",
|
||||
"light": "#fafafa"
|
||||
},
|
||||
"backgroundPanel": {
|
||||
"dark": "#1e2132",
|
||||
"light": "#f5f5f5"
|
||||
},
|
||||
"backgroundElement": {
|
||||
"dark": "#32364a",
|
||||
"light": "#e7e7e8"
|
||||
},
|
||||
"border": {
|
||||
"dark": "#32364a",
|
||||
"light": "#e0e0e0"
|
||||
},
|
||||
"borderActive": {
|
||||
"dark": "blue",
|
||||
"light": "#4976eb"
|
||||
},
|
||||
"borderSubtle": {
|
||||
"dark": "#1e2132",
|
||||
"light": "#eeeeee"
|
||||
},
|
||||
"diffAdded": {
|
||||
"dark": "green",
|
||||
"light": "#91b859"
|
||||
},
|
||||
"diffRemoved": {
|
||||
"dark": "red",
|
||||
"light": "#e53935"
|
||||
},
|
||||
"diffContext": {
|
||||
"dark": "comment",
|
||||
"light": "#8796b0"
|
||||
},
|
||||
"diffHunkHeader": {
|
||||
"dark": "cyan",
|
||||
"light": "#00acc1"
|
||||
},
|
||||
"diffHighlightAdded": {
|
||||
"dark": "green",
|
||||
"light": "#91b859"
|
||||
},
|
||||
"diffHighlightRemoved": {
|
||||
"dark": "red",
|
||||
"light": "#e53935"
|
||||
},
|
||||
"diffAddedBg": {
|
||||
"dark": "#2e3c2b",
|
||||
"light": "#e8f5e9"
|
||||
},
|
||||
"diffRemovedBg": {
|
||||
"dark": "#3c2b2b",
|
||||
"light": "#ffebee"
|
||||
},
|
||||
"diffContextBg": {
|
||||
"dark": "#1e2132",
|
||||
"light": "#f5f5f5"
|
||||
},
|
||||
"diffLineNumber": {
|
||||
"dark": "#444760",
|
||||
"light": "#cfd8dc"
|
||||
},
|
||||
"diffAddedLineNumberBg": {
|
||||
"dark": "#2e3c2b",
|
||||
"light": "#e8f5e9"
|
||||
},
|
||||
"diffRemovedLineNumberBg": {
|
||||
"dark": "#3c2b2b",
|
||||
"light": "#ffebee"
|
||||
},
|
||||
"markdownText": {
|
||||
"dark": "foreground",
|
||||
"light": "#292d3e"
|
||||
},
|
||||
"markdownHeading": {
|
||||
"dark": "purple",
|
||||
"light": "#a854f2"
|
||||
},
|
||||
"markdownLink": {
|
||||
"dark": "blue",
|
||||
"light": "#4976eb"
|
||||
},
|
||||
"markdownLinkText": {
|
||||
"dark": "cyan",
|
||||
"light": "#00acc1"
|
||||
},
|
||||
"markdownCode": {
|
||||
"dark": "green",
|
||||
"light": "#91b859"
|
||||
},
|
||||
"markdownBlockQuote": {
|
||||
"dark": "comment",
|
||||
"light": "#8796b0"
|
||||
},
|
||||
"markdownEmph": {
|
||||
"dark": "yellow",
|
||||
"light": "#ffb300"
|
||||
},
|
||||
"markdownStrong": {
|
||||
"dark": "orange",
|
||||
"light": "#f4511e"
|
||||
},
|
||||
"markdownHorizontalRule": {
|
||||
"dark": "comment",
|
||||
"light": "#8796b0"
|
||||
},
|
||||
"markdownListItem": {
|
||||
"dark": "blue",
|
||||
"light": "#4976eb"
|
||||
},
|
||||
"markdownListEnumeration": {
|
||||
"dark": "cyan",
|
||||
"light": "#00acc1"
|
||||
},
|
||||
"markdownImage": {
|
||||
"dark": "blue",
|
||||
"light": "#4976eb"
|
||||
},
|
||||
"markdownImageText": {
|
||||
"dark": "cyan",
|
||||
"light": "#00acc1"
|
||||
},
|
||||
"markdownCodeBlock": {
|
||||
"dark": "foreground",
|
||||
"light": "#292d3e"
|
||||
},
|
||||
"syntaxComment": {
|
||||
"dark": "comment",
|
||||
"light": "#8796b0"
|
||||
},
|
||||
"syntaxKeyword": {
|
||||
"dark": "purple",
|
||||
"light": "#a854f2"
|
||||
},
|
||||
"syntaxFunction": {
|
||||
"dark": "blue",
|
||||
"light": "#4976eb"
|
||||
},
|
||||
"syntaxVariable": {
|
||||
"dark": "foreground",
|
||||
"light": "#292d3e"
|
||||
},
|
||||
"syntaxString": {
|
||||
"dark": "green",
|
||||
"light": "#91b859"
|
||||
},
|
||||
"syntaxNumber": {
|
||||
"dark": "orange",
|
||||
"light": "#f4511e"
|
||||
},
|
||||
"syntaxType": {
|
||||
"dark": "yellow",
|
||||
"light": "#ffb300"
|
||||
},
|
||||
"syntaxOperator": {
|
||||
"dark": "cyan",
|
||||
"light": "#00acc1"
|
||||
},
|
||||
"syntaxPunctuation": {
|
||||
"dark": "foreground",
|
||||
"light": "#292d3e"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"base": "#191724",
|
||||
"surface": "#1f1d2e",
|
||||
"overlay": "#26233a",
|
||||
"muted": "#6e6a86",
|
||||
"subtle": "#908caa",
|
||||
"text": "#e0def4",
|
||||
"love": "#eb6f92",
|
||||
"gold": "#f6c177",
|
||||
"rose": "#ebbcba",
|
||||
"pine": "#31748f",
|
||||
"foam": "#9ccfd8",
|
||||
"iris": "#c4a7e7",
|
||||
"highlightLow": "#21202e",
|
||||
"highlightMed": "#403d52",
|
||||
"highlightHigh": "#524f67",
|
||||
"moonBase": "#232136",
|
||||
"moonSurface": "#2a273f",
|
||||
"moonOverlay": "#393552",
|
||||
"moonMuted": "#6e6a86",
|
||||
"moonSubtle": "#908caa",
|
||||
"moonText": "#e0def4",
|
||||
"dawnBase": "#faf4ed",
|
||||
"dawnSurface": "#fffaf3",
|
||||
"dawnOverlay": "#f2e9e1",
|
||||
"dawnMuted": "#9893a5",
|
||||
"dawnSubtle": "#797593",
|
||||
"dawnText": "#575279"
|
||||
},
|
||||
"theme": {
|
||||
"primary": {
|
||||
"dark": "foam",
|
||||
"light": "pine"
|
||||
},
|
||||
"secondary": {
|
||||
"dark": "iris",
|
||||
"light": "#907aa9"
|
||||
},
|
||||
"accent": {
|
||||
"dark": "rose",
|
||||
"light": "#d7827e"
|
||||
},
|
||||
"error": {
|
||||
"dark": "love",
|
||||
"light": "#b4637a"
|
||||
},
|
||||
"warning": {
|
||||
"dark": "gold",
|
||||
"light": "#ea9d34"
|
||||
},
|
||||
"success": {
|
||||
"dark": "pine",
|
||||
"light": "#286983"
|
||||
},
|
||||
"info": {
|
||||
"dark": "foam",
|
||||
"light": "#56949f"
|
||||
},
|
||||
"text": {
|
||||
"dark": "#e0def4",
|
||||
"light": "#575279"
|
||||
},
|
||||
"textMuted": {
|
||||
"dark": "muted",
|
||||
"light": "dawnMuted"
|
||||
},
|
||||
"background": {
|
||||
"dark": "base",
|
||||
"light": "dawnBase"
|
||||
},
|
||||
"backgroundPanel": {
|
||||
"dark": "surface",
|
||||
"light": "dawnSurface"
|
||||
},
|
||||
"backgroundElement": {
|
||||
"dark": "overlay",
|
||||
"light": "dawnOverlay"
|
||||
},
|
||||
"border": {
|
||||
"dark": "highlightMed",
|
||||
"light": "#dfdad9"
|
||||
},
|
||||
"borderActive": {
|
||||
"dark": "foam",
|
||||
"light": "pine"
|
||||
},
|
||||
"borderSubtle": {
|
||||
"dark": "highlightLow",
|
||||
"light": "#f4ede8"
|
||||
},
|
||||
"diffAdded": {
|
||||
"dark": "pine",
|
||||
"light": "#286983"
|
||||
},
|
||||
"diffRemoved": {
|
||||
"dark": "love",
|
||||
"light": "#b4637a"
|
||||
},
|
||||
"diffContext": {
|
||||
"dark": "muted",
|
||||
"light": "dawnMuted"
|
||||
},
|
||||
"diffHunkHeader": {
|
||||
"dark": "iris",
|
||||
"light": "#907aa9"
|
||||
},
|
||||
"diffHighlightAdded": {
|
||||
"dark": "pine",
|
||||
"light": "#286983"
|
||||
},
|
||||
"diffHighlightRemoved": {
|
||||
"dark": "love",
|
||||
"light": "#b4637a"
|
||||
},
|
||||
"diffAddedBg": {
|
||||
"dark": "#1f2d3a",
|
||||
"light": "#e5f2f3"
|
||||
},
|
||||
"diffRemovedBg": {
|
||||
"dark": "#3a1f2d",
|
||||
"light": "#fce5e8"
|
||||
},
|
||||
"diffContextBg": {
|
||||
"dark": "surface",
|
||||
"light": "dawnSurface"
|
||||
},
|
||||
"diffLineNumber": {
|
||||
"dark": "muted",
|
||||
"light": "dawnMuted"
|
||||
},
|
||||
"diffAddedLineNumberBg": {
|
||||
"dark": "#1f2d3a",
|
||||
"light": "#e5f2f3"
|
||||
},
|
||||
"diffRemovedLineNumberBg": {
|
||||
"dark": "#3a1f2d",
|
||||
"light": "#fce5e8"
|
||||
},
|
||||
"markdownText": {
|
||||
"dark": "#e0def4",
|
||||
"light": "#575279"
|
||||
},
|
||||
"markdownHeading": {
|
||||
"dark": "iris",
|
||||
"light": "#907aa9"
|
||||
},
|
||||
"markdownLink": {
|
||||
"dark": "foam",
|
||||
"light": "pine"
|
||||
},
|
||||
"markdownLinkText": {
|
||||
"dark": "rose",
|
||||
"light": "#d7827e"
|
||||
},
|
||||
"markdownCode": {
|
||||
"dark": "pine",
|
||||
"light": "#286983"
|
||||
},
|
||||
"markdownBlockQuote": {
|
||||
"dark": "muted",
|
||||
"light": "dawnMuted"
|
||||
},
|
||||
"markdownEmph": {
|
||||
"dark": "gold",
|
||||
"light": "#ea9d34"
|
||||
},
|
||||
"markdownStrong": {
|
||||
"dark": "love",
|
||||
"light": "#b4637a"
|
||||
},
|
||||
"markdownHorizontalRule": {
|
||||
"dark": "highlightMed",
|
||||
"light": "#dfdad9"
|
||||
},
|
||||
"markdownListItem": {
|
||||
"dark": "foam",
|
||||
"light": "pine"
|
||||
},
|
||||
"markdownListEnumeration": {
|
||||
"dark": "rose",
|
||||
"light": "#d7827e"
|
||||
},
|
||||
"markdownImage": {
|
||||
"dark": "foam",
|
||||
"light": "pine"
|
||||
},
|
||||
"markdownImageText": {
|
||||
"dark": "rose",
|
||||
"light": "#d7827e"
|
||||
},
|
||||
"markdownCodeBlock": {
|
||||
"dark": "#e0def4",
|
||||
"light": "#575279"
|
||||
},
|
||||
"syntaxComment": {
|
||||
"dark": "muted",
|
||||
"light": "dawnMuted"
|
||||
},
|
||||
"syntaxKeyword": {
|
||||
"dark": "pine",
|
||||
"light": "#286983"
|
||||
},
|
||||
"syntaxFunction": {
|
||||
"dark": "rose",
|
||||
"light": "#d7827e"
|
||||
},
|
||||
"syntaxVariable": {
|
||||
"dark": "#e0def4",
|
||||
"light": "#575279"
|
||||
},
|
||||
"syntaxString": {
|
||||
"dark": "gold",
|
||||
"light": "#ea9d34"
|
||||
},
|
||||
"syntaxNumber": {
|
||||
"dark": "iris",
|
||||
"light": "#907aa9"
|
||||
},
|
||||
"syntaxType": {
|
||||
"dark": "foam",
|
||||
"light": "#56949f"
|
||||
},
|
||||
"syntaxOperator": {
|
||||
"dark": "subtle",
|
||||
"light": "dawnSubtle"
|
||||
},
|
||||
"syntaxPunctuation": {
|
||||
"dark": "subtle",
|
||||
"light": "dawnSubtle"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"base03": "#002b36",
|
||||
"base02": "#073642",
|
||||
"base01": "#586e75",
|
||||
"base00": "#657b83",
|
||||
"base0": "#839496",
|
||||
"base1": "#93a1a1",
|
||||
"base2": "#eee8d5",
|
||||
"base3": "#fdf6e3",
|
||||
"yellow": "#b58900",
|
||||
"orange": "#cb4b16",
|
||||
"red": "#dc322f",
|
||||
"magenta": "#d33682",
|
||||
"violet": "#6c71c4",
|
||||
"blue": "#268bd2",
|
||||
"cyan": "#2aa198",
|
||||
"green": "#859900"
|
||||
},
|
||||
"theme": {
|
||||
"primary": {
|
||||
"dark": "blue",
|
||||
"light": "blue"
|
||||
},
|
||||
"secondary": {
|
||||
"dark": "violet",
|
||||
"light": "violet"
|
||||
},
|
||||
"accent": {
|
||||
"dark": "cyan",
|
||||
"light": "cyan"
|
||||
},
|
||||
"error": {
|
||||
"dark": "red",
|
||||
"light": "red"
|
||||
},
|
||||
"warning": {
|
||||
"dark": "yellow",
|
||||
"light": "yellow"
|
||||
},
|
||||
"success": {
|
||||
"dark": "green",
|
||||
"light": "green"
|
||||
},
|
||||
"info": {
|
||||
"dark": "orange",
|
||||
"light": "orange"
|
||||
},
|
||||
"text": {
|
||||
"dark": "base0",
|
||||
"light": "base00"
|
||||
},
|
||||
"textMuted": {
|
||||
"dark": "base01",
|
||||
"light": "base1"
|
||||
},
|
||||
"background": {
|
||||
"dark": "base03",
|
||||
"light": "base3"
|
||||
},
|
||||
"backgroundPanel": {
|
||||
"dark": "base02",
|
||||
"light": "base2"
|
||||
},
|
||||
"backgroundElement": {
|
||||
"dark": "#073642",
|
||||
"light": "#eee8d5"
|
||||
},
|
||||
"border": {
|
||||
"dark": "base02",
|
||||
"light": "base2"
|
||||
},
|
||||
"borderActive": {
|
||||
"dark": "base01",
|
||||
"light": "base1"
|
||||
},
|
||||
"borderSubtle": {
|
||||
"dark": "#073642",
|
||||
"light": "#eee8d5"
|
||||
},
|
||||
"diffAdded": {
|
||||
"dark": "green",
|
||||
"light": "green"
|
||||
},
|
||||
"diffRemoved": {
|
||||
"dark": "red",
|
||||
"light": "red"
|
||||
},
|
||||
"diffContext": {
|
||||
"dark": "base01",
|
||||
"light": "base1"
|
||||
},
|
||||
"diffHunkHeader": {
|
||||
"dark": "base01",
|
||||
"light": "base1"
|
||||
},
|
||||
"diffHighlightAdded": {
|
||||
"dark": "green",
|
||||
"light": "green"
|
||||
},
|
||||
"diffHighlightRemoved": {
|
||||
"dark": "red",
|
||||
"light": "red"
|
||||
},
|
||||
"diffAddedBg": {
|
||||
"dark": "#073642",
|
||||
"light": "#eee8d5"
|
||||
},
|
||||
"diffRemovedBg": {
|
||||
"dark": "#073642",
|
||||
"light": "#eee8d5"
|
||||
},
|
||||
"diffContextBg": {
|
||||
"dark": "base02",
|
||||
"light": "base2"
|
||||
},
|
||||
"diffLineNumber": {
|
||||
"dark": "base01",
|
||||
"light": "base1"
|
||||
},
|
||||
"diffAddedLineNumberBg": {
|
||||
"dark": "#073642",
|
||||
"light": "#eee8d5"
|
||||
},
|
||||
"diffRemovedLineNumberBg": {
|
||||
"dark": "#073642",
|
||||
"light": "#eee8d5"
|
||||
},
|
||||
"markdownText": {
|
||||
"dark": "base0",
|
||||
"light": "base00"
|
||||
},
|
||||
"markdownHeading": {
|
||||
"dark": "blue",
|
||||
"light": "blue"
|
||||
},
|
||||
"markdownLink": {
|
||||
"dark": "cyan",
|
||||
"light": "cyan"
|
||||
},
|
||||
"markdownLinkText": {
|
||||
"dark": "violet",
|
||||
"light": "violet"
|
||||
},
|
||||
"markdownCode": {
|
||||
"dark": "green",
|
||||
"light": "green"
|
||||
},
|
||||
"markdownBlockQuote": {
|
||||
"dark": "base01",
|
||||
"light": "base1"
|
||||
},
|
||||
"markdownEmph": {
|
||||
"dark": "yellow",
|
||||
"light": "yellow"
|
||||
},
|
||||
"markdownStrong": {
|
||||
"dark": "orange",
|
||||
"light": "orange"
|
||||
},
|
||||
"markdownHorizontalRule": {
|
||||
"dark": "base01",
|
||||
"light": "base1"
|
||||
},
|
||||
"markdownListItem": {
|
||||
"dark": "blue",
|
||||
"light": "blue"
|
||||
},
|
||||
"markdownListEnumeration": {
|
||||
"dark": "cyan",
|
||||
"light": "cyan"
|
||||
},
|
||||
"markdownImage": {
|
||||
"dark": "cyan",
|
||||
"light": "cyan"
|
||||
},
|
||||
"markdownImageText": {
|
||||
"dark": "violet",
|
||||
"light": "violet"
|
||||
},
|
||||
"markdownCodeBlock": {
|
||||
"dark": "base0",
|
||||
"light": "base00"
|
||||
},
|
||||
"syntaxComment": {
|
||||
"dark": "base01",
|
||||
"light": "base1"
|
||||
},
|
||||
"syntaxKeyword": {
|
||||
"dark": "green",
|
||||
"light": "green"
|
||||
},
|
||||
"syntaxFunction": {
|
||||
"dark": "blue",
|
||||
"light": "blue"
|
||||
},
|
||||
"syntaxVariable": {
|
||||
"dark": "cyan",
|
||||
"light": "cyan"
|
||||
},
|
||||
"syntaxString": {
|
||||
"dark": "cyan",
|
||||
"light": "cyan"
|
||||
},
|
||||
"syntaxNumber": {
|
||||
"dark": "magenta",
|
||||
"light": "magenta"
|
||||
},
|
||||
"syntaxType": {
|
||||
"dark": "yellow",
|
||||
"light": "yellow"
|
||||
},
|
||||
"syntaxOperator": {
|
||||
"dark": "green",
|
||||
"light": "green"
|
||||
},
|
||||
"syntaxPunctuation": {
|
||||
"dark": "base0",
|
||||
"light": "base00"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"background": "#262335",
|
||||
"backgroundAlt": "#1e1a29",
|
||||
"backgroundPanel": "#2a2139",
|
||||
"foreground": "#ffffff",
|
||||
"foregroundMuted": "#848bbd",
|
||||
"pink": "#ff7edb",
|
||||
"pinkBright": "#ff92df",
|
||||
"cyan": "#36f9f6",
|
||||
"cyanBright": "#72f1f8",
|
||||
"yellow": "#fede5d",
|
||||
"yellowBright": "#fff95d",
|
||||
"orange": "#ff8b39",
|
||||
"orangeBright": "#ff9f43",
|
||||
"purple": "#b084eb",
|
||||
"purpleBright": "#c792ea",
|
||||
"red": "#fe4450",
|
||||
"redBright": "#ff5e5b",
|
||||
"green": "#72f1b8",
|
||||
"greenBright": "#97f1d8"
|
||||
},
|
||||
"theme": {
|
||||
"primary": {
|
||||
"dark": "cyan",
|
||||
"light": "#00bcd4"
|
||||
},
|
||||
"secondary": {
|
||||
"dark": "pink",
|
||||
"light": "#e91e63"
|
||||
},
|
||||
"accent": {
|
||||
"dark": "purple",
|
||||
"light": "#9c27b0"
|
||||
},
|
||||
"error": {
|
||||
"dark": "red",
|
||||
"light": "#f44336"
|
||||
},
|
||||
"warning": {
|
||||
"dark": "yellow",
|
||||
"light": "#ff9800"
|
||||
},
|
||||
"success": {
|
||||
"dark": "green",
|
||||
"light": "#4caf50"
|
||||
},
|
||||
"info": {
|
||||
"dark": "orange",
|
||||
"light": "#ff5722"
|
||||
},
|
||||
"text": {
|
||||
"dark": "foreground",
|
||||
"light": "#262335"
|
||||
},
|
||||
"textMuted": {
|
||||
"dark": "foregroundMuted",
|
||||
"light": "#5c5c8a"
|
||||
},
|
||||
"background": {
|
||||
"dark": "#262335",
|
||||
"light": "#fafafa"
|
||||
},
|
||||
"backgroundPanel": {
|
||||
"dark": "#1e1a29",
|
||||
"light": "#f5f5f5"
|
||||
},
|
||||
"backgroundElement": {
|
||||
"dark": "#2a2139",
|
||||
"light": "#eeeeee"
|
||||
},
|
||||
"border": {
|
||||
"dark": "#495495",
|
||||
"light": "#e0e0e0"
|
||||
},
|
||||
"borderActive": {
|
||||
"dark": "cyan",
|
||||
"light": "#00bcd4"
|
||||
},
|
||||
"borderSubtle": {
|
||||
"dark": "#241b2f",
|
||||
"light": "#f0f0f0"
|
||||
},
|
||||
"diffAdded": {
|
||||
"dark": "green",
|
||||
"light": "#4caf50"
|
||||
},
|
||||
"diffRemoved": {
|
||||
"dark": "red",
|
||||
"light": "#f44336"
|
||||
},
|
||||
"diffContext": {
|
||||
"dark": "foregroundMuted",
|
||||
"light": "#5c5c8a"
|
||||
},
|
||||
"diffHunkHeader": {
|
||||
"dark": "purple",
|
||||
"light": "#9c27b0"
|
||||
},
|
||||
"diffHighlightAdded": {
|
||||
"dark": "greenBright",
|
||||
"light": "#4caf50"
|
||||
},
|
||||
"diffHighlightRemoved": {
|
||||
"dark": "redBright",
|
||||
"light": "#f44336"
|
||||
},
|
||||
"diffAddedBg": {
|
||||
"dark": "#1a3a2a",
|
||||
"light": "#e8f5e9"
|
||||
},
|
||||
"diffRemovedBg": {
|
||||
"dark": "#3a1a2a",
|
||||
"light": "#ffebee"
|
||||
},
|
||||
"diffContextBg": {
|
||||
"dark": "#1e1a29",
|
||||
"light": "#f5f5f5"
|
||||
},
|
||||
"diffLineNumber": {
|
||||
"dark": "#495495",
|
||||
"light": "#b0b0b0"
|
||||
},
|
||||
"diffAddedLineNumberBg": {
|
||||
"dark": "#1a3a2a",
|
||||
"light": "#e8f5e9"
|
||||
},
|
||||
"diffRemovedLineNumberBg": {
|
||||
"dark": "#3a1a2a",
|
||||
"light": "#ffebee"
|
||||
},
|
||||
"markdownText": {
|
||||
"dark": "foreground",
|
||||
"light": "#262335"
|
||||
},
|
||||
"markdownHeading": {
|
||||
"dark": "pink",
|
||||
"light": "#e91e63"
|
||||
},
|
||||
"markdownLink": {
|
||||
"dark": "cyan",
|
||||
"light": "#00bcd4"
|
||||
},
|
||||
"markdownLinkText": {
|
||||
"dark": "purple",
|
||||
"light": "#9c27b0"
|
||||
},
|
||||
"markdownCode": {
|
||||
"dark": "green",
|
||||
"light": "#4caf50"
|
||||
},
|
||||
"markdownBlockQuote": {
|
||||
"dark": "foregroundMuted",
|
||||
"light": "#5c5c8a"
|
||||
},
|
||||
"markdownEmph": {
|
||||
"dark": "yellow",
|
||||
"light": "#ff9800"
|
||||
},
|
||||
"markdownStrong": {
|
||||
"dark": "orange",
|
||||
"light": "#ff5722"
|
||||
},
|
||||
"markdownHorizontalRule": {
|
||||
"dark": "#495495",
|
||||
"light": "#e0e0e0"
|
||||
},
|
||||
"markdownListItem": {
|
||||
"dark": "cyan",
|
||||
"light": "#00bcd4"
|
||||
},
|
||||
"markdownListEnumeration": {
|
||||
"dark": "purple",
|
||||
"light": "#9c27b0"
|
||||
},
|
||||
"markdownImage": {
|
||||
"dark": "cyan",
|
||||
"light": "#00bcd4"
|
||||
},
|
||||
"markdownImageText": {
|
||||
"dark": "purple",
|
||||
"light": "#9c27b0"
|
||||
},
|
||||
"markdownCodeBlock": {
|
||||
"dark": "foreground",
|
||||
"light": "#262335"
|
||||
},
|
||||
"syntaxComment": {
|
||||
"dark": "foregroundMuted",
|
||||
"light": "#5c5c8a"
|
||||
},
|
||||
"syntaxKeyword": {
|
||||
"dark": "pink",
|
||||
"light": "#e91e63"
|
||||
},
|
||||
"syntaxFunction": {
|
||||
"dark": "orange",
|
||||
"light": "#ff5722"
|
||||
},
|
||||
"syntaxVariable": {
|
||||
"dark": "foreground",
|
||||
"light": "#262335"
|
||||
},
|
||||
"syntaxString": {
|
||||
"dark": "yellow",
|
||||
"light": "#ff9800"
|
||||
},
|
||||
"syntaxNumber": {
|
||||
"dark": "purple",
|
||||
"light": "#9c27b0"
|
||||
},
|
||||
"syntaxType": {
|
||||
"dark": "cyan",
|
||||
"light": "#00bcd4"
|
||||
},
|
||||
"syntaxOperator": {
|
||||
"dark": "pink",
|
||||
"light": "#e91e63"
|
||||
},
|
||||
"syntaxPunctuation": {
|
||||
"dark": "foreground",
|
||||
"light": "#262335"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -241,4 +241,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"bg": "#3f3f3f",
|
||||
"bgAlt": "#4f4f4f",
|
||||
"bgPanel": "#5f5f5f",
|
||||
"fg": "#dcdccc",
|
||||
"fgMuted": "#9f9f9f",
|
||||
"red": "#cc9393",
|
||||
"redBright": "#dca3a3",
|
||||
"green": "#7f9f7f",
|
||||
"greenBright": "#8fb28f",
|
||||
"yellow": "#f0dfaf",
|
||||
"yellowDim": "#e0cf9f",
|
||||
"blue": "#8cd0d3",
|
||||
"blueDim": "#7cb8bb",
|
||||
"magenta": "#dc8cc3",
|
||||
"cyan": "#93e0e3",
|
||||
"orange": "#dfaf8f"
|
||||
},
|
||||
"theme": {
|
||||
"primary": {
|
||||
"dark": "blue",
|
||||
"light": "#5f7f8f"
|
||||
},
|
||||
"secondary": {
|
||||
"dark": "magenta",
|
||||
"light": "#8f5f8f"
|
||||
},
|
||||
"accent": {
|
||||
"dark": "cyan",
|
||||
"light": "#5f8f8f"
|
||||
},
|
||||
"error": {
|
||||
"dark": "red",
|
||||
"light": "#8f5f5f"
|
||||
},
|
||||
"warning": {
|
||||
"dark": "yellow",
|
||||
"light": "#8f8f5f"
|
||||
},
|
||||
"success": {
|
||||
"dark": "green",
|
||||
"light": "#5f8f5f"
|
||||
},
|
||||
"info": {
|
||||
"dark": "orange",
|
||||
"light": "#8f7f5f"
|
||||
},
|
||||
"text": {
|
||||
"dark": "fg",
|
||||
"light": "#3f3f3f"
|
||||
},
|
||||
"textMuted": {
|
||||
"dark": "fgMuted",
|
||||
"light": "#6f6f6f"
|
||||
},
|
||||
"background": {
|
||||
"dark": "bg",
|
||||
"light": "#ffffef"
|
||||
},
|
||||
"backgroundPanel": {
|
||||
"dark": "bgAlt",
|
||||
"light": "#f5f5e5"
|
||||
},
|
||||
"backgroundElement": {
|
||||
"dark": "bgPanel",
|
||||
"light": "#ebebdb"
|
||||
},
|
||||
"border": {
|
||||
"dark": "#5f5f5f",
|
||||
"light": "#d0d0c0"
|
||||
},
|
||||
"borderActive": {
|
||||
"dark": "blue",
|
||||
"light": "#5f7f8f"
|
||||
},
|
||||
"borderSubtle": {
|
||||
"dark": "#4f4f4f",
|
||||
"light": "#e0e0d0"
|
||||
},
|
||||
"diffAdded": {
|
||||
"dark": "green",
|
||||
"light": "#5f8f5f"
|
||||
},
|
||||
"diffRemoved": {
|
||||
"dark": "red",
|
||||
"light": "#8f5f5f"
|
||||
},
|
||||
"diffContext": {
|
||||
"dark": "fgMuted",
|
||||
"light": "#6f6f6f"
|
||||
},
|
||||
"diffHunkHeader": {
|
||||
"dark": "cyan",
|
||||
"light": "#5f8f8f"
|
||||
},
|
||||
"diffHighlightAdded": {
|
||||
"dark": "greenBright",
|
||||
"light": "#5f8f5f"
|
||||
},
|
||||
"diffHighlightRemoved": {
|
||||
"dark": "redBright",
|
||||
"light": "#8f5f5f"
|
||||
},
|
||||
"diffAddedBg": {
|
||||
"dark": "#4f5f4f",
|
||||
"light": "#efffef"
|
||||
},
|
||||
"diffRemovedBg": {
|
||||
"dark": "#5f4f4f",
|
||||
"light": "#ffefef"
|
||||
},
|
||||
"diffContextBg": {
|
||||
"dark": "bgAlt",
|
||||
"light": "#f5f5e5"
|
||||
},
|
||||
"diffLineNumber": {
|
||||
"dark": "#6f6f6f",
|
||||
"light": "#b0b0a0"
|
||||
},
|
||||
"diffAddedLineNumberBg": {
|
||||
"dark": "#4f5f4f",
|
||||
"light": "#efffef"
|
||||
},
|
||||
"diffRemovedLineNumberBg": {
|
||||
"dark": "#5f4f4f",
|
||||
"light": "#ffefef"
|
||||
},
|
||||
"markdownText": {
|
||||
"dark": "fg",
|
||||
"light": "#3f3f3f"
|
||||
},
|
||||
"markdownHeading": {
|
||||
"dark": "yellow",
|
||||
"light": "#8f8f5f"
|
||||
},
|
||||
"markdownLink": {
|
||||
"dark": "blue",
|
||||
"light": "#5f7f8f"
|
||||
},
|
||||
"markdownLinkText": {
|
||||
"dark": "cyan",
|
||||
"light": "#5f8f8f"
|
||||
},
|
||||
"markdownCode": {
|
||||
"dark": "green",
|
||||
"light": "#5f8f5f"
|
||||
},
|
||||
"markdownBlockQuote": {
|
||||
"dark": "fgMuted",
|
||||
"light": "#6f6f6f"
|
||||
},
|
||||
"markdownEmph": {
|
||||
"dark": "yellowDim",
|
||||
"light": "#8f8f5f"
|
||||
},
|
||||
"markdownStrong": {
|
||||
"dark": "orange",
|
||||
"light": "#8f7f5f"
|
||||
},
|
||||
"markdownHorizontalRule": {
|
||||
"dark": "fgMuted",
|
||||
"light": "#6f6f6f"
|
||||
},
|
||||
"markdownListItem": {
|
||||
"dark": "blue",
|
||||
"light": "#5f7f8f"
|
||||
},
|
||||
"markdownListEnumeration": {
|
||||
"dark": "cyan",
|
||||
"light": "#5f8f8f"
|
||||
},
|
||||
"markdownImage": {
|
||||
"dark": "blue",
|
||||
"light": "#5f7f8f"
|
||||
},
|
||||
"markdownImageText": {
|
||||
"dark": "cyan",
|
||||
"light": "#5f8f8f"
|
||||
},
|
||||
"markdownCodeBlock": {
|
||||
"dark": "fg",
|
||||
"light": "#3f3f3f"
|
||||
},
|
||||
"syntaxComment": {
|
||||
"dark": "#7f9f7f",
|
||||
"light": "#5f7f5f"
|
||||
},
|
||||
"syntaxKeyword": {
|
||||
"dark": "yellow",
|
||||
"light": "#8f8f5f"
|
||||
},
|
||||
"syntaxFunction": {
|
||||
"dark": "blue",
|
||||
"light": "#5f7f8f"
|
||||
},
|
||||
"syntaxVariable": {
|
||||
"dark": "fg",
|
||||
"light": "#3f3f3f"
|
||||
},
|
||||
"syntaxString": {
|
||||
"dark": "red",
|
||||
"light": "#8f5f5f"
|
||||
},
|
||||
"syntaxNumber": {
|
||||
"dark": "greenBright",
|
||||
"light": "#5f8f5f"
|
||||
},
|
||||
"syntaxType": {
|
||||
"dark": "cyan",
|
||||
"light": "#5f8f8f"
|
||||
},
|
||||
"syntaxOperator": {
|
||||
"dark": "yellow",
|
||||
"light": "#8f8f5f"
|
||||
},
|
||||
"syntaxPunctuation": {
|
||||
"dark": "fg",
|
||||
"light": "#3f3f3f"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
|
||||
"github.com/sst/opencode-sdk-go"
|
||||
"github.com/sst/opencode/internal/app"
|
||||
"github.com/sst/opencode/internal/commands"
|
||||
"github.com/sst/opencode/internal/completions"
|
||||
@@ -22,8 +23,8 @@ import (
|
||||
"github.com/sst/opencode/internal/components/toast"
|
||||
"github.com/sst/opencode/internal/layout"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
"github.com/sst/opencode/internal/util"
|
||||
"github.com/sst/opencode/pkg/client"
|
||||
)
|
||||
|
||||
// InterruptDebounceTimeoutMsg is sent when the interrupt key debounce timeout expires
|
||||
@@ -55,6 +56,7 @@ type appModel struct {
|
||||
isLeaderSequence bool
|
||||
toastManager *toast.ToastManager
|
||||
interruptKeyState InterruptKeyState
|
||||
lastScroll time.Time
|
||||
}
|
||||
|
||||
func (a appModel) Init() tea.Cmd {
|
||||
@@ -73,19 +75,39 @@ func (a appModel) Init() tea.Cmd {
|
||||
|
||||
// Check if we should show the init dialog
|
||||
cmds = append(cmds, func() tea.Msg {
|
||||
shouldShow := a.app.Info.Git && a.app.Info.Time.Initialized == nil
|
||||
shouldShow := a.app.Info.Git && a.app.Info.Time.Initialized > 0
|
||||
return dialog.ShowInitDialogMsg{Show: shouldShow}
|
||||
})
|
||||
|
||||
return tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
var BUGGED_SCROLL_KEYS = map[string]bool{
|
||||
"0": true,
|
||||
"1": true,
|
||||
"2": true,
|
||||
"3": true,
|
||||
"4": true,
|
||||
"5": true,
|
||||
"6": true,
|
||||
"7": true,
|
||||
"8": true,
|
||||
"9": true,
|
||||
"M": true,
|
||||
"m": true,
|
||||
"[": true,
|
||||
";": true,
|
||||
}
|
||||
|
||||
func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyPressMsg:
|
||||
keyString := msg.String()
|
||||
if time.Since(a.lastScroll) < time.Millisecond*100 && BUGGED_SCROLL_KEYS[keyString] {
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// 1. Handle active modal
|
||||
if a.modal != nil {
|
||||
@@ -221,6 +243,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
a.editor = updatedEditor.(chat.EditorComponent)
|
||||
return a, cmd
|
||||
case tea.MouseWheelMsg:
|
||||
a.lastScroll = time.Now()
|
||||
if a.modal != nil {
|
||||
return a, nil
|
||||
}
|
||||
@@ -230,9 +253,19 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return a, tea.Batch(cmds...)
|
||||
case tea.BackgroundColorMsg:
|
||||
styles.Terminal = &styles.TerminalInfo{
|
||||
Background: msg.Color,
|
||||
BackgroundIsDark: msg.IsDark(),
|
||||
}
|
||||
slog.Debug("Background color", "isDark", msg.IsDark())
|
||||
slog.Debug("Background color", "color", msg.String(), "isDark", msg.IsDark())
|
||||
return a, func() tea.Msg {
|
||||
theme.UpdateSystemTheme(
|
||||
styles.Terminal.Background,
|
||||
styles.Terminal.BackgroundIsDark,
|
||||
)
|
||||
return dialog.ThemeSelectedMsg{
|
||||
ThemeName: theme.CurrentThemeName(),
|
||||
}
|
||||
}
|
||||
case modal.CloseModalMsg:
|
||||
var cmd tea.Cmd
|
||||
if a.modal != nil {
|
||||
@@ -256,31 +289,31 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
cmds = append(cmds, cmd)
|
||||
case dialog.CompletionDialogCloseMsg:
|
||||
a.showCompletionDialog = false
|
||||
case client.EventInstallationUpdated:
|
||||
case opencode.EventListResponseEventInstallationUpdated:
|
||||
return a, toast.NewSuccessToast(
|
||||
"opencode updated to "+msg.Properties.Version+", restart to apply.",
|
||||
toast.WithTitle("New version installed"),
|
||||
)
|
||||
case client.EventSessionDeleted:
|
||||
if a.app.Session != nil && msg.Properties.Info.Id == a.app.Session.Id {
|
||||
a.app.Session = &client.SessionInfo{}
|
||||
a.app.Messages = []client.MessageInfo{}
|
||||
case opencode.EventListResponseEventSessionDeleted:
|
||||
if a.app.Session != nil && msg.Properties.Info.ID == a.app.Session.ID {
|
||||
a.app.Session = &opencode.Session{}
|
||||
a.app.Messages = []opencode.Message{}
|
||||
}
|
||||
return a, toast.NewSuccessToast("Session deleted successfully")
|
||||
case client.EventSessionUpdated:
|
||||
if msg.Properties.Info.Id == a.app.Session.Id {
|
||||
case opencode.EventListResponseEventSessionUpdated:
|
||||
if msg.Properties.Info.ID == a.app.Session.ID {
|
||||
a.app.Session = &msg.Properties.Info
|
||||
}
|
||||
case client.EventMessageUpdated:
|
||||
if msg.Properties.Info.Metadata.SessionID == a.app.Session.Id {
|
||||
case opencode.EventListResponseEventMessageUpdated:
|
||||
if msg.Properties.Info.Metadata.SessionID == a.app.Session.ID {
|
||||
exists := false
|
||||
optimisticReplaced := false
|
||||
|
||||
// First check if this is replacing an optimistic message
|
||||
if msg.Properties.Info.Role == client.User {
|
||||
if msg.Properties.Info.Role == opencode.MessageRoleUser {
|
||||
// Look for optimistic messages to replace
|
||||
for i, m := range a.app.Messages {
|
||||
if strings.HasPrefix(m.Id, "optimistic-") && m.Role == client.User {
|
||||
if strings.HasPrefix(m.ID, "optimistic-") && m.Role == opencode.MessageRoleUser {
|
||||
// Replace the optimistic message with the real one
|
||||
a.app.Messages[i] = msg.Properties.Info
|
||||
exists = true
|
||||
@@ -293,7 +326,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
// If not replacing optimistic, check for existing message with same ID
|
||||
if !optimisticReplaced {
|
||||
for i, m := range a.app.Messages {
|
||||
if m.Id == msg.Properties.Info.Id {
|
||||
if m.ID == msg.Properties.Info.ID {
|
||||
a.app.Messages[i] = msg.Properties.Info
|
||||
exists = true
|
||||
break
|
||||
@@ -305,11 +338,15 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
a.app.Messages = append(a.app.Messages, msg.Properties.Info)
|
||||
}
|
||||
}
|
||||
case client.EventSessionError:
|
||||
unknownError, err := msg.Properties.Error.AsUnknownError()
|
||||
if err == nil {
|
||||
slog.Error("Server error", "name", unknownError.Name, "message", unknownError.Data.Message)
|
||||
return a, toast.NewErrorToast(unknownError.Data.Message, toast.WithTitle(unknownError.Name))
|
||||
case opencode.EventListResponseEventSessionError:
|
||||
switch err := msg.Properties.Error.AsUnion().(type) {
|
||||
case nil:
|
||||
case opencode.ProviderAuthError:
|
||||
slog.Error("Failed to authenticate with provider", "error", err.Data.Message)
|
||||
return a, toast.NewErrorToast("Provider error: " + err.Data.Message)
|
||||
case opencode.UnknownError:
|
||||
slog.Error("Server error", "name", err.Name, "message", err.Data.Message)
|
||||
return a, toast.NewErrorToast(err.Data.Message, toast.WithTitle(string(err.Name)))
|
||||
}
|
||||
case tea.WindowSizeMsg:
|
||||
msg.Height -= 2 // Make space for the status bar
|
||||
@@ -325,7 +362,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
a.layout.SetSize(a.width, a.height)
|
||||
case app.SessionSelectedMsg:
|
||||
messages, err := a.app.ListMessages(context.Background(), msg.Id)
|
||||
messages, err := a.app.ListMessages(context.Background(), msg.ID)
|
||||
if err != nil {
|
||||
slog.Error("Failed to list messages", "error", err)
|
||||
return a, toast.NewErrorToast("Failed to open session")
|
||||
@@ -335,8 +372,8 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case app.ModelSelectedMsg:
|
||||
a.app.Provider = &msg.Provider
|
||||
a.app.Model = &msg.Model
|
||||
a.app.State.Provider = msg.Provider.Id
|
||||
a.app.State.Model = msg.Model.Id
|
||||
a.app.State.Provider = msg.Provider.ID
|
||||
a.app.State.Model = msg.Model.ID
|
||||
a.app.SaveState()
|
||||
case dialog.ThemeSelectedMsg:
|
||||
a.app.State.Theme = msg.ThemeName
|
||||
@@ -424,6 +461,9 @@ func (a appModel) View() string {
|
||||
|
||||
appView = a.toastManager.RenderOverlay(appView)
|
||||
|
||||
if theme.CurrentThemeUsesAnsiColors() {
|
||||
appView = util.ConvertRGBToAnsi16Colors(appView)
|
||||
}
|
||||
return appView
|
||||
}
|
||||
|
||||
@@ -485,42 +525,35 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
|
||||
})
|
||||
cmds = append(cmds, cmd)
|
||||
case commands.SessionNewCommand:
|
||||
if a.app.Session.Id == "" {
|
||||
if a.app.Session.ID == "" {
|
||||
return a, nil
|
||||
}
|
||||
a.app.Session = &client.SessionInfo{}
|
||||
a.app.Messages = []client.MessageInfo{}
|
||||
a.app.Session = &opencode.Session{}
|
||||
a.app.Messages = []opencode.Message{}
|
||||
cmds = append(cmds, util.CmdHandler(app.SessionClearedMsg{}))
|
||||
case commands.SessionListCommand:
|
||||
sessionDialog := dialog.NewSessionDialog(a.app)
|
||||
a.modal = sessionDialog
|
||||
case commands.SessionShareCommand:
|
||||
if a.app.Session.Id == "" {
|
||||
if a.app.Session.ID == "" {
|
||||
return a, nil
|
||||
}
|
||||
response, err := a.app.Client.PostSessionShareWithResponse(
|
||||
context.Background(),
|
||||
client.PostSessionShareJSONRequestBody{
|
||||
SessionID: a.app.Session.Id,
|
||||
},
|
||||
)
|
||||
response, err := a.app.Client.Session.Share(context.Background(), a.app.Session.ID)
|
||||
if err != nil {
|
||||
slog.Error("Failed to share session", "error", err)
|
||||
return a, toast.NewErrorToast("Failed to share session")
|
||||
}
|
||||
if response.JSON200 != nil && response.JSON200.Share != nil {
|
||||
shareUrl := response.JSON200.Share.Url
|
||||
cmds = append(cmds, tea.SetClipboard(shareUrl))
|
||||
cmds = append(cmds, toast.NewSuccessToast("Share URL copied to clipboard!"))
|
||||
}
|
||||
shareUrl := response.Share.URL
|
||||
cmds = append(cmds, tea.SetClipboard(shareUrl))
|
||||
cmds = append(cmds, toast.NewSuccessToast("Share URL copied to clipboard!"))
|
||||
case commands.SessionInterruptCommand:
|
||||
if a.app.Session.Id == "" {
|
||||
if a.app.Session.ID == "" {
|
||||
return a, nil
|
||||
}
|
||||
a.app.Cancel(context.Background(), a.app.Session.Id)
|
||||
a.app.Cancel(context.Background(), a.app.Session.ID)
|
||||
return a, nil
|
||||
case commands.SessionCompactCommand:
|
||||
if a.app.Session.Id == "" {
|
||||
if a.app.Session.ID == "" {
|
||||
return a, nil
|
||||
}
|
||||
// TODO: block until compaction is complete
|
||||
@@ -628,8 +661,8 @@ func NewModel(app *app.App) tea.Model {
|
||||
messagesContainer := layout.NewContainer(messages)
|
||||
|
||||
var leaderBinding *key.Binding
|
||||
if (*app.Config.Keybinds).Leader != nil {
|
||||
binding := key.NewBinding(key.WithKeys(*app.Config.Keybinds.Leader))
|
||||
if app.Config.Keybinds.Leader != "" {
|
||||
binding := key.NewBinding(key.WithKeys(app.Config.Keybinds.Leader))
|
||||
leaderBinding = &binding
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var csiRE *regexp.Regexp
|
||||
|
||||
func init() {
|
||||
csiRE = regexp.MustCompile(`\x1b\[([0-9;]+)m`)
|
||||
}
|
||||
|
||||
var targetFGMap = map[string]string{
|
||||
"0;0;0": "\x1b[30m", // Black
|
||||
"128;0;0": "\x1b[31m", // Red
|
||||
"0;128;0": "\x1b[32m", // Green
|
||||
"128;128;0": "\x1b[33m", // Yellow
|
||||
"0;0;128": "\x1b[34m", // Blue
|
||||
"128;0;128": "\x1b[35m", // Magenta
|
||||
"0;128;128": "\x1b[36m", // Cyan
|
||||
"192;192;192": "\x1b[37m", // White (light grey)
|
||||
"128;128;128": "\x1b[90m", // Bright Black (dark grey)
|
||||
"255;0;0": "\x1b[91m", // Bright Red
|
||||
"0;255;0": "\x1b[92m", // Bright Green
|
||||
"255;255;0": "\x1b[93m", // Bright Yellow
|
||||
"0;0;255": "\x1b[94m", // Bright Blue
|
||||
"255;0;255": "\x1b[95m", // Bright Magenta
|
||||
"0;255;255": "\x1b[96m", // Bright Cyan
|
||||
"255;255;255": "\x1b[97m", // Bright White
|
||||
}
|
||||
|
||||
var targetBGMap = map[string]string{
|
||||
"0;0;0": "\x1b[40m",
|
||||
"128;0;0": "\x1b[41m",
|
||||
"0;128;0": "\x1b[42m",
|
||||
"128;128;0": "\x1b[43m",
|
||||
"0;0;128": "\x1b[44m",
|
||||
"128;0;128": "\x1b[45m",
|
||||
"0;128;128": "\x1b[46m",
|
||||
"192;192;192": "\x1b[47m",
|
||||
"128;128;128": "\x1b[100m",
|
||||
"255;0;0": "\x1b[101m",
|
||||
"0;255;0": "\x1b[102m",
|
||||
"255;255;0": "\x1b[103m",
|
||||
"0;0;255": "\x1b[104m",
|
||||
"255;0;255": "\x1b[105m",
|
||||
"0;255;255": "\x1b[106m",
|
||||
"255;255;255": "\x1b[107m",
|
||||
}
|
||||
|
||||
func ConvertRGBToAnsi16Colors(s string) string {
|
||||
return csiRE.ReplaceAllStringFunc(s, func(seq string) string {
|
||||
params := strings.Split(csiRE.FindStringSubmatch(seq)[1], ";")
|
||||
out := make([]string, 0, len(params))
|
||||
|
||||
for i := 0; i < len(params); {
|
||||
// Detect “38 | 48 ; 2 ; r ; g ; b ( ; alpha? )”
|
||||
if (params[i] == "38" || params[i] == "48") &&
|
||||
i+4 < len(params) &&
|
||||
params[i+1] == "2" {
|
||||
|
||||
key := strings.Join(params[i+2:i+5], ";")
|
||||
var repl string
|
||||
if params[i] == "38" {
|
||||
repl = targetFGMap[key]
|
||||
} else {
|
||||
repl = targetBGMap[key]
|
||||
}
|
||||
|
||||
if repl != "" { // exact RGB hit
|
||||
out = append(out, repl[2:len(repl)-1])
|
||||
i += 5 // skip 38/48;2;r;g;b
|
||||
|
||||
// if i == len(params)-1 && looksLikeByte(params[i]) {
|
||||
// i++ // swallow the alpha byte
|
||||
// }
|
||||
continue
|
||||
}
|
||||
}
|
||||
// Normal token — keep verbatim.
|
||||
out = append(out, params[i])
|
||||
i++
|
||||
}
|
||||
|
||||
return "\x1b[" + strings.Join(out, ";") + "m"
|
||||
})
|
||||
}
|
||||
|
||||
// func looksLikeByte(tok string) bool {
|
||||
// v, err := strconv.Atoi(tok)
|
||||
// return err == nil && v >= 0 && v <= 255
|
||||
// }
|
||||
@@ -0,0 +1,50 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// MapReducePar performs a parallel map-reduce operation on a slice of items.
|
||||
// It applies a function to each item in the slice concurrently,
|
||||
// and combines the results serially using a reducer returned from
|
||||
// each one of the functions, allowing the use of closures.
|
||||
func MapReducePar[a, b any](items []a, init b, fn func(a) func(b) b) b {
|
||||
itemCount := len(items)
|
||||
locks := make([]*sync.Mutex, itemCount)
|
||||
mapped := make([]func(b) b, itemCount)
|
||||
|
||||
for i, value := range items {
|
||||
lock := &sync.Mutex{}
|
||||
lock.Lock()
|
||||
locks[i] = lock
|
||||
go func() {
|
||||
defer lock.Unlock()
|
||||
mapped[i] = fn(value)
|
||||
}()
|
||||
}
|
||||
|
||||
result := init
|
||||
for i := range itemCount {
|
||||
locks[i].Lock()
|
||||
defer locks[i].Unlock()
|
||||
f := mapped[i]
|
||||
if f != nil {
|
||||
result = f(result)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// WriteStringsPar allows to iterate over a list and compute strings in parallel,
|
||||
// yet write them in order.
|
||||
func WriteStringsPar[a any](sb *strings.Builder, items []a, fn func(a) string) {
|
||||
MapReducePar(items, sb, func(item a) func(*strings.Builder) *strings.Builder {
|
||||
str := fn(item)
|
||||
return func(sbdr *strings.Builder) *strings.Builder {
|
||||
sbdr.WriteString(str)
|
||||
return sbdr
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
)
|
||||
@@ -35,3 +37,11 @@ func IsWsl() bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func Measure(tag string) func(...any) {
|
||||
startTime := time.Now()
|
||||
return func(tags ...any) {
|
||||
args := append([]any{"timeTakenMs", time.Since(startTime).Milliseconds()}, tags...)
|
||||
slog.Info(tag, args...)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
package client
|
||||
|
||||
//go:generate bun run ../../../opencode/src/index.ts generate
|
||||
//go:generate go tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --package=client --generate=types,client,models -o generated-client.go ./gen/openapi.json
|
||||
@@ -6,11 +6,13 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/sst/opencode-sdk-go"
|
||||
)
|
||||
|
||||
func (c *Client) Event(ctx context.Context) (<-chan any, error) {
|
||||
func Event(c *opencode.Client, url string, ctx context.Context) (<-chan any, error) {
|
||||
events := make(chan any)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", c.Server+"event", nil)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url+"event", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -31,15 +33,12 @@ func (c *Client) Event(ctx context.Context) (<-chan any, error) {
|
||||
if strings.HasPrefix(line, "data: ") {
|
||||
data := strings.TrimPrefix(line, "data: ")
|
||||
|
||||
var event Event
|
||||
var event opencode.EventListResponse
|
||||
if err := json.Unmarshal([]byte(data), &event); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
val, err := event.ValueByDiscriminator()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
val := event.AsUnion()
|
||||
|
||||
select {
|
||||
case events <- val:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user