mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-04 01:06:16 -04:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 26bab00dab | |||
| 568c04753e | |||
| 4a06e164d2 | |||
| c57b52c300 | |||
| 0b8f48f17f | |||
| 3862184ccb | |||
| 8619c50976 | |||
| bb6b56b72a | |||
| 1252b65166 | |||
| 6840276dad | |||
| bd8c3cd0f1 | |||
| e5e9b3e3c0 | |||
| 1e8a681de9 | |||
| a834bedc17 | |||
| 6a3392385e | |||
| 6a00e063c4 | |||
| 73a0ce2b7d | |||
| 4d1afd01fa | |||
| 801d5f47bd | |||
| b6caae9708 | |||
| 183ca64ef9 | |||
| 8c32cfe829 | |||
| 73dcc88da1 | |||
| 14bded65dc | |||
| 87d1d3fb62 | |||
| e054454109 | |||
| a6142cf975 | |||
| 69332e5fa3 | |||
| 20201ba3c4 | |||
| 658067186a | |||
| ac777b77cf | |||
| 5944ae2023 | |||
| 2f10961ba8 | |||
| fae97978a3 |
@@ -61,103 +61,56 @@ The Models.dev dataset is also used to detect common environment variables like
|
||||
|
||||
If there are additional providers you want to use you can submit a PR to the [Models.dev repo](https://github.com/sst/models.dev). If configuring just for yourself check out the Config section below.
|
||||
|
||||
### Global Config
|
||||
### Config
|
||||
|
||||
Some basic configuration is available in the global config file.
|
||||
|
||||
```toml
|
||||
# ~/.config/opencode/config
|
||||
theme = "opencode"
|
||||
provider = "anthropic"
|
||||
model = "claude-sonnet-4-20250514"
|
||||
autoupdate = true
|
||||
|
||||
keybinds.leader = "ctrl+x"
|
||||
keybinds.session_new = "<leader>n"
|
||||
keybinds.editor_open = "<leader>e"
|
||||
```
|
||||
|
||||
#### Keybinds
|
||||
|
||||
You can configure the keybinds in the global config file. (Note: values listed below are the defaults.)
|
||||
|
||||
```toml
|
||||
# ~/.config/opencode/config
|
||||
|
||||
[keybinds]
|
||||
leader = "ctrl+x"
|
||||
help = "<leader>h"
|
||||
editor_open = "<leader>e"
|
||||
session_new = "<leader>n"
|
||||
session_list = "<leader>l"
|
||||
session_share = "<leader>s"
|
||||
session_interrupt = "esc"
|
||||
session_compact = "<leader>c"
|
||||
tool_details = "<leader>d"
|
||||
model_list = "<leader>m"
|
||||
theme_list = "<leader>t"
|
||||
project_init = "<leader>i"
|
||||
input_clear = "ctrl+c"
|
||||
input_paste = "ctrl+v"
|
||||
input_submit = "enter"
|
||||
input_newline = "shift+enter"
|
||||
history_previous = "up"
|
||||
history_next = "down"
|
||||
messages_page_up = "pgup"
|
||||
messages_page_down = "pgdown"
|
||||
messages_half_page_up = "ctrl+alt+u"
|
||||
messages_half_page_down = "ctrl+alt+d"
|
||||
messages_previous = "ctrl+alt+k"
|
||||
messages_next = "ctrl+alt+j"
|
||||
messages_first = "ctrl+g"
|
||||
messages_last = "ctrl+alt+g"
|
||||
app_exit = "ctrl+c,<leader>q"
|
||||
```
|
||||
|
||||
#### Models.dev
|
||||
|
||||
You can also extend the models.dev database with your own providers by mirroring the structure found [here](https://github.com/sst/models.dev/tree/dev/providers/anthropic)
|
||||
|
||||
Start with a `provider.toml` file in `~/.config/opencode/providers`
|
||||
|
||||
```toml
|
||||
# ~/.config/opencode/providers/openrouter/provider.toml
|
||||
[provider]
|
||||
name = "OpenRouter"
|
||||
env = ["OPENROUTER_API_KEY"]
|
||||
npm = "@openrouter/ai-sdk-provider"
|
||||
|
||||
[options]
|
||||
baseURL = "https://api.openrouter.ai" # optional settings
|
||||
```
|
||||
|
||||
And models in `~/.config/opencode/providers/openrouter/models/[model-id]`
|
||||
|
||||
```toml
|
||||
# ~/.config/opencode/providers/openrouter/models/anthropic/claude-3.5-sonnet.toml
|
||||
name = "Claude 4 Sonnet"
|
||||
attachment = true
|
||||
reasoning = false
|
||||
temperature = true
|
||||
|
||||
[cost]
|
||||
input = 3.00
|
||||
output = 15.00
|
||||
inputCached = 3.75
|
||||
outputCached = 0.30
|
||||
|
||||
[limit]
|
||||
context = 200_000
|
||||
output = 50_000
|
||||
```
|
||||
|
||||
### Project Config
|
||||
|
||||
Project configuration is optional. You can place an `opencode.json` file in the root of your repo and is meant to be checked in and shared with your team.
|
||||
Config is optional and can be placed in the root of your repo or globally in `~/.config/opencode/config.json`. It can be checked in and shared with your team.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"$schema": "http://opencode.ai/config.json"
|
||||
"theme": "opencode",
|
||||
"model": "anthropic/claude-sonnet-4-20250514" // format is provider/model
|
||||
"autoshare": false,
|
||||
"autoupdate": true,
|
||||
}
|
||||
```
|
||||
|
||||
#### Keybinds
|
||||
|
||||
You can configure custom keybinds, the values listed below are the defaults.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"$schema": "http://opencode.ai/config.json",
|
||||
"keybinds": {
|
||||
"leader": "ctrl+x",
|
||||
"help": "<leader>h",
|
||||
"editor_open": "<leader>e",
|
||||
"session_new": "<leader>n",
|
||||
"session_list": "<leader>l",
|
||||
"session_share": "<leader>s",
|
||||
"session_interrupt": "esc",
|
||||
"session_compact": "<leader>c",
|
||||
"tool_details": "<leader>d",
|
||||
"model_list": "<leader>m",
|
||||
"theme_list": "<leader>t",
|
||||
"project_init": "<leader>i",
|
||||
"input_clear": "ctrl+c",
|
||||
"input_paste": "ctrl+v",
|
||||
"input_submit": "enter",
|
||||
"input_newline": "shift+enter,ctrl+j",
|
||||
"history_previous": "up",
|
||||
"history_next": "down",
|
||||
"messages_page_up": "pgup",
|
||||
"messages_page_down": "pgdown",
|
||||
"messages_half_page_up": "ctrl+alt+u",
|
||||
"messages_half_page_down": "ctrl+alt+d",
|
||||
"messages_previous": "ctrl+alt+k",
|
||||
"messages_next": "ctrl+alt+j",
|
||||
"messages_first": "ctrl+g",
|
||||
"messages_last": "ctrl+alt+g",
|
||||
"app_exit": "ctrl+c,<leader>q"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -184,7 +137,7 @@ Project configuration is optional. You can place an `opencode.json` file in the
|
||||
|
||||
#### Providers
|
||||
|
||||
You can use opencode with any provider listed at [here](https://ai-sdk.dev/providers/ai-sdk-providers). Be sure to specify the npm package to use to load the provider.
|
||||
You can use opencode with any provider listed at [here](https://ai-sdk.dev/providers/ai-sdk-providers). Be sure to specify the npm package to use to load the provider. Remember most popular providers are preloaded from [models.dev](https://models.dev)
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
@@ -230,9 +183,7 @@ OpenRouter is not in the Models.dev database yet, but you can configure it manua
|
||||
"openrouter": {
|
||||
"npm": "@openrouter/ai-sdk-provider",
|
||||
"name": "OpenRouter",
|
||||
"options": {
|
||||
"apiKey": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
"options": {},
|
||||
"models": {
|
||||
"anthropic/claude-3.5-sonnet": {
|
||||
"name": "Claude 3.5 Sonnet"
|
||||
@@ -243,6 +194,8 @@ OpenRouter is not in the Models.dev database yet, but you can configure it manua
|
||||
}
|
||||
```
|
||||
|
||||
And then to configure an api key you can do `opencode auth login` and select "Other -> 'openrouter'"
|
||||
|
||||
#### How is this different than Claude Code?
|
||||
|
||||
It's very similar to Claude Code in terms of capability. Here are the key differences:
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"name": "opencode",
|
||||
"devDependencies": {
|
||||
"prettier": "3.5.3",
|
||||
"sst": "3.17.4",
|
||||
"sst": "3.17.6",
|
||||
},
|
||||
},
|
||||
"packages/function": {
|
||||
@@ -79,6 +79,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"opencode": "workspace:*",
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
},
|
||||
@@ -1424,23 +1425,23 @@
|
||||
|
||||
"split2": ["split2@3.2.2", "", { "dependencies": { "readable-stream": "^3.0.0" } }, "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg=="],
|
||||
|
||||
"sst": ["sst@3.17.4", "", { "dependencies": { "aws-sdk": "2.1692.0", "aws4fetch": "1.0.18", "jose": "5.2.3", "opencontrol": "0.0.6", "openid-client": "5.6.4" }, "optionalDependencies": { "sst-darwin-arm64": "3.17.4", "sst-darwin-x64": "3.17.4", "sst-linux-arm64": "3.17.4", "sst-linux-x64": "3.17.4", "sst-linux-x86": "3.17.4", "sst-win32-arm64": "3.17.4", "sst-win32-x64": "3.17.4", "sst-win32-x86": "3.17.4" }, "bin": { "sst": "bin/sst.mjs" } }, "sha512-WpAws1ASJIilKC9/DGBhZ5wk2I4gtlzHXKpuwPC25bHWjqllv1jZiehIYhhN0PpV2pV8xCvqzyN8Gdm3J4EWQg=="],
|
||||
"sst": ["sst@3.17.6", "", { "dependencies": { "aws-sdk": "2.1692.0", "aws4fetch": "1.0.18", "jose": "5.2.3", "opencontrol": "0.0.6", "openid-client": "5.6.4" }, "optionalDependencies": { "sst-darwin-arm64": "3.17.6", "sst-darwin-x64": "3.17.6", "sst-linux-arm64": "3.17.6", "sst-linux-x64": "3.17.6", "sst-linux-x86": "3.17.6", "sst-win32-arm64": "3.17.6", "sst-win32-x64": "3.17.6", "sst-win32-x86": "3.17.6" }, "bin": { "sst": "bin/sst.mjs" } }, "sha512-p+AcqwfYQUdkxeRjCikQoTMviPCBiGoU7M0vcV6GDVmVis8hzhVw4EFfHTafZC+aWfy1Ke2UQi66vZlEVWuEqA=="],
|
||||
|
||||
"sst-darwin-arm64": ["sst-darwin-arm64@3.17.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-IJansQWlPdiaQNsJw3FQ+Q/ZXN1hzrq2Q31xG4l2HhA1doj1C3y+6s57vu4cTRDFo2OwBlC4+zlQBJHsOYGhrA=="],
|
||||
"sst-darwin-arm64": ["sst-darwin-arm64@3.17.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-6tb7KlcPR7PTi3ofQv8dX/n6Jf7pNP9VfrnYL4HBWnWrcYaZeJ5MWobILfIJ/y2jHgoqmg9e5C3266Eds0JQyw=="],
|
||||
|
||||
"sst-darwin-x64": ["sst-darwin-x64@3.17.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-mHd26/AtaQ79ajqzsutRhgEjkCxX+bXgW4KJIN0AGT3110fo2OL0x2UXmfX+sxSWOFHvJQsjFjFm4CLtQSxyBg=="],
|
||||
"sst-darwin-x64": ["sst-darwin-x64@3.17.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-lFakq6/EgTuBSjbl8Kry4pfgAPEIyn6o7ZkyRz3hz5331wUaX88yfjs3tL9JQ8Ey6jBUYxwhP/Q1n7fzIG046g=="],
|
||||
|
||||
"sst-linux-arm64": ["sst-linux-arm64@3.17.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-unaNWOY3oEI/jUUG47/2Gbreaoi/D/rLsTPeKyYEWhWEBWCojns7LfMQs1bgW0qjBGmazB2IJD4NVYhYqYQxqQ=="],
|
||||
"sst-linux-arm64": ["sst-linux-arm64@3.17.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-SdTxXMbTEdiwOqp37w31kXv97vHqSx3oK9h/76lKg7V9k5JxPJ6JMefPLhoKWwK0Zh6AndY2zo2oRoEv4SIaDw=="],
|
||||
|
||||
"sst-linux-x64": ["sst-linux-x64@3.17.4", "", { "os": "linux", "cpu": "x64" }, "sha512-zoErI6dVoRxWcmoVVrzNJWKEqfUF/MyQInEkGROGY2YsFFzOM5RD5Dsdm9q6oDGwx+NxFAhQWc8/8C+OmoW1nA=="],
|
||||
"sst-linux-x64": ["sst-linux-x64@3.17.6", "", { "os": "linux", "cpu": "x64" }, "sha512-qneh7uWDiTUYx8X1Y3h2YVw3SJ0ybBBlRrVybIvCM09JqQ8+qq/XjKXGzA/3/EF0Jr7Ug8cARSn9CwxhdQGN7Q=="],
|
||||
|
||||
"sst-linux-x86": ["sst-linux-x86@3.17.4", "", { "os": "linux", "cpu": "none" }, "sha512-7ZHS2rxzxVAxMFW3u5+GMRGGACaBMuLht8JYxqruD8mFVqk9UaPQgrFKIHGKWHLBJLVnF2AdwmlHOcEKP+UJWA=="],
|
||||
"sst-linux-x86": ["sst-linux-x86@3.17.6", "", { "os": "linux", "cpu": "none" }, "sha512-pU3D5OeqnmfxGqN31DxuwWnc1OayxhkErnITHhZ39D0MTiwbIgCapH26FuLW8B08/uxJWG8djUlOboCRhSBvWA=="],
|
||||
|
||||
"sst-win32-arm64": ["sst-win32-arm64@3.17.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-q4cedr6WD3NqeQkDvmAsIgMgPIjziIWy81wA3ZmnY6UT0jFgFus23ppLIi6F4BFJfOygvAP2PeGrRR3o8giclw=="],
|
||||
"sst-win32-arm64": ["sst-win32-arm64@3.17.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-Rr3RTYWAsH9sM9CbM/sAZCk7dB1OsSAljjJuuHMvdSAYW3RDpXEza0PBJGxnBID2eOrpswEchzMPL2d8LtL7oA=="],
|
||||
|
||||
"sst-win32-x64": ["sst-win32-x64@3.17.4", "", { "os": "win32", "cpu": "x64" }, "sha512-sSQL041YCusZ8/0ynYGe9DCmPYVZOFsemXKUA9tX4IGSDqXae1FN0Sj7HQ17JyY24UUirY1zR7LFk+7KrP6wiA=="],
|
||||
"sst-win32-x64": ["sst-win32-x64@3.17.6", "", { "os": "win32", "cpu": "x64" }, "sha512-yZ3roxwI0Wve9PFzdrrF1kfzCmIMFCCoa8qKeXY7LxCJ4QQIqHbCOccLK1Wv/MIU/mcZHWXTQVCLHw77uaa0GQ=="],
|
||||
|
||||
"sst-win32-x86": ["sst-win32-x86@3.17.4", "", { "os": "win32", "cpu": "none" }, "sha512-WhjsD2dkA2fbQ03CgwIJb+2p0osll2PTXlr7HC3L+H8wG2DgLFPjoE+6N8n6r2dVMVaDzuNwy/7J8hRB29blaw=="],
|
||||
"sst-win32-x86": ["sst-win32-x86@3.17.6", "", { "os": "win32", "cpu": "none" }, "sha512-zV7TJWPJN9PmIXr15iXFSs0tbGsa52oBR3+xiKrUj2qj9XsZe7HBFwskRnHyiFq0durZY9kk9ZtoVlpuUuzr1g=="],
|
||||
|
||||
"stacktracey": ["stacktracey@2.1.8", "", { "dependencies": { "as-table": "^1.0.36", "get-source": "^2.0.12" } }, "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw=="],
|
||||
|
||||
|
||||
+1
-2
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"mcp": {},
|
||||
"provider": {}
|
||||
"mcp": {}
|
||||
}
|
||||
|
||||
+3
-2
@@ -5,7 +5,8 @@
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.2.14",
|
||||
"scripts": {
|
||||
"typecheck": "bun run --filter='*' typecheck"
|
||||
"typecheck": "bun run --filter='*' typecheck",
|
||||
"postinstall": "./scripts/hooks"
|
||||
},
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
@@ -20,7 +21,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "3.5.3",
|
||||
"sst": "3.17.4"
|
||||
"sst": "3.17.6"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -19,9 +19,9 @@ export class SyncServer extends DurableObject<Env> {
|
||||
this.ctx.acceptWebSocket(server)
|
||||
|
||||
const data = await this.ctx.storage.list()
|
||||
for (const [key, content] of data.entries()) {
|
||||
server.send(JSON.stringify({ key, content }))
|
||||
}
|
||||
Array.from(data.entries())
|
||||
.filter(([key, _]) => key.startsWith("session/"))
|
||||
.map(([key, content]) => server.send(JSON.stringify({ key, content })))
|
||||
|
||||
return new Response(null, {
|
||||
status: 101,
|
||||
@@ -71,11 +71,9 @@ export class SyncServer extends DurableObject<Env> {
|
||||
|
||||
public async getData() {
|
||||
const data = await this.ctx.storage.list()
|
||||
const messages = []
|
||||
for (const [key, content] of data.entries()) {
|
||||
messages.push({ key, content })
|
||||
}
|
||||
return messages
|
||||
return Array.from(data.entries())
|
||||
.filter(([key, _]) => key.startsWith("session/"))
|
||||
.map(([key, content]) => ({ key, content }))
|
||||
}
|
||||
|
||||
private async getSecret() {
|
||||
|
||||
@@ -4,11 +4,123 @@
|
||||
"$schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"theme": {
|
||||
"type": "string"
|
||||
},
|
||||
"keybinds": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"leader": {
|
||||
"type": "string"
|
||||
},
|
||||
"help": {
|
||||
"type": "string"
|
||||
},
|
||||
"editor_open": {
|
||||
"type": "string"
|
||||
},
|
||||
"session_new": {
|
||||
"type": "string"
|
||||
},
|
||||
"session_list": {
|
||||
"type": "string"
|
||||
},
|
||||
"session_share": {
|
||||
"type": "string"
|
||||
},
|
||||
"session_interrupt": {
|
||||
"type": "string"
|
||||
},
|
||||
"session_compact": {
|
||||
"type": "string"
|
||||
},
|
||||
"tool_details": {
|
||||
"type": "string"
|
||||
},
|
||||
"model_list": {
|
||||
"type": "string"
|
||||
},
|
||||
"theme_list": {
|
||||
"type": "string"
|
||||
},
|
||||
"project_init": {
|
||||
"type": "string"
|
||||
},
|
||||
"input_clear": {
|
||||
"type": "string"
|
||||
},
|
||||
"input_paste": {
|
||||
"type": "string"
|
||||
},
|
||||
"input_submit": {
|
||||
"type": "string"
|
||||
},
|
||||
"input_newline": {
|
||||
"type": "string"
|
||||
},
|
||||
"history_previous": {
|
||||
"type": "string"
|
||||
},
|
||||
"history_next": {
|
||||
"type": "string"
|
||||
},
|
||||
"messages_page_up": {
|
||||
"type": "string"
|
||||
},
|
||||
"messages_page_down": {
|
||||
"type": "string"
|
||||
},
|
||||
"messages_half_page_up": {
|
||||
"type": "string"
|
||||
},
|
||||
"messages_half_page_down": {
|
||||
"type": "string"
|
||||
},
|
||||
"messages_previous": {
|
||||
"type": "string"
|
||||
},
|
||||
"messages_next": {
|
||||
"type": "string"
|
||||
},
|
||||
"messages_first": {
|
||||
"type": "string"
|
||||
},
|
||||
"messages_last": {
|
||||
"type": "string"
|
||||
},
|
||||
"app_exit": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"autoshare": {
|
||||
"type": "boolean",
|
||||
"description": "Share newly created sessions automatically"
|
||||
},
|
||||
"autoupdate": {
|
||||
"type": "boolean",
|
||||
"description": "Automatically update to the latest version"
|
||||
},
|
||||
"disabled_providers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Disable providers that are loaded automatically"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "Model to use in the format of provider/model, eg anthropic/claude-2"
|
||||
},
|
||||
"provider": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"api": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -50,18 +162,16 @@
|
||||
"output": {
|
||||
"type": "number"
|
||||
},
|
||||
"inputCached": {
|
||||
"cache_read": {
|
||||
"type": "number"
|
||||
},
|
||||
"outputCached": {
|
||||
"cache_write": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"input",
|
||||
"output",
|
||||
"inputCached",
|
||||
"outputCached"
|
||||
"output"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
|
||||
@@ -9,10 +9,7 @@
|
||||
"dev": "bun run ./src/index.ts"
|
||||
},
|
||||
"exports": {
|
||||
"./*": [
|
||||
"./src/*.ts",
|
||||
"./src/*/index.ts"
|
||||
]
|
||||
"./*": "./src/*.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/bun": "1.0.7",
|
||||
|
||||
@@ -7,19 +7,19 @@ import { Share } from "../../share/share"
|
||||
import { Message } from "../../session/message"
|
||||
import { UI } from "../ui"
|
||||
import { cmd } from "./cmd"
|
||||
import { GlobalConfig } from "../../global/config"
|
||||
import { Flag } from "../../flag/flag"
|
||||
import { Config } from "../../config/config"
|
||||
|
||||
const TOOL: Record<string, [string, string]> = {
|
||||
opencode_todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
|
||||
opencode_todoread: ["Todo", UI.Style.TEXT_WARNING_BOLD],
|
||||
opencode_bash: ["Bash", UI.Style.TEXT_DANGER_BOLD],
|
||||
opencode_edit: ["Edit", UI.Style.TEXT_SUCCESS_BOLD],
|
||||
opencode_glob: ["Glob", UI.Style.TEXT_INFO_BOLD],
|
||||
opencode_grep: ["Grep", UI.Style.TEXT_INFO_BOLD],
|
||||
opencode_list: ["List", UI.Style.TEXT_INFO_BOLD],
|
||||
opencode_read: ["Read", UI.Style.TEXT_HIGHLIGHT_BOLD],
|
||||
opencode_write: ["Write", UI.Style.TEXT_SUCCESS_BOLD],
|
||||
todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
|
||||
todoread: ["Todo", UI.Style.TEXT_WARNING_BOLD],
|
||||
bash: ["Bash", UI.Style.TEXT_DANGER_BOLD],
|
||||
edit: ["Edit", UI.Style.TEXT_SUCCESS_BOLD],
|
||||
glob: ["Glob", UI.Style.TEXT_INFO_BOLD],
|
||||
grep: ["Grep", UI.Style.TEXT_INFO_BOLD],
|
||||
list: ["List", UI.Style.TEXT_INFO_BOLD],
|
||||
read: ["Read", UI.Style.TEXT_HIGHLIGHT_BOLD],
|
||||
write: ["Write", UI.Style.TEXT_SUCCESS_BOLD],
|
||||
}
|
||||
|
||||
export const RunCommand = cmd({
|
||||
@@ -60,7 +60,7 @@ export const RunCommand = cmd({
|
||||
UI.println(UI.Style.TEXT_NORMAL_BOLD + "> ", message)
|
||||
UI.empty()
|
||||
|
||||
const cfg = await GlobalConfig.get()
|
||||
const cfg = await Config.get()
|
||||
if (cfg.autoshare || Flag.OPENCODE_AUTO_SHARE || args.share) {
|
||||
await Session.share(session.id)
|
||||
UI.println(
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import { Log } from "../util/log"
|
||||
import path from "path"
|
||||
import { z } from "zod"
|
||||
import { App } from "../app/app"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import { ModelsDev } from "../provider/models"
|
||||
import { mergeDeep } from "remeda"
|
||||
import { Global } from "../global"
|
||||
import fs from "fs/promises"
|
||||
import { lazy } from "../util/lazy"
|
||||
|
||||
export namespace Config {
|
||||
const log = Log.create({ service: "config" })
|
||||
|
||||
export const state = App.state("config", async (app) => {
|
||||
let result: Info = {}
|
||||
let result = await global()
|
||||
for (const file of ["opencode.jsonc", "opencode.json"]) {
|
||||
const [resolved] = await Filesystem.findUp(
|
||||
file,
|
||||
@@ -17,7 +22,10 @@ export namespace Config {
|
||||
)
|
||||
if (!resolved) continue
|
||||
try {
|
||||
result = await import(resolved).then((mod) => Info.parse(mod.default))
|
||||
result = mergeDeep(
|
||||
result,
|
||||
await import(resolved).then((mod) => Info.parse(mod.default)),
|
||||
)
|
||||
log.info("found", { path: resolved })
|
||||
break
|
||||
} catch (e) {
|
||||
@@ -34,23 +42,84 @@ export namespace Config {
|
||||
return result
|
||||
})
|
||||
|
||||
export const McpLocal = z.object({
|
||||
type: z.literal("local"),
|
||||
command: z.string().array(),
|
||||
environment: z.record(z.string(), z.string()).optional(),
|
||||
})
|
||||
export const McpLocal = z
|
||||
.object({
|
||||
type: z.literal("local"),
|
||||
command: z.string().array(),
|
||||
environment: z.record(z.string(), z.string()).optional(),
|
||||
})
|
||||
.openapi({
|
||||
ref: "Config.McpLocal",
|
||||
})
|
||||
|
||||
export const McpRemote = z.object({
|
||||
type: z.literal("remote"),
|
||||
url: z.string(),
|
||||
})
|
||||
export const McpRemote = z
|
||||
.object({
|
||||
type: z.literal("remote"),
|
||||
url: z.string(),
|
||||
})
|
||||
.openapi({
|
||||
ref: "Config.McpRemote",
|
||||
})
|
||||
|
||||
export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote])
|
||||
export type Mcp = z.infer<typeof Mcp>
|
||||
|
||||
export const Keybinds = z
|
||||
.object({
|
||||
leader: z.string().optional(),
|
||||
help: z.string().optional(),
|
||||
editor_open: z.string().optional(),
|
||||
session_new: z.string().optional(),
|
||||
session_list: z.string().optional(),
|
||||
session_share: z.string().optional(),
|
||||
session_interrupt: z.string().optional(),
|
||||
session_compact: z.string().optional(),
|
||||
tool_details: z.string().optional(),
|
||||
model_list: z.string().optional(),
|
||||
theme_list: z.string().optional(),
|
||||
project_init: z.string().optional(),
|
||||
input_clear: z.string().optional(),
|
||||
input_paste: z.string().optional(),
|
||||
input_submit: z.string().optional(),
|
||||
input_newline: z.string().optional(),
|
||||
history_previous: z.string().optional(),
|
||||
history_next: z.string().optional(),
|
||||
messages_page_up: z.string().optional(),
|
||||
messages_page_down: z.string().optional(),
|
||||
messages_half_page_up: z.string().optional(),
|
||||
messages_half_page_down: z.string().optional(),
|
||||
messages_previous: z.string().optional(),
|
||||
messages_next: z.string().optional(),
|
||||
messages_first: z.string().optional(),
|
||||
messages_last: z.string().optional(),
|
||||
app_exit: z.string().optional(),
|
||||
})
|
||||
.openapi({
|
||||
ref: "Config.Keybinds",
|
||||
})
|
||||
export const Info = z
|
||||
.object({
|
||||
$schema: z.string().optional(),
|
||||
theme: z.string().optional(),
|
||||
keybinds: Keybinds.optional(),
|
||||
autoshare: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Share newly created sessions automatically"),
|
||||
autoupdate: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Automatically update to the latest version"),
|
||||
disabled_providers: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe("Disable providers that are loaded automatically"),
|
||||
model: z
|
||||
.string()
|
||||
.describe(
|
||||
"Model to use in the format of provider/model, eg anthropic/claude-2",
|
||||
)
|
||||
.optional(),
|
||||
provider: z
|
||||
.record(
|
||||
ModelsDev.Provider.partial().extend({
|
||||
@@ -61,10 +130,38 @@ export namespace Config {
|
||||
.optional(),
|
||||
mcp: z.record(z.string(), Mcp).optional(),
|
||||
})
|
||||
.strict()
|
||||
.openapi({
|
||||
ref: "Config.Info",
|
||||
})
|
||||
|
||||
export type Info = z.output<typeof Info>
|
||||
|
||||
export const global = lazy(async () => {
|
||||
let result = await Bun.file(path.join(Global.Path.config, "config.json"))
|
||||
.json()
|
||||
.then((mod) => Info.parse(mod))
|
||||
.catch(() => ({}) as Info)
|
||||
|
||||
await import(path.join(Global.Path.config, "config"), {
|
||||
with: {
|
||||
type: "toml",
|
||||
},
|
||||
})
|
||||
.then(async (mod) => {
|
||||
const { provider, model, ...rest } = mod.default
|
||||
if (provider && model) result.model = `${provider}/${model}`
|
||||
result["$schema"] = "https://opencode.ai/config.json"
|
||||
result = mergeDeep(result, rest)
|
||||
await Bun.write(
|
||||
path.join(Global.Path.config, "config.json"),
|
||||
JSON.stringify(result, null, 2),
|
||||
)
|
||||
await fs.unlink(path.join(Global.Path.config, "config"))
|
||||
})
|
||||
.catch(() => {})
|
||||
return Info.parse(result)
|
||||
})
|
||||
|
||||
export function get() {
|
||||
return state()
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { z } from "zod"
|
||||
import { Global } from "."
|
||||
import { lazy } from "../util/lazy"
|
||||
import path from "path"
|
||||
|
||||
export namespace GlobalConfig {
|
||||
export const Info = z.object({
|
||||
provider: z.string().optional(),
|
||||
model: z.string().optional(),
|
||||
autoupdate: z.boolean().optional(),
|
||||
autoshare: z.boolean().optional(),
|
||||
disabled_providers: z.array(z.string()).optional(),
|
||||
})
|
||||
export type Info = z.infer<typeof Info>
|
||||
|
||||
export const get = lazy(async () => {
|
||||
const toml = await import(path.join(Global.Path.config, "config"), {
|
||||
with: {
|
||||
type: "toml",
|
||||
},
|
||||
})
|
||||
.then((mod) => mod.default)
|
||||
.catch(() => ({}))
|
||||
return Info.parse(toml)
|
||||
})
|
||||
}
|
||||
@@ -15,9 +15,9 @@ import { AuthCommand, AuthLoginCommand } from "./cli/cmd/auth"
|
||||
import { UpgradeCommand } from "./cli/cmd/upgrade"
|
||||
import { Provider } from "./provider/provider"
|
||||
import { UI } from "./cli/ui"
|
||||
import { GlobalConfig } from "./global/config"
|
||||
import { Installation } from "./installation"
|
||||
import { Bus } from "./bus"
|
||||
import { Config } from "./config/config"
|
||||
|
||||
const cli = yargs(hideBin(process.argv))
|
||||
.scriptName("opencode")
|
||||
@@ -87,7 +87,7 @@ const cli = yargs(hideBin(process.argv))
|
||||
;(async () => {
|
||||
if (Installation.VERSION === "dev") return
|
||||
if (Installation.isSnapshot()) return
|
||||
const config = await GlobalConfig.get()
|
||||
const config = await Config.global()
|
||||
if (config.autoupdate === false) return
|
||||
const latest = await Installation.latest()
|
||||
if (Installation.VERSION === latest) return
|
||||
|
||||
@@ -32,6 +32,7 @@ export namespace ModelsDev {
|
||||
|
||||
export const Provider = z
|
||||
.object({
|
||||
api: z.string().optional(),
|
||||
name: z.string(),
|
||||
env: z.array(z.string()),
|
||||
id: z.string(),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import z from "zod"
|
||||
import path from "path"
|
||||
import { App } from "../app/app"
|
||||
import { Config } from "../config/config"
|
||||
import { mergeDeep, sortBy } from "remeda"
|
||||
@@ -24,8 +23,6 @@ import { ModelsDev } from "./models"
|
||||
import { NamedError } from "../util/error"
|
||||
import { Auth } from "../auth"
|
||||
import { TaskTool } from "../tool/task"
|
||||
import { GlobalConfig } from "../global/config"
|
||||
import { Global } from "../global"
|
||||
|
||||
export namespace Provider {
|
||||
const log = Log.create({ service: "provider" })
|
||||
@@ -103,6 +100,7 @@ export namespace Provider {
|
||||
if (!provider) {
|
||||
const info = database[id]
|
||||
if (!info) return
|
||||
if (info.api) options["baseURL"] = info.api
|
||||
providers[id] = {
|
||||
source,
|
||||
info,
|
||||
@@ -115,32 +113,6 @@ export namespace Provider {
|
||||
}
|
||||
|
||||
const configProviders = Object.entries(config.provider ?? {})
|
||||
for await (const providerPath of new Bun.Glob("*/provider.toml").scan({
|
||||
cwd: Global.Path.providers,
|
||||
})) {
|
||||
const [providerID] = providerPath.split("/")
|
||||
const toml = await import(
|
||||
path.join(Global.Path.providers, providerPath),
|
||||
{
|
||||
with: {
|
||||
type: "toml",
|
||||
},
|
||||
}
|
||||
).then((mod) => mod.default)
|
||||
toml.models = {}
|
||||
const modelsPath = path.join(Global.Path.providers, providerID, "models")
|
||||
for await (const modelPath of new Bun.Glob("**/*.toml").scan({
|
||||
cwd: modelsPath,
|
||||
})) {
|
||||
const modelID = modelPath.slice(0, -5)
|
||||
toml.models[modelID] = await import(path.join(modelsPath, modelPath), {
|
||||
with: {
|
||||
type: "toml",
|
||||
},
|
||||
})
|
||||
}
|
||||
configProviders.unshift([providerID, toml])
|
||||
}
|
||||
|
||||
for (const [providerID, provider] of configProviders) {
|
||||
const existing = database[providerID]
|
||||
@@ -178,7 +150,7 @@ export namespace Provider {
|
||||
database[providerID] = parsed
|
||||
}
|
||||
|
||||
const disabled = await GlobalConfig.get().then(
|
||||
const disabled = await Config.get().then(
|
||||
(cfg) => new Set(cfg.disabled_providers ?? []),
|
||||
)
|
||||
// load env
|
||||
@@ -299,10 +271,10 @@ export namespace Provider {
|
||||
}
|
||||
|
||||
export async function defaultModel() {
|
||||
const cfg = await GlobalConfig.get()
|
||||
const cfg = await Config.get()
|
||||
const provider = await list()
|
||||
.then((val) => Object.values(val))
|
||||
.then((x) => x.find((p) => !cfg.provider || cfg.provider === p.info.id))
|
||||
.then((x) => x.find((p) => !cfg.provider || Object.keys(cfg.provider).includes(p.info.id)))
|
||||
if (!provider) throw new Error("no providers found")
|
||||
const [model] = sort(Object.values(provider.info.models))
|
||||
if (!model) throw new Error("no models found")
|
||||
@@ -332,7 +304,7 @@ export namespace Provider {
|
||||
]
|
||||
|
||||
const TOOL_MAPPING: Record<string, Tool.Info[]> = {
|
||||
anthropic: TOOLS.filter((t) => t.id !== "opencode.patch"),
|
||||
anthropic: TOOLS.filter((t) => t.id !== "patch"),
|
||||
openai: TOOLS.map((t) => ({
|
||||
...t,
|
||||
parameters: optionalToNullable(t.parameters),
|
||||
|
||||
@@ -15,6 +15,7 @@ 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 = {
|
||||
400: {
|
||||
@@ -140,6 +141,25 @@ export namespace Server {
|
||||
return c.json(App.info())
|
||||
},
|
||||
)
|
||||
.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",
|
||||
describeRoute({
|
||||
|
||||
@@ -30,8 +30,8 @@ import type { Tool } from "../tool/tool"
|
||||
import { SystemPrompt } from "./system"
|
||||
import { Flag } from "../flag/flag"
|
||||
import type { ModelsDev } from "../provider/models"
|
||||
import { GlobalConfig } from "../global/config"
|
||||
import { Installation } from "../installation"
|
||||
import { Config } from "../config/config"
|
||||
|
||||
export namespace Session {
|
||||
const log = Log.create({ service: "session" })
|
||||
@@ -42,7 +42,6 @@ export namespace Session {
|
||||
parentID: Identifier.schema("session").optional(),
|
||||
share: z
|
||||
.object({
|
||||
secret: z.string(),
|
||||
url: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
@@ -58,6 +57,12 @@ export namespace Session {
|
||||
})
|
||||
export type Info = z.output<typeof Info>
|
||||
|
||||
export const ShareInfo = z.object({
|
||||
secret: z.string(),
|
||||
url: z.string(),
|
||||
})
|
||||
export type ShareInfo = z.output<typeof ShareInfo>
|
||||
|
||||
export const Event = {
|
||||
Updated: Bus.event(
|
||||
"session.updated",
|
||||
@@ -109,7 +114,7 @@ export namespace Session {
|
||||
log.info("created", result)
|
||||
state().sessions.set(result.id, result)
|
||||
await Storage.writeJSON("session/info/" + result.id, result)
|
||||
const cfg = await GlobalConfig.get()
|
||||
const cfg = await Config.get()
|
||||
if (!result.parentID && (Flag.OPENCODE_AUTO_SHARE || cfg.autoshare))
|
||||
share(result.id).then((share) => {
|
||||
update(result.id, (draft) => {
|
||||
@@ -132,13 +137,21 @@ export namespace Session {
|
||||
return read as Info
|
||||
}
|
||||
|
||||
export async function getShare(id: string) {
|
||||
return Storage.readJSON<ShareInfo>("session/share/" + id)
|
||||
}
|
||||
|
||||
export async function share(id: string) {
|
||||
const session = await get(id)
|
||||
if (session.share) return session.share
|
||||
const share = await Share.create(id)
|
||||
await update(id, (draft) => {
|
||||
draft.share = share
|
||||
draft.share = {
|
||||
url: share.url,
|
||||
}
|
||||
})
|
||||
await Storage.writeJSON<ShareInfo>("session/share/" + id, share)
|
||||
await Share.sync("session/info/" + id, session)
|
||||
for (const msg of await messages(id)) {
|
||||
await Share.sync("session/message/" + id + "/" + msg.id, msg)
|
||||
}
|
||||
|
||||
@@ -19,10 +19,11 @@ export namespace Share {
|
||||
export async function sync(key: string, content: any) {
|
||||
const [root, ...splits] = key.split("/")
|
||||
if (root !== "session") return
|
||||
const [, sessionID] = splits
|
||||
const session = await Session.get(sessionID)
|
||||
if (!session.share) return
|
||||
const { secret } = session.share
|
||||
const [sub, sessionID] = splits
|
||||
if (sub === "share") return
|
||||
const share = await Session.getShare(sessionID).catch(() => {})
|
||||
if (!share) return
|
||||
const { secret } = share
|
||||
pending.set(key, content)
|
||||
queue = queue
|
||||
.then(async () => {
|
||||
|
||||
@@ -26,7 +26,7 @@ const DEFAULT_TIMEOUT = 1 * 60 * 1000
|
||||
const MAX_TIMEOUT = 10 * 60 * 1000
|
||||
|
||||
export const BashTool = Tool.define({
|
||||
id: "opencode.bash",
|
||||
id: "bash",
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
command: z.string().describe("The command to execute"),
|
||||
|
||||
@@ -9,7 +9,7 @@ import DESCRIPTION from "./edit.txt"
|
||||
import { App } from "../app/app"
|
||||
|
||||
export const EditTool = Tool.define({
|
||||
id: "opencode.edit",
|
||||
id: "edit",
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
filePath: z.string().describe("The absolute path to the file to modify"),
|
||||
@@ -35,7 +35,7 @@ export const EditTool = Tool.define({
|
||||
: path.join(app.path.cwd, params.filePath)
|
||||
|
||||
await Permission.ask({
|
||||
id: "opencode.edit",
|
||||
id: "edit",
|
||||
sessionID: ctx.sessionID,
|
||||
title: "Edit this file: " + filepath,
|
||||
metadata: {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { App } from "../app/app"
|
||||
import DESCRIPTION from "./glob.txt"
|
||||
|
||||
export const GlobTool = Tool.define({
|
||||
id: "opencode.glob",
|
||||
id: "glob",
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
pattern: z.string().describe("The glob pattern to match files against"),
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Ripgrep } from "../external/ripgrep"
|
||||
import DESCRIPTION from "./grep.txt"
|
||||
|
||||
export const GrepTool = Tool.define({
|
||||
id: "opencode.grep",
|
||||
id: "grep",
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
pattern: z
|
||||
|
||||
@@ -21,7 +21,7 @@ export const IGNORE_PATTERNS = [
|
||||
const LIMIT = 100
|
||||
|
||||
export const ListTool = Tool.define({
|
||||
id: "opencode.list",
|
||||
id: "list",
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
path: z
|
||||
|
||||
@@ -6,7 +6,7 @@ import { App } from "../app/app"
|
||||
import DESCRIPTION from "./lsp-diagnostics.txt"
|
||||
|
||||
export const LspDiagnosticTool = Tool.define({
|
||||
id: "opencode.lsp_diagnostics",
|
||||
id: "lsp_diagnostics",
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
path: z.string().describe("The path to the file to get diagnostics."),
|
||||
|
||||
@@ -6,7 +6,7 @@ import { App } from "../app/app"
|
||||
import DESCRIPTION from "./lsp-hover.txt"
|
||||
|
||||
export const LspHoverTool = Tool.define({
|
||||
id: "opencode.lsp_hover",
|
||||
id: "lsp_hover",
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
file: z.string().describe("The path to the file to get diagnostics."),
|
||||
|
||||
@@ -6,7 +6,7 @@ import path from "path"
|
||||
import { App } from "../app/app"
|
||||
|
||||
export const MultiEditTool = Tool.define({
|
||||
id: "opencode.multiedit",
|
||||
id: "multiedit",
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
filePath: z.string().describe("The absolute path to the file to modify"),
|
||||
|
||||
@@ -232,7 +232,7 @@ async function applyCommit(
|
||||
}
|
||||
|
||||
export const PatchTool = Tool.define({
|
||||
id: "opencode.patch",
|
||||
id: "patch",
|
||||
description: DESCRIPTION,
|
||||
parameters: PatchParams,
|
||||
execute: async (params, ctx) => {
|
||||
|
||||
@@ -12,7 +12,7 @@ const DEFAULT_READ_LIMIT = 2000
|
||||
const MAX_LINE_LENGTH = 2000
|
||||
|
||||
export const ReadTool = Tool.define({
|
||||
id: "opencode.read",
|
||||
id: "read",
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
filePath: z.string().describe("The path to the file to read"),
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Bus } from "../bus"
|
||||
import { Message } from "../session/message"
|
||||
|
||||
export const TaskTool = Tool.define({
|
||||
id: "opencode.task",
|
||||
id: "task",
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
description: z
|
||||
|
||||
@@ -23,7 +23,7 @@ const state = App.state("todo-tool", () => {
|
||||
})
|
||||
|
||||
export const TodoWriteTool = Tool.define({
|
||||
id: "opencode.todowrite",
|
||||
id: "todowrite",
|
||||
description: DESCRIPTION_WRITE,
|
||||
parameters: z.object({
|
||||
todos: z.array(TodoInfo).describe("The updated todo list"),
|
||||
@@ -42,7 +42,7 @@ export const TodoWriteTool = Tool.define({
|
||||
})
|
||||
|
||||
export const TodoReadTool = Tool.define({
|
||||
id: "opencode.todoread",
|
||||
id: "todoread",
|
||||
description: "Use this tool to read your todo list",
|
||||
parameters: z.object({}),
|
||||
async execute(_params, opts) {
|
||||
|
||||
@@ -8,7 +8,7 @@ const DEFAULT_TIMEOUT = 30 * 1000 // 30 seconds
|
||||
const MAX_TIMEOUT = 120 * 1000 // 2 minutes
|
||||
|
||||
export const WebFetchTool = Tool.define({
|
||||
id: "opencode.webfetch",
|
||||
id: "webfetch",
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
url: z.string().describe("The URL to fetch content from"),
|
||||
|
||||
@@ -8,7 +8,7 @@ import DESCRIPTION from "./write.txt"
|
||||
import { App } from "../app/app"
|
||||
|
||||
export const WriteTool = Tool.define({
|
||||
id: "opencode.write",
|
||||
id: "write",
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
filePath: z
|
||||
@@ -29,7 +29,7 @@ export const WriteTool = Tool.define({
|
||||
if (exists) await FileTimes.assert(ctx.sessionID, filepath)
|
||||
|
||||
await Permission.ask({
|
||||
id: "opencode.write",
|
||||
id: "write",
|
||||
sessionID: ctx.sessionID,
|
||||
title: exists
|
||||
? "Overwrite this file: " + filepath
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"log/slog"
|
||||
|
||||
@@ -22,8 +23,9 @@ type App struct {
|
||||
Info client.AppInfo
|
||||
Version string
|
||||
StatePath string
|
||||
Config *config.Config
|
||||
Configg *client.ConfigInfo
|
||||
Client *client.ClientWithResponses
|
||||
State *config.State
|
||||
Provider *client.ProviderInfo
|
||||
Model *client.ModelInfo
|
||||
Session *client.SessionInfo
|
||||
@@ -42,6 +44,9 @@ type SendMsg struct {
|
||||
Text string
|
||||
Attachments []Attachment
|
||||
}
|
||||
type CompletionDialogTriggerdMsg struct {
|
||||
InitialValue string
|
||||
}
|
||||
|
||||
func New(
|
||||
ctx context.Context,
|
||||
@@ -51,14 +56,17 @@ func New(
|
||||
) (*App, error) {
|
||||
RootPath = appInfo.Path.Root
|
||||
|
||||
appConfigPath := filepath.Join(appInfo.Path.Config, "config")
|
||||
appConfig, err := config.LoadConfig(appConfigPath)
|
||||
configResponse, err := httpClient.PostConfigGetWithResponse(ctx)
|
||||
if err != nil {
|
||||
appConfig = config.NewConfig()
|
||||
return nil, err
|
||||
}
|
||||
if len(appConfig.Keybinds) == 0 {
|
||||
appConfig.Keybinds = make(map[string]string)
|
||||
appConfig.Keybinds["leader"] = "ctrl+x"
|
||||
configInfo := configResponse.JSON200
|
||||
if configInfo.Keybinds == nil {
|
||||
leader := "ctrl+x"
|
||||
keybinds := client.ConfigKeybinds{
|
||||
Leader: &leader,
|
||||
}
|
||||
configInfo.Keybinds = &keybinds
|
||||
}
|
||||
|
||||
appStatePath := filepath.Join(appInfo.Path.State, "tui")
|
||||
@@ -68,20 +76,31 @@ func New(
|
||||
config.SaveState(appStatePath, appState)
|
||||
}
|
||||
|
||||
mergedConfig := config.MergeState(appState, appConfig)
|
||||
theme.SetTheme(mergedConfig.Theme)
|
||||
if configInfo.Theme != nil {
|
||||
appState.Theme = *configInfo.Theme
|
||||
}
|
||||
if configInfo.Model != nil {
|
||||
splits := strings.Split(*configInfo.Model, "/")
|
||||
appState.Provider = splits[0]
|
||||
appState.Model = strings.Join(splits[1:], "/")
|
||||
}
|
||||
|
||||
slog.Debug("Loaded config", "config", mergedConfig)
|
||||
if appState.Theme != "" {
|
||||
theme.SetTheme(appState.Theme)
|
||||
}
|
||||
|
||||
slog.Debug("Loaded config", "config", configInfo)
|
||||
|
||||
app := &App{
|
||||
Info: appInfo,
|
||||
Version: version,
|
||||
StatePath: appStatePath,
|
||||
Config: mergedConfig,
|
||||
Configg: configInfo,
|
||||
State: appState,
|
||||
Client: httpClient,
|
||||
Session: &client.SessionInfo{},
|
||||
Messages: []client.MessageInfo{},
|
||||
Commands: commands.LoadFromConfig(mergedConfig),
|
||||
Commands: commands.LoadFromConfig(configInfo),
|
||||
}
|
||||
|
||||
return app, nil
|
||||
@@ -127,11 +146,11 @@ func (a *App) InitializeProvider() tea.Cmd {
|
||||
var currentProvider *client.ProviderInfo
|
||||
var currentModel *client.ModelInfo
|
||||
for _, provider := range providers {
|
||||
if provider.Id == a.Config.Provider {
|
||||
if provider.Id == a.State.Provider {
|
||||
currentProvider = &provider
|
||||
|
||||
for _, model := range provider.Models {
|
||||
if model.Id == a.Config.Model {
|
||||
if model.Id == a.State.Model {
|
||||
currentModel = &model
|
||||
}
|
||||
}
|
||||
@@ -179,8 +198,7 @@ func (a *App) IsBusy() bool {
|
||||
}
|
||||
|
||||
func (a *App) SaveState() {
|
||||
state := config.ConfigToState(a.Config)
|
||||
err := config.SaveState(a.StatePath, state)
|
||||
err := config.SaveState(a.StatePath, a.State)
|
||||
if err != nil {
|
||||
slog.Error("Failed to save state", "error", err)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/sst/opencode/internal/config"
|
||||
"github.com/sst/opencode/pkg/client"
|
||||
)
|
||||
|
||||
type ExecuteCommandMsg Command
|
||||
@@ -106,13 +107,6 @@ func (k Command) Matches(msg tea.KeyPressMsg, leader bool) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (k Command) FromConfig(config *config.Config) Command {
|
||||
if keybind, ok := config.Keybinds[string(k.Name)]; ok {
|
||||
k.Keybindings = parseBindings(keybind)
|
||||
}
|
||||
return k
|
||||
}
|
||||
|
||||
func parseBindings(bindings ...string) []Keybinding {
|
||||
var parsedBindings []Keybinding
|
||||
for _, binding := range bindings {
|
||||
@@ -129,7 +123,7 @@ func parseBindings(bindings ...string) []Keybinding {
|
||||
return parsedBindings
|
||||
}
|
||||
|
||||
func LoadFromConfig(config *config.Config) CommandRegistry {
|
||||
func LoadFromConfig(config *client.ConfigInfo) CommandRegistry {
|
||||
defaults := []Command{
|
||||
{
|
||||
Name: AppHelpCommand,
|
||||
@@ -214,18 +208,18 @@ func LoadFromConfig(config *config.Config) CommandRegistry {
|
||||
{
|
||||
Name: InputNewlineCommand,
|
||||
Description: "insert newline",
|
||||
Keybindings: parseBindings("shift+enter"),
|
||||
},
|
||||
{
|
||||
Name: HistoryPreviousCommand,
|
||||
Description: "previous prompt",
|
||||
Keybindings: parseBindings("up"),
|
||||
},
|
||||
{
|
||||
Name: HistoryNextCommand,
|
||||
Description: "next prompt",
|
||||
Keybindings: parseBindings("down"),
|
||||
Keybindings: parseBindings("shift+enter", "ctrl+j"),
|
||||
},
|
||||
// {
|
||||
// Name: HistoryPreviousCommand,
|
||||
// Description: "previous prompt",
|
||||
// Keybindings: parseBindings("up"),
|
||||
// },
|
||||
// {
|
||||
// Name: HistoryNextCommand,
|
||||
// Description: "next prompt",
|
||||
// Keybindings: parseBindings("down"),
|
||||
// },
|
||||
{
|
||||
Name: MessagesPageUpCommand,
|
||||
Description: "page up",
|
||||
@@ -274,8 +268,14 @@ func LoadFromConfig(config *config.Config) CommandRegistry {
|
||||
},
|
||||
}
|
||||
registry := make(CommandRegistry)
|
||||
keybinds := map[string]string{}
|
||||
marshalled, _ := json.Marshal(*config.Keybinds)
|
||||
json.Unmarshal(marshalled, &keybinds)
|
||||
for _, command := range defaults {
|
||||
registry[command.Name] = command.FromConfig(config)
|
||||
if keybind, ok := keybinds[string(command.Name)]; ok {
|
||||
command.Keybindings = parseBindings(keybind)
|
||||
}
|
||||
registry[command.Name] = command
|
||||
}
|
||||
return registry
|
||||
}
|
||||
|
||||
@@ -20,10 +20,13 @@ func NewCompletionManager(app *app.App) *CompletionManager {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *CompletionManager) DefaultProvider() dialog.CompletionProvider {
|
||||
return m.providers["commands"]
|
||||
}
|
||||
|
||||
func (m *CompletionManager) GetProvider(input string) dialog.CompletionProvider {
|
||||
if strings.HasPrefix(input, "/") {
|
||||
return m.providers["commands"]
|
||||
}
|
||||
return m.providers["files"]
|
||||
}
|
||||
|
||||
|
||||
@@ -6,12 +6,12 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/v2/spinner"
|
||||
"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/commands"
|
||||
"github.com/sst/opencode/internal/components/dialog"
|
||||
"github.com/sst/opencode/internal/components/textarea"
|
||||
"github.com/sst/opencode/internal/image"
|
||||
"github.com/sst/opencode/internal/layout"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
@@ -23,6 +23,8 @@ type EditorComponent interface {
|
||||
tea.Model
|
||||
tea.ViewModel
|
||||
layout.Sizeable
|
||||
Content() string
|
||||
Lines() int
|
||||
Value() string
|
||||
Submit() (tea.Model, tea.Cmd)
|
||||
Clear() (tea.Model, tea.Cmd)
|
||||
@@ -50,22 +52,15 @@ func (m *editorComponent) Init() tea.Cmd {
|
||||
func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
var cmd tea.Cmd
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyPressMsg:
|
||||
// Maximize editor responsiveness for printable characters
|
||||
if msg.Text != "" {
|
||||
m.textarea, cmd = m.textarea.Update(msg)
|
||||
return m, cmd
|
||||
cmds = append(cmds, cmd)
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
// // TODO: ?
|
||||
// if key.Matches(msg, messageKeys.PageUp) ||
|
||||
// key.Matches(msg, messageKeys.PageDown) ||
|
||||
// key.Matches(msg, messageKeys.HalfPageUp) ||
|
||||
// key.Matches(msg, messageKeys.HalfPageDown) {
|
||||
// return m, nil
|
||||
// }
|
||||
|
||||
case dialog.ThemeSelectedMsg:
|
||||
m.textarea = createTextArea(&m.textarea)
|
||||
m.spinner = createSpinner()
|
||||
@@ -73,14 +68,15 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case dialog.CompletionSelectedMsg:
|
||||
if msg.IsCommand {
|
||||
commandName := strings.TrimPrefix(msg.CompletionValue, "/")
|
||||
m.textarea.Reset()
|
||||
return m, util.CmdHandler(
|
||||
commands.ExecuteCommandMsg(m.app.Commands[commands.CommandName(commandName)]),
|
||||
)
|
||||
updated, cmd := m.Clear()
|
||||
m = updated.(*editorComponent)
|
||||
cmds = append(cmds, cmd)
|
||||
cmds = append(cmds, util.CmdHandler(commands.ExecuteCommandMsg(m.app.Commands[commands.CommandName(commandName)])))
|
||||
return m, tea.Batch(cmds...)
|
||||
} else {
|
||||
existingValue := m.textarea.Value()
|
||||
modifiedValue := strings.Replace(existingValue, msg.SearchString, msg.CompletionValue, 1)
|
||||
m.textarea.SetValue(modifiedValue)
|
||||
m.textarea.SetValue(modifiedValue + " ")
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
@@ -94,7 +90,7 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m *editorComponent) View() string {
|
||||
func (m *editorComponent) Content() string {
|
||||
t := theme.CurrentTheme()
|
||||
base := styles.BaseStyle().Background(t.Background()).Render
|
||||
muted := styles.Muted().Background(t.Background()).Render
|
||||
@@ -115,7 +111,7 @@ func (m *editorComponent) View() string {
|
||||
PaddingBottom(1).
|
||||
Background(t.BackgroundElement()).
|
||||
Border(lipgloss.ThickBorder(), false, true).
|
||||
BorderForeground(t.BackgroundSubtle()).
|
||||
BorderForeground(t.BackgroundElement()).
|
||||
BorderBackground(t.Background()).
|
||||
Render(textarea)
|
||||
|
||||
@@ -126,7 +122,7 @@ func (m *editorComponent) View() string {
|
||||
|
||||
model := ""
|
||||
if m.app.Model != nil {
|
||||
model = base(m.app.Model.Name) + muted(" • /model")
|
||||
model = muted(m.app.Provider.Name) + base(" "+m.app.Model.Name)
|
||||
}
|
||||
|
||||
space := m.width - 2 - lipgloss.Width(model) - lipgloss.Width(hint)
|
||||
@@ -139,6 +135,13 @@ func (m *editorComponent) View() string {
|
||||
return content
|
||||
}
|
||||
|
||||
func (m *editorComponent) View() string {
|
||||
if m.Lines() > 1 {
|
||||
return ""
|
||||
}
|
||||
return m.Content()
|
||||
}
|
||||
|
||||
func (m *editorComponent) GetSize() (width, height int) {
|
||||
return m.width, m.height
|
||||
}
|
||||
@@ -146,18 +149,21 @@ 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) // account for info underneath
|
||||
m.textarea.SetWidth(width - 5) // account for the prompt and padding right
|
||||
// m.textarea.SetHeight(height - 4)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *editorComponent) Lines() int {
|
||||
return m.textarea.LineCount()
|
||||
}
|
||||
|
||||
func (m *editorComponent) Value() string {
|
||||
return strings.TrimSpace(m.textarea.Value())
|
||||
return m.textarea.Value()
|
||||
}
|
||||
|
||||
func (m *editorComponent) Submit() (tea.Model, tea.Cmd) {
|
||||
value := m.Value()
|
||||
m.textarea.Reset()
|
||||
value := strings.TrimSpace(m.Value())
|
||||
if value == "" {
|
||||
return m, nil
|
||||
}
|
||||
@@ -167,6 +173,11 @@ func (m *editorComponent) Submit() (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
var cmds []tea.Cmd
|
||||
updated, cmd := m.Clear()
|
||||
m = updated.(*editorComponent)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
attachments := m.attachments
|
||||
|
||||
// Save to history if not empty and not a duplicate of the last entry
|
||||
@@ -180,12 +191,8 @@ func (m *editorComponent) Submit() (tea.Model, tea.Cmd) {
|
||||
|
||||
m.attachments = nil
|
||||
|
||||
return m, tea.Batch(
|
||||
util.CmdHandler(app.SendMsg{
|
||||
Text: value,
|
||||
Attachments: attachments,
|
||||
}),
|
||||
)
|
||||
cmds = append(cmds, util.CmdHandler(app.SendMsg{Text: value, Attachments: attachments}))
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m *editorComponent) Clear() (tea.Model, tea.Cmd) {
|
||||
|
||||
@@ -253,7 +253,7 @@ func renderToolInvocation(
|
||||
showDetails bool,
|
||||
isLast bool,
|
||||
) string {
|
||||
ignoredTools := []string{"opencode_todoread"}
|
||||
ignoredTools := []string{"todoread"}
|
||||
if slices.Contains(ignoredTools, toolCall.ToolName) {
|
||||
return ""
|
||||
}
|
||||
@@ -350,7 +350,7 @@ func renderToolInvocation(
|
||||
|
||||
title := ""
|
||||
switch toolCall.ToolName {
|
||||
case "opencode_read":
|
||||
case "read":
|
||||
toolArgs = renderArgs(&toolArgsMap, "filePath")
|
||||
title = fmt.Sprintf("READ %s %s", toolArgs, elapsed)
|
||||
if preview, ok := metadata.Get("preview"); ok && toolArgsMap["filePath"] != nil {
|
||||
@@ -358,7 +358,7 @@ func renderToolInvocation(
|
||||
body = preview.(string)
|
||||
body = renderFile(filename, body, WithTruncate(6))
|
||||
}
|
||||
case "opencode_edit":
|
||||
case "edit":
|
||||
if filename, ok := toolArgsMap["filePath"].(string); ok {
|
||||
title = fmt.Sprintf("EDIT %s %s", relative(filename), elapsed)
|
||||
if d, ok := metadata.Get("diff"); ok {
|
||||
@@ -399,14 +399,14 @@ func renderToolInvocation(
|
||||
)
|
||||
}
|
||||
}
|
||||
case "opencode_write":
|
||||
case "write":
|
||||
if filename, ok := toolArgsMap["filePath"].(string); ok {
|
||||
title = fmt.Sprintf("WRITE %s %s", relative(filename), elapsed)
|
||||
if content, ok := toolArgsMap["content"].(string); ok {
|
||||
body = renderFile(filename, content)
|
||||
}
|
||||
}
|
||||
case "opencode_bash":
|
||||
case "bash":
|
||||
if description, ok := toolArgsMap["description"].(string); ok {
|
||||
title = fmt.Sprintf("SHELL %s %s", description, elapsed)
|
||||
}
|
||||
@@ -417,7 +417,7 @@ func renderToolInvocation(
|
||||
body = toMarkdown(body, innerWidth, t.BackgroundSubtle())
|
||||
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
|
||||
}
|
||||
case "opencode_webfetch":
|
||||
case "webfetch":
|
||||
toolArgs = renderArgs(&toolArgsMap, "url")
|
||||
title = fmt.Sprintf("FETCH %s %s", toolArgs, elapsed)
|
||||
if format, ok := toolArgsMap["format"].(string); ok {
|
||||
@@ -428,7 +428,7 @@ func renderToolInvocation(
|
||||
}
|
||||
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
|
||||
}
|
||||
case "opencode_todowrite":
|
||||
case "todowrite":
|
||||
title = fmt.Sprintf("PLAN %s", elapsed)
|
||||
|
||||
if to, ok := metadata.Get("todos"); ok && finished {
|
||||
@@ -498,11 +498,11 @@ func renderToolName(name string) string {
|
||||
switch name {
|
||||
// case agent.AgentToolName:
|
||||
// return "Task"
|
||||
case "opencode_ls":
|
||||
case "list":
|
||||
return "LIST"
|
||||
case "opencode_webfetch":
|
||||
case "webfetch":
|
||||
return "FETCH"
|
||||
case "opencode_todowrite":
|
||||
case "todowrite":
|
||||
return "PLAN"
|
||||
default:
|
||||
normalizedName := name
|
||||
@@ -559,27 +559,27 @@ func renderToolAction(name string) string {
|
||||
switch name {
|
||||
// case agent.AgentToolName:
|
||||
// return "Preparing prompt..."
|
||||
case "opencode_bash":
|
||||
case "bash":
|
||||
return "Building command..."
|
||||
case "opencode_edit":
|
||||
case "edit":
|
||||
return "Preparing edit..."
|
||||
case "opencode_fetch":
|
||||
case "webfetch":
|
||||
return "Writing fetch..."
|
||||
case "opencode_glob":
|
||||
case "glob":
|
||||
return "Finding files..."
|
||||
case "opencode_grep":
|
||||
case "grep":
|
||||
return "Searching content..."
|
||||
case "opencode_ls":
|
||||
case "list":
|
||||
return "Listing directory..."
|
||||
case "opencode_read":
|
||||
case "read":
|
||||
return "Reading file..."
|
||||
case "opencode_write":
|
||||
case "write":
|
||||
return "Preparing write..."
|
||||
case "opencode_todowrite", "opencode_todoread":
|
||||
case "todowrite", "todoread":
|
||||
return "Planning..."
|
||||
case "opencode_patch":
|
||||
case "patch":
|
||||
return "Preparing patch..."
|
||||
case "opencode_batch":
|
||||
case "batch":
|
||||
return "Running batch operations..."
|
||||
}
|
||||
return "Working..."
|
||||
|
||||
@@ -266,7 +266,7 @@ func (m *messagesComponent) header() string {
|
||||
BorderLeft(true).
|
||||
BorderRight(true).
|
||||
BorderBackground(t.Background()).
|
||||
BorderForeground(t.BackgroundSubtle()).
|
||||
BorderForeground(t.BackgroundElement()).
|
||||
BorderStyle(lipgloss.ThickBorder()).
|
||||
Render(header)
|
||||
|
||||
@@ -322,7 +322,7 @@ func (m *messagesComponent) home() string {
|
||||
{"/new", "start a new session"},
|
||||
{"/model", "switch model"},
|
||||
{"/theme", "switch theme"},
|
||||
{"/quit", "exit the app"},
|
||||
{"/exit", "exit the app"},
|
||||
}
|
||||
|
||||
commandLines := []string{}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"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"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
@@ -100,10 +101,10 @@ type completionDialogKeyMap struct {
|
||||
|
||||
var completionDialogKeys = completionDialogKeyMap{
|
||||
Complete: key.NewBinding(
|
||||
key.WithKeys("tab", "enter"),
|
||||
key.WithKeys("tab", "enter", "right"),
|
||||
),
|
||||
Cancel: key.NewBinding(
|
||||
key.WithKeys(" ", "esc", "backspace"),
|
||||
key.WithKeys(" ", "esc", "backspace", "ctrl+c"),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -111,42 +112,15 @@ func (c *completionDialogComponent) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *completionDialogComponent) complete(item CompletionItemI) tea.Cmd {
|
||||
value := c.pseudoSearchTextArea.Value()
|
||||
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if this is a command completion
|
||||
isCommand := c.completionProvider.GetId() == "commands"
|
||||
|
||||
return tea.Batch(
|
||||
util.CmdHandler(CompletionSelectedMsg{
|
||||
SearchString: value,
|
||||
CompletionValue: item.GetValue(),
|
||||
IsCommand: isCommand,
|
||||
}),
|
||||
c.close(),
|
||||
)
|
||||
}
|
||||
|
||||
func (c *completionDialogComponent) close() tea.Cmd {
|
||||
c.list.SetItems([]CompletionItemI{})
|
||||
c.pseudoSearchTextArea.Reset()
|
||||
c.pseudoSearchTextArea.Blur()
|
||||
|
||||
return util.CmdHandler(CompletionDialogCloseMsg{})
|
||||
}
|
||||
|
||||
func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
switch msg := msg.(type) {
|
||||
case []CompletionItemI:
|
||||
c.list.SetItems(msg)
|
||||
case app.CompletionDialogTriggerdMsg:
|
||||
c.pseudoSearchTextArea.SetValue(msg.InitialValue)
|
||||
case tea.KeyMsg:
|
||||
if c.pseudoSearchTextArea.Focused() {
|
||||
slog.Info("CompletionDialog", "key", msg.String(), "focused", true)
|
||||
if !key.Matches(msg, completionDialogKeys.Complete) {
|
||||
var cmd tea.Cmd
|
||||
c.pseudoSearchTextArea, cmd = c.pseudoSearchTextArea.Update(msg)
|
||||
@@ -162,7 +136,6 @@ func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
c.query = query
|
||||
cmd = func() tea.Msg {
|
||||
items, err := c.completionProvider.GetChildEntries(query)
|
||||
slog.Info("CompletionDialog", "query", query, "items", len(items))
|
||||
if err != nil {
|
||||
slog.Error("Failed to get completion items", "error", err)
|
||||
}
|
||||
@@ -192,18 +165,16 @@ func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
return c, tea.Batch(cmds...)
|
||||
} else {
|
||||
slog.Info("CompletionDialog", "key", msg.String(), "focused", false)
|
||||
cmd := func() tea.Msg {
|
||||
items, err := c.completionProvider.GetChildEntries("")
|
||||
if err != nil {
|
||||
slog.Error("Failed to get completion items", "error", err)
|
||||
// status.Error(err.Error())
|
||||
}
|
||||
return items
|
||||
}
|
||||
cmds = append(cmds, cmd)
|
||||
cmds = append(cmds, c.pseudoSearchTextArea.Focus())
|
||||
c.pseudoSearchTextArea.SetValue(msg.String())
|
||||
// c.pseudoSearchTextArea.SetValue(msg.String())
|
||||
return c, tea.Batch(cmds...)
|
||||
}
|
||||
case tea.WindowSizeMsg:
|
||||
@@ -238,7 +209,7 @@ func (c *completionDialogComponent) View() string {
|
||||
BorderRight(true).
|
||||
BorderLeft(true).
|
||||
BorderBackground(t.Background()).
|
||||
BorderForeground(t.BackgroundSubtle()).
|
||||
BorderForeground(t.BackgroundElement()).
|
||||
Width(c.width).
|
||||
Render(c.list.View())
|
||||
}
|
||||
@@ -258,6 +229,34 @@ func (c *completionDialogComponent) SetProvider(provider CompletionProvider) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *completionDialogComponent) complete(item CompletionItemI) tea.Cmd {
|
||||
value := c.pseudoSearchTextArea.Value()
|
||||
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if this is a command completion
|
||||
isCommand := c.completionProvider.GetId() == "commands"
|
||||
|
||||
return tea.Batch(
|
||||
util.CmdHandler(CompletionSelectedMsg{
|
||||
SearchString: value,
|
||||
CompletionValue: item.GetValue(),
|
||||
IsCommand: isCommand,
|
||||
}),
|
||||
c.close(),
|
||||
)
|
||||
}
|
||||
|
||||
func (c *completionDialogComponent) close() tea.Cmd {
|
||||
c.list.SetItems([]CompletionItemI{})
|
||||
c.pseudoSearchTextArea.Reset()
|
||||
c.pseudoSearchTextArea.Blur()
|
||||
|
||||
return util.CmdHandler(CompletionDialogCloseMsg{})
|
||||
}
|
||||
|
||||
func NewCompletionDialogComponent(completionProvider CompletionProvider) CompletionDialog {
|
||||
ti := textarea.New()
|
||||
|
||||
@@ -271,7 +270,7 @@ func NewCompletionDialogComponent(completionProvider CompletionProvider) Complet
|
||||
go func() {
|
||||
items, err := completionProvider.GetChildEntries("")
|
||||
if err != nil {
|
||||
// status.Error(err.Error())
|
||||
slog.Error("Failed to get completion items", "error", err)
|
||||
}
|
||||
li.SetItems(items)
|
||||
}()
|
||||
|
||||
@@ -83,6 +83,6 @@ type HelpDialog interface {
|
||||
func NewHelpDialog(commands []commands.Command) HelpDialog {
|
||||
return &helpDialog{
|
||||
commands: commands,
|
||||
modal: modal.New(),
|
||||
modal: modal.New(modal.WithTitle("Help")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,6 +180,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)
|
||||
}
|
||||
|
||||
@@ -189,14 +190,6 @@ func (m *modelDialog) View() string {
|
||||
Background(t.BackgroundElement()).
|
||||
Foreground(t.Text())
|
||||
|
||||
// Capitalize first letter of provider name
|
||||
title := baseStyle.
|
||||
Foreground(t.Primary()).
|
||||
Bold(true).
|
||||
Width(maxDialogWidth).
|
||||
Padding(0, 0, 1).
|
||||
Render(fmt.Sprintf("Select %s Model", m.provider.Name))
|
||||
|
||||
// Render visible models
|
||||
endIdx := min(m.scrollOffset+numVisibleModels, len(m.provider.Models))
|
||||
modelItems := make([]string, 0, endIdx-m.scrollOffset)
|
||||
@@ -217,8 +210,9 @@ func (m *modelDialog) View() string {
|
||||
|
||||
content := lipgloss.JoinVertical(
|
||||
lipgloss.Left,
|
||||
title,
|
||||
baseStyle.Width(maxDialogWidth).Render(lipgloss.JoinVertical(lipgloss.Left, modelItems...)),
|
||||
baseStyle.
|
||||
Width(maxDialogWidth).
|
||||
Render(lipgloss.JoinVertical(lipgloss.Left, modelItems...)),
|
||||
scrollIndicator,
|
||||
)
|
||||
|
||||
@@ -238,12 +232,7 @@ func (m *modelDialog) getScrollIndicators(maxWidth int) string {
|
||||
}
|
||||
|
||||
if m.hScrollPossible {
|
||||
if m.hScrollOffset > 0 {
|
||||
indicator = "← " + indicator
|
||||
}
|
||||
if m.hScrollOffset < len(m.availableProviders)-1 {
|
||||
indicator += "→"
|
||||
}
|
||||
indicator = "← " + indicator + "→"
|
||||
}
|
||||
|
||||
if indicator == "" {
|
||||
@@ -313,6 +302,6 @@ func NewModelDialog(app *app.App) ModelDialog {
|
||||
hScrollOffset: 0,
|
||||
hScrollPossible: len(availableProviders) > 1,
|
||||
provider: availableProviders[0],
|
||||
modal: modal.New(),
|
||||
modal: modal.New(modal.WithTitle(fmt.Sprintf("Select %s Model", availableProviders[0].Name))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,14 +19,12 @@ type SessionDialog interface {
|
||||
layout.Modal
|
||||
}
|
||||
|
||||
type sessionItem struct {
|
||||
session client.SessionInfo
|
||||
}
|
||||
type sessionItem client.SessionInfo
|
||||
|
||||
func (s sessionItem) Render(selected bool, width int) string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle().
|
||||
Width(width - 2).
|
||||
Width(width - 4).
|
||||
Background(t.BackgroundElement())
|
||||
|
||||
if selected {
|
||||
@@ -39,7 +37,7 @@ func (s sessionItem) Render(selected bool, width int) string {
|
||||
Foreground(t.Text())
|
||||
}
|
||||
|
||||
return baseStyle.Padding(0, 1).Render(s.session.Title)
|
||||
return baseStyle.Padding(0, 1).Render(s.Title)
|
||||
}
|
||||
|
||||
type sessionDialog struct {
|
||||
@@ -60,15 +58,14 @@ func (s *sessionDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
s.width = msg.Width
|
||||
s.height = msg.Height
|
||||
s.list.SetMaxWidth(layout.Current.Container.Width - 12)
|
||||
case tea.KeyMsg:
|
||||
case tea.KeyPressMsg:
|
||||
switch msg.String() {
|
||||
case "enter":
|
||||
if item, idx := s.list.GetSelectedItem(); idx >= 0 {
|
||||
selectedSession := item.session
|
||||
s.selectedSessionID = selectedSession.Id
|
||||
s.selectedSessionID = item.Id
|
||||
return s, tea.Sequence(
|
||||
util.CmdHandler(modal.CloseModalMsg{}),
|
||||
util.CmdHandler(app.SessionSelectedMsg(&selectedSession)),
|
||||
util.CmdHandler(app.SessionSelectedMsg(&item)),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -94,7 +91,10 @@ func NewSessionDialog(app *app.App) SessionDialog {
|
||||
|
||||
var sessionItems []sessionItem
|
||||
for _, sess := range sessions {
|
||||
sessionItems = append(sessionItems, sessionItem{session: sess})
|
||||
if sess.ParentID != nil {
|
||||
continue
|
||||
}
|
||||
sessionItems = append(sessionItems, sessionItem(sess))
|
||||
}
|
||||
|
||||
list := list.NewListComponent(
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package list
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/v2/key"
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
)
|
||||
|
||||
type ListItem interface {
|
||||
@@ -148,7 +149,7 @@ func (c *listComponent[T]) View() string {
|
||||
listItems = append(listItems, title)
|
||||
}
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Left, listItems...)
|
||||
return strings.Join(listItems, "\n")
|
||||
}
|
||||
|
||||
func NewListComponent[T ListItem](items []T, maxVisibleItems int, fallbackMsg string, useAlphaNumericKeys bool) List[T] {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package modal
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/sst/opencode/internal/layout"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
@@ -66,6 +68,10 @@ func New(opts ...ModalOption) *Modal {
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Modal) SetTitle(title string) {
|
||||
m.title = title
|
||||
}
|
||||
|
||||
// Render renders the modal centered on the screen
|
||||
func (m *Modal) Render(contentView string, background string) string {
|
||||
t := theme.CurrentTheme()
|
||||
@@ -95,15 +101,21 @@ func (m *Modal) Render(contentView string, background string) string {
|
||||
titleStyle := baseStyle.
|
||||
Foreground(t.Primary()).
|
||||
Bold(true).
|
||||
Width(innerWidth).
|
||||
Padding(0, 1)
|
||||
|
||||
titleView := titleStyle.Render(m.title)
|
||||
finalContent = lipgloss.JoinVertical(
|
||||
lipgloss.Left,
|
||||
titleView,
|
||||
contentView,
|
||||
)
|
||||
// titleView := titleStyle.Render(m.title)
|
||||
escStyle := baseStyle.Foreground(t.TextMuted()).Bold(false)
|
||||
escText := escStyle.Render("esc")
|
||||
|
||||
// Calculate position for esc text
|
||||
titleWidth := lipgloss.Width(m.title)
|
||||
escWidth := lipgloss.Width(escText)
|
||||
spacesNeeded := max(0, innerWidth-titleWidth-escWidth-3)
|
||||
spacer := strings.Repeat(" ", spacesNeeded)
|
||||
titleLine := m.title + spacer + escText
|
||||
titleLine = titleStyle.Render(titleLine)
|
||||
|
||||
finalContent = strings.Join([]string{titleLine, contentView}, "\n") + "\n"
|
||||
} else {
|
||||
finalContent = contentView
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
// Package memoization implement a simple memoization cache. It's designed to
|
||||
// improve performance in textarea.
|
||||
package textarea
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Hasher is an interface that requires a Hash method. The Hash method is
|
||||
// expected to return a string representation of the hash of the object.
|
||||
type Hasher interface {
|
||||
Hash() string
|
||||
}
|
||||
|
||||
// entry is a struct that holds a key-value pair. It is used as an element
|
||||
// in the evictionList of the MemoCache.
|
||||
type entry[T any] struct {
|
||||
key string
|
||||
value T
|
||||
}
|
||||
|
||||
// MemoCache is a struct that represents a cache with a set capacity. It
|
||||
// uses an LRU (Least Recently Used) eviction policy. It is safe for
|
||||
// concurrent use.
|
||||
type MemoCache[H Hasher, T any] struct {
|
||||
capacity int
|
||||
mutex sync.Mutex
|
||||
cache map[string]*list.Element // The cache holding the results
|
||||
evictionList *list.List // A list to keep track of the order for LRU
|
||||
hashableItems map[string]T // This map keeps track of the original hashable items (optional)
|
||||
}
|
||||
|
||||
// NewMemoCache is a function that creates a new MemoCache with a given
|
||||
// capacity. It returns a pointer to the created MemoCache.
|
||||
func NewMemoCache[H Hasher, T any](capacity int) *MemoCache[H, T] {
|
||||
return &MemoCache[H, T]{
|
||||
capacity: capacity,
|
||||
cache: make(map[string]*list.Element),
|
||||
evictionList: list.New(),
|
||||
hashableItems: make(map[string]T),
|
||||
}
|
||||
}
|
||||
|
||||
// Capacity is a method that returns the capacity of the MemoCache.
|
||||
func (m *MemoCache[H, T]) Capacity() int {
|
||||
return m.capacity
|
||||
}
|
||||
|
||||
// Size is a method that returns the current size of the MemoCache. It is
|
||||
// the number of items currently stored in the cache.
|
||||
func (m *MemoCache[H, T]) Size() int {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
return m.evictionList.Len()
|
||||
}
|
||||
|
||||
// Get is a method that returns the value associated with the given
|
||||
// hashable item in the MemoCache. If there is no corresponding value, the
|
||||
// method returns nil.
|
||||
func (m *MemoCache[H, T]) Get(h H) (T, bool) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
hashedKey := h.Hash()
|
||||
if element, found := m.cache[hashedKey]; found {
|
||||
m.evictionList.MoveToFront(element)
|
||||
return element.Value.(*entry[T]).value, true
|
||||
}
|
||||
var result T
|
||||
return result, false
|
||||
}
|
||||
|
||||
// Set is a method that sets the value for the given hashable item in the
|
||||
// MemoCache. If the cache is at capacity, it evicts the least recently
|
||||
// used item before adding the new item.
|
||||
func (m *MemoCache[H, T]) Set(h H, value T) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
hashedKey := h.Hash()
|
||||
if element, found := m.cache[hashedKey]; found {
|
||||
m.evictionList.MoveToFront(element)
|
||||
element.Value.(*entry[T]).value = value
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the cache is at capacity
|
||||
if m.evictionList.Len() >= m.capacity {
|
||||
// Evict the least recently used item from the cache
|
||||
toEvict := m.evictionList.Back()
|
||||
if toEvict != nil {
|
||||
evictedEntry := m.evictionList.Remove(toEvict).(*entry[T])
|
||||
delete(m.cache, evictedEntry.key)
|
||||
delete(m.hashableItems, evictedEntry.key) // if you're keeping track of original items
|
||||
}
|
||||
}
|
||||
|
||||
// Add the value to the cache and the evictionList
|
||||
newEntry := &entry[T]{
|
||||
key: hashedKey,
|
||||
value: value,
|
||||
}
|
||||
element := m.evictionList.PushFront(newEntry)
|
||||
m.cache[hashedKey] = element
|
||||
m.hashableItems[hashedKey] = value // if you're keeping track of original items
|
||||
}
|
||||
|
||||
// HString is a type that implements the Hasher interface for strings.
|
||||
type HString string
|
||||
|
||||
// Hash is a method that returns the hash of the string.
|
||||
func (h HString) Hash() string {
|
||||
return fmt.Sprintf("%x", sha256.Sum256([]byte(h)))
|
||||
}
|
||||
|
||||
// HInt is a type that implements the Hasher interface for integers.
|
||||
type HInt int
|
||||
|
||||
// Hash is a method that returns the hash of the integer.
|
||||
func (h HInt) Hash() string {
|
||||
return fmt.Sprintf("%x", sha256.Sum256([]byte(fmt.Sprintf("%d", h))))
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// Package runeutil provides utility functions for tidying up incoming runes
|
||||
// from Key messages.
|
||||
package textarea
|
||||
|
||||
import (
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Sanitizer is a helper for bubble widgets that want to process
|
||||
// Runes from input key messages.
|
||||
type Sanitizer interface {
|
||||
// Sanitize removes control characters from runes in a KeyRunes
|
||||
// message, and optionally replaces newline/carriage return/tabs by a
|
||||
// specified character.
|
||||
//
|
||||
// The rune array is modified in-place if possible. In that case, the
|
||||
// returned slice is the original slice shortened after the control
|
||||
// characters have been removed/translated.
|
||||
Sanitize(runes []rune) []rune
|
||||
}
|
||||
|
||||
// NewSanitizer constructs a rune sanitizer.
|
||||
func NewSanitizer(opts ...Option) Sanitizer {
|
||||
s := sanitizer{
|
||||
replaceNewLine: []rune("\n"),
|
||||
replaceTab: []rune(" "),
|
||||
}
|
||||
for _, o := range opts {
|
||||
s = o(s)
|
||||
}
|
||||
return &s
|
||||
}
|
||||
|
||||
// Option is the type of option that can be passed to Sanitize().
|
||||
type Option func(sanitizer) sanitizer
|
||||
|
||||
// ReplaceTabs replaces tabs by the specified string.
|
||||
func ReplaceTabs(tabRepl string) Option {
|
||||
return func(s sanitizer) sanitizer {
|
||||
s.replaceTab = []rune(tabRepl)
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
// ReplaceNewlines replaces newline characters by the specified string.
|
||||
func ReplaceNewlines(nlRepl string) Option {
|
||||
return func(s sanitizer) sanitizer {
|
||||
s.replaceNewLine = []rune(nlRepl)
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sanitizer) Sanitize(runes []rune) []rune {
|
||||
// dstrunes are where we are storing the result.
|
||||
dstrunes := runes[:0:len(runes)]
|
||||
// copied indicates whether dstrunes is an alias of runes
|
||||
// or a copy. We need a copy when dst moves past src.
|
||||
// We use this as an optimization to avoid allocating
|
||||
// a new rune slice in the common case where the output
|
||||
// is smaller or equal to the input.
|
||||
copied := false
|
||||
|
||||
for src := 0; src < len(runes); src++ {
|
||||
r := runes[src]
|
||||
switch {
|
||||
case r == utf8.RuneError:
|
||||
// skip
|
||||
|
||||
case r == '\r' || r == '\n':
|
||||
if len(dstrunes)+len(s.replaceNewLine) > src && !copied {
|
||||
dst := len(dstrunes)
|
||||
dstrunes = make([]rune, dst, len(runes)+len(s.replaceNewLine))
|
||||
copy(dstrunes, runes[:dst])
|
||||
copied = true
|
||||
}
|
||||
dstrunes = append(dstrunes, s.replaceNewLine...)
|
||||
|
||||
case r == '\t':
|
||||
if len(dstrunes)+len(s.replaceTab) > src && !copied {
|
||||
dst := len(dstrunes)
|
||||
dstrunes = make([]rune, dst, len(runes)+len(s.replaceTab))
|
||||
copy(dstrunes, runes[:dst])
|
||||
copied = true
|
||||
}
|
||||
dstrunes = append(dstrunes, s.replaceTab...)
|
||||
|
||||
case unicode.IsControl(r):
|
||||
// Other control characters: skip.
|
||||
|
||||
default:
|
||||
// Keep the character.
|
||||
dstrunes = append(dstrunes, runes[src])
|
||||
}
|
||||
}
|
||||
return dstrunes
|
||||
}
|
||||
|
||||
type sanitizer struct {
|
||||
replaceNewLine []rune
|
||||
replaceTab []rune
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/sst/opencode/pkg/client"
|
||||
)
|
||||
|
||||
type State struct {
|
||||
@@ -15,44 +16,15 @@ type State struct {
|
||||
Model string `toml:"model"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Theme string `toml:"theme"`
|
||||
Provider string `toml:"provider"`
|
||||
Model string `toml:"model"`
|
||||
Keybinds map[string]string `toml:"keybinds"`
|
||||
}
|
||||
|
||||
func NewState() *State {
|
||||
return &State{
|
||||
Theme: "opencode",
|
||||
}
|
||||
}
|
||||
|
||||
func NewConfig() *Config {
|
||||
keybinds := make(map[string]string)
|
||||
keybinds["leader"] = "ctrl+x"
|
||||
return &Config{
|
||||
Keybinds: keybinds,
|
||||
}
|
||||
}
|
||||
|
||||
func ConfigToState(config *Config) *State {
|
||||
return &State{
|
||||
Theme: config.Theme,
|
||||
Provider: config.Provider,
|
||||
Model: config.Model,
|
||||
}
|
||||
}
|
||||
|
||||
func MergeState(state *State, config *Config) *Config {
|
||||
if config.Theme == "" {
|
||||
config.Theme = state.Theme
|
||||
}
|
||||
if config.Provider == "" {
|
||||
config.Provider = state.Provider
|
||||
}
|
||||
if config.Model == "" {
|
||||
config.Model = state.Model
|
||||
func MergeState(state *State, config *client.ConfigInfo) *client.ConfigInfo {
|
||||
if config.Theme == nil {
|
||||
config.Theme = &state.Theme
|
||||
}
|
||||
return config
|
||||
}
|
||||
@@ -79,19 +51,6 @@ func SaveState(filePath string, state *State) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadConfig reads a Config struct from the specified TOML file.
|
||||
// It returns a pointer to the Config struct and an error if any issues occur.
|
||||
func LoadConfig(filePath string) (*Config, error) {
|
||||
var config Config
|
||||
if _, err := toml.DecodeFile(filePath, &config); err != nil {
|
||||
if _, statErr := os.Stat(filePath); os.IsNotExist(statErr) {
|
||||
return nil, fmt.Errorf("config file not found at %s: %w", filePath, statErr)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to decode TOML from file %s: %w", filePath, err)
|
||||
}
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// LoadState loads the state from the specified TOML file.
|
||||
// It returns a pointer to the State struct and an error if any issues occur.
|
||||
func LoadState(filePath string) (*State, error) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/v2/key"
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
@@ -90,10 +91,49 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
// 3. Handle completions trigger
|
||||
switch msg.String() {
|
||||
case "/":
|
||||
a.showCompletionDialog = true
|
||||
if !a.showCompletionDialog {
|
||||
a.showCompletionDialog = true
|
||||
|
||||
initialValue := "/"
|
||||
currentInput := a.editor.Value()
|
||||
// if the input doesn't end with a space,
|
||||
// then we want to include the last word
|
||||
if !strings.HasSuffix(currentInput, " ") {
|
||||
words := strings.Split(a.editor.Value(), " ")
|
||||
if len(words) > 0 {
|
||||
lastWord := words[len(words)-1]
|
||||
lastWord = strings.TrimSpace(lastWord)
|
||||
initialValue = lastWord + "/"
|
||||
}
|
||||
}
|
||||
updated, cmd := a.completions.Update(
|
||||
app.CompletionDialogTriggerdMsg{
|
||||
InitialValue: initialValue,
|
||||
},
|
||||
)
|
||||
a.completions = updated.(dialog.CompletionDialog)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
updated, cmd = a.completions.Update(msg)
|
||||
a.completions = updated.(dialog.CompletionDialog)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
updated, cmd = a.editor.Update(msg)
|
||||
a.editor = updated.(chat.EditorComponent)
|
||||
cmds = append(cmds, cmd)
|
||||
return a, tea.Sequence(cmds...)
|
||||
}
|
||||
}
|
||||
|
||||
if a.showCompletionDialog {
|
||||
switch msg.String() {
|
||||
case "tab", "enter", "esc", "ctrl+c":
|
||||
context, contextCmd := a.completions.Update(msg)
|
||||
a.completions = context.(dialog.CompletionDialog)
|
||||
cmds = append(cmds, contextCmd)
|
||||
return a, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
updated, cmd := a.editor.Update(msg)
|
||||
a.editor = updated.(chat.EditorComponent)
|
||||
cmds = append(cmds, cmd)
|
||||
@@ -106,11 +146,6 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
a.completions = context.(dialog.CompletionDialog)
|
||||
cmds = append(cmds, contextCmd)
|
||||
return a, tea.Batch(cmds...)
|
||||
|
||||
// Doesn't forward event if enter key is pressed
|
||||
// if msg.String() == "enter" {
|
||||
// return a, tea.Batch(cmds...)
|
||||
// }
|
||||
}
|
||||
|
||||
// 4. Maximize editor responsiveness for printable characters
|
||||
@@ -173,6 +208,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
cmds = append(cmds, cmd)
|
||||
case dialog.CompletionDialogCloseMsg:
|
||||
a.showCompletionDialog = false
|
||||
a.completions.SetProvider(a.completionManager.DefaultProvider())
|
||||
case client.EventSessionUpdated:
|
||||
if msg.Properties.Info.Id == a.app.Session.Id {
|
||||
a.app.Session = &msg.Properties.Info
|
||||
@@ -204,16 +240,20 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
a.layout.SetSize(a.width, a.height)
|
||||
case app.SessionSelectedMsg:
|
||||
var err error
|
||||
a.app.Session = msg
|
||||
a.app.Messages, _ = a.app.ListMessages(context.Background(), msg.Id)
|
||||
a.app.Messages, err = a.app.ListMessages(context.Background(), msg.Id)
|
||||
if err != nil {
|
||||
slog.Error("Failed to list messages", "error", err)
|
||||
}
|
||||
case app.ModelSelectedMsg:
|
||||
a.app.Provider = &msg.Provider
|
||||
a.app.Model = &msg.Model
|
||||
a.app.Config.Provider = msg.Provider.Id
|
||||
a.app.Config.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.Config.Theme = msg.ThemeName
|
||||
a.app.State.Theme = msg.ThemeName
|
||||
a.app.SaveState()
|
||||
}
|
||||
|
||||
@@ -250,14 +290,22 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
func (a appModel) View() string {
|
||||
layoutView := a.layout.View()
|
||||
editorWidth, _ := a.editorContainer.GetSize()
|
||||
editorX, editorY := a.editorContainer.GetPosition()
|
||||
|
||||
if a.editor.Lines() > 1 {
|
||||
editorY = editorY - a.editor.Lines() + 1
|
||||
layoutView = layout.PlaceOverlay(
|
||||
editorX,
|
||||
editorY,
|
||||
a.editor.Content(),
|
||||
layoutView,
|
||||
)
|
||||
}
|
||||
|
||||
if a.showCompletionDialog {
|
||||
editorWidth, _ := a.editorContainer.GetSize()
|
||||
editorX, editorY := a.editorContainer.GetPosition()
|
||||
|
||||
a.completions.SetWidth(editorWidth)
|
||||
overlay := a.completions.View()
|
||||
|
||||
layoutView = layout.PlaceOverlay(
|
||||
editorX,
|
||||
editorY-lipgloss.Height(overlay)+2,
|
||||
@@ -455,7 +503,7 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
|
||||
|
||||
func NewModel(app *app.App) tea.Model {
|
||||
completionManager := completions.NewCompletionManager(app)
|
||||
initialProvider := completionManager.GetProvider("")
|
||||
initialProvider := completionManager.DefaultProvider()
|
||||
|
||||
messages := chat.NewMessagesComponent(app)
|
||||
editor := chat.NewEditorComponent(app)
|
||||
@@ -469,8 +517,8 @@ func NewModel(app *app.App) tea.Model {
|
||||
messagesContainer := layout.NewContainer(messages)
|
||||
|
||||
var leaderBinding *key.Binding
|
||||
if leader, ok := app.Config.Keybinds["leader"]; ok {
|
||||
binding := key.NewBinding(key.WithKeys(leader))
|
||||
if (*app.Configg.Keybinds).Leader != nil {
|
||||
binding := key.NewBinding(key.WithKeys(*app.Configg.Keybinds.Leader))
|
||||
leaderBinding = &binding
|
||||
}
|
||||
|
||||
@@ -490,7 +538,7 @@ func NewModel(app *app.App) tea.Model {
|
||||
layout.WithDirection(layout.FlexDirectionVertical),
|
||||
layout.WithSizes(
|
||||
layout.FlexChildSizeGrow,
|
||||
layout.FlexChildSizeFixed(6),
|
||||
layout.FlexChildSizeFixed(5),
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -44,6 +44,25 @@
|
||||
"description": "Get app info"
|
||||
}
|
||||
},
|
||||
"/config_get": {
|
||||
"post": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Get config info",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Config.Info"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"operationId": "postConfig_get",
|
||||
"parameters": [],
|
||||
"description": "Get config info"
|
||||
}
|
||||
},
|
||||
"/app_initialize": {
|
||||
"post": {
|
||||
"responses": {
|
||||
@@ -518,14 +537,14 @@
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.message.part.updated"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.installation.updated"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.session.updated"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.session.error"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.installation.updated"
|
||||
}
|
||||
],
|
||||
"discriminator": {
|
||||
@@ -536,9 +555,9 @@
|
||||
"permission.updated": "#/components/schemas/Event.permission.updated",
|
||||
"message.updated": "#/components/schemas/Event.message.updated",
|
||||
"message.part.updated": "#/components/schemas/Event.message.part.updated",
|
||||
"installation.updated": "#/components/schemas/Event.installation.updated",
|
||||
"session.updated": "#/components/schemas/Event.session.updated",
|
||||
"session.error": "#/components/schemas/Event.session.error",
|
||||
"installation.updated": "#/components/schemas/Event.installation.updated"
|
||||
"session.error": "#/components/schemas/Event.session.error"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1182,6 +1201,30 @@
|
||||
"properties"
|
||||
]
|
||||
},
|
||||
"Event.installation.updated": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "installation.updated"
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"version"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"properties"
|
||||
]
|
||||
},
|
||||
"Event.session.updated": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1220,21 +1263,20 @@
|
||||
"share": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret",
|
||||
"url"
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"time": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1254,6 +1296,7 @@
|
||||
"required": [
|
||||
"id",
|
||||
"title",
|
||||
"version",
|
||||
"time"
|
||||
]
|
||||
},
|
||||
@@ -1292,30 +1335,6 @@
|
||||
"properties"
|
||||
]
|
||||
},
|
||||
"Event.installation.updated": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "installation.updated"
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"version"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"properties"
|
||||
]
|
||||
},
|
||||
"App.Info": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1368,21 +1387,243 @@
|
||||
"time"
|
||||
]
|
||||
},
|
||||
"Error": {
|
||||
"Config.Info": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"theme": {
|
||||
"type": "string"
|
||||
},
|
||||
"keybinds": {
|
||||
"$ref": "#/components/schemas/Config.Keybinds"
|
||||
},
|
||||
"autoshare": {
|
||||
"type": "boolean",
|
||||
"description": "Share newly created sessions automatically"
|
||||
},
|
||||
"autoupdate": {
|
||||
"type": "boolean",
|
||||
"description": "Automatically update to the latest version"
|
||||
},
|
||||
"disabled_providers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Disable providers that are loaded automatically"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "Model to use in the format of provider/model, eg anthropic/claude-2"
|
||||
},
|
||||
"provider": {
|
||||
"type": "object",
|
||||
"additionalProperties": {}
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"api": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"env": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"npm": {
|
||||
"type": "string"
|
||||
},
|
||||
"models": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"attachment": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"reasoning": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"cost": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"input": {
|
||||
"type": "number"
|
||||
},
|
||||
"output": {
|
||||
"type": "number"
|
||||
},
|
||||
"cache_read": {
|
||||
"type": "number"
|
||||
},
|
||||
"cache_write": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"input",
|
||||
"output"
|
||||
]
|
||||
},
|
||||
"limit": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"context": {
|
||||
"type": "number"
|
||||
},
|
||||
"output": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"context",
|
||||
"output"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"type": "object",
|
||||
"additionalProperties": {}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"models"
|
||||
]
|
||||
}
|
||||
},
|
||||
"mcp": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Config.McpLocal"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Config.McpRemote"
|
||||
}
|
||||
],
|
||||
"discriminator": {
|
||||
"propertyName": "type",
|
||||
"mapping": {
|
||||
"local": "#/components/schemas/Config.McpLocal",
|
||||
"remote": "#/components/schemas/Config.McpRemote"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data"
|
||||
]
|
||||
}
|
||||
},
|
||||
"Config.Keybinds": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"leader": {
|
||||
"type": "string"
|
||||
},
|
||||
"help": {
|
||||
"type": "string"
|
||||
},
|
||||
"editor_open": {
|
||||
"type": "string"
|
||||
},
|
||||
"session_new": {
|
||||
"type": "string"
|
||||
},
|
||||
"session_list": {
|
||||
"type": "string"
|
||||
},
|
||||
"session_share": {
|
||||
"type": "string"
|
||||
},
|
||||
"session_interrupt": {
|
||||
"type": "string"
|
||||
},
|
||||
"session_compact": {
|
||||
"type": "string"
|
||||
},
|
||||
"tool_details": {
|
||||
"type": "string"
|
||||
},
|
||||
"model_list": {
|
||||
"type": "string"
|
||||
},
|
||||
"theme_list": {
|
||||
"type": "string"
|
||||
},
|
||||
"project_init": {
|
||||
"type": "string"
|
||||
},
|
||||
"input_clear": {
|
||||
"type": "string"
|
||||
},
|
||||
"input_paste": {
|
||||
"type": "string"
|
||||
},
|
||||
"input_submit": {
|
||||
"type": "string"
|
||||
},
|
||||
"input_newline": {
|
||||
"type": "string"
|
||||
},
|
||||
"history_previous": {
|
||||
"type": "string"
|
||||
},
|
||||
"history_next": {
|
||||
"type": "string"
|
||||
},
|
||||
"messages_page_up": {
|
||||
"type": "string"
|
||||
},
|
||||
"messages_page_down": {
|
||||
"type": "string"
|
||||
},
|
||||
"messages_half_page_up": {
|
||||
"type": "string"
|
||||
},
|
||||
"messages_half_page_down": {
|
||||
"type": "string"
|
||||
},
|
||||
"messages_previous": {
|
||||
"type": "string"
|
||||
},
|
||||
"messages_next": {
|
||||
"type": "string"
|
||||
},
|
||||
"messages_first": {
|
||||
"type": "string"
|
||||
},
|
||||
"messages_last": {
|
||||
"type": "string"
|
||||
},
|
||||
"app_exit": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Provider.Info": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"api": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1436,18 +1677,16 @@
|
||||
"output": {
|
||||
"type": "number"
|
||||
},
|
||||
"inputCached": {
|
||||
"cache_read": {
|
||||
"type": "number"
|
||||
},
|
||||
"outputCached": {
|
||||
"cache_write": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"input",
|
||||
"output",
|
||||
"inputCached",
|
||||
"outputCached"
|
||||
"output"
|
||||
]
|
||||
},
|
||||
"limit": {
|
||||
@@ -1479,6 +1718,59 @@
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"Config.McpLocal": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "local"
|
||||
},
|
||||
"command": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"environment": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"command"
|
||||
]
|
||||
},
|
||||
"Config.McpRemote": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "remote"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"url"
|
||||
]
|
||||
},
|
||||
"Error": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "object",
|
||||
"additionalProperties": {}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"InstallationInfo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -39,6 +39,100 @@ type AppInfo struct {
|
||||
User string `json:"user"`
|
||||
}
|
||||
|
||||
// ConfigInfo defines model for Config.Info.
|
||||
type ConfigInfo struct {
|
||||
Schema *string `json:"$schema,omitempty"`
|
||||
|
||||
// Autoshare Share newly created sessions automatically
|
||||
Autoshare *bool `json:"autoshare,omitempty"`
|
||||
|
||||
// Autoupdate Automatically update to the latest version
|
||||
Autoupdate *bool `json:"autoupdate,omitempty"`
|
||||
|
||||
// DisabledProviders Disable providers that are loaded automatically
|
||||
DisabledProviders *[]string `json:"disabled_providers,omitempty"`
|
||||
Keybinds *ConfigKeybinds `json:"keybinds,omitempty"`
|
||||
Mcp *map[string]ConfigInfo_Mcp_AdditionalProperties `json:"mcp,omitempty"`
|
||||
|
||||
// Model Model to use in the format of provider/model, eg anthropic/claude-2
|
||||
Model *string `json:"model,omitempty"`
|
||||
Provider *map[string]struct {
|
||||
Api *string `json:"api,omitempty"`
|
||||
Env *[]string `json:"env,omitempty"`
|
||||
Id *string `json:"id,omitempty"`
|
||||
Models map[string]struct {
|
||||
Attachment *bool `json:"attachment,omitempty"`
|
||||
Cost *struct {
|
||||
CacheRead *float32 `json:"cache_read,omitempty"`
|
||||
CacheWrite *float32 `json:"cache_write,omitempty"`
|
||||
Input float32 `json:"input"`
|
||||
Output float32 `json:"output"`
|
||||
} `json:"cost,omitempty"`
|
||||
Id *string `json:"id,omitempty"`
|
||||
Limit *struct {
|
||||
Context float32 `json:"context"`
|
||||
Output float32 `json:"output"`
|
||||
} `json:"limit,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
Reasoning *bool `json:"reasoning,omitempty"`
|
||||
Temperature *bool `json:"temperature,omitempty"`
|
||||
} `json:"models"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
Npm *string `json:"npm,omitempty"`
|
||||
Options *map[string]interface{} `json:"options,omitempty"`
|
||||
} `json:"provider,omitempty"`
|
||||
Theme *string `json:"theme,omitempty"`
|
||||
}
|
||||
|
||||
// ConfigInfo_Mcp_AdditionalProperties defines model for Config.Info.mcp.AdditionalProperties.
|
||||
type ConfigInfo_Mcp_AdditionalProperties struct {
|
||||
union json.RawMessage
|
||||
}
|
||||
|
||||
// ConfigKeybinds defines model for Config.Keybinds.
|
||||
type ConfigKeybinds struct {
|
||||
AppExit *string `json:"app_exit,omitempty"`
|
||||
EditorOpen *string `json:"editor_open,omitempty"`
|
||||
Help *string `json:"help,omitempty"`
|
||||
HistoryNext *string `json:"history_next,omitempty"`
|
||||
HistoryPrevious *string `json:"history_previous,omitempty"`
|
||||
InputClear *string `json:"input_clear,omitempty"`
|
||||
InputNewline *string `json:"input_newline,omitempty"`
|
||||
InputPaste *string `json:"input_paste,omitempty"`
|
||||
InputSubmit *string `json:"input_submit,omitempty"`
|
||||
Leader *string `json:"leader,omitempty"`
|
||||
MessagesFirst *string `json:"messages_first,omitempty"`
|
||||
MessagesHalfPageDown *string `json:"messages_half_page_down,omitempty"`
|
||||
MessagesHalfPageUp *string `json:"messages_half_page_up,omitempty"`
|
||||
MessagesLast *string `json:"messages_last,omitempty"`
|
||||
MessagesNext *string `json:"messages_next,omitempty"`
|
||||
MessagesPageDown *string `json:"messages_page_down,omitempty"`
|
||||
MessagesPageUp *string `json:"messages_page_up,omitempty"`
|
||||
MessagesPrevious *string `json:"messages_previous,omitempty"`
|
||||
ModelList *string `json:"model_list,omitempty"`
|
||||
ProjectInit *string `json:"project_init,omitempty"`
|
||||
SessionCompact *string `json:"session_compact,omitempty"`
|
||||
SessionInterrupt *string `json:"session_interrupt,omitempty"`
|
||||
SessionList *string `json:"session_list,omitempty"`
|
||||
SessionNew *string `json:"session_new,omitempty"`
|
||||
SessionShare *string `json:"session_share,omitempty"`
|
||||
ThemeList *string `json:"theme_list,omitempty"`
|
||||
ToolDetails *string `json:"tool_details,omitempty"`
|
||||
}
|
||||
|
||||
// ConfigMcpLocal defines model for Config.McpLocal.
|
||||
type ConfigMcpLocal struct {
|
||||
Command []string `json:"command"`
|
||||
Environment *map[string]string `json:"environment,omitempty"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// ConfigMcpRemote defines model for Config.McpRemote.
|
||||
type ConfigMcpRemote struct {
|
||||
Type string `json:"type"`
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
// Error defines model for Error.
|
||||
type Error struct {
|
||||
Data map[string]interface{} `json:"data"`
|
||||
@@ -263,10 +357,10 @@ type MessageToolInvocationToolResult struct {
|
||||
type ModelInfo struct {
|
||||
Attachment bool `json:"attachment"`
|
||||
Cost struct {
|
||||
Input float32 `json:"input"`
|
||||
InputCached float32 `json:"inputCached"`
|
||||
Output float32 `json:"output"`
|
||||
OutputCached float32 `json:"outputCached"`
|
||||
CacheRead *float32 `json:"cache_read,omitempty"`
|
||||
CacheWrite *float32 `json:"cache_write,omitempty"`
|
||||
Input float32 `json:"input"`
|
||||
Output float32 `json:"output"`
|
||||
} `json:"cost"`
|
||||
Id string `json:"id"`
|
||||
Limit struct {
|
||||
@@ -280,6 +374,7 @@ type ModelInfo struct {
|
||||
|
||||
// ProviderInfo defines model for Provider.Info.
|
||||
type ProviderInfo struct {
|
||||
Api *string `json:"api,omitempty"`
|
||||
Env []string `json:"env"`
|
||||
Id string `json:"id"`
|
||||
Models map[string]ModelInfo `json:"models"`
|
||||
@@ -320,14 +415,14 @@ type SessionInfo struct {
|
||||
Id string `json:"id"`
|
||||
ParentID *string `json:"parentID,omitempty"`
|
||||
Share *struct {
|
||||
Secret string `json:"secret"`
|
||||
Url string `json:"url"`
|
||||
Url string `json:"url"`
|
||||
} `json:"share,omitempty"`
|
||||
Time struct {
|
||||
Created float32 `json:"created"`
|
||||
Updated float32 `json:"updated"`
|
||||
} `json:"time"`
|
||||
Title string `json:"title"`
|
||||
Title string `json:"title"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// PostFileSearchJSONBody defines parameters for PostFileSearch.
|
||||
@@ -472,6 +567,95 @@ func (a MessageInfo_Metadata_Tool_AdditionalProperties) MarshalJSON() ([]byte, e
|
||||
return json.Marshal(object)
|
||||
}
|
||||
|
||||
// AsConfigMcpLocal returns the union data inside the ConfigInfo_Mcp_AdditionalProperties as a ConfigMcpLocal
|
||||
func (t ConfigInfo_Mcp_AdditionalProperties) AsConfigMcpLocal() (ConfigMcpLocal, error) {
|
||||
var body ConfigMcpLocal
|
||||
err := json.Unmarshal(t.union, &body)
|
||||
return body, err
|
||||
}
|
||||
|
||||
// FromConfigMcpLocal overwrites any union data inside the ConfigInfo_Mcp_AdditionalProperties as the provided ConfigMcpLocal
|
||||
func (t *ConfigInfo_Mcp_AdditionalProperties) FromConfigMcpLocal(v ConfigMcpLocal) error {
|
||||
v.Type = "local"
|
||||
b, err := json.Marshal(v)
|
||||
t.union = b
|
||||
return err
|
||||
}
|
||||
|
||||
// MergeConfigMcpLocal performs a merge with any union data inside the ConfigInfo_Mcp_AdditionalProperties, using the provided ConfigMcpLocal
|
||||
func (t *ConfigInfo_Mcp_AdditionalProperties) MergeConfigMcpLocal(v ConfigMcpLocal) error {
|
||||
v.Type = "local"
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
merged, err := runtime.JSONMerge(t.union, b)
|
||||
t.union = merged
|
||||
return err
|
||||
}
|
||||
|
||||
// AsConfigMcpRemote returns the union data inside the ConfigInfo_Mcp_AdditionalProperties as a ConfigMcpRemote
|
||||
func (t ConfigInfo_Mcp_AdditionalProperties) AsConfigMcpRemote() (ConfigMcpRemote, error) {
|
||||
var body ConfigMcpRemote
|
||||
err := json.Unmarshal(t.union, &body)
|
||||
return body, err
|
||||
}
|
||||
|
||||
// FromConfigMcpRemote overwrites any union data inside the ConfigInfo_Mcp_AdditionalProperties as the provided ConfigMcpRemote
|
||||
func (t *ConfigInfo_Mcp_AdditionalProperties) FromConfigMcpRemote(v ConfigMcpRemote) error {
|
||||
v.Type = "remote"
|
||||
b, err := json.Marshal(v)
|
||||
t.union = b
|
||||
return err
|
||||
}
|
||||
|
||||
// MergeConfigMcpRemote performs a merge with any union data inside the ConfigInfo_Mcp_AdditionalProperties, using the provided ConfigMcpRemote
|
||||
func (t *ConfigInfo_Mcp_AdditionalProperties) MergeConfigMcpRemote(v ConfigMcpRemote) error {
|
||||
v.Type = "remote"
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
merged, err := runtime.JSONMerge(t.union, b)
|
||||
t.union = merged
|
||||
return err
|
||||
}
|
||||
|
||||
func (t ConfigInfo_Mcp_AdditionalProperties) Discriminator() (string, error) {
|
||||
var discriminator struct {
|
||||
Discriminator string `json:"type"`
|
||||
}
|
||||
err := json.Unmarshal(t.union, &discriminator)
|
||||
return discriminator.Discriminator, err
|
||||
}
|
||||
|
||||
func (t ConfigInfo_Mcp_AdditionalProperties) ValueByDiscriminator() (interface{}, error) {
|
||||
discriminator, err := t.Discriminator()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch discriminator {
|
||||
case "local":
|
||||
return t.AsConfigMcpLocal()
|
||||
case "remote":
|
||||
return t.AsConfigMcpRemote()
|
||||
default:
|
||||
return nil, errors.New("unknown discriminator value: " + discriminator)
|
||||
}
|
||||
}
|
||||
|
||||
func (t ConfigInfo_Mcp_AdditionalProperties) MarshalJSON() ([]byte, error) {
|
||||
b, err := t.union.MarshalJSON()
|
||||
return b, err
|
||||
}
|
||||
|
||||
func (t *ConfigInfo_Mcp_AdditionalProperties) UnmarshalJSON(b []byte) error {
|
||||
err := t.union.UnmarshalJSON(b)
|
||||
return err
|
||||
}
|
||||
|
||||
// AsEventStorageWrite returns the union data inside the Event as a EventStorageWrite
|
||||
func (t Event) AsEventStorageWrite() (EventStorageWrite, error) {
|
||||
var body EventStorageWrite
|
||||
@@ -612,6 +796,34 @@ func (t *Event) MergeEventMessagePartUpdated(v EventMessagePartUpdated) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// AsEventInstallationUpdated returns the union data inside the Event as a EventInstallationUpdated
|
||||
func (t Event) AsEventInstallationUpdated() (EventInstallationUpdated, error) {
|
||||
var body EventInstallationUpdated
|
||||
err := json.Unmarshal(t.union, &body)
|
||||
return body, err
|
||||
}
|
||||
|
||||
// FromEventInstallationUpdated overwrites any union data inside the Event as the provided EventInstallationUpdated
|
||||
func (t *Event) FromEventInstallationUpdated(v EventInstallationUpdated) error {
|
||||
v.Type = "installation.updated"
|
||||
b, err := json.Marshal(v)
|
||||
t.union = b
|
||||
return err
|
||||
}
|
||||
|
||||
// MergeEventInstallationUpdated performs a merge with any union data inside the Event, using the provided EventInstallationUpdated
|
||||
func (t *Event) MergeEventInstallationUpdated(v EventInstallationUpdated) error {
|
||||
v.Type = "installation.updated"
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
merged, err := runtime.JSONMerge(t.union, b)
|
||||
t.union = merged
|
||||
return err
|
||||
}
|
||||
|
||||
// AsEventSessionUpdated returns the union data inside the Event as a EventSessionUpdated
|
||||
func (t Event) AsEventSessionUpdated() (EventSessionUpdated, error) {
|
||||
var body EventSessionUpdated
|
||||
@@ -668,34 +880,6 @@ func (t *Event) MergeEventSessionError(v EventSessionError) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// AsEventInstallationUpdated returns the union data inside the Event as a EventInstallationUpdated
|
||||
func (t Event) AsEventInstallationUpdated() (EventInstallationUpdated, error) {
|
||||
var body EventInstallationUpdated
|
||||
err := json.Unmarshal(t.union, &body)
|
||||
return body, err
|
||||
}
|
||||
|
||||
// FromEventInstallationUpdated overwrites any union data inside the Event as the provided EventInstallationUpdated
|
||||
func (t *Event) FromEventInstallationUpdated(v EventInstallationUpdated) error {
|
||||
v.Type = "installation.updated"
|
||||
b, err := json.Marshal(v)
|
||||
t.union = b
|
||||
return err
|
||||
}
|
||||
|
||||
// MergeEventInstallationUpdated performs a merge with any union data inside the Event, using the provided EventInstallationUpdated
|
||||
func (t *Event) MergeEventInstallationUpdated(v EventInstallationUpdated) error {
|
||||
v.Type = "installation.updated"
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
merged, err := runtime.JSONMerge(t.union, b)
|
||||
t.union = merged
|
||||
return err
|
||||
}
|
||||
|
||||
func (t Event) Discriminator() (string, error) {
|
||||
var discriminator struct {
|
||||
Discriminator string `json:"type"`
|
||||
@@ -1326,6 +1510,9 @@ type ClientInterface interface {
|
||||
// PostAppInitialize request
|
||||
PostAppInitialize(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error)
|
||||
|
||||
// PostConfigGet request
|
||||
PostConfigGet(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error)
|
||||
|
||||
// GetEvent request
|
||||
GetEvent(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error)
|
||||
|
||||
@@ -1404,6 +1591,18 @@ func (c *Client) PostAppInitialize(ctx context.Context, reqEditors ...RequestEdi
|
||||
return c.Client.Do(req)
|
||||
}
|
||||
|
||||
func (c *Client) PostConfigGet(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) {
|
||||
req, err := NewPostConfigGetRequest(c.Server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
if err := c.applyEditors(ctx, req, reqEditors); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.Client.Do(req)
|
||||
}
|
||||
|
||||
func (c *Client) GetEvent(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) {
|
||||
req, err := NewGetEventRequest(c.Server)
|
||||
if err != nil {
|
||||
@@ -1698,6 +1897,33 @@ func NewPostAppInitializeRequest(server string) (*http.Request, error) {
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// NewPostConfigGetRequest generates requests for PostConfigGet
|
||||
func NewPostConfigGetRequest(server string) (*http.Request, error) {
|
||||
var err error
|
||||
|
||||
serverURL, err := url.Parse(server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
operationPath := fmt.Sprintf("/config_get")
|
||||
if operationPath[0] == '/' {
|
||||
operationPath = "." + operationPath
|
||||
}
|
||||
|
||||
queryURL, err := serverURL.Parse(operationPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", queryURL.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// NewGetEventRequest generates requests for GetEvent
|
||||
func NewGetEventRequest(server string) (*http.Request, error) {
|
||||
var err error
|
||||
@@ -2189,6 +2415,9 @@ type ClientWithResponsesInterface interface {
|
||||
// PostAppInitializeWithResponse request
|
||||
PostAppInitializeWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*PostAppInitializeResponse, error)
|
||||
|
||||
// PostConfigGetWithResponse request
|
||||
PostConfigGetWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*PostConfigGetResponse, error)
|
||||
|
||||
// GetEventWithResponse request
|
||||
GetEventWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetEventResponse, error)
|
||||
|
||||
@@ -2287,6 +2516,28 @@ func (r PostAppInitializeResponse) StatusCode() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
type PostConfigGetResponse struct {
|
||||
Body []byte
|
||||
HTTPResponse *http.Response
|
||||
JSON200 *ConfigInfo
|
||||
}
|
||||
|
||||
// Status returns HTTPResponse.Status
|
||||
func (r PostConfigGetResponse) Status() string {
|
||||
if r.HTTPResponse != nil {
|
||||
return r.HTTPResponse.Status
|
||||
}
|
||||
return http.StatusText(0)
|
||||
}
|
||||
|
||||
// StatusCode returns HTTPResponse.StatusCode
|
||||
func (r PostConfigGetResponse) StatusCode() int {
|
||||
if r.HTTPResponse != nil {
|
||||
return r.HTTPResponse.StatusCode
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type GetEventResponse struct {
|
||||
Body []byte
|
||||
HTTPResponse *http.Response
|
||||
@@ -2600,6 +2851,15 @@ func (c *ClientWithResponses) PostAppInitializeWithResponse(ctx context.Context,
|
||||
return ParsePostAppInitializeResponse(rsp)
|
||||
}
|
||||
|
||||
// PostConfigGetWithResponse request returning *PostConfigGetResponse
|
||||
func (c *ClientWithResponses) PostConfigGetWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*PostConfigGetResponse, error) {
|
||||
rsp, err := c.PostConfigGet(ctx, reqEditors...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ParsePostConfigGetResponse(rsp)
|
||||
}
|
||||
|
||||
// GetEventWithResponse request returning *GetEventResponse
|
||||
func (c *ClientWithResponses) GetEventWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetEventResponse, error) {
|
||||
rsp, err := c.GetEvent(ctx, reqEditors...)
|
||||
@@ -2825,6 +3085,32 @@ func ParsePostAppInitializeResponse(rsp *http.Response) (*PostAppInitializeRespo
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// ParsePostConfigGetResponse parses an HTTP response from a PostConfigGetWithResponse call
|
||||
func ParsePostConfigGetResponse(rsp *http.Response) (*PostConfigGetResponse, error) {
|
||||
bodyBytes, err := io.ReadAll(rsp.Body)
|
||||
defer func() { _ = rsp.Body.Close() }()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := &PostConfigGetResponse{
|
||||
Body: bodyBytes,
|
||||
HTTPResponse: rsp,
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
|
||||
var dest ConfigInfo
|
||||
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.JSON200 = &dest
|
||||
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// ParseGetEventResponse parses an HTTP response from a GetEventWithResponse call
|
||||
func ParseGetEventResponse(rsp *http.Response) (*GetEventResponse, error) {
|
||||
bodyBytes, err := io.ReadAll(rsp.Body)
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"toolbeam-docs-theme": "0.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"opencode": "workspace:*",
|
||||
"@types/node": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
<svg width="33" height="42" viewBox="0 0 33 42" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.5 0H32.5V41.5955H0.5V0ZM24.5 8.5H8.5V33H24.5V8.5Z" fill="black"/>
|
||||
<svg width="600" height="600" viewBox="0 0 600 600" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="600" height="600" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M115 180H300V420H115V180ZM253.75 229.044H161.25V370.405H253.75V229.044Z" fill="white"/>
|
||||
<path d="M346.25 180H485V229.044H392.5V370.405H485V419.449H346.25V180Z" fill="white"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 221 B After Width: | Height: | Size: 377 B |
@@ -10,7 +10,7 @@ const path = Astro.url.pathname;
|
||||
const links = config.social || [];
|
||||
---
|
||||
|
||||
{ path.startsWith("/share")
|
||||
{ path.startsWith("/s")
|
||||
? <div class="header sl-flex">
|
||||
<div class="title-wrapper sl-flex">
|
||||
<SiteTitle {...Astro.props} />
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -35,3 +35,12 @@ export function IconGemini(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IconOpencode(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox="0 0 70 70" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 13H35V58H0V13ZM26.25 22.1957H8.75V48.701H26.25V22.1957Z" fill="currentColor" />
|
||||
<path d="M43.75 13H70V22.1957H52.5V48.701H70V57.8967H43.75V13Z" fill="currentColor" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
.root {
|
||||
padding-top: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2.5rem;
|
||||
line-height: 1;
|
||||
padding: 1.5rem;
|
||||
|
||||
@media (max-width: 30rem) {
|
||||
padding: 1rem;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
--sm-tool-width: 28rem;
|
||||
--md-tool-width: 40rem;
|
||||
@@ -48,86 +53,35 @@
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
gap: 1rem;
|
||||
|
||||
@media (max-width: 30rem) {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
[data-section="title"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 3rem;
|
||||
h1 {
|
||||
font-size: 2.75rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.15;
|
||||
letter-spacing: -0.05em;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
overflow: hidden;
|
||||
|
||||
& > div {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
align-items: flex-end;
|
||||
|
||||
[data-section="date"] {
|
||||
span {
|
||||
font-size: 0.875rem;
|
||||
color: var(--sl-color-text-secondary);
|
||||
|
||||
&[data-placeholder] {
|
||||
color: var(--sl-color-text-dimmed);
|
||||
}
|
||||
}
|
||||
@media (max-width: 30rem) {
|
||||
font-size: 1.75rem;
|
||||
line-height: 1.25;
|
||||
-webkit-line-clamp: 3;
|
||||
}
|
||||
|
||||
p[data-section="status"] {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
gap: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
|
||||
span:first-child {
|
||||
color: var(--sl-color-divider);
|
||||
|
||||
&[data-status="connected"] {
|
||||
color: var(--sl-color-green);
|
||||
}
|
||||
&[data-status="connecting"] {
|
||||
color: var(--sl-color-orange);
|
||||
}
|
||||
&[data-status="disconnected"] {
|
||||
color: var(--sl-color-divider);
|
||||
}
|
||||
&[data-status="reconnecting"] {
|
||||
color: var(--sl-color-orange);
|
||||
}
|
||||
&[data-status="error"] {
|
||||
color: var(--sl-color-red);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
[data-section="row"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.25;
|
||||
letter-spacing: -0.05em;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
|
||||
@media (max-width: 30rem) {
|
||||
font-size: 1.25rem;
|
||||
-webkit-line-clamp: 3;
|
||||
}
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
[data-section="stats"] {
|
||||
@@ -150,11 +104,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
[data-section="stats"][data-section-root],
|
||||
[data-section="stats"][data-section-models] {
|
||||
li {
|
||||
gap: 0.3125rem;
|
||||
|
||||
[data-stat-model-icon] {
|
||||
[data-stat-icon] {
|
||||
flex: 0 0 auto;
|
||||
color: var(--sl-color-text-dimmed);
|
||||
opacity: 0.85;
|
||||
@@ -168,6 +123,13 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-section="time"] {
|
||||
span {
|
||||
color: var(--sl-color-text-dimmed);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.parts {
|
||||
@@ -187,14 +149,81 @@
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
a:first-child {
|
||||
display: block;
|
||||
flex: 0 0 auto;
|
||||
width: 18px;
|
||||
opacity: 0.65;
|
||||
svg {
|
||||
color: var(--sl-color-text-secondary);
|
||||
[data-element-anchor] {
|
||||
position: relative;
|
||||
|
||||
a:first-child {
|
||||
display: block;
|
||||
flex: 0 0 auto;
|
||||
width: 18px;
|
||||
opacity: 0.65;
|
||||
|
||||
svg {
|
||||
color: var(--sl-color-text-secondary);
|
||||
display: block;
|
||||
|
||||
&:nth-child(3) {
|
||||
color: var(--sl-color-green-high);
|
||||
}
|
||||
}
|
||||
|
||||
svg:nth-child(2),
|
||||
svg:nth-child(3) {
|
||||
display: none;
|
||||
}
|
||||
&:hover {
|
||||
svg:nth-child(1) {
|
||||
display: none;
|
||||
}
|
||||
svg:nth-child(2) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-element-tooltip] {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(100% + 12px);
|
||||
transform: translate(0, -50%);
|
||||
line-height: 1.1;
|
||||
padding: 0.375em 0.5em calc(0.375em + 2px);
|
||||
background: var(--sl-color-white);
|
||||
color: var(--sl-color-text-invert);
|
||||
font-size: 0.6875rem;
|
||||
border-radius: 7px;
|
||||
white-space: nowrap;
|
||||
|
||||
z-index: 1;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: -15px;
|
||||
transform: translateY(-50%);
|
||||
border: 8px solid transparent;
|
||||
border-right-color: var(--sl-color-white);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-status="copied"] {
|
||||
[data-element-tooltip] {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
a,
|
||||
a:hover {
|
||||
svg:nth-child(1),
|
||||
svg:nth-child(2) {
|
||||
display: none;
|
||||
}
|
||||
svg:nth-child(3) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,6 +375,44 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
[data-part-type="connection-status"] {
|
||||
& > [data-section="decoration"] {
|
||||
span:first-child {
|
||||
flex: 0 0 auto;
|
||||
display: block;
|
||||
margin: 2px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--sl-color-divider);
|
||||
|
||||
&[data-status="connected"] {
|
||||
background-color: var(--sl-color-green);
|
||||
}
|
||||
&[data-status="connecting"] {
|
||||
background-color: var(--sl-color-orange);
|
||||
}
|
||||
&[data-status="disconnected"] {
|
||||
background-color: var(--sl-color-divider);
|
||||
}
|
||||
&[data-status="reconnecting"] {
|
||||
background-color: var(--sl-color-orange);
|
||||
}
|
||||
&[data-status="error"] {
|
||||
background-color: var(--sl-color-red);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > [data-section="content"] {
|
||||
span {
|
||||
display: block;
|
||||
line-height: 18px;
|
||||
font-size: 0.875rem;
|
||||
color: var(--sl-color-text-dimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-text {
|
||||
@@ -433,7 +500,7 @@
|
||||
max-width: var(--md-tool-width);
|
||||
|
||||
[data-section="content"] {
|
||||
p {
|
||||
pre {
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1.5;
|
||||
font-size: 0.75rem;
|
||||
@@ -493,12 +560,13 @@
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
max-width: var(--sm-tool-width);
|
||||
|
||||
& > [data-section="body"] {
|
||||
width: var(--sm-tool-width);
|
||||
width: 100%;
|
||||
border: 1px solid var(--sl-color-divider);
|
||||
border-radius: 0.25rem;
|
||||
max-width: 100%;
|
||||
|
||||
[data-section="header"] {
|
||||
position: relative;
|
||||
@@ -616,7 +684,8 @@
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: var(--sm-tool-width);
|
||||
width: 100%;
|
||||
max-width: var(--sm-tool-width);
|
||||
border: 1px solid var(--sl-color-divider);
|
||||
border-radius: 0.25rem;
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ const data = await res.json();
|
||||
|
||||
let cost = 0;
|
||||
const models: Set<string> = new Set();
|
||||
const version = "v0.1.1";
|
||||
const version = `v${data.info.version}`;
|
||||
Object.values(data.messages).forEach((d) => {
|
||||
const assistant = d.metadata?.assistant;
|
||||
if (assistant) {
|
||||
@@ -74,7 +74,11 @@ const ogImage = `https://social-cards.sst.dev/opencode-share/${encodedTitle}.png
|
||||
body > .page > .main-frame .main-pane > main > .content-panel:first-of-type {
|
||||
display: none;
|
||||
}
|
||||
body > .page > .main-frame .main-pane > main {
|
||||
padding: 0;
|
||||
}
|
||||
body > .page > .main-frame .main-pane > main > .content-panel + .content-panel {
|
||||
border-top: none !important;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ ! -d ".git" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mkdir -p .git/hooks
|
||||
|
||||
cat > .git/hooks/pre-push << 'EOF'
|
||||
#!/bin/sh
|
||||
bun run typecheck
|
||||
EOF
|
||||
|
||||
chmod +x .git/hooks/pre-push
|
||||
echo "✅ Pre-push hook installed"
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
goreleaser build --clean --snapshot --skip validate
|
||||
Reference in New Issue
Block a user