Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9befd3aa6 | ||
|
|
49de703ba1 | ||
|
|
22988894c8 | ||
|
|
34b1754f25 | ||
|
|
54fe3504ba | ||
|
|
d2c862e32d | ||
|
|
afc53afb35 | ||
|
|
b56e49c5dc | ||
|
|
8b2a909e1f | ||
|
|
e9c954d45e | ||
|
|
6f449d13af | ||
|
|
6e375bef0d | ||
|
|
67106a6967 | ||
|
|
b5d690620d | ||
|
|
9db3ce1d0b | ||
|
|
1cc55b68ef | ||
|
|
469f667774 | ||
|
|
6603d9a9f0 | ||
|
|
5dc1920a4c | ||
|
|
d3e5f3f3a8 | ||
|
|
ce4cb820f7 | ||
|
|
ba5be6b625 | ||
|
|
f95c3f4177 | ||
|
|
d2b1307bff | ||
|
|
b40ba32adc | ||
|
|
ce0cebb7d7 | ||
|
|
f478f89a68 | ||
|
|
85d95f0f2b | ||
|
|
1515efc77c | ||
|
|
6d393759e1 | ||
|
|
a1701678cd | ||
|
|
c411a26d6f | ||
|
|
85dbfeb314 | ||
|
|
085c0e4e2b | ||
|
|
8404a97c3e | ||
|
|
0ee3b1ede2 | ||
|
|
a826936702 | ||
|
|
fd4a5d5a63 | ||
|
|
69cf1d7b7e | ||
|
|
8e0a1d1167 | ||
|
|
f22021187d | ||
|
|
febecc348a | ||
|
|
c5ccfc3e94 | ||
|
|
1f6efc6b94 | ||
|
|
727fe6f942 | ||
|
|
a91e79382e | ||
|
|
5c626e0a2f | ||
|
|
8e9e383219 | ||
|
|
f383008cc1 | ||
|
|
303ade25ed | ||
|
|
53f8e7850e | ||
|
|
ca8ce88354 | ||
|
|
37a86439c4 | ||
|
|
269b43f4de | ||
|
|
3f25e5bf86 | ||
|
|
67765fa47c | ||
|
|
58b1c58bc5 | ||
|
|
d80badc50f | ||
|
|
75279e5ccf | ||
|
|
7893b84614 | ||
|
|
cfc715bd48 | ||
|
|
39bcba85a9 | ||
|
|
da3df51316 | ||
|
|
12190e4efc | ||
|
|
d2a9b2f64a | ||
|
|
aacadd8a8a | ||
|
|
969154a473 | ||
|
|
4d6ca3fab1 | ||
|
|
00ea5082e7 | ||
|
|
4a878b88c0 | ||
|
|
6de955847c | ||
|
|
3ba5d528b4 | ||
|
|
f99e2b3429 | ||
|
|
7e4e6f6e51 | ||
|
|
0514f3f43b | ||
|
|
1e07384364 | ||
|
|
4c4739c422 | ||
|
|
2d8b90a6ff | ||
|
|
a2fa7ffa42 | ||
|
|
f7d6175283 | ||
|
|
9ed187ee52 | ||
|
|
14d81e574b | ||
|
|
6efe8cc8df | ||
|
|
daa5fc916a | ||
|
|
c659496b96 | ||
|
|
21fbf21cb6 | ||
|
|
f31cbf2744 | ||
|
|
8322f18e03 | ||
|
|
562bdb95e2 | ||
|
|
a57ce8365d | ||
|
|
0da83ae67e | ||
|
|
662d022a48 | ||
|
|
9efef03919 | ||
|
|
7a9fb3fa92 | ||
|
|
ea96ead346 | ||
|
|
6100a77b85 | ||
|
|
c7a59ee2b1 | ||
|
|
a272b58fe9 | ||
|
|
9948fcf1b6 | ||
|
|
0d50c867ff | ||
|
|
27f7e02f12 | ||
|
|
0f93ecd564 |
@@ -9,7 +9,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<p align="center">AI coding agent, built for the terminal.</p>
|
<p align="center">AI coding agent, built for the terminal.</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://discord.gg/opencode"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
|
<a href="https://opencode.ai/discord"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
|
||||||
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
|
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
|
||||||
<a href="https://github.com/sst/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/sst/opencode/publish.yml?style=flat-square&branch=dev" /></a>
|
<a href="https://github.com/sst/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/sst/opencode/publish.yml?style=flat-square&branch=dev" /></a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
# Download Stats
|
# Download Stats
|
||||||
|
|
||||||
| Date | GitHub Downloads | npm Downloads | Total |
|
| Date | GitHub Downloads | npm Downloads | Total |
|
||||||
| ---------- | ---------------- | --------------- | --------------- |
|
| ---------- | ---------------- | --------------- | ----------------- |
|
||||||
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
|
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
|
||||||
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
|
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
|
||||||
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
|
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
|
||||||
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
|
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
|
||||||
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
|
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
|
||||||
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
|
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
|
||||||
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
|
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
|
||||||
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
|
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
|
||||||
|
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
|
||||||
|
| 2025-07-10 | 43,796 (+5,744) | 71,402 (+6,934) | 115,198 (+12,678) |
|
||||||
|
|||||||
@@ -83,7 +83,7 @@
|
|||||||
"sharp": "0.32.5",
|
"sharp": "0.32.5",
|
||||||
"shiki": "3.4.2",
|
"shiki": "3.4.2",
|
||||||
"solid-js": "1.9.7",
|
"solid-js": "1.9.7",
|
||||||
"toolbeam-docs-theme": "0.4.1",
|
"toolbeam-docs-theme": "0.4.3",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
@@ -1531,7 +1531,7 @@
|
|||||||
|
|
||||||
"token-types": ["token-types@6.0.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA=="],
|
"token-types": ["token-types@6.0.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA=="],
|
||||||
|
|
||||||
"toolbeam-docs-theme": ["toolbeam-docs-theme@0.4.1", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-lTI4dHZaVNQky29m7sb36Oy4tWPwxsCuFxFjF8hgGW0vpV+S6qPvI9SwsJFvdE/OHO5DoI7VMbryV1pxZHkkHQ=="],
|
"toolbeam-docs-theme": ["toolbeam-docs-theme@0.4.3", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-3um/NsSq4xFeKbKrNGPHIzfTixwnEVvroqA8Q+lecnYHHJ5TtiYTggHDqewOW+I67t0J1IVBwVKUPjxiQfIcog=="],
|
||||||
|
|
||||||
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://opencode.ai/config.json",
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"mcp": {
|
||||||
|
"weather": {
|
||||||
|
"type": "local",
|
||||||
|
"command": ["opencode", "x", "@h1deya/mcp-server-weather"]
|
||||||
|
}
|
||||||
|
},
|
||||||
"experimental": {
|
"experimental": {
|
||||||
"hook": {
|
"hook": {
|
||||||
"file_edited": {
|
"file_edited": {
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run packages/opencode/src/index.ts",
|
"dev": "bun run packages/opencode/src/index.ts",
|
||||||
"typecheck": "bun run --filter='*' typecheck",
|
"typecheck": "bun run --filter='*' typecheck",
|
||||||
"stainless": "bun run ./packages/opencode/src/index.ts serve ",
|
"stainless": "./scripts/stainless",
|
||||||
"postinstall": "./scripts/hooks"
|
"postinstall": "./scripts/hooks"
|
||||||
},
|
},
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
|
|||||||
@@ -1,369 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"$schema": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "JSON schema reference for configuration validation"
|
|
||||||
},
|
|
||||||
"theme": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Theme name to use for the interface"
|
|
||||||
},
|
|
||||||
"keybinds": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"leader": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Leader key for keybind combinations"
|
|
||||||
},
|
|
||||||
"help": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Show help dialog"
|
|
||||||
},
|
|
||||||
"editor_open": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Open external editor"
|
|
||||||
},
|
|
||||||
"session_new": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Create a new session"
|
|
||||||
},
|
|
||||||
"session_list": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "List all sessions"
|
|
||||||
},
|
|
||||||
"session_share": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Share current session"
|
|
||||||
},
|
|
||||||
"session_interrupt": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Interrupt current session"
|
|
||||||
},
|
|
||||||
"session_compact": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Toggle compact mode for session"
|
|
||||||
},
|
|
||||||
"tool_details": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Show tool details"
|
|
||||||
},
|
|
||||||
"model_list": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "List available models"
|
|
||||||
},
|
|
||||||
"theme_list": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "List available themes"
|
|
||||||
},
|
|
||||||
"project_init": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Initialize project configuration"
|
|
||||||
},
|
|
||||||
"input_clear": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Clear input field"
|
|
||||||
},
|
|
||||||
"input_paste": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Paste from clipboard"
|
|
||||||
},
|
|
||||||
"input_submit": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Submit input"
|
|
||||||
},
|
|
||||||
"input_newline": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Insert newline in input"
|
|
||||||
},
|
|
||||||
"history_previous": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Navigate to previous history item"
|
|
||||||
},
|
|
||||||
"history_next": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Navigate to next history item"
|
|
||||||
},
|
|
||||||
"messages_page_up": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Scroll messages up by one page"
|
|
||||||
},
|
|
||||||
"messages_page_down": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Scroll messages down by one page"
|
|
||||||
},
|
|
||||||
"messages_half_page_up": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Scroll messages up by half page"
|
|
||||||
},
|
|
||||||
"messages_half_page_down": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Scroll messages down by half page"
|
|
||||||
},
|
|
||||||
"messages_previous": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Navigate to previous message"
|
|
||||||
},
|
|
||||||
"messages_next": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Navigate to next message"
|
|
||||||
},
|
|
||||||
"messages_first": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Navigate to first message"
|
|
||||||
},
|
|
||||||
"messages_last": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Navigate to last message"
|
|
||||||
},
|
|
||||||
"app_exit": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Exit the application"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalProperties": false,
|
|
||||||
"description": "Custom keybind configurations"
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
},
|
|
||||||
"tool_call": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"cost": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"input": {
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
"output": {
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
"cache_read": {
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
"cache_write": {
|
|
||||||
"type": "number"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["input", "output"],
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"limit": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"context": {
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
"output": {
|
|
||||||
"type": "number"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["context", "output"],
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"options": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"options": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["models"],
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"description": "Custom provider configurations and model overrides"
|
|
||||||
},
|
|
||||||
"mcp": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"type": {
|
|
||||||
"type": "string",
|
|
||||||
"const": "local",
|
|
||||||
"description": "Type of MCP server connection"
|
|
||||||
},
|
|
||||||
"command": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"description": "Command and arguments to run the MCP server"
|
|
||||||
},
|
|
||||||
"environment": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"description": "Environment variables to set when running the MCP server"
|
|
||||||
},
|
|
||||||
"enabled": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Enable or disable the MCP server on startup"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["type", "command"],
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"type": {
|
|
||||||
"type": "string",
|
|
||||||
"const": "remote",
|
|
||||||
"description": "Type of MCP server connection"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "URL of the remote MCP server"
|
|
||||||
},
|
|
||||||
"enabled": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Enable or disable the MCP server on startup"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["type", "url"],
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "MCP (Model Context Protocol) server configurations"
|
|
||||||
},
|
|
||||||
"instructions": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"description": "Additional instruction files or patterns to include"
|
|
||||||
},
|
|
||||||
"experimental": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"hook": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"file_edited": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"command": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"environment": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["command"],
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"session_completed": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"command": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"environment": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["command"],
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalProperties": false,
|
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#"
|
|
||||||
}
|
|
||||||
@@ -103,6 +103,7 @@ if (!snapshot) {
|
|||||||
const lower = x.toLowerCase()
|
const lower = x.toLowerCase()
|
||||||
return (
|
return (
|
||||||
!lower.includes("ignore:") &&
|
!lower.includes("ignore:") &&
|
||||||
|
!lower.includes("chore:") &&
|
||||||
!lower.includes("ci:") &&
|
!lower.includes("ci:") &&
|
||||||
!lower.includes("wip:") &&
|
!lower.includes("wip:") &&
|
||||||
!lower.includes("docs:") &&
|
!lower.includes("docs:") &&
|
||||||
|
|||||||
@@ -4,5 +4,32 @@ import "zod-openapi/extend"
|
|||||||
import { Config } from "../src/config/config"
|
import { Config } from "../src/config/config"
|
||||||
import { zodToJsonSchema } from "zod-to-json-schema"
|
import { zodToJsonSchema } from "zod-to-json-schema"
|
||||||
|
|
||||||
const result = zodToJsonSchema(Config.Info)
|
const file = process.argv[2]
|
||||||
await Bun.write("config.schema.json", JSON.stringify(result, null, 2))
|
|
||||||
|
const result = zodToJsonSchema(Config.Info, {
|
||||||
|
/**
|
||||||
|
* We'll use the `default` values of the field as the only value in `examples`.
|
||||||
|
* This will ensure no docs are needed to be read, as the configuration is
|
||||||
|
* self-documenting.
|
||||||
|
*
|
||||||
|
* See https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.9.5
|
||||||
|
*/
|
||||||
|
postProcess(jsonSchema) {
|
||||||
|
const schema = jsonSchema as typeof jsonSchema & {
|
||||||
|
examples?: unknown[]
|
||||||
|
}
|
||||||
|
if (schema && typeof schema === "object" && "type" in schema && schema.type === "string" && schema?.default) {
|
||||||
|
if (!schema.examples) {
|
||||||
|
schema.examples = [schema.default]
|
||||||
|
}
|
||||||
|
|
||||||
|
schema.description = [schema.description || "", `default: \`${schema.default}\``]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n\n")
|
||||||
|
.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonSchema
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await Bun.write(file, JSON.stringify(result, null, 2))
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { App } from "../app/app"
|
import { App } from "../app/app"
|
||||||
import { ConfigHooks } from "../config/hooks"
|
import { ConfigHooks } from "../config/hooks"
|
||||||
import { FileWatcher } from "../file/watch"
|
|
||||||
import { Format } from "../format"
|
import { Format } from "../format"
|
||||||
import { LSP } from "../lsp"
|
import { LSP } from "../lsp"
|
||||||
import { Share } from "../share/share"
|
import { Share } from "../share/share"
|
||||||
@@ -11,7 +10,6 @@ export async function bootstrap<T>(input: App.Input, cb: (app: App.Info) => Prom
|
|||||||
Format.init()
|
Format.init()
|
||||||
ConfigHooks.init()
|
ConfigHooks.init()
|
||||||
LSP.init()
|
LSP.init()
|
||||||
FileWatcher.init()
|
|
||||||
|
|
||||||
return cb(app)
|
return cb(app)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { cmd } from "../cmd"
|
|||||||
import { FileCommand } from "./file"
|
import { FileCommand } from "./file"
|
||||||
import { LSPCommand } from "./lsp"
|
import { LSPCommand } from "./lsp"
|
||||||
import { RipgrepCommand } from "./ripgrep"
|
import { RipgrepCommand } from "./ripgrep"
|
||||||
|
import { ScrapCommand } from "./scrap"
|
||||||
import { SnapshotCommand } from "./snapshot"
|
import { SnapshotCommand } from "./snapshot"
|
||||||
|
|
||||||
export const DebugCommand = cmd({
|
export const DebugCommand = cmd({
|
||||||
@@ -12,6 +13,7 @@ export const DebugCommand = cmd({
|
|||||||
.command(LSPCommand)
|
.command(LSPCommand)
|
||||||
.command(RipgrepCommand)
|
.command(RipgrepCommand)
|
||||||
.command(FileCommand)
|
.command(FileCommand)
|
||||||
|
.command(ScrapCommand)
|
||||||
.command(SnapshotCommand)
|
.command(SnapshotCommand)
|
||||||
.command({
|
.command({
|
||||||
command: "wait",
|
command: "wait",
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import { Log } from "../../../util/log"
|
|||||||
|
|
||||||
export const LSPCommand = cmd({
|
export const LSPCommand = cmd({
|
||||||
command: "lsp",
|
command: "lsp",
|
||||||
builder: (yargs) => yargs.command(DiagnosticsCommand).command(SymbolsCommand).demandCommand(),
|
builder: (yargs) =>
|
||||||
|
yargs.command(DiagnosticsCommand).command(SymbolsCommand).command(DocumentSymbolsCommand).demandCommand(),
|
||||||
async handler() {},
|
async handler() {},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -25,10 +26,21 @@ export const SymbolsCommand = cmd({
|
|||||||
builder: (yargs) => yargs.positional("query", { type: "string", demandOption: true }),
|
builder: (yargs) => yargs.positional("query", { type: "string", demandOption: true }),
|
||||||
async handler(args) {
|
async handler(args) {
|
||||||
await bootstrap({ cwd: process.cwd() }, async () => {
|
await bootstrap({ cwd: process.cwd() }, async () => {
|
||||||
await LSP.touchFile("./src/index.ts", true)
|
|
||||||
using _ = Log.Default.time("symbols")
|
using _ = Log.Default.time("symbols")
|
||||||
const results = await LSP.workspaceSymbol(args.query)
|
const results = await LSP.workspaceSymbol(args.query)
|
||||||
console.log(JSON.stringify(results, null, 2))
|
console.log(JSON.stringify(results, null, 2))
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const DocumentSymbolsCommand = cmd({
|
||||||
|
command: "document-symbols <uri>",
|
||||||
|
builder: (yargs) => yargs.positional("uri", { type: "string", demandOption: true }),
|
||||||
|
async handler(args) {
|
||||||
|
await bootstrap({ cwd: process.cwd() }, async () => {
|
||||||
|
using _ = Log.Default.time("document-symbols")
|
||||||
|
const results = await LSP.documentSymbol(args.uri)
|
||||||
|
console.log(JSON.stringify(results, null, 2))
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const FilesCommand = cmd({
|
|||||||
const files = await Ripgrep.files({
|
const files = await Ripgrep.files({
|
||||||
cwd: app.path.cwd,
|
cwd: app.path.cwd,
|
||||||
query: args.query,
|
query: args.query,
|
||||||
glob: args.glob,
|
glob: args.glob ? [args.glob] : undefined,
|
||||||
limit: args.limit,
|
limit: args.limit,
|
||||||
})
|
})
|
||||||
console.log(files.join("\n"))
|
console.log(files.join("\n"))
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { cmd } from "../cmd"
|
||||||
|
|
||||||
|
export const ScrapCommand = cmd({
|
||||||
|
command: "scrap",
|
||||||
|
builder: (yargs) => yargs,
|
||||||
|
async handler() {},
|
||||||
|
})
|
||||||
@@ -8,6 +8,7 @@ import { Flag } from "../../flag/flag"
|
|||||||
import { Config } from "../../config/config"
|
import { Config } from "../../config/config"
|
||||||
import { bootstrap } from "../bootstrap"
|
import { bootstrap } from "../bootstrap"
|
||||||
import { MessageV2 } from "../../session/message-v2"
|
import { MessageV2 } from "../../session/message-v2"
|
||||||
|
import { Mode } from "../../session/mode"
|
||||||
|
|
||||||
const TOOL: Record<string, [string, string]> = {
|
const TOOL: Record<string, [string, string]> = {
|
||||||
todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
|
todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
|
||||||
@@ -52,6 +53,10 @@ export const RunCommand = cmd({
|
|||||||
alias: ["m"],
|
alias: ["m"],
|
||||||
describe: "model to use in the format of provider/model",
|
describe: "model to use in the format of provider/model",
|
||||||
})
|
})
|
||||||
|
.option("mode", {
|
||||||
|
type: "string",
|
||||||
|
describe: "mode to use",
|
||||||
|
})
|
||||||
},
|
},
|
||||||
handler: async (args) => {
|
handler: async (args) => {
|
||||||
let message = args.message.join(" ")
|
let message = args.message.join(" ")
|
||||||
@@ -81,7 +86,8 @@ export const RunCommand = cmd({
|
|||||||
UI.empty()
|
UI.empty()
|
||||||
UI.println(UI.logo())
|
UI.println(UI.logo())
|
||||||
UI.empty()
|
UI.empty()
|
||||||
UI.println(UI.Style.TEXT_NORMAL_BOLD + "> ", message)
|
const displayMessage = message.length > 300 ? message.slice(0, 300) + "..." : message
|
||||||
|
UI.println(UI.Style.TEXT_NORMAL_BOLD + "> ", displayMessage)
|
||||||
UI.empty()
|
UI.empty()
|
||||||
|
|
||||||
const cfg = await Config.get()
|
const cfg = await Config.get()
|
||||||
@@ -124,10 +130,31 @@ export const RunCommand = cmd({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let errorMsg: string | undefined
|
||||||
|
Bus.subscribe(Session.Event.Error, async (evt) => {
|
||||||
|
const { sessionID, error } = evt.properties
|
||||||
|
if (sessionID !== session.id || !error) return
|
||||||
|
let err = String(error.name)
|
||||||
|
|
||||||
|
if ("data" in error && error.data && "message" in error.data) {
|
||||||
|
err = error.data.message
|
||||||
|
}
|
||||||
|
errorMsg = errorMsg ? errorMsg + "\n" + err : err
|
||||||
|
|
||||||
|
UI.error(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
const mode = args.mode ? await Mode.get(args.mode) : await Mode.list().then((x) => x[0])
|
||||||
|
|
||||||
const result = await Session.chat({
|
const result = await Session.chat({
|
||||||
sessionID: session.id,
|
sessionID: session.id,
|
||||||
providerID,
|
...(mode.model
|
||||||
modelID,
|
? mode.model
|
||||||
|
: {
|
||||||
|
providerID,
|
||||||
|
modelID,
|
||||||
|
}),
|
||||||
|
mode: mode.name,
|
||||||
parts: [
|
parts: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
@@ -139,6 +166,7 @@ export const RunCommand = cmd({
|
|||||||
if (isPiped) {
|
if (isPiped) {
|
||||||
const match = result.parts.findLast((x) => x.type === "text")
|
const match = result.parts.findLast((x) => x.type === "text")
|
||||||
if (match) process.stdout.write(match.text)
|
if (match) process.stdout.write(match.text)
|
||||||
|
if (errorMsg) process.stdout.write(errorMsg)
|
||||||
}
|
}
|
||||||
UI.empty()
|
UI.empty()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import { Installation } from "../../installation"
|
|||||||
import { Config } from "../../config/config"
|
import { Config } from "../../config/config"
|
||||||
import { Bus } from "../../bus"
|
import { Bus } from "../../bus"
|
||||||
import { Log } from "../../util/log"
|
import { Log } from "../../util/log"
|
||||||
|
import { FileWatcher } from "../../file/watch"
|
||||||
|
import { Mode } from "../../session/mode"
|
||||||
|
|
||||||
export const TuiCommand = cmd({
|
export const TuiCommand = cmd({
|
||||||
command: "$0 [project]",
|
command: "$0 [project]",
|
||||||
@@ -29,6 +31,10 @@ export const TuiCommand = cmd({
|
|||||||
alias: ["p"],
|
alias: ["p"],
|
||||||
type: "string",
|
type: "string",
|
||||||
describe: "prompt to use",
|
describe: "prompt to use",
|
||||||
|
})
|
||||||
|
.option("mode", {
|
||||||
|
type: "string",
|
||||||
|
describe: "mode to use",
|
||||||
}),
|
}),
|
||||||
handler: async (args) => {
|
handler: async (args) => {
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -40,6 +46,7 @@ export const TuiCommand = cmd({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const result = await bootstrap({ cwd }, async (app) => {
|
const result = await bootstrap({ cwd }, async (app) => {
|
||||||
|
FileWatcher.init()
|
||||||
const providers = await Provider.list()
|
const providers = await Provider.list()
|
||||||
if (Object.keys(providers).length === 0) {
|
if (Object.keys(providers).length === 0) {
|
||||||
return "needs_provider"
|
return "needs_provider"
|
||||||
@@ -75,6 +82,7 @@ export const TuiCommand = cmd({
|
|||||||
...cmd,
|
...cmd,
|
||||||
...(args.model ? ["--model", args.model] : []),
|
...(args.model ? ["--model", args.model] : []),
|
||||||
...(args.prompt ? ["--prompt", args.prompt] : []),
|
...(args.prompt ? ["--prompt", args.prompt] : []),
|
||||||
|
...(args.mode ? ["--mode", args.mode] : []),
|
||||||
],
|
],
|
||||||
cwd,
|
cwd,
|
||||||
stdout: "inherit",
|
stdout: "inherit",
|
||||||
@@ -82,8 +90,10 @@ export const TuiCommand = cmd({
|
|||||||
stdin: "inherit",
|
stdin: "inherit",
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
|
CGO_ENABLED: "0",
|
||||||
OPENCODE_SERVER: server.url.toString(),
|
OPENCODE_SERVER: server.url.toString(),
|
||||||
OPENCODE_APP_INFO: JSON.stringify(app),
|
OPENCODE_APP_INFO: JSON.stringify(app),
|
||||||
|
OPENCODE_MODES: JSON.stringify(await Mode.list()),
|
||||||
},
|
},
|
||||||
onExit: () => {
|
onExit: () => {
|
||||||
server.stop()
|
server.stop()
|
||||||
|
|||||||
@@ -55,40 +55,63 @@ export namespace Config {
|
|||||||
export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote])
|
export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote])
|
||||||
export type Mcp = z.infer<typeof Mcp>
|
export type Mcp = z.infer<typeof Mcp>
|
||||||
|
|
||||||
|
export const Mode = z
|
||||||
|
.object({
|
||||||
|
model: z.string().optional(),
|
||||||
|
prompt: z.string().optional(),
|
||||||
|
tools: z.record(z.string(), z.boolean()).optional(),
|
||||||
|
})
|
||||||
|
.openapi({
|
||||||
|
ref: "ModeConfig",
|
||||||
|
})
|
||||||
|
export type Mode = z.infer<typeof Mode>
|
||||||
|
|
||||||
export const Keybinds = z
|
export const Keybinds = z
|
||||||
.object({
|
.object({
|
||||||
leader: z.string().optional().describe("Leader key for keybind combinations"),
|
leader: z.string().optional().default("ctrl+x").describe("Leader key for keybind combinations"),
|
||||||
help: z.string().optional().describe("Show help dialog"),
|
app_help: z.string().optional().default("<leader>h").describe("Show help dialog"),
|
||||||
editor_open: z.string().optional().describe("Open external editor"),
|
switch_mode: z.string().optional().default("tab").describe("Switch mode"),
|
||||||
session_new: z.string().optional().describe("Create a new session"),
|
editor_open: z.string().optional().default("<leader>e").describe("Open external editor"),
|
||||||
session_list: z.string().optional().describe("List all sessions"),
|
session_new: z.string().optional().default("<leader>n").describe("Create a new session"),
|
||||||
session_share: z.string().optional().describe("Share current session"),
|
session_list: z.string().optional().default("<leader>l").describe("List all sessions"),
|
||||||
session_interrupt: z.string().optional().describe("Interrupt current session"),
|
session_share: z.string().optional().default("<leader>s").describe("Share current session"),
|
||||||
session_compact: z.string().optional().describe("Toggle compact mode for session"),
|
session_unshare: z.string().optional().default("<leader>u").describe("Unshare current session"),
|
||||||
tool_details: z.string().optional().describe("Show tool details"),
|
session_interrupt: z.string().optional().default("esc").describe("Interrupt current session"),
|
||||||
model_list: z.string().optional().describe("List available models"),
|
session_compact: z.string().optional().default("<leader>c").describe("Compact the session"),
|
||||||
theme_list: z.string().optional().describe("List available themes"),
|
tool_details: z.string().optional().default("<leader>d").describe("Toggle tool details"),
|
||||||
project_init: z.string().optional().describe("Initialize project configuration"),
|
model_list: z.string().optional().default("<leader>m").describe("List available models"),
|
||||||
input_clear: z.string().optional().describe("Clear input field"),
|
theme_list: z.string().optional().default("<leader>t").describe("List available themes"),
|
||||||
input_paste: z.string().optional().describe("Paste from clipboard"),
|
file_list: z.string().optional().default("<leader>f").describe("List files"),
|
||||||
input_submit: z.string().optional().describe("Submit input"),
|
file_close: z.string().optional().default("esc").describe("Close file"),
|
||||||
input_newline: z.string().optional().describe("Insert newline in input"),
|
file_search: z.string().optional().default("<leader>/").describe("Search file"),
|
||||||
history_previous: z.string().optional().describe("Navigate to previous history item"),
|
file_diff_toggle: z.string().optional().default("<leader>v").describe("Split/unified diff"),
|
||||||
history_next: z.string().optional().describe("Navigate to next history item"),
|
project_init: z.string().optional().default("<leader>i").describe("Create/update AGENTS.md"),
|
||||||
messages_page_up: z.string().optional().describe("Scroll messages up by one page"),
|
input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"),
|
||||||
messages_page_down: z.string().optional().describe("Scroll messages down by one page"),
|
input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"),
|
||||||
messages_half_page_up: z.string().optional().describe("Scroll messages up by half page"),
|
input_submit: z.string().optional().default("enter").describe("Submit input"),
|
||||||
messages_half_page_down: z.string().optional().describe("Scroll messages down by half page"),
|
input_newline: z.string().optional().default("shift+enter,ctrl+j").describe("Insert newline in input"),
|
||||||
messages_previous: z.string().optional().describe("Navigate to previous message"),
|
messages_page_up: z.string().optional().default("pgup").describe("Scroll messages up by one page"),
|
||||||
messages_next: z.string().optional().describe("Navigate to next message"),
|
messages_page_down: z.string().optional().default("pgdown").describe("Scroll messages down by one page"),
|
||||||
messages_first: z.string().optional().describe("Navigate to first message"),
|
messages_half_page_up: z.string().optional().default("ctrl+alt+u").describe("Scroll messages up by half page"),
|
||||||
messages_last: z.string().optional().describe("Navigate to last message"),
|
messages_half_page_down: z
|
||||||
app_exit: z.string().optional().describe("Exit the application"),
|
.string()
|
||||||
|
.optional()
|
||||||
|
.default("ctrl+alt+d")
|
||||||
|
.describe("Scroll messages down by half page"),
|
||||||
|
messages_previous: z.string().optional().default("ctrl+up").describe("Navigate to previous message"),
|
||||||
|
messages_next: z.string().optional().default("ctrl+down").describe("Navigate to next message"),
|
||||||
|
messages_first: z.string().optional().default("ctrl+g").describe("Navigate to first message"),
|
||||||
|
messages_last: z.string().optional().default("ctrl+alt+g").describe("Navigate to last message"),
|
||||||
|
messages_layout_toggle: z.string().optional().default("<leader>p").describe("Toggle layout"),
|
||||||
|
messages_copy: z.string().optional().default("<leader>y").describe("Copy message"),
|
||||||
|
messages_revert: z.string().optional().default("<leader>r").describe("Revert message"),
|
||||||
|
app_exit: z.string().optional().default("ctrl+c,<leader>q").describe("Exit the application"),
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.openapi({
|
.openapi({
|
||||||
ref: "KeybindsConfig",
|
ref: "KeybindsConfig",
|
||||||
})
|
})
|
||||||
|
|
||||||
export const Info = z
|
export const Info = z
|
||||||
.object({
|
.object({
|
||||||
$schema: z.string().optional().describe("JSON schema reference for configuration validation"),
|
$schema: z.string().optional().describe("JSON schema reference for configuration validation"),
|
||||||
@@ -98,6 +121,14 @@ export namespace Config {
|
|||||||
autoupdate: z.boolean().optional().describe("Automatically update to the latest version"),
|
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"),
|
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(),
|
model: z.string().describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(),
|
||||||
|
mode: z
|
||||||
|
.object({
|
||||||
|
build: Mode.optional(),
|
||||||
|
plan: Mode.optional(),
|
||||||
|
})
|
||||||
|
.catchall(Mode)
|
||||||
|
.optional(),
|
||||||
|
log_level: Log.Level.optional().describe("Minimum log level to write to log files"),
|
||||||
provider: z
|
provider: z
|
||||||
.record(
|
.record(
|
||||||
ModelsDev.Provider.partial().extend({
|
ModelsDev.Provider.partial().extend({
|
||||||
@@ -164,19 +195,40 @@ export namespace Config {
|
|||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
|
|
||||||
async function load(path: string) {
|
async function load(configPath: string) {
|
||||||
const data = await Bun.file(path)
|
let text = await Bun.file(configPath)
|
||||||
.json()
|
.text()
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (err.code === "ENOENT") return {}
|
if (err.code === "ENOENT") return "{}"
|
||||||
throw new JsonError({ path }, { cause: err })
|
throw new JsonError({ path: configPath }, { cause: err })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
text = text.replace(/\{env:([^}]+)\}/g, (_, varName) => {
|
||||||
|
return process.env[varName] || ""
|
||||||
|
})
|
||||||
|
|
||||||
|
const fileMatches = text.match(/"?\{file:([^}]+)\}"?/g)
|
||||||
|
if (fileMatches) {
|
||||||
|
const configDir = path.dirname(configPath)
|
||||||
|
for (const match of fileMatches) {
|
||||||
|
const filePath = match.replace(/^"?\{file:/, "").replace(/\}"?$/, "")
|
||||||
|
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(configDir, filePath)
|
||||||
|
const fileContent = await Bun.file(resolvedPath).text()
|
||||||
|
text = text.replace(match, JSON.stringify(fileContent))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let data: any
|
||||||
|
try {
|
||||||
|
data = JSON.parse(text)
|
||||||
|
} catch (err) {
|
||||||
|
throw new JsonError({ path: configPath }, { cause: err as Error })
|
||||||
|
}
|
||||||
|
|
||||||
const parsed = Info.safeParse(data)
|
const parsed = Info.safeParse(data)
|
||||||
if (parsed.success) return parsed.data
|
if (parsed.success) return parsed.data
|
||||||
throw new InvalidError({ path, issues: parsed.error.issues })
|
throw new InvalidError({ path: configPath, issues: parsed.error.issues })
|
||||||
}
|
}
|
||||||
|
|
||||||
export const JsonError = NamedError.create(
|
export const JsonError = NamedError.create(
|
||||||
"ConfigJsonError",
|
"ConfigJsonError",
|
||||||
z.object({
|
z.object({
|
||||||
|
|||||||
@@ -11,6 +11,19 @@ import { Log } from "../util/log"
|
|||||||
export namespace File {
|
export namespace File {
|
||||||
const log = Log.create({ service: "file" })
|
const log = Log.create({ service: "file" })
|
||||||
|
|
||||||
|
export const Info = z
|
||||||
|
.object({
|
||||||
|
path: z.string(),
|
||||||
|
added: z.number().int(),
|
||||||
|
removed: z.number().int(),
|
||||||
|
status: z.enum(["added", "deleted", "modified"]),
|
||||||
|
})
|
||||||
|
.openapi({
|
||||||
|
ref: "File",
|
||||||
|
})
|
||||||
|
|
||||||
|
export type Info = z.infer<typeof Info>
|
||||||
|
|
||||||
export const Event = {
|
export const Event = {
|
||||||
Edited: Bus.event(
|
Edited: Bus.event(
|
||||||
"file.edited",
|
"file.edited",
|
||||||
@@ -26,14 +39,14 @@ export namespace File {
|
|||||||
|
|
||||||
const diffOutput = await $`git diff --numstat HEAD`.cwd(app.path.cwd).quiet().nothrow().text()
|
const diffOutput = await $`git diff --numstat HEAD`.cwd(app.path.cwd).quiet().nothrow().text()
|
||||||
|
|
||||||
const changedFiles = []
|
const changedFiles: Info[] = []
|
||||||
|
|
||||||
if (diffOutput.trim()) {
|
if (diffOutput.trim()) {
|
||||||
const lines = diffOutput.trim().split("\n")
|
const lines = diffOutput.trim().split("\n")
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const [added, removed, filepath] = line.split("\t")
|
const [added, removed, filepath] = line.split("\t")
|
||||||
changedFiles.push({
|
changedFiles.push({
|
||||||
file: filepath,
|
path: filepath,
|
||||||
added: added === "-" ? 0 : parseInt(added, 10),
|
added: added === "-" ? 0 : parseInt(added, 10),
|
||||||
removed: removed === "-" ? 0 : parseInt(removed, 10),
|
removed: removed === "-" ? 0 : parseInt(removed, 10),
|
||||||
status: "modified",
|
status: "modified",
|
||||||
@@ -50,7 +63,7 @@ export namespace File {
|
|||||||
const content = await Bun.file(path.join(app.path.root, filepath)).text()
|
const content = await Bun.file(path.join(app.path.root, filepath)).text()
|
||||||
const lines = content.split("\n").length
|
const lines = content.split("\n").length
|
||||||
changedFiles.push({
|
changedFiles.push({
|
||||||
file: filepath,
|
path: filepath,
|
||||||
added: lines,
|
added: lines,
|
||||||
removed: 0,
|
removed: 0,
|
||||||
status: "added",
|
status: "added",
|
||||||
@@ -68,7 +81,7 @@ export namespace File {
|
|||||||
const deletedFiles = deletedOutput.trim().split("\n")
|
const deletedFiles = deletedOutput.trim().split("\n")
|
||||||
for (const filepath of deletedFiles) {
|
for (const filepath of deletedFiles) {
|
||||||
changedFiles.push({
|
changedFiles.push({
|
||||||
file: filepath,
|
path: filepath,
|
||||||
added: 0,
|
added: 0,
|
||||||
removed: 0, // Could get original line count but would require another git command
|
removed: 0, // Could get original line count but would require another git command
|
||||||
status: "deleted",
|
status: "deleted",
|
||||||
@@ -78,7 +91,7 @@ export namespace File {
|
|||||||
|
|
||||||
return changedFiles.map((x) => ({
|
return changedFiles.map((x) => ({
|
||||||
...x,
|
...x,
|
||||||
file: path.relative(app.path.cwd, path.join(app.path.root, x.file)),
|
path: path.relative(app.path.cwd, path.join(app.path.root, x.path)),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,25 +34,27 @@ export namespace Ripgrep {
|
|||||||
|
|
||||||
export const Match = z.object({
|
export const Match = z.object({
|
||||||
type: z.literal("match"),
|
type: z.literal("match"),
|
||||||
data: z.object({
|
data: z
|
||||||
path: z.object({
|
.object({
|
||||||
text: z.string(),
|
path: z.object({
|
||||||
}),
|
text: z.string(),
|
||||||
lines: z.object({
|
|
||||||
text: z.string(),
|
|
||||||
}),
|
|
||||||
line_number: z.number(),
|
|
||||||
absolute_offset: z.number(),
|
|
||||||
submatches: z.array(
|
|
||||||
z.object({
|
|
||||||
match: z.object({
|
|
||||||
text: z.string(),
|
|
||||||
}),
|
|
||||||
start: z.number(),
|
|
||||||
end: z.number(),
|
|
||||||
}),
|
}),
|
||||||
),
|
lines: z.object({
|
||||||
}),
|
text: z.string(),
|
||||||
|
}),
|
||||||
|
line_number: z.number(),
|
||||||
|
absolute_offset: z.number(),
|
||||||
|
submatches: z.array(
|
||||||
|
z.object({
|
||||||
|
match: z.object({
|
||||||
|
text: z.string(),
|
||||||
|
}),
|
||||||
|
start: z.number(),
|
||||||
|
end: z.number(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.openapi({ ref: "Match" }),
|
||||||
})
|
})
|
||||||
|
|
||||||
const End = z.object({
|
const End = z.object({
|
||||||
@@ -185,10 +187,15 @@ export namespace Ripgrep {
|
|||||||
return filepath
|
return filepath
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function files(input: { cwd: string; query?: string; glob?: string; limit?: number }) {
|
export async function files(input: { cwd: string; query?: string; glob?: string[]; limit?: number }) {
|
||||||
const commands = [
|
const commands = [`${$.escape(await filepath())} --files --follow --hidden --glob='!.git/*'`]
|
||||||
`${await filepath()} --files --hidden --glob='!.git/*' ${input.glob ? `--glob='${input.glob}'` : ``}`,
|
|
||||||
]
|
if (input.glob) {
|
||||||
|
for (const g of input.glob) {
|
||||||
|
commands[0] += ` --glob='${g}'`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (input.query) commands.push(`${await Fzf.filepath()} --filter=${input.query}`)
|
if (input.query) commands.push(`${await Fzf.filepath()} --filter=${input.query}`)
|
||||||
if (input.limit) commands.push(`head -n ${input.limit}`)
|
if (input.limit) commands.push(`head -n ${input.limit}`)
|
||||||
const joined = commands.join(" | ")
|
const joined = commands.join(" | ")
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export namespace FileWatcher {
|
|||||||
)
|
)
|
||||||
|
|
||||||
export function init() {
|
export function init() {
|
||||||
if (Flag.OPENCODE_DISABLE_WATCHER) return
|
if (Flag.OPENCODE_DISABLE_WATCHER || true) return
|
||||||
state()
|
state()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { App } from "../app/app"
|
import { App } from "../app/app"
|
||||||
import { BunProc } from "../bun"
|
import { BunProc } from "../bun"
|
||||||
|
import { Filesystem } from "../util/filesystem"
|
||||||
|
import path from "path"
|
||||||
|
|
||||||
export interface Info {
|
export interface Info {
|
||||||
name: string
|
name: string
|
||||||
@@ -29,7 +31,7 @@ export const mix: Info = {
|
|||||||
|
|
||||||
export const prettier: Info = {
|
export const prettier: Info = {
|
||||||
name: "prettier",
|
name: "prettier",
|
||||||
command: [BunProc.which(), "run", "prettier", "--write", "$FILE"],
|
command: [BunProc.which(), "x", "prettier", "--write", "$FILE"],
|
||||||
environment: {
|
environment: {
|
||||||
BUN_BE_BUN: "1",
|
BUN_BE_BUN: "1",
|
||||||
},
|
},
|
||||||
@@ -62,23 +64,12 @@ export const prettier: Info = {
|
|||||||
".gql",
|
".gql",
|
||||||
],
|
],
|
||||||
async enabled() {
|
async enabled() {
|
||||||
// this is more complicated because we only want to use prettier if it's
|
const app = App.info()
|
||||||
// being used with the current project
|
const nms = await Filesystem.findUp("node_modules", app.path.cwd, app.path.root)
|
||||||
try {
|
for (const item of nms) {
|
||||||
const proc = Bun.spawn({
|
if (await Bun.file(path.join(item, ".bin", "prettier")).exists()) return true
|
||||||
cmd: [BunProc.which(), "run", "prettier", "--version"],
|
|
||||||
cwd: App.info().path.cwd,
|
|
||||||
env: {
|
|
||||||
BUN_BE_BUN: "1",
|
|
||||||
},
|
|
||||||
stdout: "ignore",
|
|
||||||
stderr: "ignore",
|
|
||||||
})
|
|
||||||
const exit = await proc.exited
|
|
||||||
return exit === 0
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,24 @@ const cli = yargs(hideBin(process.argv))
|
|||||||
})
|
})
|
||||||
.middleware(async () => {
|
.middleware(async () => {
|
||||||
await Log.init({ print: process.argv.includes("--print-logs") })
|
await Log.init({ print: process.argv.includes("--print-logs") })
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { Config } = await import("./config/config")
|
||||||
|
const { App } = await import("./app/app")
|
||||||
|
|
||||||
|
App.provide({ cwd: process.cwd() }, async () => {
|
||||||
|
const cfg = await Config.get()
|
||||||
|
if (cfg.log_level) {
|
||||||
|
Log.setLevel(cfg.log_level as Log.Level)
|
||||||
|
} else {
|
||||||
|
const defaultLevel = Installation.isDev() ? "DEBUG" : "INFO"
|
||||||
|
Log.setLevel(defaultLevel)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
Log.Default.error("failed to load config", { error: e })
|
||||||
|
}
|
||||||
|
|
||||||
Log.Default.info("opencode", {
|
Log.Default.info("opencode", {
|
||||||
version: Installation.VERSION,
|
version: Installation.VERSION,
|
||||||
args: process.argv.slice(2),
|
args: process.argv.slice(2),
|
||||||
|
|||||||
@@ -34,46 +34,54 @@ export namespace LSPClient {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function create(serverID: string, server: LSPServer.Handle) {
|
export async function create(input: { serverID: string; server: LSPServer.Handle; root: string }) {
|
||||||
const app = App.info()
|
const app = App.info()
|
||||||
log.info("starting client", { id: serverID })
|
const l = log.clone().tag("serverID", input.serverID)
|
||||||
|
l.info("starting client")
|
||||||
|
|
||||||
const connection = createMessageConnection(
|
const connection = createMessageConnection(
|
||||||
new StreamMessageReader(server.process.stdout),
|
new StreamMessageReader(input.server.process.stdout),
|
||||||
new StreamMessageWriter(server.process.stdin),
|
new StreamMessageWriter(input.server.process.stdin),
|
||||||
)
|
)
|
||||||
|
|
||||||
const diagnostics = new Map<string, Diagnostic[]>()
|
const diagnostics = new Map<string, Diagnostic[]>()
|
||||||
connection.onNotification("textDocument/publishDiagnostics", (params) => {
|
connection.onNotification("textDocument/publishDiagnostics", (params) => {
|
||||||
const path = new URL(params.uri).pathname
|
const path = new URL(params.uri).pathname
|
||||||
log.info("textDocument/publishDiagnostics", {
|
l.info("textDocument/publishDiagnostics", {
|
||||||
path,
|
path,
|
||||||
})
|
})
|
||||||
const exists = diagnostics.has(path)
|
const exists = diagnostics.has(path)
|
||||||
diagnostics.set(path, params.diagnostics)
|
diagnostics.set(path, params.diagnostics)
|
||||||
if (!exists && serverID === "typescript") return
|
if (!exists && input.serverID === "typescript") return
|
||||||
Bus.publish(Event.Diagnostics, { path, serverID })
|
Bus.publish(Event.Diagnostics, { path, serverID: input.serverID })
|
||||||
|
})
|
||||||
|
connection.onRequest("window/workDoneProgress/create", (params) => {
|
||||||
|
l.info("window/workDoneProgress/create", params)
|
||||||
|
return null
|
||||||
})
|
})
|
||||||
connection.onRequest("workspace/configuration", async () => {
|
connection.onRequest("workspace/configuration", async () => {
|
||||||
return [{}]
|
return [{}]
|
||||||
})
|
})
|
||||||
connection.listen()
|
connection.listen()
|
||||||
|
|
||||||
log.info("sending initialize", { id: serverID })
|
l.info("sending initialize")
|
||||||
await withTimeout(
|
await withTimeout(
|
||||||
connection.sendRequest("initialize", {
|
connection.sendRequest("initialize", {
|
||||||
rootUri: "file://" + app.path.cwd,
|
rootUri: "file://" + input.root,
|
||||||
processId: server.process.pid,
|
processId: input.server.process.pid,
|
||||||
workspaceFolders: [
|
workspaceFolders: [
|
||||||
{
|
{
|
||||||
name: "workspace",
|
name: "workspace",
|
||||||
uri: "file://" + app.path.cwd,
|
uri: "file://" + input.root,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
initializationOptions: {
|
initializationOptions: {
|
||||||
...server.initialization,
|
...input.server.initialization,
|
||||||
},
|
},
|
||||||
capabilities: {
|
capabilities: {
|
||||||
|
window: {
|
||||||
|
workDoneProgress: true,
|
||||||
|
},
|
||||||
workspace: {
|
workspace: {
|
||||||
configuration: true,
|
configuration: true,
|
||||||
},
|
},
|
||||||
@@ -90,9 +98,9 @@ export namespace LSPClient {
|
|||||||
}),
|
}),
|
||||||
5_000,
|
5_000,
|
||||||
).catch((err) => {
|
).catch((err) => {
|
||||||
log.error("initialize error", { error: err })
|
l.error("initialize error", { error: err })
|
||||||
throw new InitializeError(
|
throw new InitializeError(
|
||||||
{ serverID },
|
{ serverID: input.serverID },
|
||||||
{
|
{
|
||||||
cause: err,
|
cause: err,
|
||||||
},
|
},
|
||||||
@@ -100,17 +108,15 @@ export namespace LSPClient {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await connection.sendNotification("initialized", {})
|
await connection.sendNotification("initialized", {})
|
||||||
log.info("initialized", {
|
|
||||||
serverID,
|
|
||||||
})
|
|
||||||
|
|
||||||
const files: {
|
const files: {
|
||||||
[path: string]: number
|
[path: string]: number
|
||||||
} = {}
|
} = {}
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
|
root: input.root,
|
||||||
get serverID() {
|
get serverID() {
|
||||||
return serverID
|
return input.serverID
|
||||||
},
|
},
|
||||||
get connection() {
|
get connection() {
|
||||||
return connection
|
return connection
|
||||||
@@ -170,13 +176,16 @@ export namespace LSPClient {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
async shutdown() {
|
async shutdown() {
|
||||||
log.info("shutting down", { serverID })
|
l.info("shutting down")
|
||||||
connection.end()
|
connection.end()
|
||||||
connection.dispose()
|
connection.dispose()
|
||||||
log.info("shutdown", { serverID })
|
input.server.process.kill()
|
||||||
|
l.info("shutdown")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
l.info("initialized")
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,62 +3,65 @@ import { Log } from "../util/log"
|
|||||||
import { LSPClient } from "./client"
|
import { LSPClient } from "./client"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { LSPServer } from "./server"
|
import { LSPServer } from "./server"
|
||||||
import { Ripgrep } from "../file/ripgrep"
|
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
export namespace LSP {
|
export namespace LSP {
|
||||||
const log = Log.create({ service: "lsp" })
|
const log = Log.create({ service: "lsp" })
|
||||||
|
|
||||||
|
export const Range = z
|
||||||
|
.object({
|
||||||
|
start: z.object({
|
||||||
|
line: z.number(),
|
||||||
|
character: z.number(),
|
||||||
|
}),
|
||||||
|
end: z.object({
|
||||||
|
line: z.number(),
|
||||||
|
character: z.number(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.openapi({
|
||||||
|
ref: "Range",
|
||||||
|
})
|
||||||
|
export type Range = z.infer<typeof Range>
|
||||||
|
|
||||||
export const Symbol = z
|
export const Symbol = z
|
||||||
.object({
|
.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
kind: z.number(),
|
kind: z.number(),
|
||||||
location: z.object({
|
location: z.object({
|
||||||
uri: z.string(),
|
uri: z.string(),
|
||||||
range: z.object({
|
range: Range,
|
||||||
start: z.object({
|
|
||||||
line: z.number(),
|
|
||||||
character: z.number(),
|
|
||||||
}),
|
|
||||||
end: z.object({
|
|
||||||
line: z.number(),
|
|
||||||
character: z.number(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.openapi({
|
.openapi({
|
||||||
ref: "LSP.Symbol",
|
ref: "Symbol",
|
||||||
})
|
})
|
||||||
export type Symbol = z.infer<typeof Symbol>
|
export type Symbol = z.infer<typeof Symbol>
|
||||||
|
|
||||||
|
export const DocumentSymbol = z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
detail: z.string().optional(),
|
||||||
|
kind: z.number(),
|
||||||
|
range: Range,
|
||||||
|
selectionRange: Range,
|
||||||
|
})
|
||||||
|
.openapi({
|
||||||
|
ref: "DocumentSymbol",
|
||||||
|
})
|
||||||
|
export type DocumentSymbol = z.infer<typeof DocumentSymbol>
|
||||||
|
|
||||||
const state = App.state(
|
const state = App.state(
|
||||||
"lsp",
|
"lsp",
|
||||||
async (app) => {
|
async () => {
|
||||||
log.info("initializing")
|
const clients: LSPClient.Info[] = []
|
||||||
const clients = new Map<string, LSPClient.Info>()
|
|
||||||
for (const server of Object.values(LSPServer)) {
|
|
||||||
for (const extension of server.extensions) {
|
|
||||||
const [file] = await Ripgrep.files({
|
|
||||||
cwd: app.path.cwd,
|
|
||||||
glob: "*" + extension,
|
|
||||||
})
|
|
||||||
if (!file) continue
|
|
||||||
const handle = await server.spawn(App.info())
|
|
||||||
if (!handle) break
|
|
||||||
const client = await LSPClient.create(server.id, handle).catch((err) => log.error("", { error: err }))
|
|
||||||
if (!client) break
|
|
||||||
clients.set(server.id, client)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.info("initialized")
|
|
||||||
return {
|
return {
|
||||||
|
broken: new Set<string>(),
|
||||||
clients,
|
clients,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async (state) => {
|
async (state) => {
|
||||||
for (const client of state.clients.values()) {
|
for (const client of state.clients) {
|
||||||
await client.shutdown()
|
await client.shutdown()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -68,13 +71,43 @@ export namespace LSP {
|
|||||||
return state()
|
return state()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getClients(file: string) {
|
||||||
|
const s = await state()
|
||||||
|
const extension = path.parse(file).ext
|
||||||
|
const result: LSPClient.Info[] = []
|
||||||
|
for (const server of Object.values(LSPServer)) {
|
||||||
|
if (!server.extensions.includes(extension)) continue
|
||||||
|
const root = await server.root(file, App.info())
|
||||||
|
if (!root) continue
|
||||||
|
if (s.broken.has(root + server.id)) continue
|
||||||
|
|
||||||
|
const match = s.clients.find((x) => x.root === root && x.serverID === server.id)
|
||||||
|
if (match) {
|
||||||
|
result.push(match)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const handle = await server.spawn(App.info(), root)
|
||||||
|
if (!handle) continue
|
||||||
|
const client = await LSPClient.create({
|
||||||
|
serverID: server.id,
|
||||||
|
server: handle,
|
||||||
|
root,
|
||||||
|
}).catch((err) => {
|
||||||
|
s.broken.add(root + server.id)
|
||||||
|
handle.process.kill()
|
||||||
|
log.error("", { error: err })
|
||||||
|
})
|
||||||
|
if (!client) continue
|
||||||
|
s.clients.push(client)
|
||||||
|
result.push(client)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
export async function touchFile(input: string, waitForDiagnostics?: boolean) {
|
export async function touchFile(input: string, waitForDiagnostics?: boolean) {
|
||||||
const extension = path.parse(input).ext
|
const clients = await getClients(input)
|
||||||
const matches = Object.values(LSPServer)
|
|
||||||
.filter((x) => x.extensions.includes(extension))
|
|
||||||
.map((x) => x.id)
|
|
||||||
await run(async (client) => {
|
await run(async (client) => {
|
||||||
if (!matches.includes(client.serverID)) return
|
if (!clients.includes(client)) return
|
||||||
const wait = waitForDiagnostics ? client.waitForDiagnostics({ path: input }) : Promise.resolve()
|
const wait = waitForDiagnostics ? client.waitForDiagnostics({ path: input }) : Promise.resolve()
|
||||||
await client.notify.open({ path: input })
|
await client.notify.open({ path: input })
|
||||||
return wait
|
return wait
|
||||||
@@ -107,16 +140,74 @@ export namespace LSP {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum SymbolKind {
|
||||||
|
File = 1,
|
||||||
|
Module = 2,
|
||||||
|
Namespace = 3,
|
||||||
|
Package = 4,
|
||||||
|
Class = 5,
|
||||||
|
Method = 6,
|
||||||
|
Property = 7,
|
||||||
|
Field = 8,
|
||||||
|
Constructor = 9,
|
||||||
|
Enum = 10,
|
||||||
|
Interface = 11,
|
||||||
|
Function = 12,
|
||||||
|
Variable = 13,
|
||||||
|
Constant = 14,
|
||||||
|
String = 15,
|
||||||
|
Number = 16,
|
||||||
|
Boolean = 17,
|
||||||
|
Array = 18,
|
||||||
|
Object = 19,
|
||||||
|
Key = 20,
|
||||||
|
Null = 21,
|
||||||
|
EnumMember = 22,
|
||||||
|
Struct = 23,
|
||||||
|
Event = 24,
|
||||||
|
Operator = 25,
|
||||||
|
TypeParameter = 26,
|
||||||
|
}
|
||||||
|
|
||||||
|
const kinds = [
|
||||||
|
SymbolKind.Class,
|
||||||
|
SymbolKind.Function,
|
||||||
|
SymbolKind.Method,
|
||||||
|
SymbolKind.Interface,
|
||||||
|
SymbolKind.Variable,
|
||||||
|
SymbolKind.Constant,
|
||||||
|
SymbolKind.Struct,
|
||||||
|
SymbolKind.Enum,
|
||||||
|
]
|
||||||
|
|
||||||
export async function workspaceSymbol(query: string) {
|
export async function workspaceSymbol(query: string) {
|
||||||
return run((client) =>
|
return run((client) =>
|
||||||
client.connection.sendRequest("workspace/symbol", {
|
client.connection
|
||||||
query,
|
.sendRequest("workspace/symbol", {
|
||||||
}),
|
query,
|
||||||
|
})
|
||||||
|
.then((result: any) => result.filter((x: LSP.Symbol) => kinds.includes(x.kind)))
|
||||||
|
.then((result: any) => result.slice(0, 10))
|
||||||
|
.catch(() => []),
|
||||||
).then((result) => result.flat() as LSP.Symbol[])
|
).then((result) => result.flat() as LSP.Symbol[])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function documentSymbol(uri: string) {
|
||||||
|
return run((client) =>
|
||||||
|
client.connection
|
||||||
|
.sendRequest("textDocument/documentSymbol", {
|
||||||
|
textDocument: {
|
||||||
|
uri,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch(() => []),
|
||||||
|
)
|
||||||
|
.then((result) => result.flat() as (LSP.DocumentSymbol | LSP.Symbol)[])
|
||||||
|
.then((result) => result.filter(Boolean))
|
||||||
|
}
|
||||||
|
|
||||||
async function run<T>(input: (client: LSPClient.Info) => Promise<T>): Promise<T[]> {
|
async function run<T>(input: (client: LSPClient.Info) => Promise<T>): Promise<T[]> {
|
||||||
const clients = await state().then((x) => [...x.clients.values()])
|
const clients = await state().then((x) => x.clients)
|
||||||
const tasks = clients.map((x) => input(x))
|
const tasks = clients.map((x) => input(x))
|
||||||
return Promise.all(tasks)
|
return Promise.all(tasks)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,4 +94,6 @@ export const LANGUAGE_EXTENSIONS: Record<string, string> = {
|
|||||||
".yml": "yaml",
|
".yml": "yaml",
|
||||||
".mjs": "javascript",
|
".mjs": "javascript",
|
||||||
".cjs": "javascript",
|
".cjs": "javascript",
|
||||||
|
".zig": "zig",
|
||||||
|
".zon": "zig",
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Log } from "../util/log"
|
|||||||
import { BunProc } from "../bun"
|
import { BunProc } from "../bun"
|
||||||
import { $ } from "bun"
|
import { $ } from "bun"
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
|
import { Filesystem } from "../util/filesystem"
|
||||||
|
|
||||||
export namespace LSPServer {
|
export namespace LSPServer {
|
||||||
const log = Log.create({ service: "lsp.server" })
|
const log = Log.create({ service: "lsp.server" })
|
||||||
@@ -15,19 +16,39 @@ export namespace LSPServer {
|
|||||||
initialization?: Record<string, any>
|
initialization?: Record<string, any>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RootFunction = (file: string, app: App.Info) => Promise<string | undefined>
|
||||||
|
|
||||||
|
const NearestRoot = (patterns: string[]): RootFunction => {
|
||||||
|
return async (file, app) => {
|
||||||
|
const files = Filesystem.up({
|
||||||
|
targets: patterns,
|
||||||
|
start: path.dirname(file),
|
||||||
|
stop: app.path.root,
|
||||||
|
})
|
||||||
|
const first = await files.next()
|
||||||
|
await files.return()
|
||||||
|
if (!first.value) return app.path.root
|
||||||
|
return path.dirname(first.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface Info {
|
export interface Info {
|
||||||
id: string
|
id: string
|
||||||
extensions: string[]
|
extensions: string[]
|
||||||
spawn(app: App.Info): Promise<Handle | undefined>
|
global?: boolean
|
||||||
|
root: RootFunction
|
||||||
|
spawn(app: App.Info, root: string): Promise<Handle | undefined>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Typescript: Info = {
|
export const Typescript: Info = {
|
||||||
id: "typescript",
|
id: "typescript",
|
||||||
|
root: NearestRoot(["tsconfig.json", "package.json", "jsconfig.json"]),
|
||||||
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
|
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
|
||||||
async spawn(app) {
|
async spawn(app, root) {
|
||||||
const tsserver = await Bun.resolve("typescript/lib/tsserver.js", app.path.cwd).catch(() => {})
|
const tsserver = await Bun.resolve("typescript/lib/tsserver.js", app.path.cwd).catch(() => {})
|
||||||
if (!tsserver) return
|
if (!tsserver) return
|
||||||
const proc = spawn(BunProc.which(), ["x", "typescript-language-server", "--stdio"], {
|
const proc = spawn(BunProc.which(), ["x", "typescript-language-server", "--stdio"], {
|
||||||
|
cwd: root,
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
BUN_BE_BUN: "1",
|
BUN_BE_BUN: "1",
|
||||||
@@ -46,8 +67,13 @@ export namespace LSPServer {
|
|||||||
|
|
||||||
export const Gopls: Info = {
|
export const Gopls: Info = {
|
||||||
id: "golang",
|
id: "golang",
|
||||||
|
root: async (file, app) => {
|
||||||
|
const work = await NearestRoot(["go.work"])(file, app)
|
||||||
|
if (work) return work
|
||||||
|
return NearestRoot(["go.mod", "go.sum"])(file, app)
|
||||||
|
},
|
||||||
extensions: [".go"],
|
extensions: [".go"],
|
||||||
async spawn() {
|
async spawn(_, root) {
|
||||||
let bin = Bun.which("gopls", {
|
let bin = Bun.which("gopls", {
|
||||||
PATH: process.env["PATH"] + ":" + Global.Path.bin,
|
PATH: process.env["PATH"] + ":" + Global.Path.bin,
|
||||||
})
|
})
|
||||||
@@ -72,15 +98,18 @@ export namespace LSPServer {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
process: spawn(bin!),
|
process: spawn(bin!, {
|
||||||
|
cwd: root,
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RubyLsp: Info = {
|
export const RubyLsp: Info = {
|
||||||
id: "ruby-lsp",
|
id: "ruby-lsp",
|
||||||
|
root: NearestRoot(["Gemfile"]),
|
||||||
extensions: [".rb", ".rake", ".gemspec", ".ru"],
|
extensions: [".rb", ".rake", ".gemspec", ".ru"],
|
||||||
async spawn() {
|
async spawn(_, root) {
|
||||||
let bin = Bun.which("ruby-lsp", {
|
let bin = Bun.which("ruby-lsp", {
|
||||||
PATH: process.env["PATH"] + ":" + Global.Path.bin,
|
PATH: process.env["PATH"] + ":" + Global.Path.bin,
|
||||||
})
|
})
|
||||||
@@ -109,7 +138,9 @@ export namespace LSPServer {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
process: spawn(bin!, ["--stdio"]),
|
process: spawn(bin!, ["--stdio"], {
|
||||||
|
cwd: root,
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -117,8 +148,10 @@ export namespace LSPServer {
|
|||||||
export const Pyright: Info = {
|
export const Pyright: Info = {
|
||||||
id: "pyright",
|
id: "pyright",
|
||||||
extensions: [".py", ".pyi"],
|
extensions: [".py", ".pyi"],
|
||||||
async spawn() {
|
root: NearestRoot(["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile", "pyrightconfig.json"]),
|
||||||
|
async spawn(_, root) {
|
||||||
const proc = spawn(BunProc.which(), ["x", "pyright-langserver", "--stdio"], {
|
const proc = spawn(BunProc.which(), ["x", "pyright-langserver", "--stdio"], {
|
||||||
|
cwd: root,
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
BUN_BE_BUN: "1",
|
BUN_BE_BUN: "1",
|
||||||
@@ -133,7 +166,8 @@ export namespace LSPServer {
|
|||||||
export const ElixirLS: Info = {
|
export const ElixirLS: Info = {
|
||||||
id: "elixir-ls",
|
id: "elixir-ls",
|
||||||
extensions: [".ex", ".exs"],
|
extensions: [".ex", ".exs"],
|
||||||
async spawn() {
|
root: NearestRoot(["mix.exs", "mix.lock"]),
|
||||||
|
async spawn(_, root) {
|
||||||
let binary = Bun.which("elixir-ls")
|
let binary = Bun.which("elixir-ls")
|
||||||
if (!binary) {
|
if (!binary) {
|
||||||
const elixirLsPath = path.join(Global.Path.bin, "elixir-ls")
|
const elixirLsPath = path.join(Global.Path.bin, "elixir-ls")
|
||||||
@@ -177,7 +211,114 @@ export namespace LSPServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
process: spawn(binary),
|
process: spawn(binary, {
|
||||||
|
cwd: root,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Zls: Info = {
|
||||||
|
id: "zls",
|
||||||
|
extensions: [".zig", ".zon"],
|
||||||
|
root: NearestRoot(["build.zig"]),
|
||||||
|
async spawn(_, root) {
|
||||||
|
let bin = Bun.which("zls", {
|
||||||
|
PATH: process.env["PATH"] + ":" + Global.Path.bin,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!bin) {
|
||||||
|
const zig = Bun.which("zig")
|
||||||
|
if (!zig) {
|
||||||
|
log.error("Zig is required to use zls. Please install Zig first.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("downloading zls from GitHub releases")
|
||||||
|
|
||||||
|
const releaseResponse = await fetch("https://api.github.com/repos/zigtools/zls/releases/latest")
|
||||||
|
if (!releaseResponse.ok) {
|
||||||
|
log.error("Failed to fetch zls release info")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const release = await releaseResponse.json()
|
||||||
|
|
||||||
|
const platform = process.platform
|
||||||
|
const arch = process.arch
|
||||||
|
let assetName = ""
|
||||||
|
|
||||||
|
let zlsArch: string = arch
|
||||||
|
if (arch === "arm64") zlsArch = "aarch64"
|
||||||
|
else if (arch === "x64") zlsArch = "x86_64"
|
||||||
|
else if (arch === "ia32") zlsArch = "x86"
|
||||||
|
|
||||||
|
let zlsPlatform: string = platform
|
||||||
|
if (platform === "darwin") zlsPlatform = "macos"
|
||||||
|
else if (platform === "win32") zlsPlatform = "windows"
|
||||||
|
|
||||||
|
const ext = platform === "win32" ? "zip" : "tar.xz"
|
||||||
|
|
||||||
|
assetName = `zls-${zlsArch}-${zlsPlatform}.${ext}`
|
||||||
|
|
||||||
|
const supportedCombos = [
|
||||||
|
"zls-x86_64-linux.tar.xz",
|
||||||
|
"zls-x86_64-macos.tar.xz",
|
||||||
|
"zls-x86_64-windows.zip",
|
||||||
|
"zls-aarch64-linux.tar.xz",
|
||||||
|
"zls-aarch64-macos.tar.xz",
|
||||||
|
"zls-aarch64-windows.zip",
|
||||||
|
"zls-x86-linux.tar.xz",
|
||||||
|
"zls-x86-windows.zip",
|
||||||
|
]
|
||||||
|
|
||||||
|
if (!supportedCombos.includes(assetName)) {
|
||||||
|
log.error(`Platform ${platform} and architecture ${arch} is not supported by zls`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const asset = release.assets.find((a: any) => a.name === assetName)
|
||||||
|
if (!asset) {
|
||||||
|
log.error(`Could not find asset ${assetName} in latest zls release`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadUrl = asset.browser_download_url
|
||||||
|
const downloadResponse = await fetch(downloadUrl)
|
||||||
|
if (!downloadResponse.ok) {
|
||||||
|
log.error("Failed to download zls")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const tempPath = path.join(Global.Path.bin, assetName)
|
||||||
|
await Bun.file(tempPath).write(downloadResponse)
|
||||||
|
|
||||||
|
if (ext === "zip") {
|
||||||
|
await $`unzip -o -q ${tempPath}`.cwd(Global.Path.bin).nothrow()
|
||||||
|
} else {
|
||||||
|
await $`tar -xf ${tempPath}`.cwd(Global.Path.bin).nothrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.rm(tempPath, { force: true })
|
||||||
|
|
||||||
|
bin = path.join(Global.Path.bin, "zls" + (platform === "win32" ? ".exe" : ""))
|
||||||
|
|
||||||
|
if (!(await Bun.file(bin).exists())) {
|
||||||
|
log.error("Failed to extract zls binary")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (platform !== "win32") {
|
||||||
|
await $`chmod +x ${bin}`.nothrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(`installed zls`, { bin })
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
process: spawn(bin, {
|
||||||
|
cwd: root,
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,19 +99,27 @@ export namespace Provider {
|
|||||||
info.access = tokens.access
|
info.access = tokens.access
|
||||||
}
|
}
|
||||||
let isAgentCall = false
|
let isAgentCall = false
|
||||||
|
let isVisionRequest = false
|
||||||
try {
|
try {
|
||||||
const body = typeof init.body === "string" ? JSON.parse(init.body) : init.body
|
const body = typeof init.body === "string" ? JSON.parse(init.body) : init.body
|
||||||
if (body?.messages) {
|
if (body?.messages) {
|
||||||
isAgentCall = body.messages.some((msg: any) => msg.role && ["tool", "assistant"].includes(msg.role))
|
isAgentCall = body.messages.some((msg: any) => msg.role && ["tool", "assistant"].includes(msg.role))
|
||||||
|
isVisionRequest = body.messages.some(
|
||||||
|
(msg: any) =>
|
||||||
|
Array.isArray(msg.content) && msg.content.some((part: any) => part.type === "image_url"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
const headers = {
|
const headers: Record<string, string> = {
|
||||||
...init.headers,
|
...init.headers,
|
||||||
...copilot.HEADERS,
|
...copilot.HEADERS,
|
||||||
Authorization: `Bearer ${info.access}`,
|
Authorization: `Bearer ${info.access}`,
|
||||||
"Openai-Intent": "conversation-edits",
|
"Openai-Intent": "conversation-edits",
|
||||||
"X-Initiator": isAgentCall ? "agent" : "user",
|
"X-Initiator": isAgentCall ? "agent" : "user",
|
||||||
}
|
}
|
||||||
|
if (isVisionRequest) {
|
||||||
|
headers["Copilot-Vision-Request"] = "true"
|
||||||
|
}
|
||||||
delete headers["x-api-key"]
|
delete headers["x-api-key"]
|
||||||
return fetch(input, {
|
return fetch(input, {
|
||||||
...init,
|
...init,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { Config } from "../config/config"
|
|||||||
import { File } from "../file"
|
import { File } from "../file"
|
||||||
import { LSP } from "../lsp"
|
import { LSP } from "../lsp"
|
||||||
import { MessageV2 } from "../session/message-v2"
|
import { MessageV2 } from "../session/message-v2"
|
||||||
|
import { Mode } from "../session/mode"
|
||||||
|
|
||||||
const ERRORS = {
|
const ERRORS = {
|
||||||
400: {
|
400: {
|
||||||
@@ -447,6 +448,7 @@ export namespace Server {
|
|||||||
z.object({
|
z.object({
|
||||||
providerID: z.string(),
|
providerID: z.string(),
|
||||||
modelID: z.string(),
|
modelID: z.string(),
|
||||||
|
mode: z.string(),
|
||||||
parts: MessageV2.UserPart.array(),
|
parts: MessageV2.UserPart.array(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -558,7 +560,7 @@ export namespace Server {
|
|||||||
description: "Symbols",
|
description: "Symbols",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: resolver(z.unknown().array()),
|
schema: resolver(LSP.Symbol.array()),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -621,16 +623,7 @@ export namespace Server {
|
|||||||
description: "File status",
|
description: "File status",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: resolver(
|
schema: resolver(File.Info.array()),
|
||||||
z
|
|
||||||
.object({
|
|
||||||
file: z.string(),
|
|
||||||
added: z.number().int(),
|
|
||||||
removed: z.number().int(),
|
|
||||||
status: z.enum(["added", "deleted", "modified"]),
|
|
||||||
})
|
|
||||||
.array(),
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -641,6 +634,75 @@ export namespace Server {
|
|||||||
return c.json(content)
|
return c.json(content)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
.post(
|
||||||
|
"/log",
|
||||||
|
describeRoute({
|
||||||
|
description: "Write a log entry to the server logs",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Log entry written successfully",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(z.boolean()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
zValidator(
|
||||||
|
"json",
|
||||||
|
z.object({
|
||||||
|
service: z.string().openapi({ description: "Service name for the log entry" }),
|
||||||
|
level: z.enum(["debug", "info", "error", "warn"]).openapi({ description: "Log level" }),
|
||||||
|
message: z.string().openapi({ description: "Log message" }),
|
||||||
|
extra: z
|
||||||
|
.record(z.string(), z.any())
|
||||||
|
.optional()
|
||||||
|
.openapi({ description: "Additional metadata for the log entry" }),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
async (c) => {
|
||||||
|
const { service, level, message, extra } = c.req.valid("json")
|
||||||
|
const logger = Log.create({ service })
|
||||||
|
|
||||||
|
switch (level) {
|
||||||
|
case "debug":
|
||||||
|
logger.debug(message, extra)
|
||||||
|
break
|
||||||
|
case "info":
|
||||||
|
logger.info(message, extra)
|
||||||
|
break
|
||||||
|
case "error":
|
||||||
|
logger.error(message, extra)
|
||||||
|
break
|
||||||
|
case "warn":
|
||||||
|
logger.warn(message, extra)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json(true)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.get(
|
||||||
|
"/mode",
|
||||||
|
describeRoute({
|
||||||
|
description: "List all modes",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "List of modes",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(Mode.Info.array()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
async (c) => {
|
||||||
|
const modes = await Mode.list()
|
||||||
|
return c.json(modes)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
} from "ai"
|
} from "ai"
|
||||||
|
|
||||||
import PROMPT_INITIALIZE from "../session/prompt/initialize.txt"
|
import PROMPT_INITIALIZE from "../session/prompt/initialize.txt"
|
||||||
|
import PROMPT_PLAN from "../session/prompt/plan.txt"
|
||||||
|
|
||||||
import { App } from "../app/app"
|
import { App } from "../app/app"
|
||||||
import { Bus } from "../bus"
|
import { Bus } from "../bus"
|
||||||
@@ -29,16 +30,20 @@ import type { ModelsDev } from "../provider/models"
|
|||||||
import { Share } from "../share/share"
|
import { Share } from "../share/share"
|
||||||
import { Snapshot } from "../snapshot"
|
import { Snapshot } from "../snapshot"
|
||||||
import { Storage } from "../storage/storage"
|
import { Storage } from "../storage/storage"
|
||||||
import type { Tool } from "../tool/tool"
|
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { NamedError } from "../util/error"
|
import { NamedError } from "../util/error"
|
||||||
import { SystemPrompt } from "./system"
|
import { SystemPrompt } from "./system"
|
||||||
import { FileTime } from "../file/time"
|
import { FileTime } from "../file/time"
|
||||||
import { MessageV2 } from "./message-v2"
|
import { MessageV2 } from "./message-v2"
|
||||||
|
import { Mode } from "./mode"
|
||||||
|
import { LSP } from "../lsp"
|
||||||
|
import { ReadTool } from "../tool/read"
|
||||||
|
|
||||||
export namespace Session {
|
export namespace Session {
|
||||||
const log = Log.create({ service: "session" })
|
const log = Log.create({ service: "session" })
|
||||||
|
|
||||||
|
const OUTPUT_TOKEN_MAX = 32_000
|
||||||
|
|
||||||
export const Info = z
|
export const Info = z
|
||||||
.object({
|
.object({
|
||||||
id: Identifier.schema("session"),
|
id: Identifier.schema("session"),
|
||||||
@@ -99,6 +104,7 @@ export namespace Session {
|
|||||||
Error: Bus.event(
|
Error: Bus.event(
|
||||||
"session.error",
|
"session.error",
|
||||||
z.object({
|
z.object({
|
||||||
|
sessionID: z.string().optional(),
|
||||||
error: MessageV2.Assistant.shape.error,
|
error: MessageV2.Assistant.shape.error,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -280,13 +286,12 @@ export namespace Session {
|
|||||||
sessionID: string
|
sessionID: string
|
||||||
providerID: string
|
providerID: string
|
||||||
modelID: string
|
modelID: string
|
||||||
|
mode?: string
|
||||||
parts: MessageV2.UserPart[]
|
parts: MessageV2.UserPart[]
|
||||||
system?: string[]
|
|
||||||
tools?: Tool.Info[]
|
|
||||||
}) {
|
}) {
|
||||||
using abort = lock(input.sessionID)
|
|
||||||
const l = log.clone().tag("session", input.sessionID)
|
const l = log.clone().tag("session", input.sessionID)
|
||||||
l.info("chatting")
|
l.info("chatting")
|
||||||
|
|
||||||
const model = await Provider.getModel(input.providerID, input.modelID)
|
const model = await Provider.getModel(input.providerID, input.modelID)
|
||||||
let msgs = await messages(input.sessionID)
|
let msgs = await messages(input.sessionID)
|
||||||
const session = await get(input.sessionID)
|
const session = await get(input.sessionID)
|
||||||
@@ -316,15 +321,13 @@ export namespace Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const previous = msgs.at(-1) as MessageV2.Assistant
|
const previous = msgs.at(-1) as MessageV2.Assistant
|
||||||
|
const outputLimit = Math.min(model.info.limit.output, OUTPUT_TOKEN_MAX)
|
||||||
|
|
||||||
// auto summarize if too long
|
// auto summarize if too long
|
||||||
if (previous) {
|
if (previous) {
|
||||||
const tokens =
|
const tokens =
|
||||||
previous.tokens.input + previous.tokens.cache.read + previous.tokens.cache.write + previous.tokens.output
|
previous.tokens.input + previous.tokens.cache.read + previous.tokens.cache.write + previous.tokens.output
|
||||||
if (
|
if (model.info.limit.context && tokens > Math.max((model.info.limit.context - outputLimit) * 0.9, 0)) {
|
||||||
model.info.limit.context &&
|
|
||||||
tokens > Math.max((model.info.limit.context - (model.info.limit.output ?? 0)) * 0.9, 0)
|
|
||||||
) {
|
|
||||||
await summarize({
|
await summarize({
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
providerID: input.providerID,
|
providerID: input.providerID,
|
||||||
@@ -334,6 +337,8 @@ export namespace Session {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
using abort = lock(input.sessionID)
|
||||||
|
|
||||||
const lastSummary = msgs.findLast((msg) => msg.role === "assistant" && msg.summary === true)
|
const lastSummary = msgs.findLast((msg) => msg.role === "assistant" && msg.summary === true)
|
||||||
if (lastSummary) msgs = msgs.filter((msg) => msg.id >= lastSummary.id)
|
if (lastSummary) msgs = msgs.filter((msg) => msg.id >= lastSummary.id)
|
||||||
|
|
||||||
@@ -344,38 +349,79 @@ export namespace Session {
|
|||||||
const url = new URL(part.url)
|
const url = new URL(part.url)
|
||||||
switch (url.protocol) {
|
switch (url.protocol) {
|
||||||
case "file:":
|
case "file:":
|
||||||
const filepath = path.join(app.path.cwd, url.pathname)
|
// have to normalize, symbol search returns absolute paths
|
||||||
let file = Bun.file(filepath)
|
// Decode the pathname since URL constructor doesn't automatically decode it
|
||||||
|
const pathname = decodeURIComponent(url.pathname)
|
||||||
|
const relativePath = pathname.replace(app.path.cwd, ".")
|
||||||
|
const filePath = path.join(app.path.cwd, relativePath)
|
||||||
|
|
||||||
if (part.mime === "text/plain") {
|
if (part.mime === "text/plain") {
|
||||||
let text = await file.text()
|
let offset: number | undefined = undefined
|
||||||
|
let limit: number | undefined = undefined
|
||||||
const range = {
|
const range = {
|
||||||
start: url.searchParams.get("start"),
|
start: url.searchParams.get("start"),
|
||||||
end: url.searchParams.get("end"),
|
end: url.searchParams.get("end"),
|
||||||
}
|
}
|
||||||
if (range.start != null && part.mime === "text/plain") {
|
if (range.start != null) {
|
||||||
const lines = text.split("\n")
|
const filePath = part.url.split("?")[0]
|
||||||
const start = parseInt(range.start)
|
let start = parseInt(range.start)
|
||||||
const end = range.end ? parseInt(range.end) : lines.length
|
let end = range.end ? parseInt(range.end) : undefined
|
||||||
text = lines.slice(start, end).join("\n")
|
// some LSP servers (eg, gopls) don't give full range in
|
||||||
|
// workspace/symbol searches, so we'll try to find the
|
||||||
|
// symbol in the document to get the full range
|
||||||
|
if (start === end) {
|
||||||
|
const symbols = await LSP.documentSymbol(filePath)
|
||||||
|
for (const symbol of symbols) {
|
||||||
|
let range: LSP.Range | undefined
|
||||||
|
if ("range" in symbol) {
|
||||||
|
range = symbol.range
|
||||||
|
} else if ("location" in symbol) {
|
||||||
|
range = symbol.location.range
|
||||||
|
}
|
||||||
|
if (range?.start?.line && range?.start?.line === start) {
|
||||||
|
start = range.start.line
|
||||||
|
end = range?.end?.line ?? start
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
offset = Math.max(start - 2, 0)
|
||||||
|
if (end) {
|
||||||
|
limit = end - offset + 2
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
FileTime.read(input.sessionID, filepath)
|
const args = { filePath, offset, limit }
|
||||||
|
const result = await ReadTool.execute(args, {
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
abort: abort.signal,
|
||||||
|
messageID: "", // read tool doesn't use message ID
|
||||||
|
metadata: async () => {},
|
||||||
|
})
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: ["Called the Read tool on " + url.pathname, "<results>", text, "</results>"].join("\n"),
|
synthetic: true,
|
||||||
|
text: `Called the Read tool with the following input: ${JSON.stringify(args)}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
synthetic: true,
|
||||||
|
text: result.output,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let file = Bun.file(filePath)
|
||||||
|
FileTime.read(input.sessionID, filePath)
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: ["Called the Read tool on " + url.pathname].join("\n"),
|
text: `Called the Read tool with the following input: {\"filePath\":\"${pathname}\"}`,
|
||||||
|
synthetic: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "file",
|
type: "file",
|
||||||
url: `data:${part.mime};base64,` + Buffer.from(await file.bytes()).toString("base64url"),
|
url: `data:${part.mime};base64,` + Buffer.from(await file.bytes()).toString("base64"),
|
||||||
mime: part.mime,
|
mime: part.mime,
|
||||||
filename: part.filename!,
|
filename: part.filename!,
|
||||||
},
|
},
|
||||||
@@ -385,6 +431,14 @@ export namespace Session {
|
|||||||
return [part]
|
return [part]
|
||||||
}),
|
}),
|
||||||
).then((x) => x.flat())
|
).then((x) => x.flat())
|
||||||
|
|
||||||
|
if (input.mode === "plan")
|
||||||
|
input.parts.push({
|
||||||
|
type: "text",
|
||||||
|
text: PROMPT_PLAN,
|
||||||
|
synthetic: true,
|
||||||
|
})
|
||||||
|
|
||||||
if (msgs.length === 0 && !session.parentID) {
|
if (msgs.length === 0 && !session.parentID) {
|
||||||
generateText({
|
generateText({
|
||||||
maxOutputTokens: input.providerID === "google" ? 1024 : 20,
|
maxOutputTokens: input.providerID === "google" ? 1024 : 20,
|
||||||
@@ -430,9 +484,13 @@ export namespace Session {
|
|||||||
await updateMessage(msg)
|
await updateMessage(msg)
|
||||||
msgs.push(msg)
|
msgs.push(msg)
|
||||||
|
|
||||||
const system = input.system ?? SystemPrompt.provider(input.providerID)
|
const mode = await Mode.get(input.mode ?? "build")
|
||||||
|
let system = mode.prompt ? [mode.prompt] : SystemPrompt.provider(input.providerID, input.modelID)
|
||||||
system.push(...(await SystemPrompt.environment()))
|
system.push(...(await SystemPrompt.environment()))
|
||||||
system.push(...(await SystemPrompt.custom()))
|
system.push(...(await SystemPrompt.custom()))
|
||||||
|
// max 2 system prompt messages for caching purposes
|
||||||
|
const [first, ...rest] = system
|
||||||
|
system = [first, rest.join("\n")]
|
||||||
|
|
||||||
const next: MessageV2.Info = {
|
const next: MessageV2.Info = {
|
||||||
id: Identifier.ascending("message"),
|
id: Identifier.ascending("message"),
|
||||||
@@ -461,7 +519,8 @@ export namespace Session {
|
|||||||
const tools: Record<string, AITool> = {}
|
const tools: Record<string, AITool> = {}
|
||||||
|
|
||||||
for (const item of await Provider.tools(input.providerID)) {
|
for (const item of await Provider.tools(input.providerID)) {
|
||||||
tools[item.id.replaceAll(".", "_")] = tool({
|
if (mode.tools[item.id] === false) continue
|
||||||
|
tools[item.id] = tool({
|
||||||
id: item.id as any,
|
id: item.id as any,
|
||||||
description: item.description,
|
description: item.description,
|
||||||
inputSchema: item.parameters as ZodSchema,
|
inputSchema: item.parameters as ZodSchema,
|
||||||
@@ -493,17 +552,24 @@ export namespace Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const [key, item] of Object.entries(await MCP.tools())) {
|
for (const [key, item] of Object.entries(await MCP.tools())) {
|
||||||
|
if (mode.tools[key] === false) continue
|
||||||
const execute = item.execute
|
const execute = item.execute
|
||||||
if (!execute) continue
|
if (!execute) continue
|
||||||
item.execute = async (args, opts) => {
|
item.execute = async (args, opts) => {
|
||||||
try {
|
const result = await execute(args, opts)
|
||||||
const result = await execute(args, opts)
|
const output = result.content
|
||||||
return result.content
|
.filter((x: any) => x.type === "text")
|
||||||
.filter((x: any) => x.type === "text")
|
.map((x: any) => x.text)
|
||||||
.map((x: any) => x.text)
|
.join("\n\n")
|
||||||
.join("\n\n")
|
|
||||||
} catch (e: any) {
|
return {
|
||||||
return e.toString()
|
output,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item.toModelOutput = (result) => {
|
||||||
|
return {
|
||||||
|
type: "text",
|
||||||
|
value: result.output,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tools[key] = item
|
tools[key] = item
|
||||||
@@ -516,7 +582,7 @@ export namespace Session {
|
|||||||
const result = streamText({
|
const result = streamText({
|
||||||
onError() {},
|
onError() {},
|
||||||
maxRetries: 10,
|
maxRetries: 10,
|
||||||
maxOutputTokens: Math.max(0, model.info.limit.output) || undefined,
|
maxOutputTokens: outputLimit,
|
||||||
abortSignal: abort.signal,
|
abortSignal: abort.signal,
|
||||||
stopWhen: stepCountIs(1000),
|
stopWhen: stepCountIs(1000),
|
||||||
providerOptions: model.info.options,
|
providerOptions: model.info.options,
|
||||||
@@ -546,167 +612,197 @@ export namespace Session {
|
|||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
for await (const value of result.fullStream) {
|
try {
|
||||||
l.info("part", {
|
for await (const value of result.fullStream) {
|
||||||
type: value.type,
|
l.info("part", {
|
||||||
})
|
type: value.type,
|
||||||
switch (value.type) {
|
})
|
||||||
case "start":
|
switch (value.type) {
|
||||||
break
|
case "start":
|
||||||
|
break
|
||||||
|
|
||||||
case "tool-input-start":
|
case "tool-input-start":
|
||||||
next.parts.push({
|
next.parts.push({
|
||||||
type: "tool",
|
type: "tool",
|
||||||
tool: value.toolName,
|
tool: value.toolName,
|
||||||
id: value.id,
|
id: value.id,
|
||||||
state: {
|
state: {
|
||||||
status: "pending",
|
status: "pending",
|
||||||
},
|
|
||||||
})
|
|
||||||
Bus.publish(MessageV2.Event.PartUpdated, {
|
|
||||||
part: next.parts[next.parts.length - 1],
|
|
||||||
sessionID: next.sessionID,
|
|
||||||
messageID: next.id,
|
|
||||||
})
|
|
||||||
break
|
|
||||||
|
|
||||||
case "tool-input-delta":
|
|
||||||
break
|
|
||||||
|
|
||||||
case "tool-call": {
|
|
||||||
const match = next.parts.find((p): p is MessageV2.ToolPart => p.type === "tool" && p.id === value.toolCallId)
|
|
||||||
if (match) {
|
|
||||||
match.state = {
|
|
||||||
status: "running",
|
|
||||||
input: value.input,
|
|
||||||
time: {
|
|
||||||
start: Date.now(),
|
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
Bus.publish(MessageV2.Event.PartUpdated, {
|
Bus.publish(MessageV2.Event.PartUpdated, {
|
||||||
part: match,
|
part: next.parts[next.parts.length - 1],
|
||||||
sessionID: next.sessionID,
|
sessionID: next.sessionID,
|
||||||
messageID: next.id,
|
messageID: next.id,
|
||||||
})
|
})
|
||||||
}
|
break
|
||||||
break
|
|
||||||
}
|
|
||||||
case "tool-result": {
|
|
||||||
const match = next.parts.find((p): p is MessageV2.ToolPart => p.type === "tool" && p.id === value.toolCallId)
|
|
||||||
if (match && match.state.status === "running") {
|
|
||||||
match.state = {
|
|
||||||
status: "completed",
|
|
||||||
input: value.input,
|
|
||||||
output: value.output.output,
|
|
||||||
metadata: value.output.metadata,
|
|
||||||
title: value.output.title,
|
|
||||||
time: {
|
|
||||||
start: match.state.time.start,
|
|
||||||
end: Date.now(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
Bus.publish(MessageV2.Event.PartUpdated, {
|
|
||||||
part: match,
|
|
||||||
sessionID: next.sessionID,
|
|
||||||
messageID: next.id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case "tool-error": {
|
case "tool-input-delta":
|
||||||
const match = next.parts.find((p): p is MessageV2.ToolPart => p.type === "tool" && p.id === value.toolCallId)
|
break
|
||||||
if (match && match.state.status === "running") {
|
|
||||||
match.state = {
|
|
||||||
status: "error",
|
|
||||||
input: value.input,
|
|
||||||
error: (value.error as any).toString(),
|
|
||||||
time: {
|
|
||||||
start: match.state.time.start,
|
|
||||||
end: Date.now(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
Bus.publish(MessageV2.Event.PartUpdated, {
|
|
||||||
part: match,
|
|
||||||
sessionID: next.sessionID,
|
|
||||||
messageID: next.id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case "error":
|
case "tool-call": {
|
||||||
const e = value.error
|
const match = next.parts.find(
|
||||||
log.error("", {
|
(p): p is MessageV2.ToolPart => p.type === "tool" && p.id === value.toolCallId,
|
||||||
error: e,
|
)
|
||||||
})
|
if (match) {
|
||||||
switch (true) {
|
match.state = {
|
||||||
case MessageV2.OutputLengthError.isInstance(e):
|
status: "running",
|
||||||
next.error = e
|
input: value.input,
|
||||||
break
|
time: {
|
||||||
case LoadAPIKeyError.isInstance(e):
|
start: Date.now(),
|
||||||
next.error = new Provider.AuthError(
|
|
||||||
{
|
|
||||||
providerID: input.providerID,
|
|
||||||
message: e.message,
|
|
||||||
},
|
},
|
||||||
{ cause: e },
|
}
|
||||||
).toObject()
|
Bus.publish(MessageV2.Event.PartUpdated, {
|
||||||
break
|
part: match,
|
||||||
case e instanceof Error:
|
sessionID: next.sessionID,
|
||||||
next.error = new NamedError.Unknown({ message: e.toString() }, { cause: e }).toObject()
|
messageID: next.id,
|
||||||
break
|
})
|
||||||
default:
|
}
|
||||||
next.error = new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e })
|
break
|
||||||
}
|
}
|
||||||
Bus.publish(Event.Error, {
|
case "tool-result": {
|
||||||
error: next.error,
|
const match = next.parts.find(
|
||||||
})
|
(p): p is MessageV2.ToolPart => p.type === "tool" && p.id === value.toolCallId,
|
||||||
break
|
)
|
||||||
|
if (match && match.state.status === "running") {
|
||||||
case "start-step":
|
match.state = {
|
||||||
next.parts.push({
|
status: "completed",
|
||||||
type: "step-start",
|
input: value.input,
|
||||||
})
|
output: value.output.output,
|
||||||
break
|
metadata: value.output.metadata,
|
||||||
|
title: value.output.title,
|
||||||
case "finish-step":
|
time: {
|
||||||
const usage = getUsage(model.info, value.usage, value.providerMetadata)
|
start: match.state.time.start,
|
||||||
next.cost += usage.cost
|
end: Date.now(),
|
||||||
next.tokens = usage.tokens
|
},
|
||||||
break
|
}
|
||||||
|
Bus.publish(MessageV2.Event.PartUpdated, {
|
||||||
case "text-start":
|
part: match,
|
||||||
text = {
|
sessionID: next.sessionID,
|
||||||
type: "text",
|
messageID: next.id,
|
||||||
text: "",
|
})
|
||||||
|
}
|
||||||
|
break
|
||||||
}
|
}
|
||||||
break
|
|
||||||
|
|
||||||
case "text":
|
case "tool-error": {
|
||||||
if (text.text === "") next.parts.push(text)
|
const match = next.parts.find(
|
||||||
text.text += value.text
|
(p): p is MessageV2.ToolPart => p.type === "tool" && p.id === value.toolCallId,
|
||||||
break
|
)
|
||||||
|
if (match && match.state.status === "running") {
|
||||||
|
match.state = {
|
||||||
|
status: "error",
|
||||||
|
input: value.input,
|
||||||
|
error: (value.error as any).toString(),
|
||||||
|
time: {
|
||||||
|
start: match.state.time.start,
|
||||||
|
end: Date.now(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Bus.publish(MessageV2.Event.PartUpdated, {
|
||||||
|
part: match,
|
||||||
|
sessionID: next.sessionID,
|
||||||
|
messageID: next.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
case "text-end":
|
case "error":
|
||||||
Bus.publish(MessageV2.Event.PartUpdated, {
|
throw value.error
|
||||||
part: text,
|
|
||||||
sessionID: next.sessionID,
|
|
||||||
messageID: next.id,
|
|
||||||
})
|
|
||||||
break
|
|
||||||
|
|
||||||
case "finish":
|
case "start-step":
|
||||||
next.time.completed = Date.now()
|
next.parts.push({
|
||||||
break
|
type: "step-start",
|
||||||
|
})
|
||||||
|
break
|
||||||
|
|
||||||
|
case "finish-step":
|
||||||
|
const usage = getUsage(model.info, value.usage, value.providerMetadata)
|
||||||
|
next.cost += usage.cost
|
||||||
|
next.tokens = usage.tokens
|
||||||
|
break
|
||||||
|
|
||||||
|
case "text-start":
|
||||||
|
text = {
|
||||||
|
type: "text",
|
||||||
|
text: "",
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case "text":
|
||||||
|
if (text.text === "") next.parts.push(text)
|
||||||
|
text.text += value.text
|
||||||
|
break
|
||||||
|
|
||||||
|
case "text-end":
|
||||||
|
Bus.publish(MessageV2.Event.PartUpdated, {
|
||||||
|
part: text,
|
||||||
|
sessionID: next.sessionID,
|
||||||
|
messageID: next.id,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
|
||||||
|
case "finish":
|
||||||
|
next.time.completed = Date.now()
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
l.info("unhandled", {
|
||||||
|
...value,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
await updateMessage(next)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log.error("", {
|
||||||
|
error: e,
|
||||||
|
})
|
||||||
|
switch (true) {
|
||||||
|
case e instanceof DOMException && e.name === "AbortError":
|
||||||
|
next.error = new MessageV2.AbortedError(
|
||||||
|
{ message: e.message },
|
||||||
|
{
|
||||||
|
cause: e,
|
||||||
|
},
|
||||||
|
).toObject()
|
||||||
|
break
|
||||||
|
case MessageV2.OutputLengthError.isInstance(e):
|
||||||
|
next.error = e
|
||||||
|
break
|
||||||
|
case LoadAPIKeyError.isInstance(e):
|
||||||
|
next.error = new Provider.AuthError(
|
||||||
|
{
|
||||||
|
providerID: input.providerID,
|
||||||
|
message: e.message,
|
||||||
|
},
|
||||||
|
{ cause: e },
|
||||||
|
).toObject()
|
||||||
|
break
|
||||||
|
case e instanceof Error:
|
||||||
|
next.error = new NamedError.Unknown({ message: e.toString() }, { cause: e }).toObject()
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
l.info("unhandled", {
|
next.error = new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e })
|
||||||
...value,
|
}
|
||||||
})
|
Bus.publish(Event.Error, {
|
||||||
continue
|
sessionID: next.sessionID,
|
||||||
|
error: next.error,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for (const part of next.parts) {
|
||||||
|
if (part.type === "tool" && part.state.status !== "completed") {
|
||||||
|
part.state = {
|
||||||
|
status: "error",
|
||||||
|
error: "Tool execution aborted",
|
||||||
|
time: {
|
||||||
|
start: Date.now(),
|
||||||
|
end: Date.now(),
|
||||||
|
},
|
||||||
|
input: {},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await updateMessage(next)
|
|
||||||
}
|
}
|
||||||
next.time.completed = Date.now()
|
next.time.completed = Date.now()
|
||||||
await updateMessage(next)
|
await updateMessage(next)
|
||||||
@@ -836,20 +932,58 @@ export namespace Session {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
for await (const value of result.fullStream) {
|
try {
|
||||||
switch (value.type) {
|
for await (const value of result.fullStream) {
|
||||||
case "text":
|
switch (value.type) {
|
||||||
if (!text) {
|
case "text":
|
||||||
text = {
|
if (!text) {
|
||||||
type: "text",
|
text = {
|
||||||
text: value.text,
|
type: "text",
|
||||||
}
|
text: value.text,
|
||||||
next.parts.push(text)
|
}
|
||||||
} else text.text += value.text
|
next.parts.push(text)
|
||||||
await updateMessage(next)
|
} else text.text += value.text
|
||||||
break
|
await updateMessage(next)
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
log.error("summarize stream error", {
|
||||||
|
error: e,
|
||||||
|
})
|
||||||
|
switch (true) {
|
||||||
|
case e instanceof DOMException && e.name === "AbortError":
|
||||||
|
next.error = new MessageV2.AbortedError(
|
||||||
|
{ message: e.message },
|
||||||
|
{
|
||||||
|
cause: e,
|
||||||
|
},
|
||||||
|
).toObject()
|
||||||
|
break
|
||||||
|
case MessageV2.OutputLengthError.isInstance(e):
|
||||||
|
next.error = e
|
||||||
|
break
|
||||||
|
case LoadAPIKeyError.isInstance(e):
|
||||||
|
next.error = new Provider.AuthError(
|
||||||
|
{
|
||||||
|
providerID: input.providerID,
|
||||||
|
message: e.message,
|
||||||
|
},
|
||||||
|
{ cause: e },
|
||||||
|
).toObject()
|
||||||
|
break
|
||||||
|
case e instanceof Error:
|
||||||
|
next.error = new NamedError.Unknown({ message: e.toString() }, { cause: e }).toObject()
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
next.error = new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e }).toObject()
|
||||||
|
}
|
||||||
|
Bus.publish(Event.Error, {
|
||||||
|
error: next.error,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
next.time.completed = Date.now()
|
||||||
|
await updateMessage(next)
|
||||||
}
|
}
|
||||||
|
|
||||||
function lock(sessionID: string) {
|
function lock(sessionID: string) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { convertToModelMessages, type ModelMessage, type UIMessage } from "ai"
|
|||||||
|
|
||||||
export namespace MessageV2 {
|
export namespace MessageV2 {
|
||||||
export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
|
export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
|
||||||
|
export const AbortedError = NamedError.create("MessageAbortedError", z.object({}))
|
||||||
|
|
||||||
export const ToolStatePending = z
|
export const ToolStatePending = z
|
||||||
.object({
|
.object({
|
||||||
@@ -75,6 +76,7 @@ export namespace MessageV2 {
|
|||||||
.object({
|
.object({
|
||||||
type: z.literal("text"),
|
type: z.literal("text"),
|
||||||
text: z.string(),
|
text: z.string(),
|
||||||
|
synthetic: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
.openapi({
|
.openapi({
|
||||||
ref: "TextPart",
|
ref: "TextPart",
|
||||||
@@ -148,7 +150,12 @@ export namespace MessageV2 {
|
|||||||
completed: z.number().optional(),
|
completed: z.number().optional(),
|
||||||
}),
|
}),
|
||||||
error: z
|
error: z
|
||||||
.discriminatedUnion("name", [Provider.AuthError.Schema, NamedError.Unknown.Schema, OutputLengthError.Schema])
|
.discriminatedUnion("name", [
|
||||||
|
Provider.AuthError.Schema,
|
||||||
|
NamedError.Unknown.Schema,
|
||||||
|
OutputLengthError.Schema,
|
||||||
|
AbortedError.Schema,
|
||||||
|
])
|
||||||
.optional(),
|
.optional(),
|
||||||
system: z.string().array(),
|
system: z.string().array(),
|
||||||
modelID: z.string(),
|
modelID: z.string(),
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { mergeDeep } from "remeda"
|
||||||
|
import { App } from "../app/app"
|
||||||
|
import { Config } from "../config/config"
|
||||||
|
import z from "zod"
|
||||||
|
|
||||||
|
export namespace Mode {
|
||||||
|
export const Info = z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
model: z
|
||||||
|
.object({
|
||||||
|
modelID: z.string(),
|
||||||
|
providerID: z.string(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
prompt: z.string().optional(),
|
||||||
|
tools: z.record(z.boolean()),
|
||||||
|
})
|
||||||
|
.openapi({
|
||||||
|
ref: "Mode",
|
||||||
|
})
|
||||||
|
export type Info = z.infer<typeof Info>
|
||||||
|
const state = App.state("mode", async () => {
|
||||||
|
const cfg = await Config.get()
|
||||||
|
const mode = mergeDeep(
|
||||||
|
{
|
||||||
|
build: {},
|
||||||
|
plan: {
|
||||||
|
tools: {
|
||||||
|
write: false,
|
||||||
|
edit: false,
|
||||||
|
patch: false,
|
||||||
|
bash: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cfg.mode ?? {},
|
||||||
|
)
|
||||||
|
const result: Record<string, Info> = {}
|
||||||
|
for (const [key, value] of Object.entries(mode)) {
|
||||||
|
let item = result[key]
|
||||||
|
if (!item)
|
||||||
|
item = result[key] = {
|
||||||
|
name: key,
|
||||||
|
tools: {},
|
||||||
|
}
|
||||||
|
const model = value.model ?? cfg.model
|
||||||
|
if (model) {
|
||||||
|
const [providerID, ...rest] = model.split("/")
|
||||||
|
const modelID = rest.join("/")
|
||||||
|
item.model = {
|
||||||
|
modelID,
|
||||||
|
providerID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (value.prompt) item.prompt = await Bun.file(value.prompt).text()
|
||||||
|
if (value.tools) item.tools = value.tools
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
export async function get(mode: string) {
|
||||||
|
return state().then((x) => x[mode])
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function list() {
|
||||||
|
return state().then((x) => Object.values(x))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
You are an agent known as opencode - please keep going until the user’s query is completely resolved, before ending your turn and yielding back to the user.
|
||||||
|
|
||||||
|
Your thinking should be thorough and so it's fine if it's very long. However, avoid unnecessary repetition and verbosity. You should be concise, but thorough.
|
||||||
|
|
||||||
|
You MUST iterate and keep going until the problem is solved.
|
||||||
|
|
||||||
|
I want you to fully solve this autonomously before coming back to me.
|
||||||
|
|
||||||
|
Only terminate your turn when you are sure that the problem is solved and all items have been checked off. Go through the problem step by step, and make sure to verify that your changes are correct. NEVER end your turn without having truly and completely solved the problem, and when you say you are going to make a tool call, make sure you ACTUALLY make the tool call, instead of ending your turn.
|
||||||
|
|
||||||
|
Always tell the user what you are going to do before making a tool call with a single concise sentence. This will help them understand what you are doing and why.
|
||||||
|
|
||||||
|
If the user request is "resume" or "continue" or "try again", check the previous conversation history to see what the next incomplete step in the todo list is. Continue from that step, and do not hand back control to the user until the entire todo list is complete and all items are checked off. Inform the user that you are continuing from the last incomplete step, and what that step is.
|
||||||
|
|
||||||
|
Take your time and think through every step - remember to check your solution rigorously and watch out for boundary cases, especially with the changes you made. Your solution must be perfect. If not, continue working on it. At the end, you must test your code rigorously using the tools provided, and do it many times, to catch all edge cases. If it is not robust, iterate more and make it perfect. Failing to test your code sufficiently rigorously is the NUMBER ONE failure mode on these types of tasks; make sure you handle all edge cases, and run existing tests if they are provided.
|
||||||
|
|
||||||
|
You MUST plan extensively before each function call, and reflect extensively on the outcomes of the previous function calls. DO NOT do this entire process by making function calls only, as this can impair your ability to solve the problem and think insightfully.
|
||||||
|
|
||||||
|
# Workflow
|
||||||
|
|
||||||
|
1. Understand the problem deeply. Carefully read the issue and think critically about what is required.
|
||||||
|
2. Investigate the codebase. Explore relevant files, search for key functions, and gather context.
|
||||||
|
3. Develop a clear, step-by-step plan. Break down the fix into manageable, incremental steps. Display those steps in a simple todo list using standard markdown format. Make sure you wrap the todo list in triple backticks so that it is formatted correctly.
|
||||||
|
4. Implement the fix incrementally. Make small, testable code changes.
|
||||||
|
5. Debug as needed. Use debugging techniques to isolate and resolve issues.
|
||||||
|
6. Test frequently. Run tests after each change to verify correctness.
|
||||||
|
7. Iterate until the root cause is fixed and all tests pass.
|
||||||
|
8. Reflect and validate comprehensively. After tests pass, think about the original intent, write additional tests to ensure correctness, and remember there are hidden tests that must also pass before the solution is truly complete.
|
||||||
|
|
||||||
|
Refer to the detailed sections below for more information on each step.
|
||||||
|
|
||||||
|
## 1. Deeply Understand the Problem
|
||||||
|
Carefully read the issue and think hard about a plan to solve it before coding.
|
||||||
|
|
||||||
|
## 2. Codebase Investigation
|
||||||
|
- Explore relevant files and directories.
|
||||||
|
- Search for key functions, classes, or variables related to the issue.
|
||||||
|
- Read and understand relevant code snippets.
|
||||||
|
- Identify the root cause of the problem.
|
||||||
|
- Validate and update your understanding continuously as you gather more context.
|
||||||
|
|
||||||
|
## 3. Fetch Provided URLs
|
||||||
|
- If the user provides a URL, use the `functions.fetch_webpage` tool to retrieve the content of the provided URL.
|
||||||
|
- After fetching, review the content returned by the fetch tool.
|
||||||
|
- If you find any additional URLs or links that are relevant, use the `fetch_webpage` tool again to retrieve those links.
|
||||||
|
- Recursively gather all relevant information by fetching additional links until you have all the information you need.
|
||||||
|
|
||||||
|
## 4. Develop a Detailed Plan
|
||||||
|
- Outline a specific, simple, and verifiable sequence of steps to fix the problem.
|
||||||
|
- Create a todo list in markdown format to track your progress.
|
||||||
|
- Each time you complete a step, check it off using `[x]` syntax.
|
||||||
|
- Each time you check off a step, display the updated todo list to the user.
|
||||||
|
- Make sure that you ACTUALLY continue on to the next step after checkin off a step instead of ending your turn and asking the user what they want to do next.
|
||||||
|
|
||||||
|
## 5. Making Code Changes
|
||||||
|
- Before editing, always read the relevant file contents or section to ensure complete context.
|
||||||
|
- Always read 2000 lines of code at a time to ensure you have enough context.
|
||||||
|
- If a patch is not applied correctly, attempt to reapply it.
|
||||||
|
- Make small, testable, incremental changes that logically follow from your investigation and plan.
|
||||||
|
|
||||||
|
## 6. Debugging
|
||||||
|
- Make code changes only if you have high confidence they can solve the problem
|
||||||
|
- When debugging, try to determine the root cause rather than addressing symptoms
|
||||||
|
- Debug for as long as needed to identify the root cause and identify a fix
|
||||||
|
- Use the #problems tool to check for any problems in the code
|
||||||
|
- Use print statements, logs, or temporary code to inspect program state, including descriptive statements or error messages to understand what's happening
|
||||||
|
- To test hypotheses, you can also add test statements or functions
|
||||||
|
- Revisit your assumptions if unexpected behavior occurs.
|
||||||
|
|
||||||
|
# Fetch Webpage
|
||||||
|
Use the `webfetch` tool when the user provides a URL. Follow these steps exactly.
|
||||||
|
|
||||||
|
1. Use the `webfetch` tool to retrieve the content of the provided URL.
|
||||||
|
2. After fetching, review the content returned by the fetch tool.
|
||||||
|
3. If you find any additional URLs or links that are relevant, use the `webfetch` tool again to retrieve those links.
|
||||||
|
4. Go back to step 2 and repeat until you have all the information you need.
|
||||||
|
|
||||||
|
IMPORTANT: Recursively fetching links is crucial. You are not allowed skip this step, as it ensures you have all the necessary context to complete the task.
|
||||||
|
|
||||||
|
# How to create a Todo List
|
||||||
|
Use the following format to create a todo list:
|
||||||
|
```markdown
|
||||||
|
- [ ] Step 1: Description of the first step
|
||||||
|
- [ ] Step 2: Description of the second step
|
||||||
|
- [ ] Step 3: Description of the third step
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not ever use HTML tags or any other formatting for the todo list, as it will not be rendered correctly. Always use the markdown format shown above.
|
||||||
|
|
||||||
|
# Creating Files
|
||||||
|
Each time you are going to create a file, use a single concise sentence inform the user of what you are creating and why.
|
||||||
|
|
||||||
|
# Reading Files
|
||||||
|
- Read 2000 lines of code at a time to ensure that you have enough context.
|
||||||
|
- Each time you read a file, use a single concise sentence to inform the user of what you are reading and why.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<system-reminder>
|
||||||
|
Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supercedes any other instructions you have received (for example, to make edits).
|
||||||
|
</system-reminder>
|
||||||
@@ -7,23 +7,16 @@ import path from "path"
|
|||||||
import os from "os"
|
import os from "os"
|
||||||
|
|
||||||
import PROMPT_ANTHROPIC from "./prompt/anthropic.txt"
|
import PROMPT_ANTHROPIC from "./prompt/anthropic.txt"
|
||||||
|
import PROMPT_BEAST from "./prompt/beast.txt"
|
||||||
import PROMPT_ANTHROPIC_SPOOF from "./prompt/anthropic_spoof.txt"
|
import PROMPT_ANTHROPIC_SPOOF from "./prompt/anthropic_spoof.txt"
|
||||||
import PROMPT_SUMMARIZE from "./prompt/summarize.txt"
|
import PROMPT_SUMMARIZE from "./prompt/summarize.txt"
|
||||||
import PROMPT_TITLE from "./prompt/title.txt"
|
import PROMPT_TITLE from "./prompt/title.txt"
|
||||||
|
|
||||||
export namespace SystemPrompt {
|
export namespace SystemPrompt {
|
||||||
export function provider(providerID: string) {
|
export function provider(providerID: string, modelID: string) {
|
||||||
const result = []
|
if (providerID === "anthropic") return [PROMPT_ANTHROPIC_SPOOF.trim(), PROMPT_ANTHROPIC]
|
||||||
switch (providerID) {
|
if (modelID.includes("gpt-")) return [PROMPT_BEAST]
|
||||||
case "anthropic":
|
return [PROMPT_ANTHROPIC]
|
||||||
result.push(PROMPT_ANTHROPIC_SPOOF.trim())
|
|
||||||
result.push(PROMPT_ANTHROPIC)
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
result.push(PROMPT_ANTHROPIC)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function environment() {
|
export async function environment() {
|
||||||
|
|||||||
@@ -17,27 +17,30 @@ export namespace Storage {
|
|||||||
|
|
||||||
const MIGRATIONS: Migration[] = [
|
const MIGRATIONS: Migration[] = [
|
||||||
async (dir: string) => {
|
async (dir: string) => {
|
||||||
const files = new Bun.Glob("session/message/*/*.json").scanSync({
|
try {
|
||||||
cwd: dir,
|
const files = new Bun.Glob("session/message/*/*.json").scanSync({
|
||||||
absolute: true,
|
cwd: dir,
|
||||||
})
|
absolute: true,
|
||||||
for (const file of files) {
|
})
|
||||||
const content = await Bun.file(file).json()
|
for (const file of files) {
|
||||||
if (!content.metadata) continue
|
const content = await Bun.file(file).json()
|
||||||
log.info("migrating to v2 message", { file })
|
if (!content.metadata) continue
|
||||||
try {
|
log.info("migrating to v2 message", { file })
|
||||||
const result = MessageV2.fromV1(content)
|
try {
|
||||||
await Bun.write(file, JSON.stringify(result, null, 2))
|
const result = MessageV2.fromV1(content)
|
||||||
} catch (e) {
|
await Bun.write(file, JSON.stringify(result, null, 2))
|
||||||
await fs.rename(file, file.replace("storage", "broken"))
|
} catch (e) {
|
||||||
|
await fs.rename(file, file.replace("storage", "broken"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
} catch {}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const state = App.state("storage", async () => {
|
const state = App.state("storage", async () => {
|
||||||
const app = App.info()
|
const app = App.info()
|
||||||
const dir = path.join(app.path.data, "storage")
|
const dir = path.normalize(path.join(app.path.data, "storage"))
|
||||||
|
await fs.mkdir(dir, { recursive: true })
|
||||||
const migration = await Bun.file(path.join(dir, "migration"))
|
const migration = await Bun.file(path.join(dir, "migration"))
|
||||||
.json()
|
.json()
|
||||||
.then((x) => parseInt(x))
|
.then((x) => parseInt(x))
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export const GlobTool = Tool.define({
|
|||||||
let truncated = false
|
let truncated = false
|
||||||
for (const file of await Ripgrep.files({
|
for (const file of await Ripgrep.files({
|
||||||
cwd: search,
|
cwd: search,
|
||||||
glob: params.pattern,
|
glob: [params.pattern],
|
||||||
})) {
|
})) {
|
||||||
if (files.length >= limit) {
|
if (files.length >= limit) {
|
||||||
truncated = true
|
truncated = true
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ export const IGNORE_PATTERNS = [
|
|||||||
"obj/",
|
"obj/",
|
||||||
".idea/",
|
".idea/",
|
||||||
".vscode/",
|
".vscode/",
|
||||||
|
".zig-cache/",
|
||||||
|
"zig-out",
|
||||||
]
|
]
|
||||||
|
|
||||||
const LIMIT = 100
|
const LIMIT = 100
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export const ReadTool = Tool.define({
|
|||||||
output += "\n</file>"
|
output += "\n</file>"
|
||||||
|
|
||||||
// just warms the lsp client
|
// just warms the lsp client
|
||||||
await LSP.touchFile(filePath, false)
|
LSP.touchFile(filePath, false)
|
||||||
FileTime.read(ctx.sessionID, filePath)
|
FileTime.read(ctx.sessionID, filePath)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ Reads a file from the local filesystem. You can access any file directly by usin
|
|||||||
Assume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.
|
Assume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
- The file_path parameter must be an absolute path, not a relative path
|
- The filePath parameter must be an absolute path, not a relative path
|
||||||
- By default, it reads up to 2000 lines starting from the beginning of the file
|
- By default, it reads up to 2000 lines starting from the beginning of the file
|
||||||
- You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters
|
- You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters
|
||||||
- Any lines longer than 2000 characters will be truncated
|
- Any lines longer than 2000 characters will be truncated
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { App } from "../app/app"
|
|||||||
|
|
||||||
const TodoInfo = z.object({
|
const TodoInfo = z.object({
|
||||||
content: z.string().min(1).describe("Brief description of the task"),
|
content: z.string().min(1).describe("Brief description of the task"),
|
||||||
status: z.enum(["pending", "in_progress", "completed"]).describe("Current status of the task"),
|
status: z.enum(["pending", "in_progress", "completed", "cancelled"]).describe("Current status of the task"),
|
||||||
priority: z.enum(["high", "medium", "low"]).describe("Priority level of the task"),
|
priority: z.enum(["high", "medium", "low"]).describe("Priority level of the task"),
|
||||||
id: z.string().describe("Unique identifier for the todo item"),
|
id: z.string().describe("Unique identifier for the todo item"),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
import { exists } from "fs/promises"
|
import { exists } from "fs/promises"
|
||||||
import { dirname, join } from "path"
|
import { dirname, join, relative } from "path"
|
||||||
|
|
||||||
export namespace Filesystem {
|
export namespace Filesystem {
|
||||||
|
export function overlaps(a: string, b: string) {
|
||||||
|
const relA = relative(a, b)
|
||||||
|
const relB = relative(b, a)
|
||||||
|
return !relA || !relA.startsWith("..") || !relB || !relB.startsWith("..")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function contains(parent: string, child: string) {
|
||||||
|
return relative(parent, child).startsWith("..")
|
||||||
|
}
|
||||||
|
|
||||||
export async function findUp(target: string, start: string, stop?: string) {
|
export async function findUp(target: string, start: string, stop?: string) {
|
||||||
let current = start
|
let current = start
|
||||||
const result = []
|
const result = []
|
||||||
@@ -16,6 +26,21 @@ export namespace Filesystem {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function* up(options: { targets: string[]; start: string; stop?: string }) {
|
||||||
|
const { targets, start, stop } = options
|
||||||
|
let current = start
|
||||||
|
while (true) {
|
||||||
|
for (const target of targets) {
|
||||||
|
const search = join(current, target)
|
||||||
|
if (await exists(search)) yield search
|
||||||
|
}
|
||||||
|
if (stop === current) break
|
||||||
|
const parent = dirname(current)
|
||||||
|
if (parent === current) break
|
||||||
|
current = parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function globUp(pattern: string, start: string, stop?: string) {
|
export async function globUp(pattern: string, start: string, stop?: string) {
|
||||||
let current = start
|
let current = start
|
||||||
const result = []
|
const result = []
|
||||||
|
|||||||
@@ -1,15 +1,59 @@
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
|
import z from "zod"
|
||||||
|
|
||||||
export namespace Log {
|
export namespace Log {
|
||||||
|
export const Level = z.enum(["DEBUG", "INFO", "WARN", "ERROR"]).openapi({ ref: "LogLevel", description: "Log level" })
|
||||||
|
export type Level = z.infer<typeof Level>
|
||||||
|
|
||||||
|
const levelPriority: Record<Level, number> = {
|
||||||
|
DEBUG: 0,
|
||||||
|
INFO: 1,
|
||||||
|
WARN: 2,
|
||||||
|
ERROR: 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentLevel: Level = "INFO"
|
||||||
|
|
||||||
|
export function setLevel(level: Level) {
|
||||||
|
currentLevel = level
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLevel(): Level {
|
||||||
|
return currentLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldLog(level: Level): boolean {
|
||||||
|
return levelPriority[level] >= levelPriority[currentLevel]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Logger = {
|
||||||
|
debug(message?: any, extra?: Record<string, any>): void
|
||||||
|
info(message?: any, extra?: Record<string, any>): void
|
||||||
|
error(message?: any, extra?: Record<string, any>): void
|
||||||
|
warn(message?: any, extra?: Record<string, any>): void
|
||||||
|
tag(key: string, value: string): Logger
|
||||||
|
clone(): Logger
|
||||||
|
time(
|
||||||
|
message: string,
|
||||||
|
extra?: Record<string, any>,
|
||||||
|
): {
|
||||||
|
stop(): void
|
||||||
|
[Symbol.dispose](): void
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loggers = new Map<string, Logger>()
|
||||||
|
|
||||||
export const Default = create({ service: "default" })
|
export const Default = create({ service: "default" })
|
||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
print: boolean
|
print: boolean
|
||||||
|
level?: Level
|
||||||
}
|
}
|
||||||
|
|
||||||
let logpath = ""
|
let logpath = ""
|
||||||
|
|
||||||
export function file() {
|
export function file() {
|
||||||
return logpath
|
return logpath
|
||||||
}
|
}
|
||||||
@@ -47,6 +91,14 @@ export namespace Log {
|
|||||||
export function create(tags?: Record<string, any>) {
|
export function create(tags?: Record<string, any>) {
|
||||||
tags = tags || {}
|
tags = tags || {}
|
||||||
|
|
||||||
|
const service = tags["service"]
|
||||||
|
if (service && typeof service === "string") {
|
||||||
|
const cached = loggers.get(service)
|
||||||
|
if (cached) {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function build(message: any, extra?: Record<string, any>) {
|
function build(message: any, extra?: Record<string, any>) {
|
||||||
const prefix = Object.entries({
|
const prefix = Object.entries({
|
||||||
...tags,
|
...tags,
|
||||||
@@ -60,15 +112,26 @@ export namespace Log {
|
|||||||
last = next.getTime()
|
last = next.getTime()
|
||||||
return [next.toISOString().split(".")[0], "+" + diff + "ms", prefix, message].filter(Boolean).join(" ") + "\n"
|
return [next.toISOString().split(".")[0], "+" + diff + "ms", prefix, message].filter(Boolean).join(" ") + "\n"
|
||||||
}
|
}
|
||||||
const result = {
|
const result: Logger = {
|
||||||
|
debug(message?: any, extra?: Record<string, any>) {
|
||||||
|
if (shouldLog("DEBUG")) {
|
||||||
|
process.stderr.write("DEBUG " + build(message, extra))
|
||||||
|
}
|
||||||
|
},
|
||||||
info(message?: any, extra?: Record<string, any>) {
|
info(message?: any, extra?: Record<string, any>) {
|
||||||
process.stderr.write("INFO " + build(message, extra))
|
if (shouldLog("INFO")) {
|
||||||
|
process.stderr.write("INFO " + build(message, extra))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
error(message?: any, extra?: Record<string, any>) {
|
error(message?: any, extra?: Record<string, any>) {
|
||||||
process.stderr.write("ERROR " + build(message, extra))
|
if (shouldLog("ERROR")) {
|
||||||
|
process.stderr.write("ERROR " + build(message, extra))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
warn(message?: any, extra?: Record<string, any>) {
|
warn(message?: any, extra?: Record<string, any>) {
|
||||||
process.stderr.write("WARN " + build(message, extra))
|
if (shouldLog("WARN")) {
|
||||||
|
process.stderr.write("WARN " + build(message, extra))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
tag(key: string, value: string) {
|
tag(key: string, value: string) {
|
||||||
if (tags) tags[key] = value
|
if (tags) tags[key] = value
|
||||||
@@ -96,6 +159,10 @@ export namespace Log {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (service && typeof service === "string") {
|
||||||
|
loggers.set(service, result)
|
||||||
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,16 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea/v2"
|
tea "github.com/charmbracelet/bubbletea/v2"
|
||||||
|
flag "github.com/spf13/pflag"
|
||||||
"github.com/sst/opencode-sdk-go"
|
"github.com/sst/opencode-sdk-go"
|
||||||
"github.com/sst/opencode-sdk-go/option"
|
"github.com/sst/opencode-sdk-go/option"
|
||||||
"github.com/sst/opencode/internal/app"
|
"github.com/sst/opencode/internal/app"
|
||||||
|
"github.com/sst/opencode/internal/clipboard"
|
||||||
"github.com/sst/opencode/internal/tui"
|
"github.com/sst/opencode/internal/tui"
|
||||||
|
"github.com/sst/opencode/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Version = "dev"
|
var Version = "dev"
|
||||||
@@ -23,6 +25,11 @@ func main() {
|
|||||||
version = "v" + Version
|
version = "v" + Version
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var model *string = flag.String("model", "", "model to begin with")
|
||||||
|
var prompt *string = flag.String("prompt", "", "prompt to begin with")
|
||||||
|
var mode *string = flag.String("mode", "", "mode to begin with")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
url := os.Getenv("OPENCODE_SERVER")
|
url := os.Getenv("OPENCODE_SERVER")
|
||||||
|
|
||||||
appInfoStr := os.Getenv("OPENCODE_APP_INFO")
|
appInfoStr := os.Getenv("OPENCODE_APP_INFO")
|
||||||
@@ -33,39 +40,36 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
logfile := filepath.Join(appInfo.Path.Data, "log", "tui.log")
|
modesStr := os.Getenv("OPENCODE_MODES")
|
||||||
if _, err := os.Stat(filepath.Dir(logfile)); os.IsNotExist(err) {
|
var modes []opencode.Mode
|
||||||
err := os.MkdirAll(filepath.Dir(logfile), 0755)
|
err = json.Unmarshal([]byte(modesStr), &modes)
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to create log directory", "error", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
file, err := os.Create(logfile)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to create log file", "error", err)
|
slog.Error("Failed to unmarshal modes", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
defer file.Close()
|
|
||||||
logger := slog.New(slog.NewTextHandler(file, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
||||||
slog.SetDefault(logger)
|
|
||||||
|
|
||||||
slog.Debug("TUI launched", "app", appInfo)
|
|
||||||
|
|
||||||
httpClient := opencode.NewClient(
|
httpClient := opencode.NewClient(
|
||||||
option.WithBaseURL(url),
|
option.WithBaseURL(url),
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
apiHandler := util.NewAPILogHandler(httpClient, "tui", slog.LevelDebug)
|
||||||
slog.Error("Failed to create client", "error", err)
|
logger := slog.New(apiHandler)
|
||||||
os.Exit(1)
|
slog.SetDefault(logger)
|
||||||
}
|
|
||||||
|
slog.Debug("TUI launched", "app", appInfoStr, "modes", modesStr)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err = clipboard.Init()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to initialize clipboard", "error", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// Create main context for the application
|
// Create main context for the application
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
app_, err := app.New(ctx, version, appInfo, httpClient)
|
app_, err := app.New(ctx, version, appInfo, modes, httpClient, model, prompt, mode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-12
@@ -17,7 +17,7 @@ require (
|
|||||||
github.com/muesli/termenv v0.16.0
|
github.com/muesli/termenv v0.16.0
|
||||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3
|
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3
|
||||||
github.com/sst/opencode-sdk-go v0.1.0-alpha.8
|
github.com/sst/opencode-sdk-go v0.1.0-alpha.8
|
||||||
github.com/tidwall/gjson v1.14.4
|
golang.org/x/image v0.28.0
|
||||||
rsc.io/qr v0.2.0
|
rsc.io/qr v0.2.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -50,23 +50,23 @@ require (
|
|||||||
github.com/sosodev/duration v1.3.1 // indirect
|
github.com/sosodev/duration v1.3.1 // indirect
|
||||||
github.com/speakeasy-api/openapi-overlay v0.9.0 // indirect
|
github.com/speakeasy-api/openapi-overlay v0.9.0 // indirect
|
||||||
github.com/spf13/cobra v1.9.1 // indirect
|
github.com/spf13/cobra v1.9.1 // indirect
|
||||||
|
github.com/tidwall/gjson v1.14.4 // indirect
|
||||||
github.com/tidwall/match v1.1.1 // indirect
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
github.com/tidwall/pretty v1.2.1 // indirect
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
github.com/tidwall/sjson v1.2.5 // indirect
|
github.com/tidwall/sjson v1.2.5 // indirect
|
||||||
github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect
|
github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect
|
||||||
golang.org/x/mod v0.24.0 // indirect
|
golang.org/x/mod v0.25.0 // indirect
|
||||||
golang.org/x/tools v0.31.0 // indirect
|
golang.org/x/tools v0.34.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/atotto/clipboard v0.1.4
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/aymerick/douceur v0.2.0 // indirect
|
github.com/aymerick/douceur v0.2.0 // indirect
|
||||||
github.com/charmbracelet/colorprofile v0.3.1 // indirect
|
github.com/charmbracelet/colorprofile v0.3.1 // indirect
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.14-0.20250501183327-ad3bc78c6a81 // indirect
|
github.com/charmbracelet/x/cellbuf v0.0.14-0.20250501183327-ad3bc78c6a81 // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||||
github.com/disintegration/imaging v1.6.2
|
|
||||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||||
github.com/google/go-cmp v0.7.0 // indirect
|
github.com/google/go-cmp v0.7.0 // indirect
|
||||||
github.com/gorilla/css v1.0.1 // indirect
|
github.com/gorilla/css v1.0.1 // indirect
|
||||||
@@ -78,16 +78,15 @@ require (
|
|||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7
|
github.com/rivo/uniseg v0.4.7
|
||||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
github.com/spf13/pflag v1.0.6 // indirect
|
github.com/spf13/pflag v1.0.6
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
github.com/yuin/goldmark v1.7.8 // indirect
|
github.com/yuin/goldmark v1.7.8 // indirect
|
||||||
github.com/yuin/goldmark-emoji v1.0.5 // indirect
|
github.com/yuin/goldmark-emoji v1.0.5 // indirect
|
||||||
golang.org/x/image v0.26.0
|
golang.org/x/net v0.41.0 // indirect
|
||||||
golang.org/x/net v0.39.0 // indirect
|
golang.org/x/sync v0.15.0 // indirect
|
||||||
golang.org/x/sync v0.13.0 // indirect
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
golang.org/x/sys v0.32.0 // indirect
|
golang.org/x/term v0.32.0 // indirect
|
||||||
golang.org/x/term v0.31.0 // indirect
|
golang.org/x/text v0.26.0
|
||||||
golang.org/x/text v0.24.0
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+16
-19
@@ -54,8 +54,6 @@ github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2
|
|||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
|
||||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
|
||||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58=
|
github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58=
|
||||||
@@ -218,14 +216,13 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
|||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=
|
||||||
golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY=
|
golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY=
|
||||||
golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c=
|
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
@@ -236,15 +233,15 @@ golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT
|
|||||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -263,28 +260,28 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
|
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||||
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||||
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea/v2"
|
tea "github.com/charmbracelet/bubbletea/v2"
|
||||||
"github.com/sst/opencode-sdk-go"
|
"github.com/sst/opencode-sdk-go"
|
||||||
|
"github.com/sst/opencode/internal/clipboard"
|
||||||
"github.com/sst/opencode/internal/commands"
|
"github.com/sst/opencode/internal/commands"
|
||||||
"github.com/sst/opencode/internal/components/toast"
|
"github.com/sst/opencode/internal/components/toast"
|
||||||
"github.com/sst/opencode/internal/config"
|
"github.com/sst/opencode/internal/config"
|
||||||
@@ -21,17 +22,26 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
Info opencode.App
|
Info opencode.App
|
||||||
Version string
|
Modes []opencode.Mode
|
||||||
StatePath string
|
Providers []opencode.Provider
|
||||||
Config *opencode.Config
|
Version string
|
||||||
Client *opencode.Client
|
StatePath string
|
||||||
State *config.State
|
Config *opencode.Config
|
||||||
Provider *opencode.Provider
|
Client *opencode.Client
|
||||||
Model *opencode.Model
|
State *config.State
|
||||||
Session *opencode.Session
|
ModeIndex int
|
||||||
Messages []opencode.MessageUnion
|
Mode *opencode.Mode
|
||||||
Commands commands.CommandRegistry
|
Provider *opencode.Provider
|
||||||
|
Model *opencode.Model
|
||||||
|
Session *opencode.Session
|
||||||
|
Messages []opencode.MessageUnion
|
||||||
|
Commands commands.CommandRegistry
|
||||||
|
InitialModel *string
|
||||||
|
InitialPrompt *string
|
||||||
|
IntitialMode *string
|
||||||
|
compactCancel context.CancelFunc
|
||||||
|
IsLeaderSequence bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type SessionSelectedMsg = *opencode.Session
|
type SessionSelectedMsg = *opencode.Session
|
||||||
@@ -46,6 +56,9 @@ type SendMsg struct {
|
|||||||
Text string
|
Text string
|
||||||
Attachments []opencode.FilePartParam
|
Attachments []opencode.FilePartParam
|
||||||
}
|
}
|
||||||
|
type SetEditorContentMsg struct {
|
||||||
|
Text string
|
||||||
|
}
|
||||||
type OptimisticMessageAddedMsg struct {
|
type OptimisticMessageAddedMsg struct {
|
||||||
Message opencode.MessageUnion
|
Message opencode.MessageUnion
|
||||||
}
|
}
|
||||||
@@ -57,7 +70,11 @@ func New(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
version string,
|
version string,
|
||||||
appInfo opencode.App,
|
appInfo opencode.App,
|
||||||
|
modes []opencode.Mode,
|
||||||
httpClient *opencode.Client,
|
httpClient *opencode.Client,
|
||||||
|
initialModel *string,
|
||||||
|
initialPrompt *string,
|
||||||
|
initialMode *string,
|
||||||
) (*App, error) {
|
) (*App, error) {
|
||||||
util.RootPath = appInfo.Path.Root
|
util.RootPath = appInfo.Path.Root
|
||||||
util.CwdPath = appInfo.Path.Cwd
|
util.CwdPath = appInfo.Path.Cwd
|
||||||
@@ -78,14 +95,36 @@ func New(
|
|||||||
config.SaveState(appStatePath, appState)
|
config.SaveState(appStatePath, appState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if appState.ModeModel == nil {
|
||||||
|
appState.ModeModel = make(map[string]config.ModeModel)
|
||||||
|
}
|
||||||
|
|
||||||
if configInfo.Theme != "" {
|
if configInfo.Theme != "" {
|
||||||
appState.Theme = configInfo.Theme
|
appState.Theme = configInfo.Theme
|
||||||
}
|
}
|
||||||
|
|
||||||
if configInfo.Model != "" {
|
var modeIndex int
|
||||||
splits := strings.Split(configInfo.Model, "/")
|
var mode *opencode.Mode
|
||||||
appState.Provider = splits[0]
|
modeName := "build"
|
||||||
appState.Model = strings.Join(splits[1:], "/")
|
if appState.Mode != "" {
|
||||||
|
modeName = appState.Mode
|
||||||
|
}
|
||||||
|
if initialMode != nil && *initialMode != "" {
|
||||||
|
modeName = *initialMode
|
||||||
|
}
|
||||||
|
for i, m := range modes {
|
||||||
|
if m.Name == modeName {
|
||||||
|
modeIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mode = &modes[modeIndex]
|
||||||
|
|
||||||
|
if mode.Model.ModelID != "" {
|
||||||
|
appState.ModeModel[mode.Name] = config.ModeModel{
|
||||||
|
ProviderID: mode.Model.ProviderID,
|
||||||
|
ModelID: mode.Model.ModelID,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := theme.LoadThemesFromDirectories(
|
if err := theme.LoadThemesFromDirectories(
|
||||||
@@ -109,15 +148,21 @@ func New(
|
|||||||
slog.Debug("Loaded config", "config", configInfo)
|
slog.Debug("Loaded config", "config", configInfo)
|
||||||
|
|
||||||
app := &App{
|
app := &App{
|
||||||
Info: appInfo,
|
Info: appInfo,
|
||||||
Version: version,
|
Modes: modes,
|
||||||
StatePath: appStatePath,
|
Version: version,
|
||||||
Config: configInfo,
|
StatePath: appStatePath,
|
||||||
State: appState,
|
Config: configInfo,
|
||||||
Client: httpClient,
|
State: appState,
|
||||||
Session: &opencode.Session{},
|
Client: httpClient,
|
||||||
Messages: []opencode.MessageUnion{},
|
ModeIndex: modeIndex,
|
||||||
Commands: commands.LoadFromConfig(configInfo),
|
Mode: mode,
|
||||||
|
Session: &opencode.Session{},
|
||||||
|
Messages: []opencode.MessageUnion{},
|
||||||
|
Commands: commands.LoadFromConfig(configInfo),
|
||||||
|
InitialModel: initialModel,
|
||||||
|
InitialPrompt: initialPrompt,
|
||||||
|
IntitialMode: initialMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
return app, nil
|
return app, nil
|
||||||
@@ -140,66 +185,149 @@ func (a *App) Key(commandName commands.CommandName) string {
|
|||||||
return base(key) + muted(" "+command.Description)
|
return base(key) + muted(" "+command.Description)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) InitializeProvider() tea.Cmd {
|
func (a *App) SetClipboard(text string) tea.Cmd {
|
||||||
return func() tea.Msg {
|
var cmds []tea.Cmd
|
||||||
providersResponse, err := a.Client.Config.Providers(context.Background())
|
cmds = append(cmds, func() tea.Msg {
|
||||||
if err != nil {
|
clipboard.Write(clipboard.FmtText, []byte(text))
|
||||||
slog.Error("Failed to list providers", "error", err)
|
return nil
|
||||||
// TODO: notify user
|
})
|
||||||
return nil
|
// try to set the clipboard using OSC52 for terminals that support it
|
||||||
}
|
cmds = append(cmds, tea.SetClipboard(text))
|
||||||
providers := providersResponse.Providers
|
return tea.Sequence(cmds...)
|
||||||
var defaultProvider *opencode.Provider
|
}
|
||||||
var defaultModel *opencode.Model
|
|
||||||
|
|
||||||
var anthropic *opencode.Provider
|
func (a *App) SwitchMode() (*App, tea.Cmd) {
|
||||||
for _, provider := range providers {
|
a.ModeIndex++
|
||||||
if provider.ID == "anthropic" {
|
if a.ModeIndex >= len(a.Modes) {
|
||||||
anthropic = &provider
|
a.ModeIndex = 0
|
||||||
}
|
}
|
||||||
}
|
a.Mode = &a.Modes[a.ModeIndex]
|
||||||
|
|
||||||
// default to anthropic if available
|
modelID := a.Mode.Model.ModelID
|
||||||
if anthropic != nil {
|
providerID := a.Mode.Model.ProviderID
|
||||||
defaultProvider = anthropic
|
if modelID == "" {
|
||||||
defaultModel = getDefaultModel(providersResponse, *anthropic)
|
if model, ok := a.State.ModeModel[a.Mode.Name]; ok {
|
||||||
|
modelID = model.ModelID
|
||||||
|
providerID = model.ProviderID
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, provider := range providers {
|
if modelID != "" {
|
||||||
if defaultProvider == nil || defaultModel == nil {
|
for _, provider := range a.Providers {
|
||||||
defaultProvider = &provider
|
if provider.ID == providerID {
|
||||||
defaultModel = getDefaultModel(providersResponse, provider)
|
a.Provider = &provider
|
||||||
}
|
|
||||||
providers = append(providers, provider)
|
|
||||||
}
|
|
||||||
if len(providers) == 0 {
|
|
||||||
slog.Error("No providers configured")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var currentProvider *opencode.Provider
|
|
||||||
var currentModel *opencode.Model
|
|
||||||
for _, provider := range providers {
|
|
||||||
if provider.ID == a.State.Provider {
|
|
||||||
currentProvider = &provider
|
|
||||||
|
|
||||||
for _, model := range provider.Models {
|
for _, model := range provider.Models {
|
||||||
if model.ID == a.State.Model {
|
if model.ID == modelID {
|
||||||
currentModel = &model
|
a.Model = &model
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a.State.Mode = a.Mode.Name
|
||||||
|
|
||||||
|
return a, func() tea.Msg {
|
||||||
|
a.SaveState()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) InitializeProvider() tea.Cmd {
|
||||||
|
providersResponse, err := a.Client.Config.Providers(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to list providers", "error", err)
|
||||||
|
// TODO: notify user
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
providers := providersResponse.Providers
|
||||||
|
var defaultProvider *opencode.Provider
|
||||||
|
var defaultModel *opencode.Model
|
||||||
|
|
||||||
|
var anthropic *opencode.Provider
|
||||||
|
for _, provider := range providers {
|
||||||
|
if provider.ID == "anthropic" {
|
||||||
|
anthropic = &provider
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// default to anthropic if available
|
||||||
|
if anthropic != nil {
|
||||||
|
defaultProvider = anthropic
|
||||||
|
defaultModel = getDefaultModel(providersResponse, *anthropic)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, provider := range providers {
|
||||||
|
if defaultProvider == nil || defaultModel == nil {
|
||||||
|
defaultProvider = &provider
|
||||||
|
defaultModel = getDefaultModel(providersResponse, provider)
|
||||||
|
}
|
||||||
|
providers = append(providers, provider)
|
||||||
|
}
|
||||||
|
if len(providers) == 0 {
|
||||||
|
slog.Error("No providers configured")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Providers = providers
|
||||||
|
|
||||||
|
// retains backwards compatibility with old state format
|
||||||
|
if model, ok := a.State.ModeModel[a.State.Mode]; ok {
|
||||||
|
a.State.Provider = model.ProviderID
|
||||||
|
a.State.Model = model.ModelID
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentProvider *opencode.Provider
|
||||||
|
var currentModel *opencode.Model
|
||||||
|
for _, provider := range providers {
|
||||||
|
if provider.ID == a.State.Provider {
|
||||||
|
currentProvider = &provider
|
||||||
|
|
||||||
|
for _, model := range provider.Models {
|
||||||
|
if model.ID == a.State.Model {
|
||||||
|
currentModel = &model
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if currentProvider == nil || currentModel == nil {
|
||||||
|
currentProvider = defaultProvider
|
||||||
|
currentModel = defaultModel
|
||||||
|
}
|
||||||
|
|
||||||
|
var initialProvider *opencode.Provider
|
||||||
|
var initialModel *opencode.Model
|
||||||
|
if a.InitialModel != nil && *a.InitialModel != "" {
|
||||||
|
splits := strings.Split(*a.InitialModel, "/")
|
||||||
|
for _, provider := range providers {
|
||||||
|
if provider.ID == splits[0] {
|
||||||
|
initialProvider = &provider
|
||||||
|
for _, model := range provider.Models {
|
||||||
|
modelID := strings.Join(splits[1:], "/")
|
||||||
|
if model.ID == modelID {
|
||||||
|
initialModel = &model
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if currentProvider == nil || currentModel == nil {
|
|
||||||
currentProvider = defaultProvider
|
|
||||||
currentModel = defaultModel
|
|
||||||
}
|
|
||||||
|
|
||||||
return ModelSelectedMsg{
|
|
||||||
Provider: *currentProvider,
|
|
||||||
Model: *currentModel,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if initialProvider != nil && initialModel != nil {
|
||||||
|
currentProvider = initialProvider
|
||||||
|
currentModel = initialModel
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmds []tea.Cmd
|
||||||
|
cmds = append(cmds, util.CmdHandler(ModelSelectedMsg{
|
||||||
|
Provider: *currentProvider,
|
||||||
|
Model: *currentModel,
|
||||||
|
}))
|
||||||
|
if a.InitialPrompt != nil && *a.InitialPrompt != "" {
|
||||||
|
cmds = append(cmds, util.CmdHandler(SendMsg{Text: *a.InitialPrompt}))
|
||||||
|
}
|
||||||
|
return tea.Sequence(cmds...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDefaultModel(
|
func getDefaultModel(
|
||||||
@@ -263,13 +391,30 @@ func (a *App) InitializeProject(ctx context.Context) tea.Cmd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) CompactSession(ctx context.Context) tea.Cmd {
|
func (a *App) CompactSession(ctx context.Context) tea.Cmd {
|
||||||
|
if a.compactCancel != nil {
|
||||||
|
a.compactCancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
compactCtx, cancel := context.WithCancel(ctx)
|
||||||
|
a.compactCancel = cancel
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
_, err := a.Client.Session.Summarize(ctx, a.Session.ID, opencode.SessionSummarizeParams{
|
defer func() {
|
||||||
ProviderID: opencode.F(a.Provider.ID),
|
a.compactCancel = nil
|
||||||
ModelID: opencode.F(a.Model.ID),
|
}()
|
||||||
})
|
|
||||||
|
_, err := a.Client.Session.Summarize(
|
||||||
|
compactCtx,
|
||||||
|
a.Session.ID,
|
||||||
|
opencode.SessionSummarizeParams{
|
||||||
|
ProviderID: opencode.F(a.Provider.ID),
|
||||||
|
ModelID: opencode.F(a.Model.ID),
|
||||||
|
},
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to compact session", "error", err)
|
if compactCtx.Err() != context.Canceled {
|
||||||
|
slog.Error("Failed to compact session", "error", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
return nil
|
return nil
|
||||||
@@ -357,6 +502,7 @@ func (a *App) SendChatMessage(
|
|||||||
Parts: opencode.F(parts),
|
Parts: opencode.F(parts),
|
||||||
ProviderID: opencode.F(a.Provider.ID),
|
ProviderID: opencode.F(a.Provider.ID),
|
||||||
ModelID: opencode.F(a.Model.ID),
|
ModelID: opencode.F(a.Model.ID),
|
||||||
|
Mode: opencode.F(a.Mode.Name),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errormsg := fmt.Sprintf("failed to send message: %v", err)
|
errormsg := fmt.Sprintf("failed to send message: %v", err)
|
||||||
@@ -372,6 +518,12 @@ func (a *App) SendChatMessage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) Cancel(ctx context.Context, sessionID string) error {
|
func (a *App) Cancel(ctx context.Context, sessionID string) error {
|
||||||
|
// Cancel any running compact operation
|
||||||
|
if a.compactCancel != nil {
|
||||||
|
a.compactCancel()
|
||||||
|
a.compactCancel = nil
|
||||||
|
}
|
||||||
|
|
||||||
_, err := a.Client.Session.Abort(ctx, sessionID)
|
_, err := a.Client.Session.Abort(ctx, sessionID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to cancel session", "error", err)
|
slog.Error("Failed to cancel session", "error", err)
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
// Copyright 2021 The golang.design Initiative Authors.
|
||||||
|
// All rights reserved. Use of this source code is governed
|
||||||
|
// by a MIT license that can be found in the LICENSE file.
|
||||||
|
//
|
||||||
|
// Written by Changkun Ou <changkun.de>
|
||||||
|
|
||||||
|
/*
|
||||||
|
Package clipboard provides cross platform clipboard access and supports
|
||||||
|
macOS/Linux/Windows/Android/iOS platform. Before interacting with the
|
||||||
|
clipboard, one must call Init to assert if it is possible to use this
|
||||||
|
package:
|
||||||
|
|
||||||
|
err := clipboard.Init()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
The most common operations are `Read` and `Write`. To use them:
|
||||||
|
|
||||||
|
// write/read text format data of the clipboard, and
|
||||||
|
// the byte buffer regarding the text are UTF8 encoded.
|
||||||
|
clipboard.Write(clipboard.FmtText, []byte("text data"))
|
||||||
|
clipboard.Read(clipboard.FmtText)
|
||||||
|
|
||||||
|
// write/read image format data of the clipboard, and
|
||||||
|
// the byte buffer regarding the image are PNG encoded.
|
||||||
|
clipboard.Write(clipboard.FmtImage, []byte("image data"))
|
||||||
|
clipboard.Read(clipboard.FmtImage)
|
||||||
|
|
||||||
|
Note that read/write regarding image format assumes that the bytes are
|
||||||
|
PNG encoded since it serves the alpha blending purpose that might be
|
||||||
|
used in other graphical software.
|
||||||
|
|
||||||
|
In addition, `clipboard.Write` returns a channel that can receive an
|
||||||
|
empty struct as a signal, which indicates the corresponding write call
|
||||||
|
to the clipboard is outdated, meaning the clipboard has been overwritten
|
||||||
|
by others and the previously written data is lost. For instance:
|
||||||
|
|
||||||
|
changed := clipboard.Write(clipboard.FmtText, []byte("text data"))
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-changed:
|
||||||
|
println(`"text data" is no longer available from clipboard.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
You can ignore the returning channel if you don't need this type of
|
||||||
|
notification. Furthermore, when you need more than just knowing whether
|
||||||
|
clipboard data is changed, use the watcher API:
|
||||||
|
|
||||||
|
ch := clipboard.Watch(context.TODO(), clipboard.FmtText)
|
||||||
|
for data := range ch {
|
||||||
|
// print out clipboard data whenever it is changed
|
||||||
|
println(string(data))
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
package clipboard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// activate only for running tests.
|
||||||
|
debug = false
|
||||||
|
errUnavailable = errors.New("clipboard unavailable")
|
||||||
|
errUnsupported = errors.New("unsupported format")
|
||||||
|
errNoCgo = errors.New("clipboard: cannot use when CGO_ENABLED=0")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Format represents the format of clipboard data.
|
||||||
|
type Format int
|
||||||
|
|
||||||
|
// All sorts of supported clipboard data
|
||||||
|
const (
|
||||||
|
// FmtText indicates plain text clipboard format
|
||||||
|
FmtText Format = iota
|
||||||
|
// FmtImage indicates image/png clipboard format
|
||||||
|
FmtImage
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Due to the limitation on operating systems (such as darwin),
|
||||||
|
// concurrent read can even cause panic, use a global lock to
|
||||||
|
// guarantee one read at a time.
|
||||||
|
lock = sync.Mutex{}
|
||||||
|
initOnce sync.Once
|
||||||
|
initError error
|
||||||
|
)
|
||||||
|
|
||||||
|
// Init initializes the clipboard package. It returns an error
|
||||||
|
// if the clipboard is not available to use. This may happen if the
|
||||||
|
// target system lacks required dependency, such as libx11-dev in X11
|
||||||
|
// environment. For example,
|
||||||
|
//
|
||||||
|
// err := clipboard.Init()
|
||||||
|
// if err != nil {
|
||||||
|
// panic(err)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// If Init returns an error, any subsequent Read/Write/Watch call
|
||||||
|
// may result in an unrecoverable panic.
|
||||||
|
func Init() error {
|
||||||
|
initOnce.Do(func() {
|
||||||
|
initError = initialize()
|
||||||
|
})
|
||||||
|
return initError
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read returns a chunk of bytes of the clipboard data if it presents
|
||||||
|
// in the desired format t presents. Otherwise, it returns nil.
|
||||||
|
func Read(t Format) []byte {
|
||||||
|
lock.Lock()
|
||||||
|
defer lock.Unlock()
|
||||||
|
|
||||||
|
buf, err := read(t)
|
||||||
|
if err != nil {
|
||||||
|
if debug {
|
||||||
|
fmt.Fprintf(os.Stderr, "read clipboard err: %v\n", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write writes a given buffer to the clipboard in a specified format.
|
||||||
|
// Write returned a receive-only channel can receive an empty struct
|
||||||
|
// as a signal, which indicates the clipboard has been overwritten from
|
||||||
|
// this write.
|
||||||
|
// If format t indicates an image, then the given buf assumes
|
||||||
|
// the image data is PNG encoded.
|
||||||
|
func Write(t Format, buf []byte) <-chan struct{} {
|
||||||
|
lock.Lock()
|
||||||
|
defer lock.Unlock()
|
||||||
|
|
||||||
|
changed, err := write(t, buf)
|
||||||
|
if err != nil {
|
||||||
|
if debug {
|
||||||
|
fmt.Fprintf(os.Stderr, "write to clipboard err: %v\n", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return changed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch returns a receive-only channel that received the clipboard data
|
||||||
|
// whenever any change of clipboard data in the desired format happens.
|
||||||
|
//
|
||||||
|
// The returned channel will be closed if the given context is canceled.
|
||||||
|
func Watch(ctx context.Context, t Format) <-chan []byte {
|
||||||
|
return watch(ctx, t)
|
||||||
|
}
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
// Copyright 2021 The golang.design Initiative Authors.
|
||||||
|
// All rights reserved. Use of this source code is governed
|
||||||
|
// by a MIT license that can be found in the LICENSE file.
|
||||||
|
//
|
||||||
|
// Written by Changkun Ou <changkun.de>
|
||||||
|
|
||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
package clipboard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
lastChangeCount int64
|
||||||
|
changeCountMu sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
func initialize() error { return nil }
|
||||||
|
|
||||||
|
func read(t Format) (buf []byte, err error) {
|
||||||
|
switch t {
|
||||||
|
case FmtText:
|
||||||
|
return readText()
|
||||||
|
case FmtImage:
|
||||||
|
return readImage()
|
||||||
|
default:
|
||||||
|
return nil, errUnsupported
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readText() ([]byte, error) {
|
||||||
|
// Check if clipboard contains string data
|
||||||
|
checkScript := `
|
||||||
|
try
|
||||||
|
set clipboardTypes to (clipboard info)
|
||||||
|
repeat with aType in clipboardTypes
|
||||||
|
if (first item of aType) is string then
|
||||||
|
return "hastext"
|
||||||
|
end if
|
||||||
|
end repeat
|
||||||
|
return "notext"
|
||||||
|
on error
|
||||||
|
return "error"
|
||||||
|
end try
|
||||||
|
`
|
||||||
|
|
||||||
|
cmd := exec.Command("osascript", "-e", checkScript)
|
||||||
|
checkOut, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errUnavailable
|
||||||
|
}
|
||||||
|
|
||||||
|
checkOut = bytes.TrimSpace(checkOut)
|
||||||
|
if !bytes.Equal(checkOut, []byte("hastext")) {
|
||||||
|
return nil, errUnavailable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now get the actual text
|
||||||
|
cmd = exec.Command("osascript", "-e", "get the clipboard")
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errUnavailable
|
||||||
|
}
|
||||||
|
// Remove trailing newline that osascript adds
|
||||||
|
out = bytes.TrimSuffix(out, []byte("\n"))
|
||||||
|
|
||||||
|
// If clipboard was set to empty string, return nil
|
||||||
|
if len(out) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
func readImage() ([]byte, error) {
|
||||||
|
// AppleScript to read image data from clipboard as base64
|
||||||
|
script := `
|
||||||
|
try
|
||||||
|
set theData to the clipboard as «class PNGf»
|
||||||
|
return theData
|
||||||
|
on error
|
||||||
|
return ""
|
||||||
|
end try
|
||||||
|
`
|
||||||
|
|
||||||
|
cmd := exec.Command("osascript", "-e", script)
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errUnavailable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we got any data
|
||||||
|
out = bytes.TrimSpace(out)
|
||||||
|
if len(out) == 0 {
|
||||||
|
return nil, errUnavailable
|
||||||
|
}
|
||||||
|
|
||||||
|
// The output is in hex format (e.g., «data PNGf89504E...»)
|
||||||
|
// We need to extract and convert it
|
||||||
|
outStr := string(out)
|
||||||
|
if !strings.HasPrefix(outStr, "«data PNGf") || !strings.HasSuffix(outStr, "»") {
|
||||||
|
return nil, errUnavailable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract hex data
|
||||||
|
hexData := strings.TrimPrefix(outStr, "«data PNGf")
|
||||||
|
hexData = strings.TrimSuffix(hexData, "»")
|
||||||
|
|
||||||
|
// Convert hex to bytes
|
||||||
|
buf := make([]byte, len(hexData)/2)
|
||||||
|
for i := 0; i < len(hexData); i += 2 {
|
||||||
|
b, err := strconv.ParseUint(hexData[i:i+2], 16, 8)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errUnavailable
|
||||||
|
}
|
||||||
|
buf[i/2] = byte(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// write writes the given data to clipboard and
|
||||||
|
// returns true if success or false if failed.
|
||||||
|
func write(t Format, buf []byte) (<-chan struct{}, error) {
|
||||||
|
var err error
|
||||||
|
switch t {
|
||||||
|
case FmtText:
|
||||||
|
err = writeText(buf)
|
||||||
|
case FmtImage:
|
||||||
|
err = writeImage(buf)
|
||||||
|
default:
|
||||||
|
return nil, errUnsupported
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update change count
|
||||||
|
changeCountMu.Lock()
|
||||||
|
lastChangeCount++
|
||||||
|
currentCount := lastChangeCount
|
||||||
|
changeCountMu.Unlock()
|
||||||
|
|
||||||
|
// use unbuffered channel to prevent goroutine leak
|
||||||
|
changed := make(chan struct{}, 1)
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
changeCountMu.Lock()
|
||||||
|
if lastChangeCount != currentCount {
|
||||||
|
changeCountMu.Unlock()
|
||||||
|
changed <- struct{}{}
|
||||||
|
close(changed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
changeCountMu.Unlock()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return changed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeText(buf []byte) error {
|
||||||
|
if len(buf) == 0 {
|
||||||
|
// Clear clipboard
|
||||||
|
script := `set the clipboard to ""`
|
||||||
|
cmd := exec.Command("osascript", "-e", script)
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return errUnavailable
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape the text for AppleScript
|
||||||
|
text := string(buf)
|
||||||
|
text = strings.ReplaceAll(text, "\\", "\\\\")
|
||||||
|
text = strings.ReplaceAll(text, "\"", "\\\"")
|
||||||
|
|
||||||
|
script := fmt.Sprintf(`set the clipboard to "%s"`, text)
|
||||||
|
cmd := exec.Command("osascript", "-e", script)
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return errUnavailable
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func writeImage(buf []byte) error {
|
||||||
|
if len(buf) == 0 {
|
||||||
|
// Clear clipboard
|
||||||
|
script := `set the clipboard to ""`
|
||||||
|
cmd := exec.Command("osascript", "-e", script)
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return errUnavailable
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a temporary file to store the PNG data
|
||||||
|
tmpFile, err := os.CreateTemp("", "clipboard*.png")
|
||||||
|
if err != nil {
|
||||||
|
return errUnavailable
|
||||||
|
}
|
||||||
|
defer os.Remove(tmpFile.Name())
|
||||||
|
|
||||||
|
if _, err := tmpFile.Write(buf); err != nil {
|
||||||
|
tmpFile.Close()
|
||||||
|
return errUnavailable
|
||||||
|
}
|
||||||
|
tmpFile.Close()
|
||||||
|
|
||||||
|
// Use osascript to set clipboard to the image file
|
||||||
|
script := fmt.Sprintf(`
|
||||||
|
set theFile to POSIX file "%s"
|
||||||
|
set theImage to read theFile as «class PNGf»
|
||||||
|
set the clipboard to theImage
|
||||||
|
`, tmpFile.Name())
|
||||||
|
|
||||||
|
cmd := exec.Command("osascript", "-e", script)
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return errUnavailable
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func watch(ctx context.Context, t Format) <-chan []byte {
|
||||||
|
recv := make(chan []byte, 1)
|
||||||
|
ti := time.NewTicker(time.Second)
|
||||||
|
|
||||||
|
// Get initial clipboard content
|
||||||
|
var lastContent []byte
|
||||||
|
if b := Read(t); b != nil {
|
||||||
|
lastContent = make([]byte, len(b))
|
||||||
|
copy(lastContent, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(recv)
|
||||||
|
defer ti.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ti.C:
|
||||||
|
b := Read(t)
|
||||||
|
if b == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if content changed
|
||||||
|
if !bytes.Equal(lastContent, b) {
|
||||||
|
recv <- b
|
||||||
|
lastContent = make([]byte, len(b))
|
||||||
|
copy(lastContent, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return recv
|
||||||
|
}
|
||||||
@@ -0,0 +1,301 @@
|
|||||||
|
// Copyright 2021 The golang.design Initiative Authors.
|
||||||
|
// All rights reserved. Use of this source code is governed
|
||||||
|
// by a MIT license that can be found in the LICENSE file.
|
||||||
|
//
|
||||||
|
// Written by Changkun Ou <changkun.de>
|
||||||
|
|
||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package clipboard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Clipboard tools in order of preference
|
||||||
|
clipboardTools = []struct {
|
||||||
|
name string
|
||||||
|
readCmd []string
|
||||||
|
writeCmd []string
|
||||||
|
readImg []string
|
||||||
|
writeImg []string
|
||||||
|
available bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "xclip",
|
||||||
|
readCmd: []string{"xclip", "-selection", "clipboard", "-o"},
|
||||||
|
writeCmd: []string{"xclip", "-selection", "clipboard"},
|
||||||
|
readImg: []string{"xclip", "-selection", "clipboard", "-t", "image/png", "-o"},
|
||||||
|
writeImg: []string{"xclip", "-selection", "clipboard", "-t", "image/png"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "xsel",
|
||||||
|
readCmd: []string{"xsel", "--clipboard", "--output"},
|
||||||
|
writeCmd: []string{"xsel", "--clipboard", "--input"},
|
||||||
|
readImg: []string{"xsel", "--clipboard", "--output"},
|
||||||
|
writeImg: []string{"xsel", "--clipboard", "--input"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wl-clipboard",
|
||||||
|
readCmd: []string{"wl-paste", "-n"},
|
||||||
|
writeCmd: []string{"wl-copy"},
|
||||||
|
readImg: []string{"wl-paste", "-t", "image/png", "-n"},
|
||||||
|
writeImg: []string{"wl-copy", "-t", "image/png"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedTool int = -1
|
||||||
|
toolMutex sync.Mutex
|
||||||
|
lastChangeTime time.Time
|
||||||
|
changeTimeMu sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
func initialize() error {
|
||||||
|
toolMutex.Lock()
|
||||||
|
defer toolMutex.Unlock()
|
||||||
|
|
||||||
|
if selectedTool >= 0 {
|
||||||
|
return nil // Already initialized
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check which clipboard tool is available
|
||||||
|
for i, tool := range clipboardTools {
|
||||||
|
cmd := exec.Command("which", tool.name)
|
||||||
|
if err := cmd.Run(); err == nil {
|
||||||
|
clipboardTools[i].available = true
|
||||||
|
if selectedTool < 0 {
|
||||||
|
selectedTool = i
|
||||||
|
slog.Debug("Clipboard tool found", "tool", tool.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if selectedTool < 0 {
|
||||||
|
slog.Warn(
|
||||||
|
"No clipboard utility found on system. Copy/paste functionality will be disabled. See https://opencode.ai/docs/troubleshooting/ for more information.",
|
||||||
|
)
|
||||||
|
return fmt.Errorf(`%w: No clipboard utility found. Install one of the following:
|
||||||
|
|
||||||
|
For X11 systems:
|
||||||
|
apt install -y xclip
|
||||||
|
# or
|
||||||
|
apt install -y xsel
|
||||||
|
|
||||||
|
For Wayland systems:
|
||||||
|
apt install -y wl-clipboard
|
||||||
|
|
||||||
|
If running in a headless environment, you may also need:
|
||||||
|
apt install -y xvfb
|
||||||
|
# and run:
|
||||||
|
Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
|
||||||
|
export DISPLAY=:99.0`, errUnavailable)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func read(t Format) (buf []byte, err error) {
|
||||||
|
// Ensure clipboard is initialized before attempting to read
|
||||||
|
if err := initialize(); err != nil {
|
||||||
|
slog.Debug("Clipboard read failed: not initialized", "error", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
toolMutex.Lock()
|
||||||
|
tool := clipboardTools[selectedTool]
|
||||||
|
toolMutex.Unlock()
|
||||||
|
|
||||||
|
switch t {
|
||||||
|
case FmtText:
|
||||||
|
return readText(tool)
|
||||||
|
case FmtImage:
|
||||||
|
return readImage(tool)
|
||||||
|
default:
|
||||||
|
return nil, errUnsupported
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readText(tool struct {
|
||||||
|
name string
|
||||||
|
readCmd []string
|
||||||
|
writeCmd []string
|
||||||
|
readImg []string
|
||||||
|
writeImg []string
|
||||||
|
available bool
|
||||||
|
}) ([]byte, error) {
|
||||||
|
// First check if clipboard contains text
|
||||||
|
cmd := exec.Command(tool.readCmd[0], tool.readCmd[1:]...)
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
// Check if it's because clipboard contains non-text data
|
||||||
|
if tool.name == "xclip" {
|
||||||
|
// xclip returns error when clipboard doesn't contain requested type
|
||||||
|
checkCmd := exec.Command("xclip", "-selection", "clipboard", "-t", "TARGETS", "-o")
|
||||||
|
targets, _ := checkCmd.Output()
|
||||||
|
if bytes.Contains(targets, []byte("image/png")) &&
|
||||||
|
!bytes.Contains(targets, []byte("UTF8_STRING")) {
|
||||||
|
return nil, errUnavailable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errUnavailable
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readImage(tool struct {
|
||||||
|
name string
|
||||||
|
readCmd []string
|
||||||
|
writeCmd []string
|
||||||
|
readImg []string
|
||||||
|
writeImg []string
|
||||||
|
available bool
|
||||||
|
}) ([]byte, error) {
|
||||||
|
if tool.name == "xsel" {
|
||||||
|
// xsel doesn't support image types well, return error
|
||||||
|
return nil, errUnavailable
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(tool.readImg[0], tool.readImg[1:]...)
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errUnavailable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify it's PNG data
|
||||||
|
if len(out) < 8 ||
|
||||||
|
!bytes.Equal(out[:8], []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}) {
|
||||||
|
return nil, errUnavailable
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func write(t Format, buf []byte) (<-chan struct{}, error) {
|
||||||
|
// Ensure clipboard is initialized before attempting to write
|
||||||
|
if err := initialize(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
toolMutex.Lock()
|
||||||
|
tool := clipboardTools[selectedTool]
|
||||||
|
toolMutex.Unlock()
|
||||||
|
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
switch t {
|
||||||
|
case FmtText:
|
||||||
|
if len(buf) == 0 {
|
||||||
|
// Write empty string
|
||||||
|
cmd = exec.Command(tool.writeCmd[0], tool.writeCmd[1:]...)
|
||||||
|
cmd.Stdin = bytes.NewReader([]byte{})
|
||||||
|
} else {
|
||||||
|
cmd = exec.Command(tool.writeCmd[0], tool.writeCmd[1:]...)
|
||||||
|
cmd.Stdin = bytes.NewReader(buf)
|
||||||
|
}
|
||||||
|
case FmtImage:
|
||||||
|
if tool.name == "xsel" {
|
||||||
|
// xsel doesn't support image types well
|
||||||
|
return nil, errUnavailable
|
||||||
|
}
|
||||||
|
if len(buf) == 0 {
|
||||||
|
// Clear clipboard
|
||||||
|
cmd = exec.Command(tool.writeCmd[0], tool.writeCmd[1:]...)
|
||||||
|
cmd.Stdin = bytes.NewReader([]byte{})
|
||||||
|
} else {
|
||||||
|
cmd = exec.Command(tool.writeImg[0], tool.writeImg[1:]...)
|
||||||
|
cmd.Stdin = bytes.NewReader(buf)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, errUnsupported
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return nil, errUnavailable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update change time
|
||||||
|
changeTimeMu.Lock()
|
||||||
|
lastChangeTime = time.Now()
|
||||||
|
currentTime := lastChangeTime
|
||||||
|
changeTimeMu.Unlock()
|
||||||
|
|
||||||
|
// Create change notification channel
|
||||||
|
changed := make(chan struct{}, 1)
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
changeTimeMu.Lock()
|
||||||
|
if !lastChangeTime.Equal(currentTime) {
|
||||||
|
changeTimeMu.Unlock()
|
||||||
|
changed <- struct{}{}
|
||||||
|
close(changed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
changeTimeMu.Unlock()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return changed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func watch(ctx context.Context, t Format) <-chan []byte {
|
||||||
|
recv := make(chan []byte, 1)
|
||||||
|
|
||||||
|
// Ensure clipboard is initialized before starting watch
|
||||||
|
if err := initialize(); err != nil {
|
||||||
|
close(recv)
|
||||||
|
return recv
|
||||||
|
}
|
||||||
|
|
||||||
|
ti := time.NewTicker(time.Second)
|
||||||
|
|
||||||
|
// Get initial clipboard content
|
||||||
|
var lastContent []byte
|
||||||
|
if b := Read(t); b != nil {
|
||||||
|
lastContent = make([]byte, len(b))
|
||||||
|
copy(lastContent, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(recv)
|
||||||
|
defer ti.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ti.C:
|
||||||
|
b := Read(t)
|
||||||
|
if b == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if content changed
|
||||||
|
if !bytes.Equal(lastContent, b) {
|
||||||
|
recv <- b
|
||||||
|
lastContent = make([]byte, len(b))
|
||||||
|
copy(lastContent, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return recv
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to check clipboard content type for xclip
|
||||||
|
func getClipboardTargets() []string {
|
||||||
|
cmd := exec.Command("xclip", "-selection", "clipboard", "-t", "TARGETS", "-o")
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return strings.Split(string(out), "\n")
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
//go:build !windows && !darwin && !linux && !cgo
|
||||||
|
|
||||||
|
package clipboard
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
func initialize() error {
|
||||||
|
return errNoCgo
|
||||||
|
}
|
||||||
|
|
||||||
|
func read(t Format) (buf []byte, err error) {
|
||||||
|
panic("clipboard: cannot use when CGO_ENABLED=0")
|
||||||
|
}
|
||||||
|
|
||||||
|
func readc(t string) ([]byte, error) {
|
||||||
|
panic("clipboard: cannot use when CGO_ENABLED=0")
|
||||||
|
}
|
||||||
|
|
||||||
|
func write(t Format, buf []byte) (<-chan struct{}, error) {
|
||||||
|
panic("clipboard: cannot use when CGO_ENABLED=0")
|
||||||
|
}
|
||||||
|
|
||||||
|
func watch(ctx context.Context, t Format) <-chan []byte {
|
||||||
|
panic("clipboard: cannot use when CGO_ENABLED=0")
|
||||||
|
}
|
||||||
@@ -0,0 +1,551 @@
|
|||||||
|
// Copyright 2021 The golang.design Initiative Authors.
|
||||||
|
// All rights reserved. Use of this source code is governed
|
||||||
|
// by a MIT license that can be found in the LICENSE file.
|
||||||
|
//
|
||||||
|
// Written by Changkun Ou <changkun.de>
|
||||||
|
|
||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package clipboard
|
||||||
|
|
||||||
|
// Interacting with Clipboard on Windows:
|
||||||
|
// https://docs.microsoft.com/zh-cn/windows/win32/dataxchg/using-the-clipboard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"image/png"
|
||||||
|
"reflect"
|
||||||
|
"runtime"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
"unicode/utf16"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/image/bmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initialize() error { return nil }
|
||||||
|
|
||||||
|
// readText reads the clipboard and returns the text data if presents.
|
||||||
|
// The caller is responsible for opening/closing the clipboard before
|
||||||
|
// calling this function.
|
||||||
|
func readText() (buf []byte, err error) {
|
||||||
|
hMem, _, err := getClipboardData.Call(cFmtUnicodeText)
|
||||||
|
if hMem == 0 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
p, _, err := gLock.Call(hMem)
|
||||||
|
if p == 0 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer gUnlock.Call(hMem)
|
||||||
|
|
||||||
|
// Find NUL terminator
|
||||||
|
n := 0
|
||||||
|
for ptr := unsafe.Pointer(p); *(*uint16)(ptr) != 0; n++ {
|
||||||
|
ptr = unsafe.Pointer(uintptr(ptr) +
|
||||||
|
unsafe.Sizeof(*((*uint16)(unsafe.Pointer(p)))))
|
||||||
|
}
|
||||||
|
|
||||||
|
var s []uint16
|
||||||
|
h := (*reflect.SliceHeader)(unsafe.Pointer(&s))
|
||||||
|
h.Data = p
|
||||||
|
h.Len = n
|
||||||
|
h.Cap = n
|
||||||
|
return []byte(string(utf16.Decode(s))), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeText writes given data to the clipboard. It is the caller's
|
||||||
|
// responsibility for opening/closing the clipboard before calling
|
||||||
|
// this function.
|
||||||
|
func writeText(buf []byte) error {
|
||||||
|
r, _, err := emptyClipboard.Call()
|
||||||
|
if r == 0 {
|
||||||
|
return fmt.Errorf("failed to clear clipboard: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// empty text, we are done here.
|
||||||
|
if len(buf) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := syscall.UTF16FromString(string(buf))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to convert given string: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hMem, _, err := gAlloc.Call(gmemMoveable, uintptr(len(s)*int(unsafe.Sizeof(s[0]))))
|
||||||
|
if hMem == 0 {
|
||||||
|
return fmt.Errorf("failed to alloc global memory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p, _, err := gLock.Call(hMem)
|
||||||
|
if p == 0 {
|
||||||
|
return fmt.Errorf("failed to lock global memory: %w", err)
|
||||||
|
}
|
||||||
|
defer gUnlock.Call(hMem)
|
||||||
|
|
||||||
|
// no return value
|
||||||
|
memMove.Call(p, uintptr(unsafe.Pointer(&s[0])),
|
||||||
|
uintptr(len(s)*int(unsafe.Sizeof(s[0]))))
|
||||||
|
|
||||||
|
v, _, err := setClipboardData.Call(cFmtUnicodeText, hMem)
|
||||||
|
if v == 0 {
|
||||||
|
gFree.Call(hMem)
|
||||||
|
return fmt.Errorf("failed to set text to clipboard: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// readImage reads the clipboard and returns PNG encoded image data
|
||||||
|
// if presents. The caller is responsible for opening/closing the
|
||||||
|
// clipboard before calling this function.
|
||||||
|
func readImage() ([]byte, error) {
|
||||||
|
hMem, _, err := getClipboardData.Call(cFmtDIBV5)
|
||||||
|
if hMem == 0 {
|
||||||
|
// second chance to try FmtDIB
|
||||||
|
return readImageDib()
|
||||||
|
}
|
||||||
|
p, _, err := gLock.Call(hMem)
|
||||||
|
if p == 0 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer gUnlock.Call(hMem)
|
||||||
|
|
||||||
|
// inspect header information
|
||||||
|
info := (*bitmapV5Header)(unsafe.Pointer(p))
|
||||||
|
|
||||||
|
// maybe deal with other formats?
|
||||||
|
if info.BitCount != 32 {
|
||||||
|
return nil, errUnsupported
|
||||||
|
}
|
||||||
|
|
||||||
|
var data []byte
|
||||||
|
sh := (*reflect.SliceHeader)(unsafe.Pointer(&data))
|
||||||
|
sh.Data = uintptr(p)
|
||||||
|
sh.Cap = int(info.Size + 4*uint32(info.Width)*uint32(info.Height))
|
||||||
|
sh.Len = int(info.Size + 4*uint32(info.Width)*uint32(info.Height))
|
||||||
|
img := image.NewRGBA(image.Rect(0, 0, int(info.Width), int(info.Height)))
|
||||||
|
offset := int(info.Size)
|
||||||
|
stride := int(info.Width)
|
||||||
|
for y := 0; y < int(info.Height); y++ {
|
||||||
|
for x := 0; x < int(info.Width); x++ {
|
||||||
|
idx := offset + 4*(y*stride+x)
|
||||||
|
xhat := (x + int(info.Width)) % int(info.Width)
|
||||||
|
yhat := int(info.Height) - 1 - y
|
||||||
|
r := data[idx+2]
|
||||||
|
g := data[idx+1]
|
||||||
|
b := data[idx+0]
|
||||||
|
a := data[idx+3]
|
||||||
|
img.SetRGBA(xhat, yhat, color.RGBA{r, g, b, a})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// always use PNG encoding.
|
||||||
|
var buf bytes.Buffer
|
||||||
|
png.Encode(&buf, img)
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readImageDib() ([]byte, error) {
|
||||||
|
const (
|
||||||
|
fileHeaderLen = 14
|
||||||
|
infoHeaderLen = 40
|
||||||
|
cFmtDIB = 8
|
||||||
|
)
|
||||||
|
|
||||||
|
hClipDat, _, err := getClipboardData.Call(cFmtDIB)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("not dib format data: " + err.Error())
|
||||||
|
}
|
||||||
|
pMemBlk, _, err := gLock.Call(hClipDat)
|
||||||
|
if pMemBlk == 0 {
|
||||||
|
return nil, errors.New("failed to call global lock: " + err.Error())
|
||||||
|
}
|
||||||
|
defer gUnlock.Call(hClipDat)
|
||||||
|
|
||||||
|
bmpHeader := (*bitmapHeader)(unsafe.Pointer(pMemBlk))
|
||||||
|
dataSize := bmpHeader.SizeImage + fileHeaderLen + infoHeaderLen
|
||||||
|
|
||||||
|
if bmpHeader.SizeImage == 0 && bmpHeader.Compression == 0 {
|
||||||
|
iSizeImage := bmpHeader.Height * ((bmpHeader.Width*uint32(bmpHeader.BitCount)/8 + 3) &^ 3)
|
||||||
|
dataSize += iSizeImage
|
||||||
|
}
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
binary.Write(buf, binary.LittleEndian, uint16('B')|(uint16('M')<<8))
|
||||||
|
binary.Write(buf, binary.LittleEndian, uint32(dataSize))
|
||||||
|
binary.Write(buf, binary.LittleEndian, uint32(0))
|
||||||
|
const sizeof_colorbar = 0
|
||||||
|
binary.Write(buf, binary.LittleEndian, uint32(fileHeaderLen+infoHeaderLen+sizeof_colorbar))
|
||||||
|
j := 0
|
||||||
|
for i := fileHeaderLen; i < int(dataSize); i++ {
|
||||||
|
binary.Write(buf, binary.BigEndian, *(*byte)(unsafe.Pointer(pMemBlk + uintptr(j))))
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
return bmpToPng(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func bmpToPng(bmpBuf *bytes.Buffer) (buf []byte, err error) {
|
||||||
|
var f bytes.Buffer
|
||||||
|
original_image, err := bmp.Decode(bmpBuf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = png.Encode(&f, original_image)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return f.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeImage(buf []byte) error {
|
||||||
|
r, _, err := emptyClipboard.Call()
|
||||||
|
if r == 0 {
|
||||||
|
return fmt.Errorf("failed to clear clipboard: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// empty text, we are done here.
|
||||||
|
if len(buf) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
img, err := png.Decode(bytes.NewReader(buf))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("input bytes is not PNG encoded: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := unsafe.Sizeof(bitmapV5Header{})
|
||||||
|
width := img.Bounds().Dx()
|
||||||
|
height := img.Bounds().Dy()
|
||||||
|
imageSize := 4 * width * height
|
||||||
|
|
||||||
|
data := make([]byte, int(offset)+imageSize)
|
||||||
|
for y := 0; y < height; y++ {
|
||||||
|
for x := 0; x < width; x++ {
|
||||||
|
idx := int(offset) + 4*(y*width+x)
|
||||||
|
r, g, b, a := img.At(x, height-1-y).RGBA()
|
||||||
|
data[idx+2] = uint8(r)
|
||||||
|
data[idx+1] = uint8(g)
|
||||||
|
data[idx+0] = uint8(b)
|
||||||
|
data[idx+3] = uint8(a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info := bitmapV5Header{}
|
||||||
|
info.Size = uint32(offset)
|
||||||
|
info.Width = int32(width)
|
||||||
|
info.Height = int32(height)
|
||||||
|
info.Planes = 1
|
||||||
|
info.Compression = 0 // BI_RGB
|
||||||
|
info.SizeImage = uint32(4 * info.Width * info.Height)
|
||||||
|
info.RedMask = 0xff0000 // default mask
|
||||||
|
info.GreenMask = 0xff00
|
||||||
|
info.BlueMask = 0xff
|
||||||
|
info.AlphaMask = 0xff000000
|
||||||
|
info.BitCount = 32 // we only deal with 32 bpp at the moment.
|
||||||
|
// Use calibrated RGB values as Go's image/png assumes linear color space.
|
||||||
|
// Other options:
|
||||||
|
// - LCS_CALIBRATED_RGB = 0x00000000
|
||||||
|
// - LCS_sRGB = 0x73524742
|
||||||
|
// - LCS_WINDOWS_COLOR_SPACE = 0x57696E20
|
||||||
|
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wmf/eb4bbd50-b3ce-4917-895c-be31f214797f
|
||||||
|
info.CSType = 0x73524742
|
||||||
|
// Use GL_IMAGES for GamutMappingIntent
|
||||||
|
// Other options:
|
||||||
|
// - LCS_GM_ABS_COLORIMETRIC = 0x00000008
|
||||||
|
// - LCS_GM_BUSINESS = 0x00000001
|
||||||
|
// - LCS_GM_GRAPHICS = 0x00000002
|
||||||
|
// - LCS_GM_IMAGES = 0x00000004
|
||||||
|
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wmf/9fec0834-607d-427d-abd5-ab240fb0db38
|
||||||
|
info.Intent = 4 // LCS_GM_IMAGES
|
||||||
|
|
||||||
|
infob := make([]byte, int(unsafe.Sizeof(info)))
|
||||||
|
for i, v := range *(*[unsafe.Sizeof(info)]byte)(unsafe.Pointer(&info)) {
|
||||||
|
infob[i] = v
|
||||||
|
}
|
||||||
|
copy(data[:], infob[:])
|
||||||
|
|
||||||
|
hMem, _, err := gAlloc.Call(gmemMoveable,
|
||||||
|
uintptr(len(data)*int(unsafe.Sizeof(data[0]))))
|
||||||
|
if hMem == 0 {
|
||||||
|
return fmt.Errorf("failed to alloc global memory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p, _, err := gLock.Call(hMem)
|
||||||
|
if p == 0 {
|
||||||
|
return fmt.Errorf("failed to lock global memory: %w", err)
|
||||||
|
}
|
||||||
|
defer gUnlock.Call(hMem)
|
||||||
|
|
||||||
|
memMove.Call(p, uintptr(unsafe.Pointer(&data[0])),
|
||||||
|
uintptr(len(data)*int(unsafe.Sizeof(data[0]))))
|
||||||
|
|
||||||
|
v, _, err := setClipboardData.Call(cFmtDIBV5, hMem)
|
||||||
|
if v == 0 {
|
||||||
|
gFree.Call(hMem)
|
||||||
|
return fmt.Errorf("failed to set text to clipboard: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func read(t Format) (buf []byte, err error) {
|
||||||
|
// On Windows, OpenClipboard and CloseClipboard must be executed on
|
||||||
|
// the same thread. Thus, lock the OS thread for further execution.
|
||||||
|
runtime.LockOSThread()
|
||||||
|
defer runtime.UnlockOSThread()
|
||||||
|
|
||||||
|
var format uintptr
|
||||||
|
switch t {
|
||||||
|
case FmtImage:
|
||||||
|
format = cFmtDIBV5
|
||||||
|
case FmtText:
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
format = cFmtUnicodeText
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if clipboard is avaliable for the requested format
|
||||||
|
r, _, err := isClipboardFormatAvailable.Call(format)
|
||||||
|
if r == 0 {
|
||||||
|
return nil, errUnavailable
|
||||||
|
}
|
||||||
|
|
||||||
|
// try again until open clipboard successed
|
||||||
|
for {
|
||||||
|
r, _, _ = openClipboard.Call()
|
||||||
|
if r == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
defer closeClipboard.Call()
|
||||||
|
|
||||||
|
switch format {
|
||||||
|
case cFmtDIBV5:
|
||||||
|
return readImage()
|
||||||
|
case cFmtUnicodeText:
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
return readText()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// write writes the given data to clipboard and
|
||||||
|
// returns true if success or false if failed.
|
||||||
|
func write(t Format, buf []byte) (<-chan struct{}, error) {
|
||||||
|
errch := make(chan error)
|
||||||
|
changed := make(chan struct{}, 1)
|
||||||
|
go func() {
|
||||||
|
// make sure GetClipboardSequenceNumber happens with
|
||||||
|
// OpenClipboard on the same thread.
|
||||||
|
runtime.LockOSThread()
|
||||||
|
defer runtime.UnlockOSThread()
|
||||||
|
for {
|
||||||
|
r, _, _ := openClipboard.Call(0)
|
||||||
|
if r == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// var param uintptr
|
||||||
|
switch t {
|
||||||
|
case FmtImage:
|
||||||
|
err := writeImage(buf)
|
||||||
|
if err != nil {
|
||||||
|
errch <- err
|
||||||
|
closeClipboard.Call()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case FmtText:
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
// param = cFmtUnicodeText
|
||||||
|
err := writeText(buf)
|
||||||
|
if err != nil {
|
||||||
|
errch <- err
|
||||||
|
closeClipboard.Call()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Close the clipboard otherwise other applications cannot
|
||||||
|
// paste the data.
|
||||||
|
closeClipboard.Call()
|
||||||
|
|
||||||
|
cnt, _, _ := getClipboardSequenceNumber.Call()
|
||||||
|
errch <- nil
|
||||||
|
for {
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
cur, _, _ := getClipboardSequenceNumber.Call()
|
||||||
|
if cur != cnt {
|
||||||
|
changed <- struct{}{}
|
||||||
|
close(changed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
err := <-errch
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return changed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func watch(ctx context.Context, t Format) <-chan []byte {
|
||||||
|
recv := make(chan []byte, 1)
|
||||||
|
ready := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
// not sure if we are too slow or the user too fast :)
|
||||||
|
ti := time.NewTicker(time.Second)
|
||||||
|
cnt, _, _ := getClipboardSequenceNumber.Call()
|
||||||
|
ready <- struct{}{}
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
close(recv)
|
||||||
|
return
|
||||||
|
case <-ti.C:
|
||||||
|
cur, _, _ := getClipboardSequenceNumber.Call()
|
||||||
|
if cnt != cur {
|
||||||
|
b := Read(t)
|
||||||
|
if b == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
recv <- b
|
||||||
|
cnt = cur
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
<-ready
|
||||||
|
return recv
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
cFmtBitmap = 2 // Win+PrintScreen
|
||||||
|
cFmtUnicodeText = 13
|
||||||
|
cFmtDIBV5 = 17
|
||||||
|
// Screenshot taken from special shortcut is in different format (why??), see:
|
||||||
|
// https://jpsoft.com/forums/threads/detecting-clipboard-format.5225/
|
||||||
|
cFmtDataObject = 49161 // Shift+Win+s, returned from enumClipboardFormats
|
||||||
|
gmemMoveable = 0x0002
|
||||||
|
)
|
||||||
|
|
||||||
|
// BITMAPV5Header structure, see:
|
||||||
|
// https://docs.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapv5header
|
||||||
|
type bitmapV5Header struct {
|
||||||
|
Size uint32
|
||||||
|
Width int32
|
||||||
|
Height int32
|
||||||
|
Planes uint16
|
||||||
|
BitCount uint16
|
||||||
|
Compression uint32
|
||||||
|
SizeImage uint32
|
||||||
|
XPelsPerMeter int32
|
||||||
|
YPelsPerMeter int32
|
||||||
|
ClrUsed uint32
|
||||||
|
ClrImportant uint32
|
||||||
|
RedMask uint32
|
||||||
|
GreenMask uint32
|
||||||
|
BlueMask uint32
|
||||||
|
AlphaMask uint32
|
||||||
|
CSType uint32
|
||||||
|
Endpoints struct {
|
||||||
|
CiexyzRed, CiexyzGreen, CiexyzBlue struct {
|
||||||
|
CiexyzX, CiexyzY, CiexyzZ int32 // FXPT2DOT30
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GammaRed uint32
|
||||||
|
GammaGreen uint32
|
||||||
|
GammaBlue uint32
|
||||||
|
Intent uint32
|
||||||
|
ProfileData uint32
|
||||||
|
ProfileSize uint32
|
||||||
|
Reserved uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type bitmapHeader struct {
|
||||||
|
Size uint32
|
||||||
|
Width uint32
|
||||||
|
Height uint32
|
||||||
|
PLanes uint16
|
||||||
|
BitCount uint16
|
||||||
|
Compression uint32
|
||||||
|
SizeImage uint32
|
||||||
|
XPelsPerMeter uint32
|
||||||
|
YPelsPerMeter uint32
|
||||||
|
ClrUsed uint32
|
||||||
|
ClrImportant uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calling a Windows DLL, see:
|
||||||
|
// https://github.com/golang/go/wiki/WindowsDLLs
|
||||||
|
var (
|
||||||
|
user32 = syscall.MustLoadDLL("user32")
|
||||||
|
// Opens the clipboard for examination and prevents other
|
||||||
|
// applications from modifying the clipboard content.
|
||||||
|
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-openclipboard
|
||||||
|
openClipboard = user32.MustFindProc("OpenClipboard")
|
||||||
|
// Closes the clipboard.
|
||||||
|
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-closeclipboard
|
||||||
|
closeClipboard = user32.MustFindProc("CloseClipboard")
|
||||||
|
// Empties the clipboard and frees handles to data in the clipboard.
|
||||||
|
// The function then assigns ownership of the clipboard to the
|
||||||
|
// window that currently has the clipboard open.
|
||||||
|
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-emptyclipboard
|
||||||
|
emptyClipboard = user32.MustFindProc("EmptyClipboard")
|
||||||
|
// Retrieves data from the clipboard in a specified format.
|
||||||
|
// The clipboard must have been opened previously.
|
||||||
|
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getclipboarddata
|
||||||
|
getClipboardData = user32.MustFindProc("GetClipboardData")
|
||||||
|
// Places data on the clipboard in a specified clipboard format.
|
||||||
|
// The window must be the current clipboard owner, and the
|
||||||
|
// application must have called the OpenClipboard function. (When
|
||||||
|
// responding to the WM_RENDERFORMAT message, the clipboard owner
|
||||||
|
// must not call OpenClipboard before calling SetClipboardData.)
|
||||||
|
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setclipboarddata
|
||||||
|
setClipboardData = user32.MustFindProc("SetClipboardData")
|
||||||
|
// Determines whether the clipboard contains data in the specified format.
|
||||||
|
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-isclipboardformatavailable
|
||||||
|
isClipboardFormatAvailable = user32.MustFindProc("IsClipboardFormatAvailable")
|
||||||
|
// Clipboard data formats are stored in an ordered list. To perform
|
||||||
|
// an enumeration of clipboard data formats, you make a series of
|
||||||
|
// calls to the EnumClipboardFormats function. For each call, the
|
||||||
|
// format parameter specifies an available clipboard format, and the
|
||||||
|
// function returns the next available clipboard format.
|
||||||
|
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-isclipboardformatavailable
|
||||||
|
enumClipboardFormats = user32.MustFindProc("EnumClipboardFormats")
|
||||||
|
// Retrieves the clipboard sequence number for the current window station.
|
||||||
|
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getclipboardsequencenumber
|
||||||
|
getClipboardSequenceNumber = user32.MustFindProc("GetClipboardSequenceNumber")
|
||||||
|
// Registers a new clipboard format. This format can then be used as
|
||||||
|
// a valid clipboard format.
|
||||||
|
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerclipboardformata
|
||||||
|
registerClipboardFormatA = user32.MustFindProc("RegisterClipboardFormatA")
|
||||||
|
|
||||||
|
kernel32 = syscall.NewLazyDLL("kernel32")
|
||||||
|
|
||||||
|
// Locks a global memory object and returns a pointer to the first
|
||||||
|
// byte of the object's memory block.
|
||||||
|
// https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-globallock
|
||||||
|
gLock = kernel32.NewProc("GlobalLock")
|
||||||
|
// Decrements the lock count associated with a memory object that was
|
||||||
|
// allocated with GMEM_MOVEABLE. This function has no effect on memory
|
||||||
|
// objects allocated with GMEM_FIXED.
|
||||||
|
// https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-globalunlock
|
||||||
|
gUnlock = kernel32.NewProc("GlobalUnlock")
|
||||||
|
// Allocates the specified number of bytes from the heap.
|
||||||
|
// https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-globalalloc
|
||||||
|
gAlloc = kernel32.NewProc("GlobalAlloc")
|
||||||
|
// Frees the specified global memory object and invalidates its handle.
|
||||||
|
// https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-globalfree
|
||||||
|
gFree = kernel32.NewProc("GlobalFree")
|
||||||
|
memMove = kernel32.NewProc("RtlMoveMemory")
|
||||||
|
)
|
||||||
@@ -29,7 +29,7 @@ type Command struct {
|
|||||||
Name CommandName
|
Name CommandName
|
||||||
Description string
|
Description string
|
||||||
Keybindings []Keybinding
|
Keybindings []Keybinding
|
||||||
Trigger string
|
Trigger []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Command) Keys() []string {
|
func (c Command) Keys() []string {
|
||||||
@@ -40,6 +40,21 @@ func (c Command) Keys() []string {
|
|||||||
return keys
|
return keys
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c Command) HasTrigger() bool {
|
||||||
|
return len(c.Trigger) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Command) PrimaryTrigger() string {
|
||||||
|
if len(c.Trigger) > 0 {
|
||||||
|
return c.Trigger[0]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Command) MatchesTrigger(trigger string) bool {
|
||||||
|
return slices.Contains(c.Trigger, trigger)
|
||||||
|
}
|
||||||
|
|
||||||
type CommandRegistry map[CommandName]Command
|
type CommandRegistry map[CommandName]Command
|
||||||
|
|
||||||
func (r CommandRegistry) Sorted() []Command {
|
func (r CommandRegistry) Sorted() []Command {
|
||||||
@@ -71,6 +86,7 @@ func (r CommandRegistry) Matches(msg tea.KeyPressMsg, leader bool) []Command {
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
AppHelpCommand CommandName = "app_help"
|
AppHelpCommand CommandName = "app_help"
|
||||||
|
SwitchModeCommand CommandName = "switch_mode"
|
||||||
EditorOpenCommand CommandName = "editor_open"
|
EditorOpenCommand CommandName = "editor_open"
|
||||||
SessionNewCommand CommandName = "session_new"
|
SessionNewCommand CommandName = "session_new"
|
||||||
SessionListCommand CommandName = "session_list"
|
SessionListCommand CommandName = "session_list"
|
||||||
@@ -135,37 +151,43 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
|
|||||||
Name: AppHelpCommand,
|
Name: AppHelpCommand,
|
||||||
Description: "show help",
|
Description: "show help",
|
||||||
Keybindings: parseBindings("<leader>h"),
|
Keybindings: parseBindings("<leader>h"),
|
||||||
Trigger: "help",
|
Trigger: []string{"help"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: SwitchModeCommand,
|
||||||
|
Description: "switch mode",
|
||||||
|
Keybindings: parseBindings("tab"),
|
||||||
|
Trigger: []string{"mode"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: EditorOpenCommand,
|
Name: EditorOpenCommand,
|
||||||
Description: "open editor",
|
Description: "open editor",
|
||||||
Keybindings: parseBindings("<leader>e"),
|
Keybindings: parseBindings("<leader>e"),
|
||||||
Trigger: "editor",
|
Trigger: []string{"editor"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: SessionNewCommand,
|
Name: SessionNewCommand,
|
||||||
Description: "new session",
|
Description: "new session",
|
||||||
Keybindings: parseBindings("<leader>n"),
|
Keybindings: parseBindings("<leader>n"),
|
||||||
Trigger: "new",
|
Trigger: []string{"new", "clear"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: SessionListCommand,
|
Name: SessionListCommand,
|
||||||
Description: "list sessions",
|
Description: "list sessions",
|
||||||
Keybindings: parseBindings("<leader>l"),
|
Keybindings: parseBindings("<leader>l"),
|
||||||
Trigger: "sessions",
|
Trigger: []string{"sessions", "resume", "continue"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: SessionShareCommand,
|
Name: SessionShareCommand,
|
||||||
Description: "share session",
|
Description: "share session",
|
||||||
Keybindings: parseBindings("<leader>s"),
|
Keybindings: parseBindings("<leader>s"),
|
||||||
Trigger: "share",
|
Trigger: []string{"share"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: SessionUnshareCommand,
|
Name: SessionUnshareCommand,
|
||||||
Description: "unshare session",
|
Description: "unshare session",
|
||||||
Keybindings: parseBindings("<leader>u"),
|
Keybindings: parseBindings("<leader>u"),
|
||||||
Trigger: "unshare",
|
Trigger: []string{"unshare"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: SessionInterruptCommand,
|
Name: SessionInterruptCommand,
|
||||||
@@ -176,31 +198,31 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
|
|||||||
Name: SessionCompactCommand,
|
Name: SessionCompactCommand,
|
||||||
Description: "compact the session",
|
Description: "compact the session",
|
||||||
Keybindings: parseBindings("<leader>c"),
|
Keybindings: parseBindings("<leader>c"),
|
||||||
Trigger: "compact",
|
Trigger: []string{"compact", "summarize"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: ToolDetailsCommand,
|
Name: ToolDetailsCommand,
|
||||||
Description: "toggle tool details",
|
Description: "toggle tool details",
|
||||||
Keybindings: parseBindings("<leader>d"),
|
Keybindings: parseBindings("<leader>d"),
|
||||||
Trigger: "details",
|
Trigger: []string{"details"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: ModelListCommand,
|
Name: ModelListCommand,
|
||||||
Description: "list models",
|
Description: "list models",
|
||||||
Keybindings: parseBindings("<leader>m"),
|
Keybindings: parseBindings("<leader>m"),
|
||||||
Trigger: "models",
|
Trigger: []string{"models"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: ThemeListCommand,
|
Name: ThemeListCommand,
|
||||||
Description: "list themes",
|
Description: "list themes",
|
||||||
Keybindings: parseBindings("<leader>t"),
|
Keybindings: parseBindings("<leader>t"),
|
||||||
Trigger: "themes",
|
Trigger: []string{"themes"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: FileListCommand,
|
Name: FileListCommand,
|
||||||
Description: "list files",
|
Description: "list files",
|
||||||
Keybindings: parseBindings("<leader>f"),
|
Keybindings: parseBindings("<leader>f"),
|
||||||
Trigger: "files",
|
Trigger: []string{"files"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: FileCloseCommand,
|
Name: FileCloseCommand,
|
||||||
@@ -221,7 +243,7 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
|
|||||||
Name: ProjectInitCommand,
|
Name: ProjectInitCommand,
|
||||||
Description: "create/update AGENTS.md",
|
Description: "create/update AGENTS.md",
|
||||||
Keybindings: parseBindings("<leader>i"),
|
Keybindings: parseBindings("<leader>i"),
|
||||||
Trigger: "init",
|
Trigger: []string{"init"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: InputClearCommand,
|
Name: InputClearCommand,
|
||||||
@@ -231,7 +253,7 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
|
|||||||
{
|
{
|
||||||
Name: InputPasteCommand,
|
Name: InputPasteCommand,
|
||||||
Description: "paste content",
|
Description: "paste content",
|
||||||
Keybindings: parseBindings("ctrl+v"),
|
Keybindings: parseBindings("ctrl+v", "super+v"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: InputSubmitCommand,
|
Name: InputSubmitCommand,
|
||||||
@@ -302,7 +324,7 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
|
|||||||
Name: AppExitCommand,
|
Name: AppExitCommand,
|
||||||
Description: "exit the app",
|
Description: "exit the app",
|
||||||
Keybindings: parseBindings("ctrl+c", "<leader>q"),
|
Keybindings: parseBindings("ctrl+c", "<leader>q"),
|
||||||
Trigger: "exit",
|
Trigger: []string{"exit", "quit"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
registry := make(CommandRegistry)
|
registry := make(CommandRegistry)
|
||||||
|
|||||||
@@ -29,24 +29,33 @@ func (c *CommandCompletionProvider) GetEmptyMessage() string {
|
|||||||
return "no matching commands"
|
return "no matching commands"
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCommandCompletionItem(cmd commands.Command, space int, t theme.Theme) dialog.CompletionItemI {
|
func (c *CommandCompletionProvider) getCommandCompletionItem(
|
||||||
|
cmd commands.Command,
|
||||||
|
space int,
|
||||||
|
t theme.Theme,
|
||||||
|
) dialog.CompletionItemI {
|
||||||
spacer := strings.Repeat(" ", space)
|
spacer := strings.Repeat(" ", space)
|
||||||
title := " /" + cmd.Trigger + styles.NewStyle().Foreground(t.TextMuted()).Render(spacer+cmd.Description)
|
title := " /" + cmd.PrimaryTrigger() + styles.NewStyle().
|
||||||
|
Foreground(t.TextMuted()).
|
||||||
|
Render(spacer+cmd.Description)
|
||||||
value := string(cmd.Name)
|
value := string(cmd.Name)
|
||||||
return dialog.NewCompletionItem(dialog.CompletionItem{
|
return dialog.NewCompletionItem(dialog.CompletionItem{
|
||||||
Title: title,
|
Title: title,
|
||||||
Value: value,
|
Value: value,
|
||||||
|
ProviderID: c.GetId(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CommandCompletionProvider) GetChildEntries(query string) ([]dialog.CompletionItemI, error) {
|
func (c *CommandCompletionProvider) GetChildEntries(
|
||||||
|
query string,
|
||||||
|
) ([]dialog.CompletionItemI, error) {
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
commands := c.app.Commands
|
commands := c.app.Commands
|
||||||
|
|
||||||
space := 1
|
space := 1
|
||||||
for _, cmd := range c.app.Commands {
|
for _, cmd := range c.app.Commands {
|
||||||
if lipgloss.Width(cmd.Trigger) > space {
|
if cmd.HasTrigger() && lipgloss.Width(cmd.PrimaryTrigger()) > space {
|
||||||
space = lipgloss.Width(cmd.Trigger)
|
space = lipgloss.Width(cmd.PrimaryTrigger())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
space += 2
|
space += 2
|
||||||
@@ -56,11 +65,11 @@ func (c *CommandCompletionProvider) GetChildEntries(query string) ([]dialog.Comp
|
|||||||
// If no query, return all commands
|
// If no query, return all commands
|
||||||
items := []dialog.CompletionItemI{}
|
items := []dialog.CompletionItemI{}
|
||||||
for _, cmd := range sorted {
|
for _, cmd := range sorted {
|
||||||
if cmd.Trigger == "" {
|
if !cmd.HasTrigger() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
space := space - lipgloss.Width(cmd.Trigger)
|
space := space - lipgloss.Width(cmd.PrimaryTrigger())
|
||||||
items = append(items, getCommandCompletionItem(cmd, space, t))
|
items = append(items, c.getCommandCompletionItem(cmd, space, t))
|
||||||
}
|
}
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
@@ -70,12 +79,15 @@ func (c *CommandCompletionProvider) GetChildEntries(query string) ([]dialog.Comp
|
|||||||
commandMap := make(map[string]dialog.CompletionItemI)
|
commandMap := make(map[string]dialog.CompletionItemI)
|
||||||
|
|
||||||
for _, cmd := range sorted {
|
for _, cmd := range sorted {
|
||||||
if cmd.Trigger == "" {
|
if !cmd.HasTrigger() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
space := space - lipgloss.Width(cmd.Trigger)
|
space := space - lipgloss.Width(cmd.PrimaryTrigger())
|
||||||
commandNames = append(commandNames, cmd.Trigger)
|
// Add all triggers as searchable options
|
||||||
commandMap[cmd.Trigger] = getCommandCompletionItem(cmd, space, t)
|
for _, trigger := range cmd.Trigger {
|
||||||
|
commandNames = append(commandNames, trigger)
|
||||||
|
commandMap[trigger] = c.getCommandCompletionItem(cmd, space, t)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find fuzzy matches
|
// Find fuzzy matches
|
||||||
|
|||||||
+16
-12
@@ -14,20 +14,20 @@ import (
|
|||||||
"github.com/sst/opencode/internal/theme"
|
"github.com/sst/opencode/internal/theme"
|
||||||
)
|
)
|
||||||
|
|
||||||
type filesAndFoldersContextGroup struct {
|
type filesContextGroup struct {
|
||||||
app *app.App
|
app *app.App
|
||||||
gitFiles []dialog.CompletionItemI
|
gitFiles []dialog.CompletionItemI
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cg *filesAndFoldersContextGroup) GetId() string {
|
func (cg *filesContextGroup) GetId() string {
|
||||||
return "files"
|
return "files"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cg *filesAndFoldersContextGroup) GetEmptyMessage() string {
|
func (cg *filesContextGroup) GetEmptyMessage() string {
|
||||||
return "no matching files"
|
return "no matching files"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cg *filesAndFoldersContextGroup) getGitFiles() []dialog.CompletionItemI {
|
func (cg *filesContextGroup) getGitFiles() []dialog.CompletionItemI {
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
items := make([]dialog.CompletionItemI, 0)
|
items := make([]dialog.CompletionItemI, 0)
|
||||||
base := styles.NewStyle().Background(t.BackgroundElement())
|
base := styles.NewStyle().Background(t.BackgroundElement())
|
||||||
@@ -42,7 +42,7 @@ func (cg *filesAndFoldersContextGroup) getGitFiles() []dialog.CompletionItemI {
|
|||||||
})
|
})
|
||||||
|
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
title := file.File
|
title := file.Path
|
||||||
if file.Added > 0 {
|
if file.Added > 0 {
|
||||||
title += green(" +" + strconv.Itoa(int(file.Added)))
|
title += green(" +" + strconv.Itoa(int(file.Added)))
|
||||||
}
|
}
|
||||||
@@ -50,8 +50,10 @@ func (cg *filesAndFoldersContextGroup) getGitFiles() []dialog.CompletionItemI {
|
|||||||
title += red(" -" + strconv.Itoa(int(file.Removed)))
|
title += red(" -" + strconv.Itoa(int(file.Removed)))
|
||||||
}
|
}
|
||||||
item := dialog.NewCompletionItem(dialog.CompletionItem{
|
item := dialog.NewCompletionItem(dialog.CompletionItem{
|
||||||
Title: title,
|
Title: title,
|
||||||
Value: file.File,
|
Value: file.Path,
|
||||||
|
ProviderID: cg.GetId(),
|
||||||
|
Raw: file,
|
||||||
})
|
})
|
||||||
items = append(items, item)
|
items = append(items, item)
|
||||||
}
|
}
|
||||||
@@ -60,7 +62,7 @@ func (cg *filesAndFoldersContextGroup) getGitFiles() []dialog.CompletionItemI {
|
|||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cg *filesAndFoldersContextGroup) GetChildEntries(
|
func (cg *filesContextGroup) GetChildEntries(
|
||||||
query string,
|
query string,
|
||||||
) ([]dialog.CompletionItemI, error) {
|
) ([]dialog.CompletionItemI, error) {
|
||||||
items := make([]dialog.CompletionItemI, 0)
|
items := make([]dialog.CompletionItemI, 0)
|
||||||
@@ -94,8 +96,10 @@ func (cg *filesAndFoldersContextGroup) GetChildEntries(
|
|||||||
}
|
}
|
||||||
if !exists {
|
if !exists {
|
||||||
item := dialog.NewCompletionItem(dialog.CompletionItem{
|
item := dialog.NewCompletionItem(dialog.CompletionItem{
|
||||||
Title: file,
|
Title: file,
|
||||||
Value: file,
|
Value: file,
|
||||||
|
ProviderID: cg.GetId(),
|
||||||
|
Raw: file,
|
||||||
})
|
})
|
||||||
items = append(items, item)
|
items = append(items, item)
|
||||||
}
|
}
|
||||||
@@ -104,8 +108,8 @@ func (cg *filesAndFoldersContextGroup) GetChildEntries(
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFileAndFolderContextGroup(app *app.App) dialog.CompletionProvider {
|
func NewFileContextGroup(app *app.App) dialog.CompletionProvider {
|
||||||
cg := &filesAndFoldersContextGroup{
|
cg := &filesContextGroup{
|
||||||
app: app,
|
app: app,
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
package completions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sst/opencode-sdk-go"
|
||||||
|
"github.com/sst/opencode/internal/app"
|
||||||
|
"github.com/sst/opencode/internal/components/dialog"
|
||||||
|
"github.com/sst/opencode/internal/styles"
|
||||||
|
"github.com/sst/opencode/internal/theme"
|
||||||
|
)
|
||||||
|
|
||||||
|
type symbolsContextGroup struct {
|
||||||
|
app *app.App
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cg *symbolsContextGroup) GetId() string {
|
||||||
|
return "symbols"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cg *symbolsContextGroup) GetEmptyMessage() string {
|
||||||
|
return "no matching symbols"
|
||||||
|
}
|
||||||
|
|
||||||
|
type SymbolKind int
|
||||||
|
|
||||||
|
const (
|
||||||
|
SymbolKindFile SymbolKind = 1
|
||||||
|
SymbolKindModule SymbolKind = 2
|
||||||
|
SymbolKindNamespace SymbolKind = 3
|
||||||
|
SymbolKindPackage SymbolKind = 4
|
||||||
|
SymbolKindClass SymbolKind = 5
|
||||||
|
SymbolKindMethod SymbolKind = 6
|
||||||
|
SymbolKindProperty SymbolKind = 7
|
||||||
|
SymbolKindField SymbolKind = 8
|
||||||
|
SymbolKindConstructor SymbolKind = 9
|
||||||
|
SymbolKindEnum SymbolKind = 10
|
||||||
|
SymbolKindInterface SymbolKind = 11
|
||||||
|
SymbolKindFunction SymbolKind = 12
|
||||||
|
SymbolKindVariable SymbolKind = 13
|
||||||
|
SymbolKindConstant SymbolKind = 14
|
||||||
|
SymbolKindString SymbolKind = 15
|
||||||
|
SymbolKindNumber SymbolKind = 16
|
||||||
|
SymbolKindBoolean SymbolKind = 17
|
||||||
|
SymbolKindArray SymbolKind = 18
|
||||||
|
SymbolKindObject SymbolKind = 19
|
||||||
|
SymbolKindKey SymbolKind = 20
|
||||||
|
SymbolKindNull SymbolKind = 21
|
||||||
|
SymbolKindEnumMember SymbolKind = 22
|
||||||
|
SymbolKindStruct SymbolKind = 23
|
||||||
|
SymbolKindEvent SymbolKind = 24
|
||||||
|
SymbolKindOperator SymbolKind = 25
|
||||||
|
SymbolKindTypeParameter SymbolKind = 26
|
||||||
|
)
|
||||||
|
|
||||||
|
func (cg *symbolsContextGroup) GetChildEntries(
|
||||||
|
query string,
|
||||||
|
) ([]dialog.CompletionItemI, error) {
|
||||||
|
items := make([]dialog.CompletionItemI, 0)
|
||||||
|
|
||||||
|
query = strings.TrimSpace(query)
|
||||||
|
if query == "" {
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
symbols, err := cg.app.Client.Find.Symbols(
|
||||||
|
context.Background(),
|
||||||
|
opencode.FindSymbolsParams{Query: opencode.F(query)},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to get symbol completion items", "error", err)
|
||||||
|
return items, err
|
||||||
|
}
|
||||||
|
if symbols == nil {
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
t := theme.CurrentTheme()
|
||||||
|
baseStyle := styles.NewStyle().Background(t.BackgroundElement())
|
||||||
|
base := baseStyle.Render
|
||||||
|
muted := baseStyle.Foreground(t.TextMuted()).Render
|
||||||
|
|
||||||
|
for _, sym := range *symbols {
|
||||||
|
parts := strings.Split(sym.Name, ".")
|
||||||
|
lastPart := parts[len(parts)-1]
|
||||||
|
title := base(lastPart)
|
||||||
|
|
||||||
|
uriParts := strings.Split(sym.Location.Uri, "/")
|
||||||
|
lastTwoParts := uriParts[len(uriParts)-2:]
|
||||||
|
joined := strings.Join(lastTwoParts, "/")
|
||||||
|
title += muted(fmt.Sprintf(" %s", joined))
|
||||||
|
|
||||||
|
start := int(sym.Location.Range.Start.Line)
|
||||||
|
end := int(sym.Location.Range.End.Line)
|
||||||
|
title += muted(fmt.Sprintf(":L%d-%d", start, end))
|
||||||
|
|
||||||
|
value := fmt.Sprintf("%s?start=%d&end=%d", sym.Location.Uri, start, end)
|
||||||
|
|
||||||
|
item := dialog.NewCompletionItem(dialog.CompletionItem{
|
||||||
|
Title: title,
|
||||||
|
Value: value,
|
||||||
|
ProviderID: cg.GetId(),
|
||||||
|
Raw: sym,
|
||||||
|
})
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSymbolsContextGroup(app *app.App) dialog.CompletionProvider {
|
||||||
|
return &symbolsContextGroup{
|
||||||
|
app: app,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
package chat
|
package chat
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/v2/spinner"
|
"github.com/charmbracelet/bubbles/v2/spinner"
|
||||||
@@ -12,10 +16,10 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/sst/opencode-sdk-go"
|
"github.com/sst/opencode-sdk-go"
|
||||||
"github.com/sst/opencode/internal/app"
|
"github.com/sst/opencode/internal/app"
|
||||||
|
"github.com/sst/opencode/internal/clipboard"
|
||||||
"github.com/sst/opencode/internal/commands"
|
"github.com/sst/opencode/internal/commands"
|
||||||
"github.com/sst/opencode/internal/components/dialog"
|
"github.com/sst/opencode/internal/components/dialog"
|
||||||
"github.com/sst/opencode/internal/components/textarea"
|
"github.com/sst/opencode/internal/components/textarea"
|
||||||
"github.com/sst/opencode/internal/image"
|
|
||||||
"github.com/sst/opencode/internal/styles"
|
"github.com/sst/opencode/internal/styles"
|
||||||
"github.com/sst/opencode/internal/theme"
|
"github.com/sst/opencode/internal/theme"
|
||||||
"github.com/sst/opencode/internal/util"
|
"github.com/sst/opencode/internal/util"
|
||||||
@@ -27,6 +31,7 @@ type EditorComponent interface {
|
|||||||
Content(width int) string
|
Content(width int) string
|
||||||
Lines() int
|
Lines() int
|
||||||
Value() string
|
Value() string
|
||||||
|
Length() int
|
||||||
Focused() bool
|
Focused() bool
|
||||||
Focus() (tea.Model, tea.Cmd)
|
Focus() (tea.Model, tea.Cmd)
|
||||||
Blur()
|
Blur()
|
||||||
@@ -34,7 +39,9 @@ type EditorComponent interface {
|
|||||||
Clear() (tea.Model, tea.Cmd)
|
Clear() (tea.Model, tea.Cmd)
|
||||||
Paste() (tea.Model, tea.Cmd)
|
Paste() (tea.Model, tea.Cmd)
|
||||||
Newline() (tea.Model, tea.Cmd)
|
Newline() (tea.Model, tea.Cmd)
|
||||||
|
SetValue(value string)
|
||||||
SetInterruptKeyInDebounce(inDebounce bool)
|
SetInterruptKeyInDebounce(inDebounce bool)
|
||||||
|
SetExitKeyInDebounce(inDebounce bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
type editorComponent struct {
|
type editorComponent struct {
|
||||||
@@ -42,6 +49,7 @@ type editorComponent struct {
|
|||||||
textarea textarea.Model
|
textarea textarea.Model
|
||||||
spinner spinner.Model
|
spinner spinner.Model
|
||||||
interruptKeyInDebounce bool
|
interruptKeyInDebounce bool
|
||||||
|
exitKeyInDebounce bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *editorComponent) Init() tea.Cmd {
|
func (m *editorComponent) Init() tea.Cmd {
|
||||||
@@ -63,14 +71,80 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
return m, tea.Batch(cmds...)
|
return m, tea.Batch(cmds...)
|
||||||
}
|
}
|
||||||
|
case tea.PasteMsg:
|
||||||
|
text := string(msg)
|
||||||
|
text = strings.ReplaceAll(text, "\\", "")
|
||||||
|
text, err := strconv.Unquote(`"` + text + `"`)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to unquote text", "error", err)
|
||||||
|
m.textarea.InsertRunesFromUserInput([]rune(msg))
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(text); err != nil {
|
||||||
|
slog.Error("Failed to paste file", "error", err)
|
||||||
|
m.textarea.InsertRunesFromUserInput([]rune(msg))
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath := text
|
||||||
|
ext := strings.ToLower(filepath.Ext(filePath))
|
||||||
|
|
||||||
|
mediaType := ""
|
||||||
|
switch ext {
|
||||||
|
case ".jpg":
|
||||||
|
mediaType = "image/jpeg"
|
||||||
|
case ".png", ".jpeg", ".gif", ".webp":
|
||||||
|
mediaType = "image/" + ext[1:]
|
||||||
|
case ".pdf":
|
||||||
|
mediaType = "application/pdf"
|
||||||
|
default:
|
||||||
|
attachment := &textarea.Attachment{
|
||||||
|
ID: uuid.NewString(),
|
||||||
|
Display: "@" + filePath,
|
||||||
|
URL: fmt.Sprintf("file://./%s", filePath),
|
||||||
|
Filename: filePath,
|
||||||
|
MediaType: "text/plain",
|
||||||
|
}
|
||||||
|
m.textarea.InsertAttachment(attachment)
|
||||||
|
m.textarea.InsertString(" ")
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fileBytes, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to read file", "error", err)
|
||||||
|
m.textarea.InsertRunesFromUserInput([]rune(msg))
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
base64EncodedFile := base64.StdEncoding.EncodeToString(fileBytes)
|
||||||
|
url := fmt.Sprintf("data:%s;base64,%s", mediaType, base64EncodedFile)
|
||||||
|
attachmentCount := len(m.textarea.GetAttachments())
|
||||||
|
attachmentIndex := attachmentCount + 1
|
||||||
|
label := "File"
|
||||||
|
if strings.HasPrefix(mediaType, "image/") {
|
||||||
|
label = "Image"
|
||||||
|
}
|
||||||
|
|
||||||
|
attachment := &textarea.Attachment{
|
||||||
|
ID: uuid.NewString(),
|
||||||
|
MediaType: mediaType,
|
||||||
|
Display: fmt.Sprintf("[%s #%d]", label, attachmentIndex),
|
||||||
|
URL: url,
|
||||||
|
Filename: filePath,
|
||||||
|
}
|
||||||
|
m.textarea.InsertAttachment(attachment)
|
||||||
|
m.textarea.InsertString(" ")
|
||||||
|
case tea.ClipboardMsg:
|
||||||
|
text := string(msg)
|
||||||
|
m.textarea.InsertRunesFromUserInput([]rune(text))
|
||||||
case dialog.ThemeSelectedMsg:
|
case dialog.ThemeSelectedMsg:
|
||||||
m.textarea = createTextArea(&m.textarea)
|
m.textarea = updateTextareaStyles(m.textarea)
|
||||||
m.spinner = createSpinner()
|
m.spinner = createSpinner()
|
||||||
return m, tea.Batch(m.spinner.Tick, m.textarea.Focus())
|
return m, tea.Batch(m.spinner.Tick, m.textarea.Focus())
|
||||||
case dialog.CompletionSelectedMsg:
|
case dialog.CompletionSelectedMsg:
|
||||||
switch msg.ProviderID {
|
switch msg.Item.GetProviderID() {
|
||||||
case "commands":
|
case "commands":
|
||||||
commandName := strings.TrimPrefix(msg.CompletionValue, "/")
|
commandName := strings.TrimPrefix(msg.Item.GetValue(), "/")
|
||||||
updated, cmd := m.Clear()
|
updated, cmd := m.Clear()
|
||||||
m = updated.(*editorComponent)
|
m = updated.(*editorComponent)
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
@@ -80,7 +154,7 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
atIndex := m.textarea.LastRuneIndex('@')
|
atIndex := m.textarea.LastRuneIndex('@')
|
||||||
if atIndex == -1 {
|
if atIndex == -1 {
|
||||||
// Should not happen, but as a fallback, just insert.
|
// Should not happen, but as a fallback, just insert.
|
||||||
m.textarea.InsertString(msg.CompletionValue + " ")
|
m.textarea.InsertString(msg.Item.GetValue() + " ")
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +165,7 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
|
|
||||||
// Now, insert the attachment at the position where the '@' was.
|
// Now, insert the attachment at the position where the '@' was.
|
||||||
// The cursor is now at `atIndex` after the replacement.
|
// The cursor is now at `atIndex` after the replacement.
|
||||||
filePath := msg.CompletionValue
|
filePath := msg.Item.GetValue()
|
||||||
extension := filepath.Ext(filePath)
|
extension := filepath.Ext(filePath)
|
||||||
mediaType := ""
|
mediaType := ""
|
||||||
switch extension {
|
switch extension {
|
||||||
@@ -107,22 +181,39 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
attachment := &textarea.Attachment{
|
attachment := &textarea.Attachment{
|
||||||
ID: uuid.NewString(),
|
ID: uuid.NewString(),
|
||||||
Display: "@" + filePath,
|
Display: "@" + filePath,
|
||||||
URL: fmt.Sprintf("file://./%s", filePath),
|
URL: fmt.Sprintf("file://./%s", url.PathEscape(filePath)),
|
||||||
Filename: filePath,
|
Filename: filePath,
|
||||||
MediaType: mediaType,
|
MediaType: mediaType,
|
||||||
}
|
}
|
||||||
m.textarea.InsertAttachment(attachment)
|
m.textarea.InsertAttachment(attachment)
|
||||||
m.textarea.InsertString(" ")
|
m.textarea.InsertString(" ")
|
||||||
return m, nil
|
return m, nil
|
||||||
default:
|
case "symbols":
|
||||||
existingValue := m.textarea.Value()
|
atIndex := m.textarea.LastRuneIndex('@')
|
||||||
lastSpaceIndex := strings.LastIndex(existingValue, " ")
|
if atIndex == -1 {
|
||||||
if lastSpaceIndex == -1 {
|
// Should not happen, but as a fallback, just insert.
|
||||||
m.textarea.SetValue(msg.CompletionValue + " ")
|
m.textarea.InsertString(msg.Item.GetValue() + " ")
|
||||||
} else {
|
return m, nil
|
||||||
modifiedValue := existingValue[:lastSpaceIndex+1] + msg.CompletionValue
|
|
||||||
m.textarea.SetValue(modifiedValue + " ")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cursorCol := m.textarea.CursorColumn()
|
||||||
|
m.textarea.ReplaceRange(atIndex, cursorCol, "")
|
||||||
|
|
||||||
|
symbol := msg.Item.GetRaw().(opencode.Symbol)
|
||||||
|
parts := strings.Split(symbol.Name, ".")
|
||||||
|
lastPart := parts[len(parts)-1]
|
||||||
|
attachment := &textarea.Attachment{
|
||||||
|
ID: uuid.NewString(),
|
||||||
|
Display: "@" + lastPart,
|
||||||
|
URL: msg.Item.GetValue(),
|
||||||
|
Filename: lastPart,
|
||||||
|
MediaType: "text/plain",
|
||||||
|
}
|
||||||
|
m.textarea.InsertAttachment(attachment)
|
||||||
|
m.textarea.InsertString(" ")
|
||||||
|
return m, nil
|
||||||
|
default:
|
||||||
|
slog.Debug("Unknown provider", "provider", msg.Item.GetProviderID())
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,20 +242,27 @@ func (m *editorComponent) Content(width int) string {
|
|||||||
prompt,
|
prompt,
|
||||||
m.textarea.View(),
|
m.textarea.View(),
|
||||||
)
|
)
|
||||||
|
borderForeground := t.Border()
|
||||||
|
if m.app.IsLeaderSequence {
|
||||||
|
borderForeground = t.Accent()
|
||||||
|
}
|
||||||
textarea = styles.NewStyle().
|
textarea = styles.NewStyle().
|
||||||
Background(t.BackgroundElement()).
|
Background(t.BackgroundElement()).
|
||||||
Width(width).
|
Width(width).
|
||||||
PaddingTop(1).
|
PaddingTop(1).
|
||||||
PaddingBottom(1).
|
PaddingBottom(1).
|
||||||
BorderStyle(lipgloss.ThickBorder()).
|
BorderStyle(lipgloss.ThickBorder()).
|
||||||
BorderForeground(t.Border()).
|
BorderForeground(borderForeground).
|
||||||
BorderBackground(t.Background()).
|
BorderBackground(t.Background()).
|
||||||
BorderLeft(true).
|
BorderLeft(true).
|
||||||
BorderRight(true).
|
BorderRight(true).
|
||||||
Render(textarea)
|
Render(textarea)
|
||||||
|
|
||||||
hint := base(m.getSubmitKeyText()) + muted(" send ")
|
hint := base(m.getSubmitKeyText()) + muted(" send ")
|
||||||
if m.app.IsBusy() {
|
if m.exitKeyInDebounce {
|
||||||
|
keyText := m.getExitKeyText()
|
||||||
|
hint = base(keyText+" again") + muted(" to exit")
|
||||||
|
} else if m.app.IsBusy() {
|
||||||
keyText := m.getInterruptKeyText()
|
keyText := m.getInterruptKeyText()
|
||||||
if m.interruptKeyInDebounce {
|
if m.interruptKeyInDebounce {
|
||||||
hint = muted(
|
hint = muted(
|
||||||
@@ -230,6 +328,10 @@ func (m *editorComponent) Value() string {
|
|||||||
return m.textarea.Value()
|
return m.textarea.Value()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *editorComponent) Length() int {
|
||||||
|
return m.textarea.Length()
|
||||||
|
}
|
||||||
|
|
||||||
func (m *editorComponent) Submit() (tea.Model, tea.Cmd) {
|
func (m *editorComponent) Submit() (tea.Model, tea.Cmd) {
|
||||||
value := strings.TrimSpace(m.Value())
|
value := strings.TrimSpace(m.Value())
|
||||||
if value == "" {
|
if value == "" {
|
||||||
@@ -269,24 +371,31 @@ func (m *editorComponent) Clear() (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *editorComponent) Paste() (tea.Model, tea.Cmd) {
|
func (m *editorComponent) Paste() (tea.Model, tea.Cmd) {
|
||||||
_, text, err := image.GetImageFromClipboard()
|
imageBytes := clipboard.Read(clipboard.FmtImage)
|
||||||
if err != nil {
|
if imageBytes != nil {
|
||||||
slog.Error(err.Error())
|
attachmentCount := len(m.textarea.GetAttachments())
|
||||||
|
attachmentIndex := attachmentCount + 1
|
||||||
|
base64EncodedFile := base64.StdEncoding.EncodeToString(imageBytes)
|
||||||
|
attachment := &textarea.Attachment{
|
||||||
|
ID: uuid.NewString(),
|
||||||
|
MediaType: "image/png",
|
||||||
|
Display: fmt.Sprintf("[Image #%d]", attachmentIndex),
|
||||||
|
Filename: fmt.Sprintf("image-%d.png", attachmentIndex),
|
||||||
|
URL: fmt.Sprintf("data:image/png;base64,%s", base64EncodedFile),
|
||||||
|
}
|
||||||
|
m.textarea.InsertAttachment(attachment)
|
||||||
|
m.textarea.InsertString(" ")
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
// if len(imageBytes) != 0 {
|
|
||||||
// attachmentName := fmt.Sprintf("clipboard-image-%d", len(m.attachments))
|
textBytes := clipboard.Read(clipboard.FmtText)
|
||||||
// attachment := app.Attachment{
|
if textBytes != nil {
|
||||||
// FilePath: attachmentName,
|
m.textarea.InsertRunesFromUserInput([]rune(string(textBytes)))
|
||||||
// FileName: attachmentName,
|
return m, nil
|
||||||
// Content: imageBytes,
|
}
|
||||||
// MimeType: "image/png",
|
|
||||||
// }
|
// fallback to reading the clipboard using OSC52
|
||||||
// m.attachments = append(m.attachments, attachment)
|
return m, tea.ReadClipboard
|
||||||
// } else {
|
|
||||||
m.textarea.InsertString(text)
|
|
||||||
// }
|
|
||||||
return m, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *editorComponent) Newline() (tea.Model, tea.Cmd) {
|
func (m *editorComponent) Newline() (tea.Model, tea.Cmd) {
|
||||||
@@ -298,6 +407,14 @@ func (m *editorComponent) SetInterruptKeyInDebounce(inDebounce bool) {
|
|||||||
m.interruptKeyInDebounce = inDebounce
|
m.interruptKeyInDebounce = inDebounce
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *editorComponent) SetValue(value string) {
|
||||||
|
m.textarea.SetValue(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *editorComponent) SetExitKeyInDebounce(inDebounce bool) {
|
||||||
|
m.exitKeyInDebounce = inDebounce
|
||||||
|
}
|
||||||
|
|
||||||
func (m *editorComponent) getInterruptKeyText() string {
|
func (m *editorComponent) getInterruptKeyText() string {
|
||||||
return m.app.Commands[commands.SessionInterruptCommand].Keys()[0]
|
return m.app.Commands[commands.SessionInterruptCommand].Keys()[0]
|
||||||
}
|
}
|
||||||
@@ -306,14 +423,16 @@ func (m *editorComponent) getSubmitKeyText() string {
|
|||||||
return m.app.Commands[commands.InputSubmitCommand].Keys()[0]
|
return m.app.Commands[commands.InputSubmitCommand].Keys()[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
func createTextArea(existing *textarea.Model) textarea.Model {
|
func (m *editorComponent) getExitKeyText() string {
|
||||||
|
return m.app.Commands[commands.AppExitCommand].Keys()[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateTextareaStyles(ta textarea.Model) textarea.Model {
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
bgColor := t.BackgroundElement()
|
bgColor := t.BackgroundElement()
|
||||||
textColor := t.Text()
|
textColor := t.Text()
|
||||||
textMutedColor := t.TextMuted()
|
textMutedColor := t.TextMuted()
|
||||||
|
|
||||||
ta := textarea.New()
|
|
||||||
|
|
||||||
ta.Styles.Blurred.Base = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss()
|
ta.Styles.Blurred.Base = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss()
|
||||||
ta.Styles.Blurred.CursorLine = styles.NewStyle().Background(bgColor).Lipgloss()
|
ta.Styles.Blurred.CursorLine = styles.NewStyle().Background(bgColor).Lipgloss()
|
||||||
ta.Styles.Blurred.Placeholder = styles.NewStyle().
|
ta.Styles.Blurred.Placeholder = styles.NewStyle().
|
||||||
@@ -337,17 +456,6 @@ func createTextArea(existing *textarea.Model) textarea.Model {
|
|||||||
Background(t.Secondary()).
|
Background(t.Secondary()).
|
||||||
Lipgloss()
|
Lipgloss()
|
||||||
ta.Styles.Cursor.Color = t.Primary()
|
ta.Styles.Cursor.Color = t.Primary()
|
||||||
|
|
||||||
ta.Prompt = " "
|
|
||||||
ta.ShowLineNumbers = false
|
|
||||||
ta.CharLimit = -1
|
|
||||||
|
|
||||||
if existing != nil {
|
|
||||||
ta.SetValue(existing.Value())
|
|
||||||
// ta.SetWidth(existing.Width())
|
|
||||||
ta.SetHeight(existing.Height())
|
|
||||||
}
|
|
||||||
|
|
||||||
return ta
|
return ta
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,12 +475,19 @@ func createSpinner() spinner.Model {
|
|||||||
|
|
||||||
func NewEditorComponent(app *app.App) EditorComponent {
|
func NewEditorComponent(app *app.App) EditorComponent {
|
||||||
s := createSpinner()
|
s := createSpinner()
|
||||||
ta := createTextArea(nil)
|
|
||||||
|
|
||||||
return &editorComponent{
|
ta := textarea.New()
|
||||||
|
ta.Prompt = " "
|
||||||
|
ta.ShowLineNumbers = false
|
||||||
|
ta.CharLimit = -1
|
||||||
|
ta = updateTextareaStyles(ta)
|
||||||
|
|
||||||
|
m := &editorComponent{
|
||||||
app: app,
|
app: app,
|
||||||
textarea: ta,
|
textarea: ta,
|
||||||
spinner: s,
|
spinner: s,
|
||||||
interruptKeyInDebounce: false,
|
interruptKeyInDebounce: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return m
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,7 +134,6 @@ func renderContentBlock(
|
|||||||
style := styles.NewStyle().
|
style := styles.NewStyle().
|
||||||
Foreground(renderer.textColor).
|
Foreground(renderer.textColor).
|
||||||
Background(t.BackgroundPanel()).
|
Background(t.BackgroundPanel()).
|
||||||
Width(width).
|
|
||||||
PaddingTop(renderer.paddingTop).
|
PaddingTop(renderer.paddingTop).
|
||||||
PaddingBottom(renderer.paddingBottom).
|
PaddingBottom(renderer.paddingBottom).
|
||||||
PaddingLeft(renderer.paddingLeft).
|
PaddingLeft(renderer.paddingLeft).
|
||||||
@@ -232,16 +231,15 @@ func renderText(
|
|||||||
if highlight {
|
if highlight {
|
||||||
backgroundColor = t.BackgroundElement()
|
backgroundColor = t.BackgroundElement()
|
||||||
}
|
}
|
||||||
messageStyle := styles.NewStyle().Background(backgroundColor)
|
var content string
|
||||||
content := messageStyle.Render(text)
|
|
||||||
|
|
||||||
switch casted := message.(type) {
|
switch casted := message.(type) {
|
||||||
case opencode.AssistantMessage:
|
case opencode.AssistantMessage:
|
||||||
ts = time.UnixMilli(int64(casted.Time.Created))
|
ts = time.UnixMilli(int64(casted.Time.Created))
|
||||||
content = util.ToMarkdown(text, width, backgroundColor)
|
content = util.ToMarkdown(text, width, backgroundColor)
|
||||||
case opencode.UserMessage:
|
case opencode.UserMessage:
|
||||||
ts = time.UnixMilli(int64(casted.Time.Created))
|
ts = time.UnixMilli(int64(casted.Time.Created))
|
||||||
messageStyle = messageStyle.Width(width - 6)
|
messageStyle := styles.NewStyle().Background(backgroundColor).Width(width - 6)
|
||||||
|
content = messageStyle.Render(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
timestamp := ts.
|
timestamp := ts.
|
||||||
@@ -307,8 +305,10 @@ func renderToolDetails(
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
if toolCall.State.Status == opencode.ToolPartStateStatusPending || toolCall.State.Status == opencode.ToolPartStateStatusRunning {
|
if toolCall.State.Status == opencode.ToolPartStateStatusPending ||
|
||||||
|
toolCall.State.Status == opencode.ToolPartStateStatusRunning {
|
||||||
title := renderToolTitle(toolCall, width)
|
title := renderToolTitle(toolCall, width)
|
||||||
|
title = styles.NewStyle().Width(width - 6).Render(title)
|
||||||
return renderContentBlock(app, title, highlight, width)
|
return renderContentBlock(app, title, highlight, width)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,6 +422,8 @@ func renderToolDetails(
|
|||||||
switch todo["status"] {
|
switch todo["status"] {
|
||||||
case "completed":
|
case "completed":
|
||||||
body += fmt.Sprintf("- [x] %s\n", content)
|
body += fmt.Sprintf("- [x] %s\n", content)
|
||||||
|
case "cancelled":
|
||||||
|
body += fmt.Sprintf("- [~] %s\n", content)
|
||||||
// case "in-progress":
|
// case "in-progress":
|
||||||
// body += fmt.Sprintf("- [ ] %s\n", content)
|
// body += fmt.Sprintf("- [ ] %s\n", content)
|
||||||
default:
|
default:
|
||||||
@@ -455,6 +457,7 @@ func renderToolDetails(
|
|||||||
}
|
}
|
||||||
body = *result
|
body = *result
|
||||||
body = util.TruncateHeight(body, 10)
|
body = util.TruncateHeight(body, 10)
|
||||||
|
body = styles.NewStyle().Width(width - 6).Render(body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -465,6 +468,7 @@ func renderToolDetails(
|
|||||||
|
|
||||||
if error != "" {
|
if error != "" {
|
||||||
body = styles.NewStyle().
|
body = styles.NewStyle().
|
||||||
|
Width(width - 6).
|
||||||
Foreground(t.Error()).
|
Foreground(t.Error()).
|
||||||
Background(backgroundColor).
|
Background(backgroundColor).
|
||||||
Render(error)
|
Render(error)
|
||||||
@@ -473,6 +477,7 @@ func renderToolDetails(
|
|||||||
if body == "" && error == "" && result != nil {
|
if body == "" && error == "" && result != nil {
|
||||||
body = *result
|
body = *result
|
||||||
body = util.TruncateHeight(body, 10)
|
body = util.TruncateHeight(body, 10)
|
||||||
|
body = styles.NewStyle().Width(width - 6).Render(body)
|
||||||
}
|
}
|
||||||
|
|
||||||
title := renderToolTitle(toolCall, width)
|
title := renderToolTitle(toolCall, width)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package chat
|
package chat
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/v2/viewport"
|
"github.com/charmbracelet/bubbles/v2/viewport"
|
||||||
@@ -320,12 +321,15 @@ func (m *messagesComponent) renderView(width int) {
|
|||||||
error = "Message output length exceeded"
|
error = "Message output length exceeded"
|
||||||
case opencode.ProviderAuthError:
|
case opencode.ProviderAuthError:
|
||||||
error = err.Data.Message
|
error = err.Data.Message
|
||||||
|
case opencode.MessageAbortedError:
|
||||||
|
error = "Request was aborted"
|
||||||
case opencode.UnknownError:
|
case opencode.UnknownError:
|
||||||
error = err.Data.Message
|
error = err.Data.Message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if error != "" {
|
if error != "" {
|
||||||
|
error = styles.NewStyle().Width(width - 6).Render(error)
|
||||||
error = renderContentBlock(
|
error = renderContentBlock(
|
||||||
m.app,
|
m.app,
|
||||||
error,
|
error,
|
||||||
@@ -368,11 +372,65 @@ func (m *messagesComponent) header(width int) string {
|
|||||||
headerLines,
|
headerLines,
|
||||||
util.ToMarkdown("# "+m.app.Session.Title, width-6, t.Background()),
|
util.ToMarkdown("# "+m.app.Session.Title, width-6, t.Background()),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
share := ""
|
||||||
if m.app.Session.Share.URL != "" {
|
if m.app.Session.Share.URL != "" {
|
||||||
headerLines = append(headerLines, muted(m.app.Session.Share.URL+" /unshare"))
|
share = muted(m.app.Session.Share.URL + " /unshare")
|
||||||
} else {
|
} else {
|
||||||
headerLines = append(headerLines, base("/share")+muted(" to create a shareable link"))
|
share = base("/share") + muted(" to create a shareable link")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sessionInfo := ""
|
||||||
|
tokens := float64(0)
|
||||||
|
cost := float64(0)
|
||||||
|
contextWindow := m.app.Model.Limit.Context
|
||||||
|
|
||||||
|
for _, message := range m.app.Messages {
|
||||||
|
if assistant, ok := message.(opencode.AssistantMessage); ok {
|
||||||
|
cost += assistant.Cost
|
||||||
|
usage := assistant.Tokens
|
||||||
|
if usage.Output > 0 {
|
||||||
|
if assistant.Summary {
|
||||||
|
tokens = usage.Output
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tokens = (usage.Input +
|
||||||
|
usage.Cache.Write +
|
||||||
|
usage.Cache.Read +
|
||||||
|
usage.Output +
|
||||||
|
usage.Reasoning)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if current model is a subscription model (cost is 0 for both input and output)
|
||||||
|
isSubscriptionModel := m.app.Model != nil &&
|
||||||
|
m.app.Model.Cost.Input == 0 && m.app.Model.Cost.Output == 0
|
||||||
|
|
||||||
|
sessionInfo = styles.NewStyle().
|
||||||
|
Foreground(t.TextMuted()).
|
||||||
|
Background(t.Background()).
|
||||||
|
Render(formatTokensAndCost(tokens, contextWindow, cost, isSubscriptionModel))
|
||||||
|
|
||||||
|
background := t.Background()
|
||||||
|
share = layout.Render(
|
||||||
|
layout.FlexOptions{
|
||||||
|
Background: &background,
|
||||||
|
Direction: layout.Row,
|
||||||
|
Justify: layout.JustifySpaceBetween,
|
||||||
|
Align: layout.AlignStretch,
|
||||||
|
Width: width - 6,
|
||||||
|
},
|
||||||
|
layout.FlexItem{
|
||||||
|
View: share,
|
||||||
|
},
|
||||||
|
layout.FlexItem{
|
||||||
|
View: sessionInfo,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
headerLines = append(headerLines, share)
|
||||||
|
|
||||||
header := strings.Join(headerLines, "\n")
|
header := strings.Join(headerLines, "\n")
|
||||||
|
|
||||||
header = styles.NewStyle().
|
header = styles.NewStyle().
|
||||||
@@ -390,6 +448,50 @@ func (m *messagesComponent) header(width int) string {
|
|||||||
return "\n" + header + "\n"
|
return "\n" + header + "\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func formatTokensAndCost(
|
||||||
|
tokens float64,
|
||||||
|
contextWindow float64,
|
||||||
|
cost float64,
|
||||||
|
isSubscriptionModel bool,
|
||||||
|
) string {
|
||||||
|
// Format tokens in human-readable format (e.g., 110K, 1.2M)
|
||||||
|
var formattedTokens string
|
||||||
|
switch {
|
||||||
|
case tokens >= 1_000_000:
|
||||||
|
formattedTokens = fmt.Sprintf("%.1fM", float64(tokens)/1_000_000)
|
||||||
|
case tokens >= 1_000:
|
||||||
|
formattedTokens = fmt.Sprintf("%.1fK", float64(tokens)/1_000)
|
||||||
|
default:
|
||||||
|
formattedTokens = fmt.Sprintf("%d", int(tokens))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove .0 suffix if present
|
||||||
|
if strings.HasSuffix(formattedTokens, ".0K") {
|
||||||
|
formattedTokens = strings.Replace(formattedTokens, ".0K", "K", 1)
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(formattedTokens, ".0M") {
|
||||||
|
formattedTokens = strings.Replace(formattedTokens, ".0M", "M", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
percentage := (float64(tokens) / float64(contextWindow)) * 100
|
||||||
|
|
||||||
|
if isSubscriptionModel {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"%s/%d%%",
|
||||||
|
formattedTokens,
|
||||||
|
int(percentage),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
formattedCost := fmt.Sprintf("$%.2f", cost)
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"%s/%d%% (%s)",
|
||||||
|
formattedTokens,
|
||||||
|
int(percentage),
|
||||||
|
formattedCost,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func (m *messagesComponent) View(width, height int) string {
|
func (m *messagesComponent) View(width, height int) string {
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
if m.rendering {
|
if m.rendering {
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ func (c *commandsComponent) View() string {
|
|||||||
var untriggeredCommands []commands.Command
|
var untriggeredCommands []commands.Command
|
||||||
|
|
||||||
for _, cmd := range c.app.Commands.Sorted() {
|
for _, cmd := range c.app.Commands.Sorted() {
|
||||||
if c.showAll || cmd.Trigger != "" {
|
if c.showAll || cmd.HasTrigger() {
|
||||||
if cmd.Trigger != "" {
|
if cmd.HasTrigger() {
|
||||||
triggeredCommands = append(triggeredCommands, cmd)
|
triggeredCommands = append(triggeredCommands, cmd)
|
||||||
} else if c.showAll {
|
} else if c.showAll {
|
||||||
untriggeredCommands = append(untriggeredCommands, cmd)
|
untriggeredCommands = append(untriggeredCommands, cmd)
|
||||||
@@ -97,8 +97,8 @@ func (c *commandsComponent) View() string {
|
|||||||
|
|
||||||
for _, cmd := range commandsToShow {
|
for _, cmd := range commandsToShow {
|
||||||
trigger := ""
|
trigger := ""
|
||||||
if cmd.Trigger != "" {
|
if cmd.HasTrigger() {
|
||||||
trigger = "/" + cmd.Trigger
|
trigger = "/" + cmd.PrimaryTrigger()
|
||||||
} else {
|
} else {
|
||||||
trigger = string(cmd.Name)
|
trigger = string(cmd.Name)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,15 @@ package dialog
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/v2/key"
|
"github.com/charmbracelet/bubbles/v2/key"
|
||||||
"github.com/charmbracelet/bubbles/v2/textarea"
|
"github.com/charmbracelet/bubbles/v2/textarea"
|
||||||
tea "github.com/charmbracelet/bubbletea/v2"
|
tea "github.com/charmbracelet/bubbletea/v2"
|
||||||
"github.com/charmbracelet/lipgloss/v2"
|
"github.com/charmbracelet/lipgloss/v2"
|
||||||
|
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||||
|
"github.com/muesli/reflow/truncate"
|
||||||
"github.com/sst/opencode/internal/components/list"
|
"github.com/sst/opencode/internal/components/list"
|
||||||
"github.com/sst/opencode/internal/styles"
|
"github.com/sst/opencode/internal/styles"
|
||||||
"github.com/sst/opencode/internal/theme"
|
"github.com/sst/opencode/internal/theme"
|
||||||
@@ -14,32 +18,35 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type CompletionItem struct {
|
type CompletionItem struct {
|
||||||
Title string
|
Title string
|
||||||
Value string
|
Value string
|
||||||
|
ProviderID string
|
||||||
|
Raw any
|
||||||
}
|
}
|
||||||
|
|
||||||
type CompletionItemI interface {
|
type CompletionItemI interface {
|
||||||
list.ListItem
|
list.ListItem
|
||||||
GetValue() string
|
GetValue() string
|
||||||
DisplayValue() string
|
DisplayValue() string
|
||||||
|
GetProviderID() string
|
||||||
|
GetRaw() any
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ci *CompletionItem) Render(selected bool, width int) string {
|
func (ci *CompletionItem) Render(selected bool, width int) string {
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
baseStyle := styles.NewStyle().Foreground(t.Text())
|
baseStyle := styles.NewStyle().Foreground(t.Text())
|
||||||
|
|
||||||
|
truncatedStr := truncate.String(string(ci.DisplayValue()), uint(width-4))
|
||||||
|
|
||||||
itemStyle := baseStyle.
|
itemStyle := baseStyle.
|
||||||
Background(t.BackgroundElement()).
|
Background(t.BackgroundElement()).
|
||||||
Width(width).
|
|
||||||
Padding(0, 1)
|
Padding(0, 1)
|
||||||
|
|
||||||
if selected {
|
if selected {
|
||||||
itemStyle = itemStyle.Foreground(t.Primary())
|
itemStyle = itemStyle.Foreground(t.Primary())
|
||||||
}
|
}
|
||||||
|
|
||||||
title := itemStyle.Render(
|
title := itemStyle.Render(truncatedStr)
|
||||||
ci.DisplayValue(),
|
|
||||||
)
|
|
||||||
return title
|
return title
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,6 +58,14 @@ func (ci *CompletionItem) GetValue() string {
|
|||||||
return ci.Value
|
return ci.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ci *CompletionItem) GetProviderID() string {
|
||||||
|
return ci.ProviderID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ci *CompletionItem) GetRaw() any {
|
||||||
|
return ci.Raw
|
||||||
|
}
|
||||||
|
|
||||||
func NewCompletionItem(completionItem CompletionItem) CompletionItemI {
|
func NewCompletionItem(completionItem CompletionItem) CompletionItemI {
|
||||||
return &completionItem
|
return &completionItem
|
||||||
}
|
}
|
||||||
@@ -62,9 +77,8 @@ type CompletionProvider interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CompletionSelectedMsg struct {
|
type CompletionSelectedMsg struct {
|
||||||
SearchString string
|
Item CompletionItemI
|
||||||
CompletionValue string
|
SearchString string
|
||||||
ProviderID string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type CompletionDialogCompleteItemMsg struct {
|
type CompletionDialogCompleteItemMsg struct {
|
||||||
@@ -82,11 +96,12 @@ type CompletionDialog interface {
|
|||||||
|
|
||||||
type completionDialogComponent struct {
|
type completionDialogComponent struct {
|
||||||
query string
|
query string
|
||||||
completionProvider CompletionProvider
|
providers []CompletionProvider
|
||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
pseudoSearchTextArea textarea.Model
|
pseudoSearchTextArea textarea.Model
|
||||||
list list.List[CompletionItemI]
|
list list.List[CompletionItemI]
|
||||||
|
trigger string
|
||||||
}
|
}
|
||||||
|
|
||||||
type completionDialogKeyMap struct {
|
type completionDialogKeyMap struct {
|
||||||
@@ -107,6 +122,52 @@ func (c *completionDialogComponent) Init() tea.Cmd {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *completionDialogComponent) getAllCompletions(query string) tea.Cmd {
|
||||||
|
return func() tea.Msg {
|
||||||
|
allItems := make([]CompletionItemI, 0)
|
||||||
|
|
||||||
|
// Collect results from all providers
|
||||||
|
for _, provider := range c.providers {
|
||||||
|
items, err := provider.GetChildEntries(query)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(
|
||||||
|
"Failed to get completion items",
|
||||||
|
"provider",
|
||||||
|
provider.GetId(),
|
||||||
|
"error",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
allItems = append(allItems, items...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's a query, use fuzzy ranking to sort results
|
||||||
|
if query != "" && len(allItems) > 0 {
|
||||||
|
// Create a slice of display values for fuzzy matching
|
||||||
|
displayValues := make([]string, len(allItems))
|
||||||
|
for i, item := range allItems {
|
||||||
|
displayValues[i] = item.DisplayValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get fuzzy matches with ranking
|
||||||
|
matches := fuzzy.RankFindFold(query, displayValues)
|
||||||
|
|
||||||
|
// Sort by score (best matches first)
|
||||||
|
sort.Sort(matches)
|
||||||
|
|
||||||
|
// Reorder items based on fuzzy ranking
|
||||||
|
rankedItems := make([]CompletionItemI, 0, len(matches))
|
||||||
|
for _, match := range matches {
|
||||||
|
rankedItems = append(rankedItems, allItems[match.OriginalIndex])
|
||||||
|
}
|
||||||
|
|
||||||
|
return rankedItems
|
||||||
|
}
|
||||||
|
|
||||||
|
return allItems
|
||||||
|
}
|
||||||
|
}
|
||||||
func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
var cmds []tea.Cmd
|
var cmds []tea.Cmd
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
@@ -119,19 +180,12 @@ func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
c.pseudoSearchTextArea, cmd = c.pseudoSearchTextArea.Update(msg)
|
c.pseudoSearchTextArea, cmd = c.pseudoSearchTextArea.Update(msg)
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
|
|
||||||
var query string
|
fullValue := c.pseudoSearchTextArea.Value()
|
||||||
query = c.pseudoSearchTextArea.Value()
|
query := strings.TrimPrefix(fullValue, c.trigger)
|
||||||
|
|
||||||
if query != c.query {
|
if query != c.query {
|
||||||
c.query = query
|
c.query = query
|
||||||
cmd = func() tea.Msg {
|
cmds = append(cmds, c.getAllCompletions(query))
|
||||||
items, err := c.completionProvider.GetChildEntries(query)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to get completion items", "error", err)
|
|
||||||
}
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
cmds = append(cmds, cmd)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
u, cmd := c.list.Update(msg)
|
u, cmd := c.list.Update(msg)
|
||||||
@@ -147,22 +201,18 @@ func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
return c, c.complete(item)
|
return c, c.complete(item)
|
||||||
case key.Matches(msg, completionDialogKeys.Cancel):
|
case key.Matches(msg, completionDialogKeys.Cancel):
|
||||||
// Only close on backspace when there are no characters left
|
value := c.pseudoSearchTextArea.Value()
|
||||||
if msg.String() != "backspace" || len(c.pseudoSearchTextArea.Value()) <= 0 {
|
width := lipgloss.Width(value)
|
||||||
|
triggerWidth := lipgloss.Width(c.trigger)
|
||||||
|
// Only close on backspace when there are no characters left, unless we're back to just the trigger
|
||||||
|
if msg.String() != "backspace" || (width <= triggerWidth && value != c.trigger) {
|
||||||
return c, c.close()
|
return c, c.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return c, tea.Batch(cmds...)
|
return c, tea.Batch(cmds...)
|
||||||
} else {
|
} else {
|
||||||
cmd := func() tea.Msg {
|
cmds = append(cmds, c.getAllCompletions(""))
|
||||||
items, err := c.completionProvider.GetChildEntries("")
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to get completion items", "error", err)
|
|
||||||
}
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
cmds = append(cmds, cmd)
|
|
||||||
cmds = append(cmds, c.pseudoSearchTextArea.Focus())
|
cmds = append(cmds, c.pseudoSearchTextArea.Focus())
|
||||||
return c, tea.Batch(cmds...)
|
return c, tea.Batch(cmds...)
|
||||||
}
|
}
|
||||||
@@ -174,19 +224,7 @@ func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
func (c *completionDialogComponent) View() string {
|
func (c *completionDialogComponent) View() string {
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
baseStyle := styles.NewStyle().Foreground(t.Text())
|
baseStyle := styles.NewStyle().Foreground(t.Text())
|
||||||
|
c.list.SetMaxWidth(c.width)
|
||||||
maxWidth := 40
|
|
||||||
completions := c.list.GetItems()
|
|
||||||
|
|
||||||
for _, cmd := range completions {
|
|
||||||
title := cmd.DisplayValue()
|
|
||||||
width := lipgloss.Width(title)
|
|
||||||
if width > maxWidth-4 {
|
|
||||||
maxWidth = width + 4
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c.list.SetMaxWidth(maxWidth)
|
|
||||||
|
|
||||||
return baseStyle.
|
return baseStyle.
|
||||||
Padding(0, 0).
|
Padding(0, 0).
|
||||||
@@ -210,12 +248,10 @@ func (c *completionDialogComponent) IsEmpty() bool {
|
|||||||
|
|
||||||
func (c *completionDialogComponent) complete(item CompletionItemI) tea.Cmd {
|
func (c *completionDialogComponent) complete(item CompletionItemI) tea.Cmd {
|
||||||
value := c.pseudoSearchTextArea.Value()
|
value := c.pseudoSearchTextArea.Value()
|
||||||
|
|
||||||
return tea.Batch(
|
return tea.Batch(
|
||||||
util.CmdHandler(CompletionSelectedMsg{
|
util.CmdHandler(CompletionSelectedMsg{
|
||||||
SearchString: value,
|
SearchString: value,
|
||||||
CompletionValue: item.GetValue(),
|
Item: item,
|
||||||
ProviderID: c.completionProvider.GetId(),
|
|
||||||
}),
|
}),
|
||||||
c.close(),
|
c.close(),
|
||||||
)
|
)
|
||||||
@@ -227,28 +263,53 @@ func (c *completionDialogComponent) close() tea.Cmd {
|
|||||||
return util.CmdHandler(CompletionDialogCloseMsg{})
|
return util.CmdHandler(CompletionDialogCloseMsg{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCompletionDialogComponent(completionProvider CompletionProvider) CompletionDialog {
|
func NewCompletionDialogComponent(
|
||||||
|
trigger string,
|
||||||
|
providers ...CompletionProvider,
|
||||||
|
) CompletionDialog {
|
||||||
ti := textarea.New()
|
ti := textarea.New()
|
||||||
|
ti.SetValue(trigger)
|
||||||
|
|
||||||
|
// Use a generic empty message if we have multiple providers
|
||||||
|
emptyMessage := "no matching items"
|
||||||
|
if len(providers) == 1 {
|
||||||
|
emptyMessage = providers[0].GetEmptyMessage()
|
||||||
|
}
|
||||||
|
|
||||||
li := list.NewListComponent(
|
li := list.NewListComponent(
|
||||||
[]CompletionItemI{},
|
[]CompletionItemI{},
|
||||||
7,
|
7,
|
||||||
completionProvider.GetEmptyMessage(),
|
emptyMessage,
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
|
|
||||||
go func() {
|
c := &completionDialogComponent{
|
||||||
items, err := completionProvider.GetChildEntries("")
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to get completion items", "error", err)
|
|
||||||
}
|
|
||||||
li.SetItems(items)
|
|
||||||
}()
|
|
||||||
|
|
||||||
return &completionDialogComponent{
|
|
||||||
query: "",
|
query: "",
|
||||||
completionProvider: completionProvider,
|
providers: providers,
|
||||||
pseudoSearchTextArea: ti,
|
pseudoSearchTextArea: ti,
|
||||||
list: li,
|
list: li,
|
||||||
|
trigger: trigger,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load initial items from all providers
|
||||||
|
go func() {
|
||||||
|
allItems := make([]CompletionItemI, 0)
|
||||||
|
for _, provider := range providers {
|
||||||
|
items, err := provider.GetChildEntries("")
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(
|
||||||
|
"Failed to get completion items",
|
||||||
|
"provider",
|
||||||
|
provider.GetId(),
|
||||||
|
"error",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
allItems = append(allItems, items...)
|
||||||
|
}
|
||||||
|
li.SetItems(allItems)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return c
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
package status
|
package status
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea/v2"
|
tea "github.com/charmbracelet/bubbletea/v2"
|
||||||
"github.com/charmbracelet/lipgloss/v2"
|
"github.com/charmbracelet/lipgloss/v2"
|
||||||
"github.com/sst/opencode-sdk-go"
|
"github.com/charmbracelet/lipgloss/v2/compat"
|
||||||
"github.com/sst/opencode/internal/app"
|
"github.com/sst/opencode/internal/app"
|
||||||
|
"github.com/sst/opencode/internal/commands"
|
||||||
"github.com/sst/opencode/internal/styles"
|
"github.com/sst/opencode/internal/styles"
|
||||||
"github.com/sst/opencode/internal/theme"
|
"github.com/sst/opencode/internal/theme"
|
||||||
)
|
)
|
||||||
@@ -20,6 +21,7 @@ type StatusComponent interface {
|
|||||||
type statusComponent struct {
|
type statusComponent struct {
|
||||||
app *app.App
|
app *app.App
|
||||||
width int
|
width int
|
||||||
|
cwd string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m statusComponent) Init() tea.Cmd {
|
func (m statusComponent) Init() tea.Cmd {
|
||||||
@@ -53,38 +55,6 @@ func (m statusComponent) logo() string {
|
|||||||
Render(open + code + version)
|
Render(open + code + version)
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatTokensAndCost(tokens float64, contextWindow float64, cost float64) string {
|
|
||||||
// Format tokens in human-readable format (e.g., 110K, 1.2M)
|
|
||||||
var formattedTokens string
|
|
||||||
switch {
|
|
||||||
case tokens >= 1_000_000:
|
|
||||||
formattedTokens = fmt.Sprintf("%.1fM", float64(tokens)/1_000_000)
|
|
||||||
case tokens >= 1_000:
|
|
||||||
formattedTokens = fmt.Sprintf("%.1fK", float64(tokens)/1_000)
|
|
||||||
default:
|
|
||||||
formattedTokens = fmt.Sprintf("%d", int(tokens))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove .0 suffix if present
|
|
||||||
if strings.HasSuffix(formattedTokens, ".0K") {
|
|
||||||
formattedTokens = strings.Replace(formattedTokens, ".0K", "K", 1)
|
|
||||||
}
|
|
||||||
if strings.HasSuffix(formattedTokens, ".0M") {
|
|
||||||
formattedTokens = strings.Replace(formattedTokens, ".0M", "M", 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format cost with $ symbol and 2 decimal places
|
|
||||||
formattedCost := fmt.Sprintf("$%.2f", cost)
|
|
||||||
percentage := (float64(tokens) / float64(contextWindow)) * 100
|
|
||||||
|
|
||||||
return fmt.Sprintf(
|
|
||||||
"Context: %s (%d%%), Cost: %s",
|
|
||||||
formattedTokens,
|
|
||||||
int(percentage),
|
|
||||||
formattedCost,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m statusComponent) View() string {
|
func (m statusComponent) View() string {
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
logo := m.logo()
|
logo := m.logo()
|
||||||
@@ -93,48 +63,70 @@ func (m statusComponent) View() string {
|
|||||||
Foreground(t.TextMuted()).
|
Foreground(t.TextMuted()).
|
||||||
Background(t.BackgroundPanel()).
|
Background(t.BackgroundPanel()).
|
||||||
Padding(0, 1).
|
Padding(0, 1).
|
||||||
Render(m.app.Info.Path.Cwd)
|
Render(m.cwd)
|
||||||
|
|
||||||
sessionInfo := ""
|
var modeBackground compat.AdaptiveColor
|
||||||
if m.app.Session.ID != "" {
|
var modeForeground compat.AdaptiveColor
|
||||||
tokens := float64(0)
|
switch m.app.ModeIndex {
|
||||||
cost := float64(0)
|
case 0:
|
||||||
contextWindow := m.app.Model.Limit.Context
|
modeBackground = t.BackgroundElement()
|
||||||
|
modeForeground = t.TextMuted()
|
||||||
for _, message := range m.app.Messages {
|
case 1:
|
||||||
if assistant, ok := message.(opencode.AssistantMessage); ok {
|
modeBackground = t.Secondary()
|
||||||
cost += assistant.Cost
|
modeForeground = t.BackgroundPanel()
|
||||||
usage := assistant.Tokens
|
case 2:
|
||||||
if usage.Output > 0 {
|
modeBackground = t.Accent()
|
||||||
if assistant.Summary {
|
modeForeground = t.BackgroundPanel()
|
||||||
tokens = usage.Output
|
case 3:
|
||||||
continue
|
modeBackground = t.Success()
|
||||||
}
|
modeForeground = t.BackgroundPanel()
|
||||||
tokens = (usage.Input +
|
case 4:
|
||||||
usage.Cache.Write +
|
modeBackground = t.Warning()
|
||||||
usage.Cache.Read +
|
modeForeground = t.BackgroundPanel()
|
||||||
usage.Output +
|
case 5:
|
||||||
usage.Reasoning)
|
modeBackground = t.Primary()
|
||||||
}
|
modeForeground = t.BackgroundPanel()
|
||||||
}
|
case 6:
|
||||||
}
|
modeBackground = t.Error()
|
||||||
|
modeForeground = t.BackgroundPanel()
|
||||||
sessionInfo = styles.NewStyle().
|
default:
|
||||||
Foreground(t.TextMuted()).
|
modeBackground = t.Secondary()
|
||||||
Background(t.BackgroundElement()).
|
modeForeground = t.BackgroundPanel()
|
||||||
Padding(0, 1).
|
|
||||||
Render(formatTokensAndCost(tokens, contextWindow, cost))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// diagnostics := styles.Padded().Background(t.BackgroundElement()).Render(m.projectDiagnostics())
|
command := m.app.Commands[commands.SwitchModeCommand]
|
||||||
|
kb := command.Keybindings[0]
|
||||||
|
key := kb.Key
|
||||||
|
if kb.RequiresLeader {
|
||||||
|
key = m.app.Config.Keybinds.Leader + " " + kb.Key
|
||||||
|
}
|
||||||
|
|
||||||
|
modeStyle := styles.NewStyle().Background(modeBackground).Foreground(modeForeground)
|
||||||
|
modeNameStyle := modeStyle.Bold(true).Render
|
||||||
|
modeDescStyle := modeStyle.Render
|
||||||
|
mode := modeNameStyle(strings.ToUpper(m.app.Mode.Name)) + modeDescStyle(" MODE")
|
||||||
|
mode = modeStyle.
|
||||||
|
Padding(0, 1).
|
||||||
|
BorderLeft(true).
|
||||||
|
BorderStyle(lipgloss.ThickBorder()).
|
||||||
|
BorderForeground(modeBackground).
|
||||||
|
BorderBackground(t.BackgroundPanel()).
|
||||||
|
Render(mode)
|
||||||
|
|
||||||
|
mode = styles.NewStyle().
|
||||||
|
Faint(true).
|
||||||
|
Background(t.BackgroundPanel()).
|
||||||
|
Foreground(t.TextMuted()).
|
||||||
|
Render(key+" ") +
|
||||||
|
mode
|
||||||
|
|
||||||
space := max(
|
space := max(
|
||||||
0,
|
0,
|
||||||
m.width-lipgloss.Width(logo)-lipgloss.Width(cwd)-lipgloss.Width(sessionInfo),
|
m.width-lipgloss.Width(logo)-lipgloss.Width(cwd)-lipgloss.Width(mode),
|
||||||
)
|
)
|
||||||
spacer := styles.NewStyle().Background(t.BackgroundPanel()).Width(space).Render("")
|
spacer := styles.NewStyle().Background(t.BackgroundPanel()).Width(space).Render("")
|
||||||
|
|
||||||
status := logo + cwd + spacer + sessionInfo
|
status := logo + cwd + spacer + mode
|
||||||
|
|
||||||
blank := styles.NewStyle().Background(t.Background()).Width(m.width).Render("")
|
blank := styles.NewStyle().Background(t.Background()).Width(m.width).Render("")
|
||||||
return blank + "\n" + status
|
return blank + "\n" + status
|
||||||
@@ -145,5 +137,12 @@ func NewStatusCmp(app *app.App) StatusComponent {
|
|||||||
app: app,
|
app: app,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
homePath, err := os.UserHomeDir()
|
||||||
|
cwdPath := app.Info.Path.Cwd
|
||||||
|
if err == nil && homePath != "" && strings.HasPrefix(cwdPath, homePath) {
|
||||||
|
cwdPath = "~" + cwdPath[len(homePath):]
|
||||||
|
}
|
||||||
|
statusComponent.cwd = cwdPath
|
||||||
|
|
||||||
return statusComponent
|
return statusComponent
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
|
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"github.com/atotto/clipboard"
|
|
||||||
"github.com/charmbracelet/bubbles/v2/cursor"
|
"github.com/charmbracelet/bubbles/v2/cursor"
|
||||||
"github.com/charmbracelet/bubbles/v2/key"
|
"github.com/charmbracelet/bubbles/v2/key"
|
||||||
tea "github.com/charmbracelet/bubbletea/v2"
|
tea "github.com/charmbracelet/bubbletea/v2"
|
||||||
@@ -653,12 +652,12 @@ func (m *Model) SetValue(s string) {
|
|||||||
|
|
||||||
// InsertString inserts a string at the cursor position.
|
// InsertString inserts a string at the cursor position.
|
||||||
func (m *Model) InsertString(s string) {
|
func (m *Model) InsertString(s string) {
|
||||||
m.insertRunesFromUserInput([]rune(s))
|
m.InsertRunesFromUserInput([]rune(s))
|
||||||
}
|
}
|
||||||
|
|
||||||
// InsertRune inserts a rune at the cursor position.
|
// InsertRune inserts a rune at the cursor position.
|
||||||
func (m *Model) InsertRune(r rune) {
|
func (m *Model) InsertRune(r rune) {
|
||||||
m.insertRunesFromUserInput([]rune{r})
|
m.InsertRunesFromUserInput([]rune{r})
|
||||||
}
|
}
|
||||||
|
|
||||||
// InsertAttachment inserts an attachment at the cursor position.
|
// InsertAttachment inserts an attachment at the cursor position.
|
||||||
@@ -730,8 +729,8 @@ func (m Model) GetAttachments() []*Attachment {
|
|||||||
return attachments
|
return attachments
|
||||||
}
|
}
|
||||||
|
|
||||||
// insertRunesFromUserInput inserts runes at the current cursor position.
|
// InsertRunesFromUserInput inserts runes at the current cursor position.
|
||||||
func (m *Model) insertRunesFromUserInput(runes []rune) {
|
func (m *Model) InsertRunesFromUserInput(runes []rune) {
|
||||||
// Clean up any special characters in the input provided by the
|
// Clean up any special characters in the input provided by the
|
||||||
// clipboard. This avoids bugs due to e.g. tab characters and
|
// clipboard. This avoids bugs due to e.g. tab characters and
|
||||||
// whatnot.
|
// whatnot.
|
||||||
@@ -932,7 +931,6 @@ func (m *Model) mapVisualOffsetToSliceIndex(row int, charOffset int) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CursorDown moves the cursor down by one line.
|
// CursorDown moves the cursor down by one line.
|
||||||
// Returns whether or not the cursor blink should be reset.
|
|
||||||
func (m *Model) CursorDown() {
|
func (m *Model) CursorDown() {
|
||||||
li := m.LineInfo()
|
li := m.LineInfo()
|
||||||
charOffset := max(m.lastCharOffset, li.CharOffset)
|
charOffset := max(m.lastCharOffset, li.CharOffset)
|
||||||
@@ -941,11 +939,73 @@ func (m *Model) CursorDown() {
|
|||||||
if li.RowOffset+1 >= li.Height && m.row < len(m.value)-1 {
|
if li.RowOffset+1 >= li.Height && m.row < len(m.value)-1 {
|
||||||
// Move to the next model line
|
// Move to the next model line
|
||||||
m.row++
|
m.row++
|
||||||
m.col = m.mapVisualOffsetToSliceIndex(m.row, charOffset)
|
|
||||||
|
// We want to land on the first wrapped line of the new model line.
|
||||||
|
grid := m.memoizedWrap(m.value[m.row], m.width)
|
||||||
|
targetLineContent := grid[0]
|
||||||
|
|
||||||
|
// Find position within the first wrapped line.
|
||||||
|
offset := 0
|
||||||
|
colInLine := 0
|
||||||
|
for i, item := range targetLineContent {
|
||||||
|
var itemWidth int
|
||||||
|
switch v := item.(type) {
|
||||||
|
case rune:
|
||||||
|
itemWidth = rw.RuneWidth(v)
|
||||||
|
case *Attachment:
|
||||||
|
itemWidth = uniseg.StringWidth(v.Display)
|
||||||
|
}
|
||||||
|
if offset+itemWidth > charOffset {
|
||||||
|
// Decide whether to stick with the previous index or move to the current
|
||||||
|
// one based on which is closer to the target offset.
|
||||||
|
if (charOffset - offset) > ((offset + itemWidth) - charOffset) {
|
||||||
|
colInLine = i + 1
|
||||||
|
} else {
|
||||||
|
colInLine = i
|
||||||
|
}
|
||||||
|
goto foundNextLine
|
||||||
|
}
|
||||||
|
offset += itemWidth
|
||||||
|
}
|
||||||
|
colInLine = len(targetLineContent)
|
||||||
|
foundNextLine:
|
||||||
|
m.col = colInLine // startCol is 0 for the first wrapped line
|
||||||
} else if li.RowOffset+1 < li.Height {
|
} else if li.RowOffset+1 < li.Height {
|
||||||
// Move to the next wrapped line within the same model line
|
// Move to the next wrapped line within the same model line
|
||||||
startOfNextWrappedLine := li.StartColumn + li.Width
|
grid := m.memoizedWrap(m.value[m.row], m.width)
|
||||||
m.col = startOfNextWrappedLine + m.mapVisualOffsetToSliceIndex(m.row, charOffset)
|
targetLineContent := grid[li.RowOffset+1]
|
||||||
|
|
||||||
|
startCol := 0
|
||||||
|
for i := 0; i < li.RowOffset+1; i++ {
|
||||||
|
startCol += len(grid[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find position within the target wrapped line.
|
||||||
|
offset := 0
|
||||||
|
colInLine := 0
|
||||||
|
for i, item := range targetLineContent {
|
||||||
|
var itemWidth int
|
||||||
|
switch v := item.(type) {
|
||||||
|
case rune:
|
||||||
|
itemWidth = rw.RuneWidth(v)
|
||||||
|
case *Attachment:
|
||||||
|
itemWidth = uniseg.StringWidth(v.Display)
|
||||||
|
}
|
||||||
|
if offset+itemWidth > charOffset {
|
||||||
|
// Decide whether to stick with the previous index or move to the current
|
||||||
|
// one based on which is closer to the target offset.
|
||||||
|
if (charOffset - offset) > ((offset + itemWidth) - charOffset) {
|
||||||
|
colInLine = i + 1
|
||||||
|
} else {
|
||||||
|
colInLine = i
|
||||||
|
}
|
||||||
|
goto foundSameLine
|
||||||
|
}
|
||||||
|
offset += itemWidth
|
||||||
|
}
|
||||||
|
colInLine = len(targetLineContent)
|
||||||
|
foundSameLine:
|
||||||
|
m.col = startCol + colInLine
|
||||||
}
|
}
|
||||||
m.SetCursorColumn(m.col)
|
m.SetCursorColumn(m.col)
|
||||||
}
|
}
|
||||||
@@ -957,22 +1017,77 @@ func (m *Model) CursorUp() {
|
|||||||
m.lastCharOffset = charOffset
|
m.lastCharOffset = charOffset
|
||||||
|
|
||||||
if li.RowOffset <= 0 && m.row > 0 {
|
if li.RowOffset <= 0 && m.row > 0 {
|
||||||
// Move to the previous model line
|
// Move to the previous model line. We want to land on the last wrapped
|
||||||
|
// line of the previous model line.
|
||||||
m.row--
|
m.row--
|
||||||
m.col = m.mapVisualOffsetToSliceIndex(m.row, charOffset)
|
grid := m.memoizedWrap(m.value[m.row], m.width)
|
||||||
} else if li.RowOffset > 0 {
|
targetLineContent := grid[len(grid)-1]
|
||||||
// Move to the previous wrapped line within the same model line
|
|
||||||
// To do this, we need to find the start of the previous wrapped line.
|
// Find start of last wrapped line.
|
||||||
prevLineInfo := m.LineInfo()
|
startCol := len(m.value[m.row]) - len(targetLineContent)
|
||||||
// prevLineStart := 0
|
|
||||||
if prevLineInfo.RowOffset > 0 {
|
// Find position within the last wrapped line.
|
||||||
// This is complex, so we'll approximate by moving to the start of the current wrapped line
|
offset := 0
|
||||||
// and then letting characterLeft handle it. A more precise calculation would
|
colInLine := 0
|
||||||
// require re-wrapping to find the previous line's start.
|
for i, item := range targetLineContent {
|
||||||
// For now, a simpler approach:
|
var itemWidth int
|
||||||
m.col = li.StartColumn - 1
|
switch v := item.(type) {
|
||||||
|
case rune:
|
||||||
|
itemWidth = rw.RuneWidth(v)
|
||||||
|
case *Attachment:
|
||||||
|
itemWidth = uniseg.StringWidth(v.Display)
|
||||||
|
}
|
||||||
|
if offset+itemWidth > charOffset {
|
||||||
|
// Decide whether to stick with the previous index or move to the current
|
||||||
|
// one based on which is closer to the target offset.
|
||||||
|
if (charOffset - offset) > ((offset + itemWidth) - charOffset) {
|
||||||
|
colInLine = i + 1
|
||||||
|
} else {
|
||||||
|
colInLine = i
|
||||||
|
}
|
||||||
|
goto foundPrevLine
|
||||||
|
}
|
||||||
|
offset += itemWidth
|
||||||
}
|
}
|
||||||
m.col = m.mapVisualOffsetToSliceIndex(m.row, charOffset)
|
colInLine = len(targetLineContent)
|
||||||
|
foundPrevLine:
|
||||||
|
m.col = startCol + colInLine
|
||||||
|
} else if li.RowOffset > 0 {
|
||||||
|
// Move to the previous wrapped line within the same model line.
|
||||||
|
grid := m.memoizedWrap(m.value[m.row], m.width)
|
||||||
|
targetLineContent := grid[li.RowOffset-1]
|
||||||
|
|
||||||
|
startCol := 0
|
||||||
|
for i := 0; i < li.RowOffset-1; i++ {
|
||||||
|
startCol += len(grid[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find position within the target wrapped line.
|
||||||
|
offset := 0
|
||||||
|
colInLine := 0
|
||||||
|
for i, item := range targetLineContent {
|
||||||
|
var itemWidth int
|
||||||
|
switch v := item.(type) {
|
||||||
|
case rune:
|
||||||
|
itemWidth = rw.RuneWidth(v)
|
||||||
|
case *Attachment:
|
||||||
|
itemWidth = uniseg.StringWidth(v.Display)
|
||||||
|
}
|
||||||
|
if offset+itemWidth > charOffset {
|
||||||
|
// Decide whether to stick with the previous index or move to the current
|
||||||
|
// one based on which is closer to the target offset.
|
||||||
|
if (charOffset - offset) > ((offset + itemWidth) - charOffset) {
|
||||||
|
colInLine = i + 1
|
||||||
|
} else {
|
||||||
|
colInLine = i
|
||||||
|
}
|
||||||
|
goto foundSameLine
|
||||||
|
}
|
||||||
|
offset += itemWidth
|
||||||
|
}
|
||||||
|
colInLine = len(targetLineContent)
|
||||||
|
foundSameLine:
|
||||||
|
m.col = startCol + colInLine
|
||||||
}
|
}
|
||||||
m.SetCursorColumn(m.col)
|
m.SetCursorColumn(m.col)
|
||||||
}
|
}
|
||||||
@@ -1429,8 +1544,6 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case tea.PasteMsg:
|
|
||||||
m.insertRunesFromUserInput([]rune(msg))
|
|
||||||
case tea.KeyPressMsg:
|
case tea.KeyPressMsg:
|
||||||
switch {
|
switch {
|
||||||
case key.Matches(msg, m.KeyMap.DeleteAfterCursor):
|
case key.Matches(msg, m.KeyMap.DeleteAfterCursor):
|
||||||
@@ -1490,8 +1603,6 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
|||||||
m.CursorDown()
|
m.CursorDown()
|
||||||
case key.Matches(msg, m.KeyMap.WordForward):
|
case key.Matches(msg, m.KeyMap.WordForward):
|
||||||
m.wordRight()
|
m.wordRight()
|
||||||
case key.Matches(msg, m.KeyMap.Paste):
|
|
||||||
return m, Paste
|
|
||||||
case key.Matches(msg, m.KeyMap.CharacterBackward):
|
case key.Matches(msg, m.KeyMap.CharacterBackward):
|
||||||
m.characterLeft(false /* insideLine */)
|
m.characterLeft(false /* insideLine */)
|
||||||
case key.Matches(msg, m.KeyMap.LinePrevious):
|
case key.Matches(msg, m.KeyMap.LinePrevious):
|
||||||
@@ -1512,11 +1623,11 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
|||||||
m.transposeLeft()
|
m.transposeLeft()
|
||||||
|
|
||||||
default:
|
default:
|
||||||
m.insertRunesFromUserInput([]rune(msg.Text))
|
m.InsertRunesFromUserInput([]rune(msg.Text))
|
||||||
}
|
}
|
||||||
|
|
||||||
case pasteMsg:
|
case pasteMsg:
|
||||||
m.insertRunesFromUserInput([]rune(msg))
|
m.InsertRunesFromUserInput([]rune(msg))
|
||||||
|
|
||||||
case pasteErrMsg:
|
case pasteErrMsg:
|
||||||
m.Err = msg
|
m.Err = msg
|
||||||
@@ -1596,7 +1707,7 @@ func (m Model) View() string {
|
|||||||
// guaranteed to be a space since any other character would
|
// guaranteed to be a space since any other character would
|
||||||
// have been wrapped.
|
// have been wrapped.
|
||||||
wrappedLineStr = strings.TrimSuffix(wrappedLineStr, " ")
|
wrappedLineStr = strings.TrimSuffix(wrappedLineStr, " ")
|
||||||
padding -= m.width - strwidth
|
padding = m.width - uniseg.StringWidth(wrappedLineStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.row == l && lineInfo.RowOffset == wl {
|
if m.row == l && lineInfo.RowOffset == wl {
|
||||||
@@ -1908,13 +2019,14 @@ func (m *Model) splitLine(row, col int) {
|
|||||||
m.row++
|
m.row++
|
||||||
}
|
}
|
||||||
|
|
||||||
// Paste is a command for pasting from the clipboard into the text input.
|
func itemWidth(item any) int {
|
||||||
func Paste() tea.Msg {
|
switch v := item.(type) {
|
||||||
str, err := clipboard.ReadAll()
|
case rune:
|
||||||
if err != nil {
|
return rw.RuneWidth(v)
|
||||||
return pasteErrMsg{err}
|
case *Attachment:
|
||||||
|
return uniseg.StringWidth(v.Display)
|
||||||
}
|
}
|
||||||
return pasteMsg(str)
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func wrapInterfaces(content []any, width int) [][]any {
|
func wrapInterfaces(content []any, width int) [][]any {
|
||||||
@@ -1959,19 +2071,18 @@ func wrapInterfaces(content []any, width int) [][]any {
|
|||||||
}
|
}
|
||||||
inSpaces = true
|
inSpaces = true
|
||||||
spaceW += itemW
|
spaceW += itemW
|
||||||
} else {
|
} else { // It's not a space, it's a character for a word.
|
||||||
if inSpaces {
|
if inSpaces {
|
||||||
// End of spaces
|
// We just finished a block of spaces. Handle them now.
|
||||||
if lineW > 0 && lineW+spaceW > width {
|
lineW += spaceW
|
||||||
lines = append(lines, []any{})
|
|
||||||
lineW = 0
|
|
||||||
} else {
|
|
||||||
lineW += spaceW
|
|
||||||
}
|
|
||||||
// Add spaces to current line
|
|
||||||
for i := 0; i < spaceW; i++ {
|
for i := 0; i < spaceW; i++ {
|
||||||
lines[len(lines)-1] = append(lines[len(lines)-1], rune(' '))
|
lines[len(lines)-1] = append(lines[len(lines)-1], rune(' '))
|
||||||
}
|
}
|
||||||
|
if lineW > width {
|
||||||
|
// The spaces made the line overflow. Start a new line for the upcoming word.
|
||||||
|
lines = append(lines, []any{})
|
||||||
|
lineW = 0
|
||||||
|
}
|
||||||
spaceW = 0
|
spaceW = 0
|
||||||
}
|
}
|
||||||
inSpaces = false
|
inSpaces = false
|
||||||
@@ -1980,20 +2091,24 @@ func wrapInterfaces(content []any, width int) [][]any {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle any remaining word/spaces
|
// Handle any remaining word/spaces at the end of the content.
|
||||||
if wordW > 0 {
|
if wordW > 0 {
|
||||||
if lineW > 0 && lineW+wordW > width {
|
if lineW > 0 && lineW+wordW > width {
|
||||||
lines = append(lines, word)
|
lines = append(lines, word)
|
||||||
|
lineW = wordW
|
||||||
} else {
|
} else {
|
||||||
lines[len(lines)-1] = append(lines[len(lines)-1], word...)
|
lines[len(lines)-1] = append(lines[len(lines)-1], word...)
|
||||||
|
lineW += wordW
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if spaceW > 0 {
|
if spaceW > 0 {
|
||||||
if lineW > 0 && lineW+spaceW > width {
|
// There are trailing spaces. Add them.
|
||||||
lines = append(lines, []any{})
|
|
||||||
}
|
|
||||||
for i := 0; i < spaceW; i++ {
|
for i := 0; i < spaceW; i++ {
|
||||||
lines[len(lines)-1] = append(lines[len(lines)-1], rune(' '))
|
lines[len(lines)-1] = append(lines[len(lines)-1], rune(' '))
|
||||||
|
lineW += 1
|
||||||
|
}
|
||||||
|
if lineW > width {
|
||||||
|
lines = append(lines, []any{})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,18 +16,27 @@ type ModelUsage struct {
|
|||||||
LastUsed time.Time `toml:"last_used"`
|
LastUsed time.Time `toml:"last_used"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ModeModel struct {
|
||||||
|
ProviderID string `toml:"provider_id"`
|
||||||
|
ModelID string `toml:"model_id"`
|
||||||
|
}
|
||||||
|
|
||||||
type State struct {
|
type State struct {
|
||||||
Theme string `toml:"theme"`
|
Theme string `toml:"theme"`
|
||||||
Provider string `toml:"provider"`
|
ModeModel map[string]ModeModel `toml:"mode_model"`
|
||||||
Model string `toml:"model"`
|
Provider string `toml:"provider"`
|
||||||
RecentlyUsedModels []ModelUsage `toml:"recently_used_models"`
|
Model string `toml:"model"`
|
||||||
MessagesRight bool `toml:"messages_right"`
|
Mode string `toml:"mode"`
|
||||||
SplitDiff bool `toml:"split_diff"`
|
RecentlyUsedModels []ModelUsage `toml:"recently_used_models"`
|
||||||
|
MessagesRight bool `toml:"messages_right"`
|
||||||
|
SplitDiff bool `toml:"split_diff"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewState() *State {
|
func NewState() *State {
|
||||||
return &State{
|
return &State{
|
||||||
Theme: "opencode",
|
Theme: "opencode",
|
||||||
|
Mode: "build",
|
||||||
|
ModeModel: make(map[string]ModeModel),
|
||||||
RecentlyUsedModels: make([]ModelUsage, 0),
|
RecentlyUsedModels: make([]ModelUsage, 0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
//go:build !windows
|
|
||||||
|
|
||||||
package image
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"github.com/atotto/clipboard"
|
|
||||||
"image"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetImageFromClipboard() ([]byte, string, error) {
|
|
||||||
text, err := clipboard.ReadAll()
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", fmt.Errorf("Error reading clipboard")
|
|
||||||
}
|
|
||||||
|
|
||||||
if text == "" {
|
|
||||||
return nil, "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
binaryData := []byte(text)
|
|
||||||
imageBytes, err := binaryToImage(binaryData)
|
|
||||||
if err != nil {
|
|
||||||
return nil, text, nil
|
|
||||||
}
|
|
||||||
return imageBytes, "", nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func binaryToImage(data []byte) ([]byte, error) {
|
|
||||||
reader := bytes.NewReader(data)
|
|
||||||
img, _, err := image.Decode(reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Unable to covert bytes to image")
|
|
||||||
}
|
|
||||||
|
|
||||||
return ImageToBytes(img)
|
|
||||||
}
|
|
||||||
|
|
||||||
func min(a, b int) int {
|
|
||||||
if a < b {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
//go:build windows
|
|
||||||
|
|
||||||
package image
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"image"
|
|
||||||
"image/color"
|
|
||||||
"log/slog"
|
|
||||||
"syscall"
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
user32 = syscall.NewLazyDLL("user32.dll")
|
|
||||||
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
|
||||||
openClipboard = user32.NewProc("OpenClipboard")
|
|
||||||
closeClipboard = user32.NewProc("CloseClipboard")
|
|
||||||
getClipboardData = user32.NewProc("GetClipboardData")
|
|
||||||
isClipboardFormatAvailable = user32.NewProc("IsClipboardFormatAvailable")
|
|
||||||
globalLock = kernel32.NewProc("GlobalLock")
|
|
||||||
globalUnlock = kernel32.NewProc("GlobalUnlock")
|
|
||||||
globalSize = kernel32.NewProc("GlobalSize")
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
CF_TEXT = 1
|
|
||||||
CF_UNICODETEXT = 13
|
|
||||||
CF_DIB = 8
|
|
||||||
)
|
|
||||||
|
|
||||||
type BITMAPINFOHEADER struct {
|
|
||||||
BiSize uint32
|
|
||||||
BiWidth int32
|
|
||||||
BiHeight int32
|
|
||||||
BiPlanes uint16
|
|
||||||
BiBitCount uint16
|
|
||||||
BiCompression uint32
|
|
||||||
BiSizeImage uint32
|
|
||||||
BiXPelsPerMeter int32
|
|
||||||
BiYPelsPerMeter int32
|
|
||||||
BiClrUsed uint32
|
|
||||||
BiClrImportant uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetImageFromClipboard() ([]byte, string, error) {
|
|
||||||
ret, _, _ := openClipboard.Call(0)
|
|
||||||
if ret == 0 {
|
|
||||||
return nil, "", fmt.Errorf("failed to open clipboard")
|
|
||||||
}
|
|
||||||
defer func(closeClipboard *syscall.LazyProc, a ...uintptr) {
|
|
||||||
_, _, err := closeClipboard.Call(a...)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("close clipboard failed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}(closeClipboard)
|
|
||||||
isTextAvailable, _, _ := isClipboardFormatAvailable.Call(uintptr(CF_TEXT))
|
|
||||||
isUnicodeTextAvailable, _, _ := isClipboardFormatAvailable.Call(uintptr(CF_UNICODETEXT))
|
|
||||||
|
|
||||||
if isTextAvailable != 0 || isUnicodeTextAvailable != 0 {
|
|
||||||
// Get text from clipboard
|
|
||||||
var formatToUse uintptr = CF_TEXT
|
|
||||||
if isUnicodeTextAvailable != 0 {
|
|
||||||
formatToUse = CF_UNICODETEXT
|
|
||||||
}
|
|
||||||
|
|
||||||
hClipboardText, _, _ := getClipboardData.Call(formatToUse)
|
|
||||||
if hClipboardText != 0 {
|
|
||||||
textPtr, _, _ := globalLock.Call(hClipboardText)
|
|
||||||
if textPtr != 0 {
|
|
||||||
defer func(globalUnlock *syscall.LazyProc, a ...uintptr) {
|
|
||||||
_, _, err := globalUnlock.Call(a...)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Global unlock failed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}(globalUnlock, hClipboardText)
|
|
||||||
|
|
||||||
// Get clipboard text
|
|
||||||
var clipboardText string
|
|
||||||
if formatToUse == CF_UNICODETEXT {
|
|
||||||
// Convert wide string to Go string
|
|
||||||
clipboardText = syscall.UTF16ToString((*[1 << 20]uint16)(unsafe.Pointer(textPtr))[:])
|
|
||||||
} else {
|
|
||||||
// Get size of ANSI text
|
|
||||||
size, _, _ := globalSize.Call(hClipboardText)
|
|
||||||
if size > 0 {
|
|
||||||
// Convert ANSI string to Go string
|
|
||||||
textBytes := make([]byte, size)
|
|
||||||
copy(textBytes, (*[1 << 20]byte)(unsafe.Pointer(textPtr))[:size:size])
|
|
||||||
clipboardText = bytesToString(textBytes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the text is not empty
|
|
||||||
if clipboardText != "" {
|
|
||||||
return nil, clipboardText, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hClipboardData, _, _ := getClipboardData.Call(uintptr(CF_DIB))
|
|
||||||
if hClipboardData == 0 {
|
|
||||||
return nil, "", fmt.Errorf("failed to get clipboard data")
|
|
||||||
}
|
|
||||||
|
|
||||||
dataPtr, _, _ := globalLock.Call(hClipboardData)
|
|
||||||
if dataPtr == 0 {
|
|
||||||
return nil, "", fmt.Errorf("failed to lock clipboard data")
|
|
||||||
}
|
|
||||||
defer func(globalUnlock *syscall.LazyProc, a ...uintptr) {
|
|
||||||
_, _, err := globalUnlock.Call(a...)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Global unlock failed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}(globalUnlock, hClipboardData)
|
|
||||||
|
|
||||||
bmiHeader := (*BITMAPINFOHEADER)(unsafe.Pointer(dataPtr))
|
|
||||||
|
|
||||||
width := int(bmiHeader.BiWidth)
|
|
||||||
height := int(bmiHeader.BiHeight)
|
|
||||||
if height < 0 {
|
|
||||||
height = -height
|
|
||||||
}
|
|
||||||
bitsPerPixel := int(bmiHeader.BiBitCount)
|
|
||||||
|
|
||||||
img := image.NewRGBA(image.Rect(0, 0, width, height))
|
|
||||||
|
|
||||||
var bitsOffset uintptr
|
|
||||||
if bitsPerPixel <= 8 {
|
|
||||||
numColors := uint32(1) << bitsPerPixel
|
|
||||||
if bmiHeader.BiClrUsed > 0 {
|
|
||||||
numColors = bmiHeader.BiClrUsed
|
|
||||||
}
|
|
||||||
bitsOffset = unsafe.Sizeof(*bmiHeader) + uintptr(numColors*4)
|
|
||||||
} else {
|
|
||||||
bitsOffset = unsafe.Sizeof(*bmiHeader)
|
|
||||||
}
|
|
||||||
|
|
||||||
for y := range height {
|
|
||||||
for x := range width {
|
|
||||||
|
|
||||||
srcY := height - y - 1
|
|
||||||
if bmiHeader.BiHeight < 0 {
|
|
||||||
srcY = y
|
|
||||||
}
|
|
||||||
|
|
||||||
var pixelPointer unsafe.Pointer
|
|
||||||
var r, g, b, a uint8
|
|
||||||
|
|
||||||
switch bitsPerPixel {
|
|
||||||
case 24:
|
|
||||||
stride := (width*3 + 3) &^ 3
|
|
||||||
pixelPointer = unsafe.Pointer(dataPtr + bitsOffset + uintptr(srcY*stride+x*3))
|
|
||||||
b = *(*byte)(pixelPointer)
|
|
||||||
g = *(*byte)(unsafe.Add(pixelPointer, 1))
|
|
||||||
r = *(*byte)(unsafe.Add(pixelPointer, 2))
|
|
||||||
a = 255
|
|
||||||
case 32:
|
|
||||||
pixelPointer = unsafe.Pointer(dataPtr + bitsOffset + uintptr(srcY*width*4+x*4))
|
|
||||||
b = *(*byte)(pixelPointer)
|
|
||||||
g = *(*byte)(unsafe.Add(pixelPointer, 1))
|
|
||||||
r = *(*byte)(unsafe.Add(pixelPointer, 2))
|
|
||||||
a = *(*byte)(unsafe.Add(pixelPointer, 3))
|
|
||||||
if a == 0 {
|
|
||||||
a = 255
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return nil, "", fmt.Errorf("unsupported bit count: %d", bitsPerPixel)
|
|
||||||
}
|
|
||||||
|
|
||||||
img.Set(x, y, color.RGBA{R: r, G: g, B: b, A: a})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
imageBytes, err := ImageToBytes(img)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
return imageBytes, "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func bytesToString(b []byte) string {
|
|
||||||
i := bytes.IndexByte(b, 0)
|
|
||||||
if i == -1 {
|
|
||||||
return string(b)
|
|
||||||
}
|
|
||||||
return string(b[:i])
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
package image
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"image"
|
|
||||||
"image/color"
|
|
||||||
"image/png"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/charmbracelet/lipgloss/v2"
|
|
||||||
"github.com/disintegration/imaging"
|
|
||||||
"github.com/lucasb-eyer/go-colorful"
|
|
||||||
_ "golang.org/x/image/webp"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ValidateFileSize(filePath string, sizeLimit int64) (bool, error) {
|
|
||||||
fileInfo, err := os.Stat(filePath)
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("error getting file info: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if fileInfo.Size() > sizeLimit {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToString(width int, img image.Image) string {
|
|
||||||
img = imaging.Resize(img, width, 0, imaging.Lanczos)
|
|
||||||
b := img.Bounds()
|
|
||||||
imageWidth := b.Max.X
|
|
||||||
h := b.Max.Y
|
|
||||||
str := strings.Builder{}
|
|
||||||
|
|
||||||
for heightCounter := 0; heightCounter < h; heightCounter += 2 {
|
|
||||||
for x := range imageWidth {
|
|
||||||
c1, _ := colorful.MakeColor(img.At(x, heightCounter))
|
|
||||||
color1 := lipgloss.Color(c1.Hex())
|
|
||||||
|
|
||||||
var color2 color.Color
|
|
||||||
if heightCounter+1 < h {
|
|
||||||
c2, _ := colorful.MakeColor(img.At(x, heightCounter+1))
|
|
||||||
color2 = lipgloss.Color(c2.Hex())
|
|
||||||
} else {
|
|
||||||
color2 = color1
|
|
||||||
}
|
|
||||||
|
|
||||||
str.WriteString(lipgloss.NewStyle().Foreground(color1).
|
|
||||||
Background(color2).Render("▀"))
|
|
||||||
}
|
|
||||||
|
|
||||||
str.WriteString("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
return str.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func ImagePreview(width int, filename string) (string, error) {
|
|
||||||
imageContent, err := os.Open(filename)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer imageContent.Close()
|
|
||||||
|
|
||||||
img, _, err := image.Decode(imageContent)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
imageString := ToString(width, img)
|
|
||||||
|
|
||||||
return imageString, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ImageToBytes(image image.Image) ([]byte, error) {
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
err := png.Encode(buf, image)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf.Bytes(), nil
|
|
||||||
}
|
|
||||||
@@ -23,6 +23,7 @@ import (
|
|||||||
"github.com/sst/opencode/internal/components/modal"
|
"github.com/sst/opencode/internal/components/modal"
|
||||||
"github.com/sst/opencode/internal/components/status"
|
"github.com/sst/opencode/internal/components/status"
|
||||||
"github.com/sst/opencode/internal/components/toast"
|
"github.com/sst/opencode/internal/components/toast"
|
||||||
|
"github.com/sst/opencode/internal/config"
|
||||||
"github.com/sst/opencode/internal/layout"
|
"github.com/sst/opencode/internal/layout"
|
||||||
"github.com/sst/opencode/internal/styles"
|
"github.com/sst/opencode/internal/styles"
|
||||||
"github.com/sst/opencode/internal/theme"
|
"github.com/sst/opencode/internal/theme"
|
||||||
@@ -32,15 +33,27 @@ import (
|
|||||||
// InterruptDebounceTimeoutMsg is sent when the interrupt key debounce timeout expires
|
// InterruptDebounceTimeoutMsg is sent when the interrupt key debounce timeout expires
|
||||||
type InterruptDebounceTimeoutMsg struct{}
|
type InterruptDebounceTimeoutMsg struct{}
|
||||||
|
|
||||||
|
// ExitDebounceTimeoutMsg is sent when the exit key debounce timeout expires
|
||||||
|
type ExitDebounceTimeoutMsg struct{}
|
||||||
|
|
||||||
// InterruptKeyState tracks the state of interrupt key presses for debouncing
|
// InterruptKeyState tracks the state of interrupt key presses for debouncing
|
||||||
type InterruptKeyState int
|
type InterruptKeyState int
|
||||||
|
|
||||||
|
// ExitKeyState tracks the state of exit key presses for debouncing
|
||||||
|
type ExitKeyState int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
InterruptKeyIdle InterruptKeyState = iota
|
InterruptKeyIdle InterruptKeyState = iota
|
||||||
InterruptKeyFirstPress
|
InterruptKeyFirstPress
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ExitKeyIdle ExitKeyState = iota
|
||||||
|
ExitKeyFirstPress
|
||||||
|
)
|
||||||
|
|
||||||
const interruptDebounceTimeout = 1 * time.Second
|
const interruptDebounceTimeout = 1 * time.Second
|
||||||
|
const exitDebounceTimeout = 1 * time.Second
|
||||||
const fileViewerFullWidthCutoff = 160
|
const fileViewerFullWidthCutoff = 160
|
||||||
|
|
||||||
type appModel struct {
|
type appModel struct {
|
||||||
@@ -53,19 +66,21 @@ type appModel struct {
|
|||||||
completions dialog.CompletionDialog
|
completions dialog.CompletionDialog
|
||||||
commandProvider dialog.CompletionProvider
|
commandProvider dialog.CompletionProvider
|
||||||
fileProvider dialog.CompletionProvider
|
fileProvider dialog.CompletionProvider
|
||||||
|
symbolsProvider dialog.CompletionProvider
|
||||||
showCompletionDialog bool
|
showCompletionDialog bool
|
||||||
fileCompletionActive bool
|
fileCompletionActive bool
|
||||||
leaderBinding *key.Binding
|
leaderBinding *key.Binding
|
||||||
isLeaderSequence bool
|
// isLeaderSequence bool
|
||||||
toastManager *toast.ToastManager
|
toastManager *toast.ToastManager
|
||||||
interruptKeyState InterruptKeyState
|
interruptKeyState InterruptKeyState
|
||||||
lastScroll time.Time
|
exitKeyState ExitKeyState
|
||||||
messagesRight bool
|
lastScroll time.Time
|
||||||
fileViewer fileviewer.Model
|
messagesRight bool
|
||||||
lastMouse tea.Mouse
|
fileViewer fileviewer.Model
|
||||||
fileViewerStart int
|
lastMouse tea.Mouse
|
||||||
fileViewerEnd int
|
fileViewerStart int
|
||||||
fileViewerHit bool
|
fileViewerEnd int
|
||||||
|
fileViewerHit bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a appModel) Init() tea.Cmd {
|
func (a appModel) Init() tea.Cmd {
|
||||||
@@ -169,9 +184,9 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. Check for commands that require leader
|
// 2. Check for commands that require leader
|
||||||
if a.isLeaderSequence {
|
if a.app.IsLeaderSequence {
|
||||||
matches := a.app.Commands.Matches(msg, a.isLeaderSequence)
|
matches := a.app.Commands.Matches(msg, a.app.IsLeaderSequence)
|
||||||
a.isLeaderSequence = false
|
a.app.IsLeaderSequence = false
|
||||||
if len(matches) > 0 {
|
if len(matches) > 0 {
|
||||||
return a, util.CmdHandler(commands.ExecuteCommandsMsg(matches))
|
return a, util.CmdHandler(commands.ExecuteCommandsMsg(matches))
|
||||||
}
|
}
|
||||||
@@ -189,7 +204,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
|
|
||||||
// Set command provider for command completion
|
// Set command provider for command completion
|
||||||
a.completions = dialog.NewCompletionDialogComponent(a.commandProvider)
|
a.completions = dialog.NewCompletionDialogComponent("/", a.commandProvider)
|
||||||
updated, cmd = a.completions.Update(msg)
|
updated, cmd = a.completions.Update(msg)
|
||||||
a.completions = updated.(dialog.CompletionDialog)
|
a.completions = updated.(dialog.CompletionDialog)
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
@@ -207,8 +222,8 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
a.editor = updated.(chat.EditorComponent)
|
a.editor = updated.(chat.EditorComponent)
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
|
|
||||||
// Set file provider for file completion
|
// Set both file and symbols providers for @ completion
|
||||||
a.completions = dialog.NewCompletionDialogComponent(a.fileProvider)
|
a.completions = dialog.NewCompletionDialogComponent("@", a.fileProvider, a.symbolsProvider)
|
||||||
updated, cmd = a.completions.Update(msg)
|
updated, cmd = a.completions.Update(msg)
|
||||||
a.completions = updated.(dialog.CompletionDialog)
|
a.completions = updated.(dialog.CompletionDialog)
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
@@ -246,15 +261,21 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
|
|
||||||
// 5. Check for leader key activation
|
// 5. Check for leader key activation
|
||||||
if a.leaderBinding != nil &&
|
if a.leaderBinding != nil &&
|
||||||
!a.isLeaderSequence &&
|
!a.app.IsLeaderSequence &&
|
||||||
key.Matches(msg, *a.leaderBinding) {
|
key.Matches(msg, *a.leaderBinding) {
|
||||||
a.isLeaderSequence = true
|
a.app.IsLeaderSequence = true
|
||||||
return a, nil
|
return a, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Handle interrupt key debounce for session interrupt
|
// 6 Handle input clear command
|
||||||
|
inputClearCommand := a.app.Commands[commands.InputClearCommand]
|
||||||
|
if inputClearCommand.Matches(msg, a.app.IsLeaderSequence) && a.editor.Length() > 0 {
|
||||||
|
return a, util.CmdHandler(commands.ExecuteCommandMsg(inputClearCommand))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Handle interrupt key debounce for session interrupt
|
||||||
interruptCommand := a.app.Commands[commands.SessionInterruptCommand]
|
interruptCommand := a.app.Commands[commands.SessionInterruptCommand]
|
||||||
if interruptCommand.Matches(msg, a.isLeaderSequence) && a.app.IsBusy() {
|
if interruptCommand.Matches(msg, a.app.IsLeaderSequence) && a.app.IsBusy() {
|
||||||
switch a.interruptKeyState {
|
switch a.interruptKeyState {
|
||||||
case InterruptKeyIdle:
|
case InterruptKeyIdle:
|
||||||
// First interrupt key press - start debounce timer
|
// First interrupt key press - start debounce timer
|
||||||
@@ -271,18 +292,36 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. Check again for commands that don't require leader (excluding interrupt when busy)
|
// 8. Handle exit key debounce for app exit when using non-leader command
|
||||||
matches := a.app.Commands.Matches(msg, a.isLeaderSequence)
|
exitCommand := a.app.Commands[commands.AppExitCommand]
|
||||||
|
if exitCommand.Matches(msg, a.app.IsLeaderSequence) {
|
||||||
|
switch a.exitKeyState {
|
||||||
|
case ExitKeyIdle:
|
||||||
|
// First exit key press - start debounce timer
|
||||||
|
a.exitKeyState = ExitKeyFirstPress
|
||||||
|
a.editor.SetExitKeyInDebounce(true)
|
||||||
|
return a, tea.Tick(exitDebounceTimeout, func(t time.Time) tea.Msg {
|
||||||
|
return ExitDebounceTimeoutMsg{}
|
||||||
|
})
|
||||||
|
case ExitKeyFirstPress:
|
||||||
|
// Second exit key press within timeout - actually exit
|
||||||
|
a.exitKeyState = ExitKeyIdle
|
||||||
|
a.editor.SetExitKeyInDebounce(false)
|
||||||
|
return a, util.CmdHandler(commands.ExecuteCommandMsg(exitCommand))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. Check again for commands that don't require leader (excluding interrupt when busy and exit when in debounce)
|
||||||
|
matches := a.app.Commands.Matches(msg, a.app.IsLeaderSequence)
|
||||||
if len(matches) > 0 {
|
if len(matches) > 0 {
|
||||||
// Skip interrupt key if we're in debounce mode and app is busy
|
// Skip interrupt key if we're in debounce mode and app is busy
|
||||||
if interruptCommand.Matches(msg, a.isLeaderSequence) && a.app.IsBusy() && a.interruptKeyState != InterruptKeyIdle {
|
if interruptCommand.Matches(msg, a.app.IsLeaderSequence) && a.app.IsBusy() && a.interruptKeyState != InterruptKeyIdle {
|
||||||
return a, nil
|
return a, nil
|
||||||
}
|
}
|
||||||
return a, util.CmdHandler(commands.ExecuteCommandsMsg(matches))
|
return a, util.CmdHandler(commands.ExecuteCommandsMsg(matches))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. Fallback to editor. This is for other characters
|
// 10. Fallback to editor. This is for other characters like backspace, tab, etc.
|
||||||
// like backspace, tab, etc.
|
|
||||||
updatedEditor, cmd := a.editor.Update(msg)
|
updatedEditor, cmd := a.editor.Update(msg)
|
||||||
a.editor = updatedEditor.(chat.EditorComponent)
|
a.editor = updatedEditor.(chat.EditorComponent)
|
||||||
return a, cmd
|
return a, cmd
|
||||||
@@ -352,6 +391,12 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
a.showCompletionDialog = false
|
a.showCompletionDialog = false
|
||||||
a.app, cmd = a.app.SendChatMessage(context.Background(), msg.Text, msg.Attachments)
|
a.app, cmd = a.app.SendChatMessage(context.Background(), msg.Text, msg.Attachments)
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
|
case app.SetEditorContentMsg:
|
||||||
|
// Set the editor content without sending
|
||||||
|
a.editor.SetValue(msg.Text)
|
||||||
|
updated, cmd := a.editor.Focus()
|
||||||
|
a.editor = updated.(chat.EditorComponent)
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
case dialog.CompletionDialogCloseMsg:
|
case dialog.CompletionDialogCloseMsg:
|
||||||
a.showCompletionDialog = false
|
a.showCompletionDialog = false
|
||||||
a.fileCompletionActive = false
|
a.fileCompletionActive = false
|
||||||
@@ -480,8 +525,10 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
case app.ModelSelectedMsg:
|
case app.ModelSelectedMsg:
|
||||||
a.app.Provider = &msg.Provider
|
a.app.Provider = &msg.Provider
|
||||||
a.app.Model = &msg.Model
|
a.app.Model = &msg.Model
|
||||||
a.app.State.Provider = msg.Provider.ID
|
a.app.State.ModeModel[a.app.Mode.Name] = config.ModeModel{
|
||||||
a.app.State.Model = msg.Model.ID
|
ProviderID: msg.Provider.ID,
|
||||||
|
ModelID: msg.Model.ID,
|
||||||
|
}
|
||||||
a.app.State.UpdateModelUsage(msg.Provider.ID, msg.Model.ID)
|
a.app.State.UpdateModelUsage(msg.Provider.ID, msg.Model.ID)
|
||||||
a.app.SaveState()
|
a.app.SaveState()
|
||||||
case dialog.ThemeSelectedMsg:
|
case dialog.ThemeSelectedMsg:
|
||||||
@@ -499,6 +546,10 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
// Reset interrupt key state after timeout
|
// Reset interrupt key state after timeout
|
||||||
a.interruptKeyState = InterruptKeyIdle
|
a.interruptKeyState = InterruptKeyIdle
|
||||||
a.editor.SetInterruptKeyInDebounce(false)
|
a.editor.SetInterruptKeyInDebounce(false)
|
||||||
|
case ExitDebounceTimeoutMsg:
|
||||||
|
// Reset exit key state after timeout
|
||||||
|
a.exitKeyState = ExitKeyIdle
|
||||||
|
a.editor.SetExitKeyInDebounce(false)
|
||||||
case dialog.FindSelectedMsg:
|
case dialog.FindSelectedMsg:
|
||||||
return a.openFile(msg.FilePath)
|
return a.openFile(msg.FilePath)
|
||||||
}
|
}
|
||||||
@@ -775,6 +826,10 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
|
|||||||
case commands.AppHelpCommand:
|
case commands.AppHelpCommand:
|
||||||
helpDialog := dialog.NewHelpDialog(a.app)
|
helpDialog := dialog.NewHelpDialog(a.app)
|
||||||
a.modal = helpDialog
|
a.modal = helpDialog
|
||||||
|
case commands.SwitchModeCommand:
|
||||||
|
updated, cmd := a.app.SwitchMode()
|
||||||
|
a.app = updated
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
case commands.EditorOpenCommand:
|
case commands.EditorOpenCommand:
|
||||||
if a.app.IsBusy() {
|
if a.app.IsBusy() {
|
||||||
// status.Warn("Agent is working, please wait...")
|
// status.Warn("Agent is working, please wait...")
|
||||||
@@ -816,7 +871,7 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
os.Remove(tmpfile.Name())
|
os.Remove(tmpfile.Name())
|
||||||
return app.SendMsg{
|
return app.SetEditorContentMsg{
|
||||||
Text: string(content),
|
Text: string(content),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -841,7 +896,7 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
|
|||||||
return a, toast.NewErrorToast("Failed to share session")
|
return a, toast.NewErrorToast("Failed to share session")
|
||||||
}
|
}
|
||||||
shareUrl := response.Share.URL
|
shareUrl := response.Share.URL
|
||||||
cmds = append(cmds, tea.SetClipboard(shareUrl))
|
cmds = append(cmds, a.app.SetClipboard(shareUrl))
|
||||||
cmds = append(cmds, toast.NewSuccessToast("Share URL copied to clipboard!"))
|
cmds = append(cmds, toast.NewSuccessToast("Share URL copied to clipboard!"))
|
||||||
case commands.SessionUnshareCommand:
|
case commands.SessionUnshareCommand:
|
||||||
if a.app.Session.ID == "" {
|
if a.app.Session.ID == "" {
|
||||||
@@ -881,7 +936,7 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
|
|||||||
a.modal = themeDialog
|
a.modal = themeDialog
|
||||||
case commands.FileListCommand:
|
case commands.FileListCommand:
|
||||||
a.editor.Blur()
|
a.editor.Blur()
|
||||||
provider := completions.NewFileAndFolderContextGroup(a.app)
|
provider := completions.NewFileContextGroup(a.app)
|
||||||
findDialog := dialog.NewFindDialog(provider)
|
findDialog := dialog.NewFindDialog(provider)
|
||||||
findDialog.SetWidth(layout.Current.Container.Width - 8)
|
findDialog.SetWidth(layout.Current.Container.Width - 8)
|
||||||
a.modal = findDialog
|
a.modal = findDialog
|
||||||
@@ -975,7 +1030,7 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
|
|||||||
case commands.MessagesCopyCommand:
|
case commands.MessagesCopyCommand:
|
||||||
selected := a.messages.Selected()
|
selected := a.messages.Selected()
|
||||||
if selected != "" {
|
if selected != "" {
|
||||||
cmd = tea.SetClipboard(selected)
|
cmd = a.app.SetClipboard(selected)
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
cmd = toast.NewSuccessToast("Message copied to clipboard")
|
cmd = toast.NewSuccessToast("Message copied to clipboard")
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
@@ -989,11 +1044,12 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
|
|||||||
|
|
||||||
func NewModel(app *app.App) tea.Model {
|
func NewModel(app *app.App) tea.Model {
|
||||||
commandProvider := completions.NewCommandCompletionProvider(app)
|
commandProvider := completions.NewCommandCompletionProvider(app)
|
||||||
fileProvider := completions.NewFileAndFolderContextGroup(app)
|
fileProvider := completions.NewFileContextGroup(app)
|
||||||
|
symbolsProvider := completions.NewSymbolsContextGroup(app)
|
||||||
|
|
||||||
messages := chat.NewMessagesComponent(app)
|
messages := chat.NewMessagesComponent(app)
|
||||||
editor := chat.NewEditorComponent(app)
|
editor := chat.NewEditorComponent(app)
|
||||||
completions := dialog.NewCompletionDialogComponent(commandProvider)
|
completions := dialog.NewCompletionDialogComponent("/", commandProvider)
|
||||||
|
|
||||||
var leaderBinding *key.Binding
|
var leaderBinding *key.Binding
|
||||||
if app.Config.Keybinds.Leader != "" {
|
if app.Config.Keybinds.Leader != "" {
|
||||||
@@ -1009,12 +1065,13 @@ func NewModel(app *app.App) tea.Model {
|
|||||||
completions: completions,
|
completions: completions,
|
||||||
commandProvider: commandProvider,
|
commandProvider: commandProvider,
|
||||||
fileProvider: fileProvider,
|
fileProvider: fileProvider,
|
||||||
|
symbolsProvider: symbolsProvider,
|
||||||
leaderBinding: leaderBinding,
|
leaderBinding: leaderBinding,
|
||||||
isLeaderSequence: false,
|
|
||||||
showCompletionDialog: false,
|
showCompletionDialog: false,
|
||||||
fileCompletionActive: false,
|
fileCompletionActive: false,
|
||||||
toastManager: toast.NewToastManager(),
|
toastManager: toast.NewToastManager(),
|
||||||
interruptKeyState: InterruptKeyIdle,
|
interruptKeyState: InterruptKeyIdle,
|
||||||
|
exitKeyState: ExitKeyIdle,
|
||||||
fileViewer: fileviewer.New(app),
|
fileViewer: fileviewer.New(app),
|
||||||
messagesRight: app.State.MessagesRight,
|
messagesRight: app.State.MessagesRight,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
opencode "github.com/sst/opencode-sdk-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
type APILogHandler struct {
|
||||||
|
client *opencode.Client
|
||||||
|
service string
|
||||||
|
level slog.Level
|
||||||
|
attrs []slog.Attr
|
||||||
|
groups []string
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAPILogHandler(client *opencode.Client, service string, level slog.Level) *APILogHandler {
|
||||||
|
return &APILogHandler{
|
||||||
|
client: client,
|
||||||
|
service: service,
|
||||||
|
level: level,
|
||||||
|
attrs: make([]slog.Attr, 0),
|
||||||
|
groups: make([]string, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *APILogHandler) Enabled(_ context.Context, level slog.Level) bool {
|
||||||
|
return level >= h.level
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *APILogHandler) Handle(ctx context.Context, r slog.Record) error {
|
||||||
|
var apiLevel opencode.AppLogParamsLevel
|
||||||
|
switch r.Level {
|
||||||
|
case slog.LevelDebug:
|
||||||
|
apiLevel = opencode.AppLogParamsLevelDebug
|
||||||
|
case slog.LevelInfo:
|
||||||
|
apiLevel = opencode.AppLogParamsLevelInfo
|
||||||
|
case slog.LevelWarn:
|
||||||
|
apiLevel = opencode.AppLogParamsLevelWarn
|
||||||
|
case slog.LevelError:
|
||||||
|
apiLevel = opencode.AppLogParamsLevelError
|
||||||
|
default:
|
||||||
|
apiLevel = opencode.AppLogParamsLevelInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
extra := make(map[string]any)
|
||||||
|
|
||||||
|
h.mu.Lock()
|
||||||
|
for _, attr := range h.attrs {
|
||||||
|
extra[attr.Key] = attr.Value.Any()
|
||||||
|
}
|
||||||
|
h.mu.Unlock()
|
||||||
|
|
||||||
|
r.Attrs(func(attr slog.Attr) bool {
|
||||||
|
extra[attr.Key] = attr.Value.Any()
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
params := opencode.AppLogParams{
|
||||||
|
Service: opencode.F(h.service),
|
||||||
|
Level: opencode.F(apiLevel),
|
||||||
|
Message: opencode.F(r.Message),
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(extra) > 0 {
|
||||||
|
params.Extra = opencode.F(extra)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
_, err := h.client.App.Log(context.Background(), params)
|
||||||
|
if err != nil {
|
||||||
|
// Fallback: we can't log the error using slog as it would create a loop
|
||||||
|
// TODO: fallback file?
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAttrs returns a new Handler whose attributes consist of
|
||||||
|
// both the receiver's attributes and the arguments.
|
||||||
|
func (h *APILogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
|
||||||
|
newHandler := &APILogHandler{
|
||||||
|
client: h.client,
|
||||||
|
service: h.service,
|
||||||
|
level: h.level,
|
||||||
|
attrs: make([]slog.Attr, len(h.attrs)+len(attrs)),
|
||||||
|
groups: make([]string, len(h.groups)),
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(newHandler.attrs, h.attrs)
|
||||||
|
copy(newHandler.attrs[len(h.attrs):], attrs)
|
||||||
|
copy(newHandler.groups, h.groups)
|
||||||
|
|
||||||
|
return newHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithGroup returns a new Handler with the given group appended to
|
||||||
|
// the receiver's existing groups.
|
||||||
|
func (h *APILogHandler) WithGroup(name string) slog.Handler {
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
|
||||||
|
newHandler := &APILogHandler{
|
||||||
|
client: h.client,
|
||||||
|
service: h.service,
|
||||||
|
level: h.level,
|
||||||
|
attrs: make([]slog.Attr, len(h.attrs)),
|
||||||
|
groups: make([]string, len(h.groups)+1),
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(newHandler.attrs, h.attrs)
|
||||||
|
copy(newHandler.groups, h.groups)
|
||||||
|
newHandler.groups[len(h.groups)] = name
|
||||||
|
|
||||||
|
return newHandler
|
||||||
|
}
|
||||||
+7
-7
@@ -2,15 +2,15 @@ name: CI
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches-ignore:
|
branches-ignore:
|
||||||
- "generated"
|
- 'generated'
|
||||||
- "codegen/**"
|
- 'codegen/**'
|
||||||
- "integrated/**"
|
- 'integrated/**'
|
||||||
- "stl-preview-head/**"
|
- 'stl-preview-head/**'
|
||||||
- "stl-preview-base/**"
|
- 'stl-preview-base/**'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches-ignore:
|
branches-ignore:
|
||||||
- "stl-preview-head/**"
|
- 'stl-preview-head/**'
|
||||||
- "stl-preview-base/**"
|
- 'stl-preview-base/**'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
configured_endpoints: 20
|
configured_endpoints: 22
|
||||||
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-15eeb028f79b9a065b4e54a6ea6a58631e9bd5004f97820f0c79d18e3f8bac84.yml
|
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-37247433660e125f9a79ff173d8007f13192d28a40f4e599e002446a6ed0c128.yml
|
||||||
openapi_spec_hash: 38c8bacb6c8e4c46852a3e81e3fb9fda
|
openapi_spec_hash: 8095ebe2d88259381a58e7b0c87244c4
|
||||||
config_hash: e03e9d1aad76081fa1163086e89f201b
|
config_hash: 589ec6a935a43a3c49a325ece86cbda2
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Full Changelog: [v0.1.0-alpha.7...v0.1.0-alpha.8](https://github.com/sst/opencod
|
|||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
- **api:** update via SDK Studio ([651e937](https://github.com/sst/opencode-sdk-go/commit/651e937c334e1caba3b968e6cac865c219879519))
|
* **api:** update via SDK Studio ([651e937](https://github.com/sst/opencode-sdk-go/commit/651e937c334e1caba3b968e6cac865c219879519))
|
||||||
|
|
||||||
## 0.1.0-alpha.7 (2025-06-30)
|
## 0.1.0-alpha.7 (2025-06-30)
|
||||||
|
|
||||||
@@ -14,12 +14,13 @@ Full Changelog: [v0.1.0-alpha.6...v0.1.0-alpha.7](https://github.com/sst/opencod
|
|||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
- **api:** update via SDK Studio ([13550a5](https://github.com/sst/opencode-sdk-go/commit/13550a5c65d77325e945ed99fe0799cd1107b775))
|
* **api:** update via SDK Studio ([13550a5](https://github.com/sst/opencode-sdk-go/commit/13550a5c65d77325e945ed99fe0799cd1107b775))
|
||||||
- **api:** update via SDK Studio ([7b73730](https://github.com/sst/opencode-sdk-go/commit/7b73730c7fa62ba966dda3541c3e97b49be8d2bf))
|
* **api:** update via SDK Studio ([7b73730](https://github.com/sst/opencode-sdk-go/commit/7b73730c7fa62ba966dda3541c3e97b49be8d2bf))
|
||||||
|
|
||||||
|
|
||||||
### Chores
|
### Chores
|
||||||
|
|
||||||
- **ci:** only run for pushes and fork pull requests ([bea59b8](https://github.com/sst/opencode-sdk-go/commit/bea59b886800ef555f89c47a9256d6392ed2e53d))
|
* **ci:** only run for pushes and fork pull requests ([bea59b8](https://github.com/sst/opencode-sdk-go/commit/bea59b886800ef555f89c47a9256d6392ed2e53d))
|
||||||
|
|
||||||
## 0.1.0-alpha.6 (2025-06-28)
|
## 0.1.0-alpha.6 (2025-06-28)
|
||||||
|
|
||||||
@@ -27,7 +28,7 @@ Full Changelog: [v0.1.0-alpha.5...v0.1.0-alpha.6](https://github.com/sst/opencod
|
|||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
- don't try to deserialize as json when ResponseBodyInto is []byte ([5988d04](https://github.com/sst/opencode-sdk-go/commit/5988d04839cb78b6613057280b91b72a60fef33d))
|
* don't try to deserialize as json when ResponseBodyInto is []byte ([5988d04](https://github.com/sst/opencode-sdk-go/commit/5988d04839cb78b6613057280b91b72a60fef33d))
|
||||||
|
|
||||||
## 0.1.0-alpha.5 (2025-06-27)
|
## 0.1.0-alpha.5 (2025-06-27)
|
||||||
|
|
||||||
@@ -35,7 +36,7 @@ Full Changelog: [v0.1.0-alpha.4...v0.1.0-alpha.5](https://github.com/sst/opencod
|
|||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
- **api:** update via SDK Studio ([9e39a59](https://github.com/sst/opencode-sdk-go/commit/9e39a59b3d5d1bd5e64633732521fb28362cc70e))
|
* **api:** update via SDK Studio ([9e39a59](https://github.com/sst/opencode-sdk-go/commit/9e39a59b3d5d1bd5e64633732521fb28362cc70e))
|
||||||
|
|
||||||
## 0.1.0-alpha.4 (2025-06-27)
|
## 0.1.0-alpha.4 (2025-06-27)
|
||||||
|
|
||||||
@@ -43,7 +44,7 @@ Full Changelog: [v0.1.0-alpha.3...v0.1.0-alpha.4](https://github.com/sst/opencod
|
|||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
- **api:** update via SDK Studio ([9609d1b](https://github.com/sst/opencode-sdk-go/commit/9609d1b1db7806d00cb846c9914cb4935cdedf52))
|
* **api:** update via SDK Studio ([9609d1b](https://github.com/sst/opencode-sdk-go/commit/9609d1b1db7806d00cb846c9914cb4935cdedf52))
|
||||||
|
|
||||||
## 0.1.0-alpha.3 (2025-06-27)
|
## 0.1.0-alpha.3 (2025-06-27)
|
||||||
|
|
||||||
@@ -51,7 +52,7 @@ Full Changelog: [v0.1.0-alpha.2...v0.1.0-alpha.3](https://github.com/sst/opencod
|
|||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
- **api:** update via SDK Studio ([57f3230](https://github.com/sst/opencode-sdk-go/commit/57f32309023cc1f0f20c20d02a3907e390a71f61))
|
* **api:** update via SDK Studio ([57f3230](https://github.com/sst/opencode-sdk-go/commit/57f32309023cc1f0f20c20d02a3907e390a71f61))
|
||||||
|
|
||||||
## 0.1.0-alpha.2 (2025-06-27)
|
## 0.1.0-alpha.2 (2025-06-27)
|
||||||
|
|
||||||
@@ -59,7 +60,7 @@ Full Changelog: [v0.1.0-alpha.1...v0.1.0-alpha.2](https://github.com/sst/opencod
|
|||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
- **api:** update via SDK Studio ([a766f1c](https://github.com/sst/opencode-sdk-go/commit/a766f1c54f02bbc1380151b0e22d97cc2c5892e6))
|
* **api:** update via SDK Studio ([a766f1c](https://github.com/sst/opencode-sdk-go/commit/a766f1c54f02bbc1380151b0e22d97cc2c5892e6))
|
||||||
|
|
||||||
## 0.1.0-alpha.1 (2025-06-27)
|
## 0.1.0-alpha.1 (2025-06-27)
|
||||||
|
|
||||||
@@ -67,6 +68,6 @@ Full Changelog: [v0.0.1-alpha.0...v0.1.0-alpha.1](https://github.com/sst/opencod
|
|||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
- **api:** update via SDK Studio ([27b7376](https://github.com/sst/opencode-sdk-go/commit/27b7376310466ee17a63f2104f546b53a2b8361a))
|
* **api:** update via SDK Studio ([27b7376](https://github.com/sst/opencode-sdk-go/commit/27b7376310466ee17a63f2104f546b53a2b8361a))
|
||||||
- **api:** update via SDK Studio ([0a73e04](https://github.com/sst/opencode-sdk-go/commit/0a73e04c23c90b2061611edaa8fd6282dc0ce397))
|
* **api:** update via SDK Studio ([0a73e04](https://github.com/sst/opencode-sdk-go/commit/0a73e04c23c90b2061611edaa8fd6282dc0ce397))
|
||||||
- **api:** update via SDK Studio ([9b7883a](https://github.com/sst/opencode-sdk-go/commit/9b7883a144eeac526d9d04538e0876a9d18bb844))
|
* **api:** update via SDK Studio ([9b7883a](https://github.com/sst/opencode-sdk-go/commit/9b7883a144eeac526d9d04538e0876a9d18bb844))
|
||||||
|
|||||||
@@ -9,6 +9,15 @@ import (
|
|||||||
|
|
||||||
type Error = apierror.Error
|
type Error = apierror.Error
|
||||||
|
|
||||||
|
// This is an alias to an internal type.
|
||||||
|
type MessageAbortedError = shared.MessageAbortedError
|
||||||
|
|
||||||
|
// This is an alias to an internal type.
|
||||||
|
type MessageAbortedErrorName = shared.MessageAbortedErrorName
|
||||||
|
|
||||||
|
// This is an alias to an internal value.
|
||||||
|
const MessageAbortedErrorNameMessageAbortedError = shared.MessageAbortedErrorNameMessageAbortedError
|
||||||
|
|
||||||
// This is an alias to an internal type.
|
// This is an alias to an internal type.
|
||||||
type ProviderAuthError = shared.ProviderAuthError
|
type ProviderAuthError = shared.ProviderAuthError
|
||||||
|
|
||||||
|
|||||||
+11
-6
@@ -1,5 +1,6 @@
|
|||||||
# Shared Response Types
|
# Shared Response Types
|
||||||
|
|
||||||
|
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go/shared">shared</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go/shared#MessageAbortedError">MessageAbortedError</a>
|
||||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go/shared">shared</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go/shared#ProviderAuthError">ProviderAuthError</a>
|
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go/shared">shared</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go/shared#ProviderAuthError">ProviderAuthError</a>
|
||||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go/shared">shared</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go/shared#UnknownError">UnknownError</a>
|
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go/shared">shared</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go/shared#UnknownError">UnknownError</a>
|
||||||
|
|
||||||
@@ -18,36 +19,40 @@ Methods:
|
|||||||
Response Types:
|
Response Types:
|
||||||
|
|
||||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#App">App</a>
|
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#App">App</a>
|
||||||
|
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#LogLevel">LogLevel</a>
|
||||||
|
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Mode">Mode</a>
|
||||||
|
|
||||||
Methods:
|
Methods:
|
||||||
|
|
||||||
- <code title="get /app">client.App.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppService.Get">Get</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#App">App</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
- <code title="get /app">client.App.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppService.Get">Get</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#App">App</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||||
- <code title="post /app/init">client.App.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppService.Init">Init</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
- <code title="post /app/init">client.App.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppService.Init">Init</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||||
|
- <code title="post /log">client.App.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppService.Log">Log</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppLogParams">AppLogParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||||
|
- <code title="get /mode">client.App.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppService.Modes">Modes</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Mode">Mode</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||||
|
|
||||||
# Find
|
# Find
|
||||||
|
|
||||||
Response Types:
|
Response Types:
|
||||||
|
|
||||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindSymbolsResponse">FindSymbolsResponse</a>
|
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Match">Match</a>
|
||||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindTextResponse">FindTextResponse</a>
|
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Symbol">Symbol</a>
|
||||||
|
|
||||||
Methods:
|
Methods:
|
||||||
|
|
||||||
- <code title="get /find/file">client.Find.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindService.Files">Files</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindFilesParams">FindFilesParams</a>) ([]<a href="https://pkg.go.dev/builtin#string">string</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
- <code title="get /find/file">client.Find.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindService.Files">Files</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindFilesParams">FindFilesParams</a>) ([]<a href="https://pkg.go.dev/builtin#string">string</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||||
- <code title="get /find/symbol">client.Find.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindService.Symbols">Symbols</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindSymbolsParams">FindSymbolsParams</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindSymbolsResponse">FindSymbolsResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
- <code title="get /find/symbol">client.Find.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindService.Symbols">Symbols</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindSymbolsParams">FindSymbolsParams</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Symbol">Symbol</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||||
- <code title="get /find">client.Find.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindService.Text">Text</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindTextParams">FindTextParams</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindTextResponse">FindTextResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
- <code title="get /find">client.Find.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindService.Text">Text</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindTextParams">FindTextParams</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Match">Match</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||||
|
|
||||||
# File
|
# File
|
||||||
|
|
||||||
Response Types:
|
Response Types:
|
||||||
|
|
||||||
|
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#File">File</a>
|
||||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileReadResponse">FileReadResponse</a>
|
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileReadResponse">FileReadResponse</a>
|
||||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileStatusResponse">FileStatusResponse</a>
|
|
||||||
|
|
||||||
Methods:
|
Methods:
|
||||||
|
|
||||||
- <code title="get /file">client.File.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileService.Read">Read</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileReadParams">FileReadParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileReadResponse">FileReadResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
- <code title="get /file">client.File.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileService.Read">Read</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileReadParams">FileReadParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileReadResponse">FileReadResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||||
- <code title="get /file/status">client.File.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileService.Status">Status</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileStatusResponse">FileStatusResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
- <code title="get /file/status">client.File.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileService.Status">Status</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#File">File</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||||
|
|
||||||
# Config
|
# Config
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/sst/opencode-sdk-go/internal/apijson"
|
"github.com/sst/opencode-sdk-go/internal/apijson"
|
||||||
|
"github.com/sst/opencode-sdk-go/internal/param"
|
||||||
"github.com/sst/opencode-sdk-go/internal/requestconfig"
|
"github.com/sst/opencode-sdk-go/internal/requestconfig"
|
||||||
"github.com/sst/opencode-sdk-go/option"
|
"github.com/sst/opencode-sdk-go/option"
|
||||||
)
|
)
|
||||||
@@ -46,6 +47,22 @@ func (r *AppService) Init(ctx context.Context, opts ...option.RequestOption) (re
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Write a log entry to the server logs
|
||||||
|
func (r *AppService) Log(ctx context.Context, body AppLogParams, opts ...option.RequestOption) (res *bool, err error) {
|
||||||
|
opts = append(r.Options[:], opts...)
|
||||||
|
path := "log"
|
||||||
|
err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all modes
|
||||||
|
func (r *AppService) Modes(ctx context.Context, opts ...option.RequestOption) (res *[]Mode, err error) {
|
||||||
|
opts = append(r.Options[:], opts...)
|
||||||
|
path := "mode"
|
||||||
|
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
Git bool `json:"git,required"`
|
Git bool `json:"git,required"`
|
||||||
Hostname string `json:"hostname,required"`
|
Hostname string `json:"hostname,required"`
|
||||||
@@ -121,3 +138,102 @@ func (r *AppTime) UnmarshalJSON(data []byte) (err error) {
|
|||||||
func (r appTimeJSON) RawJSON() string {
|
func (r appTimeJSON) RawJSON() string {
|
||||||
return r.raw
|
return r.raw
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log level
|
||||||
|
type LogLevel string
|
||||||
|
|
||||||
|
const (
|
||||||
|
LogLevelDebug LogLevel = "DEBUG"
|
||||||
|
LogLevelInfo LogLevel = "INFO"
|
||||||
|
LogLevelWarn LogLevel = "WARN"
|
||||||
|
LogLevelError LogLevel = "ERROR"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r LogLevel) IsKnown() bool {
|
||||||
|
switch r {
|
||||||
|
case LogLevelDebug, LogLevelInfo, LogLevelWarn, LogLevelError:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mode struct {
|
||||||
|
Name string `json:"name,required"`
|
||||||
|
Tools map[string]bool `json:"tools,required"`
|
||||||
|
Model ModeModel `json:"model"`
|
||||||
|
Prompt string `json:"prompt"`
|
||||||
|
JSON modeJSON `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// modeJSON contains the JSON metadata for the struct [Mode]
|
||||||
|
type modeJSON struct {
|
||||||
|
Name apijson.Field
|
||||||
|
Tools apijson.Field
|
||||||
|
Model apijson.Field
|
||||||
|
Prompt apijson.Field
|
||||||
|
raw string
|
||||||
|
ExtraFields map[string]apijson.Field
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Mode) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
return apijson.UnmarshalRoot(data, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r modeJSON) RawJSON() string {
|
||||||
|
return r.raw
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModeModel struct {
|
||||||
|
ModelID string `json:"modelID,required"`
|
||||||
|
ProviderID string `json:"providerID,required"`
|
||||||
|
JSON modeModelJSON `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// modeModelJSON contains the JSON metadata for the struct [ModeModel]
|
||||||
|
type modeModelJSON struct {
|
||||||
|
ModelID apijson.Field
|
||||||
|
ProviderID apijson.Field
|
||||||
|
raw string
|
||||||
|
ExtraFields map[string]apijson.Field
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ModeModel) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
return apijson.UnmarshalRoot(data, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r modeModelJSON) RawJSON() string {
|
||||||
|
return r.raw
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppLogParams struct {
|
||||||
|
// Log level
|
||||||
|
Level param.Field[AppLogParamsLevel] `json:"level,required"`
|
||||||
|
// Log message
|
||||||
|
Message param.Field[string] `json:"message,required"`
|
||||||
|
// Service name for the log entry
|
||||||
|
Service param.Field[string] `json:"service,required"`
|
||||||
|
// Additional metadata for the log entry
|
||||||
|
Extra param.Field[map[string]interface{}] `json:"extra"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r AppLogParams) MarshalJSON() (data []byte, err error) {
|
||||||
|
return apijson.MarshalRoot(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log level
|
||||||
|
type AppLogParamsLevel string
|
||||||
|
|
||||||
|
const (
|
||||||
|
AppLogParamsLevelDebug AppLogParamsLevel = "debug"
|
||||||
|
AppLogParamsLevelInfo AppLogParamsLevel = "info"
|
||||||
|
AppLogParamsLevelError AppLogParamsLevel = "error"
|
||||||
|
AppLogParamsLevelWarn AppLogParamsLevel = "warn"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r AppLogParamsLevel) IsKnown() bool {
|
||||||
|
switch r {
|
||||||
|
case AppLogParamsLevelDebug, AppLogParamsLevelInfo, AppLogParamsLevelError, AppLogParamsLevelWarn:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -56,3 +56,54 @@ func TestAppInit(t *testing.T) {
|
|||||||
t.Fatalf("err should be nil: %s", err.Error())
|
t.Fatalf("err should be nil: %s", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAppLogWithOptionalParams(t *testing.T) {
|
||||||
|
t.Skip("skipped: tests are disabled for the time being")
|
||||||
|
baseURL := "http://localhost:4010"
|
||||||
|
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
|
||||||
|
baseURL = envURL
|
||||||
|
}
|
||||||
|
if !testutil.CheckTestServer(t, baseURL) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
client := opencode.NewClient(
|
||||||
|
option.WithBaseURL(baseURL),
|
||||||
|
)
|
||||||
|
_, err := client.App.Log(context.TODO(), opencode.AppLogParams{
|
||||||
|
Level: opencode.F(opencode.AppLogParamsLevelDebug),
|
||||||
|
Message: opencode.F("message"),
|
||||||
|
Service: opencode.F("service"),
|
||||||
|
Extra: opencode.F(map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
var apierr *opencode.Error
|
||||||
|
if errors.As(err, &apierr) {
|
||||||
|
t.Log(string(apierr.DumpRequest(true)))
|
||||||
|
}
|
||||||
|
t.Fatalf("err should be nil: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppModes(t *testing.T) {
|
||||||
|
t.Skip("skipped: tests are disabled for the time being")
|
||||||
|
baseURL := "http://localhost:4010"
|
||||||
|
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
|
||||||
|
baseURL = envURL
|
||||||
|
}
|
||||||
|
if !testutil.CheckTestServer(t, baseURL) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
client := opencode.NewClient(
|
||||||
|
option.WithBaseURL(baseURL),
|
||||||
|
)
|
||||||
|
_, err := client.App.Modes(context.TODO())
|
||||||
|
if err != nil {
|
||||||
|
var apierr *opencode.Error
|
||||||
|
if errors.As(err, &apierr) {
|
||||||
|
t.Log(string(apierr.DumpRequest(true)))
|
||||||
|
}
|
||||||
|
t.Fatalf("err should be nil: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+134
-37
@@ -62,8 +62,11 @@ type Config struct {
|
|||||||
Instructions []string `json:"instructions"`
|
Instructions []string `json:"instructions"`
|
||||||
// Custom keybind configurations
|
// Custom keybind configurations
|
||||||
Keybinds Keybinds `json:"keybinds"`
|
Keybinds Keybinds `json:"keybinds"`
|
||||||
|
// Minimum log level to write to log files
|
||||||
|
LogLevel LogLevel `json:"log_level"`
|
||||||
// MCP (Model Context Protocol) server configurations
|
// MCP (Model Context Protocol) server configurations
|
||||||
Mcp map[string]ConfigMcp `json:"mcp"`
|
Mcp map[string]ConfigMcp `json:"mcp"`
|
||||||
|
Mode ConfigMode `json:"mode"`
|
||||||
// Model to use in the format of provider/model, eg anthropic/claude-2
|
// Model to use in the format of provider/model, eg anthropic/claude-2
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
// Custom provider configurations and model overrides
|
// Custom provider configurations and model overrides
|
||||||
@@ -82,7 +85,9 @@ type configJSON struct {
|
|||||||
Experimental apijson.Field
|
Experimental apijson.Field
|
||||||
Instructions apijson.Field
|
Instructions apijson.Field
|
||||||
Keybinds apijson.Field
|
Keybinds apijson.Field
|
||||||
|
LogLevel apijson.Field
|
||||||
Mcp apijson.Field
|
Mcp apijson.Field
|
||||||
|
Mode apijson.Field
|
||||||
Model apijson.Field
|
Model apijson.Field
|
||||||
Provider apijson.Field
|
Provider apijson.Field
|
||||||
Theme apijson.Field
|
Theme apijson.Field
|
||||||
@@ -273,6 +278,77 @@ func (r ConfigMcpType) IsKnown() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ConfigMode struct {
|
||||||
|
Build ConfigModeBuild `json:"build"`
|
||||||
|
Plan ConfigModePlan `json:"plan"`
|
||||||
|
ExtraFields map[string]ConfigMode `json:"-,extras"`
|
||||||
|
JSON configModeJSON `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// configModeJSON contains the JSON metadata for the struct [ConfigMode]
|
||||||
|
type configModeJSON struct {
|
||||||
|
Build apijson.Field
|
||||||
|
Plan apijson.Field
|
||||||
|
raw string
|
||||||
|
ExtraFields map[string]apijson.Field
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ConfigMode) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
return apijson.UnmarshalRoot(data, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r configModeJSON) RawJSON() string {
|
||||||
|
return r.raw
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfigModeBuild struct {
|
||||||
|
Model string `json:"model"`
|
||||||
|
Prompt string `json:"prompt"`
|
||||||
|
Tools map[string]bool `json:"tools"`
|
||||||
|
JSON configModeBuildJSON `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// configModeBuildJSON contains the JSON metadata for the struct [ConfigModeBuild]
|
||||||
|
type configModeBuildJSON struct {
|
||||||
|
Model apijson.Field
|
||||||
|
Prompt apijson.Field
|
||||||
|
Tools apijson.Field
|
||||||
|
raw string
|
||||||
|
ExtraFields map[string]apijson.Field
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ConfigModeBuild) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
return apijson.UnmarshalRoot(data, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r configModeBuildJSON) RawJSON() string {
|
||||||
|
return r.raw
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfigModePlan struct {
|
||||||
|
Model string `json:"model"`
|
||||||
|
Prompt string `json:"prompt"`
|
||||||
|
Tools map[string]bool `json:"tools"`
|
||||||
|
JSON configModePlanJSON `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// configModePlanJSON contains the JSON metadata for the struct [ConfigModePlan]
|
||||||
|
type configModePlanJSON struct {
|
||||||
|
Model apijson.Field
|
||||||
|
Prompt apijson.Field
|
||||||
|
Tools apijson.Field
|
||||||
|
raw string
|
||||||
|
ExtraFields map[string]apijson.Field
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ConfigModePlan) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
return apijson.UnmarshalRoot(data, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r configModePlanJSON) RawJSON() string {
|
||||||
|
return r.raw
|
||||||
|
}
|
||||||
|
|
||||||
type ConfigProvider struct {
|
type ConfigProvider struct {
|
||||||
Models map[string]ConfigProviderModel `json:"models,required"`
|
Models map[string]ConfigProviderModel `json:"models,required"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
@@ -396,82 +472,101 @@ func (r configProviderModelsLimitJSON) RawJSON() string {
|
|||||||
|
|
||||||
type Keybinds struct {
|
type Keybinds struct {
|
||||||
// Exit the application
|
// Exit the application
|
||||||
AppExit string `json:"app_exit"`
|
AppExit string `json:"app_exit,required"`
|
||||||
// Open external editor
|
|
||||||
EditorOpen string `json:"editor_open"`
|
|
||||||
// Show help dialog
|
// Show help dialog
|
||||||
Help string `json:"help"`
|
AppHelp string `json:"app_help,required"`
|
||||||
// Navigate to next history item
|
// Open external editor
|
||||||
HistoryNext string `json:"history_next"`
|
EditorOpen string `json:"editor_open,required"`
|
||||||
// Navigate to previous history item
|
// Close file
|
||||||
HistoryPrevious string `json:"history_previous"`
|
FileClose string `json:"file_close,required"`
|
||||||
|
// Split/unified diff
|
||||||
|
FileDiffToggle string `json:"file_diff_toggle,required"`
|
||||||
|
// List files
|
||||||
|
FileList string `json:"file_list,required"`
|
||||||
|
// Search file
|
||||||
|
FileSearch string `json:"file_search,required"`
|
||||||
// Clear input field
|
// Clear input field
|
||||||
InputClear string `json:"input_clear"`
|
InputClear string `json:"input_clear,required"`
|
||||||
// Insert newline in input
|
// Insert newline in input
|
||||||
InputNewline string `json:"input_newline"`
|
InputNewline string `json:"input_newline,required"`
|
||||||
// Paste from clipboard
|
// Paste from clipboard
|
||||||
InputPaste string `json:"input_paste"`
|
InputPaste string `json:"input_paste,required"`
|
||||||
// Submit input
|
// Submit input
|
||||||
InputSubmit string `json:"input_submit"`
|
InputSubmit string `json:"input_submit,required"`
|
||||||
// Leader key for keybind combinations
|
// Leader key for keybind combinations
|
||||||
Leader string `json:"leader"`
|
Leader string `json:"leader,required"`
|
||||||
|
// Copy message
|
||||||
|
MessagesCopy string `json:"messages_copy,required"`
|
||||||
// Navigate to first message
|
// Navigate to first message
|
||||||
MessagesFirst string `json:"messages_first"`
|
MessagesFirst string `json:"messages_first,required"`
|
||||||
// Scroll messages down by half page
|
// Scroll messages down by half page
|
||||||
MessagesHalfPageDown string `json:"messages_half_page_down"`
|
MessagesHalfPageDown string `json:"messages_half_page_down,required"`
|
||||||
// Scroll messages up by half page
|
// Scroll messages up by half page
|
||||||
MessagesHalfPageUp string `json:"messages_half_page_up"`
|
MessagesHalfPageUp string `json:"messages_half_page_up,required"`
|
||||||
// Navigate to last message
|
// Navigate to last message
|
||||||
MessagesLast string `json:"messages_last"`
|
MessagesLast string `json:"messages_last,required"`
|
||||||
|
// Toggle layout
|
||||||
|
MessagesLayoutToggle string `json:"messages_layout_toggle,required"`
|
||||||
// Navigate to next message
|
// Navigate to next message
|
||||||
MessagesNext string `json:"messages_next"`
|
MessagesNext string `json:"messages_next,required"`
|
||||||
// Scroll messages down by one page
|
// Scroll messages down by one page
|
||||||
MessagesPageDown string `json:"messages_page_down"`
|
MessagesPageDown string `json:"messages_page_down,required"`
|
||||||
// Scroll messages up by one page
|
// Scroll messages up by one page
|
||||||
MessagesPageUp string `json:"messages_page_up"`
|
MessagesPageUp string `json:"messages_page_up,required"`
|
||||||
// Navigate to previous message
|
// Navigate to previous message
|
||||||
MessagesPrevious string `json:"messages_previous"`
|
MessagesPrevious string `json:"messages_previous,required"`
|
||||||
|
// Revert message
|
||||||
|
MessagesRevert string `json:"messages_revert,required"`
|
||||||
// List available models
|
// List available models
|
||||||
ModelList string `json:"model_list"`
|
ModelList string `json:"model_list,required"`
|
||||||
// Initialize project configuration
|
// Create/update AGENTS.md
|
||||||
ProjectInit string `json:"project_init"`
|
ProjectInit string `json:"project_init,required"`
|
||||||
// Toggle compact mode for session
|
// Compact the session
|
||||||
SessionCompact string `json:"session_compact"`
|
SessionCompact string `json:"session_compact,required"`
|
||||||
// Interrupt current session
|
// Interrupt current session
|
||||||
SessionInterrupt string `json:"session_interrupt"`
|
SessionInterrupt string `json:"session_interrupt,required"`
|
||||||
// List all sessions
|
// List all sessions
|
||||||
SessionList string `json:"session_list"`
|
SessionList string `json:"session_list,required"`
|
||||||
// Create a new session
|
// Create a new session
|
||||||
SessionNew string `json:"session_new"`
|
SessionNew string `json:"session_new,required"`
|
||||||
// Share current session
|
// Share current session
|
||||||
SessionShare string `json:"session_share"`
|
SessionShare string `json:"session_share,required"`
|
||||||
|
// Unshare current session
|
||||||
|
SessionUnshare string `json:"session_unshare,required"`
|
||||||
|
// Switch mode
|
||||||
|
SwitchMode string `json:"switch_mode,required"`
|
||||||
// List available themes
|
// List available themes
|
||||||
ThemeList string `json:"theme_list"`
|
ThemeList string `json:"theme_list,required"`
|
||||||
// Show tool details
|
// Toggle tool details
|
||||||
ToolDetails string `json:"tool_details"`
|
ToolDetails string `json:"tool_details,required"`
|
||||||
JSON keybindsJSON `json:"-"`
|
JSON keybindsJSON `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// keybindsJSON contains the JSON metadata for the struct [Keybinds]
|
// keybindsJSON contains the JSON metadata for the struct [Keybinds]
|
||||||
type keybindsJSON struct {
|
type keybindsJSON struct {
|
||||||
AppExit apijson.Field
|
AppExit apijson.Field
|
||||||
|
AppHelp apijson.Field
|
||||||
EditorOpen apijson.Field
|
EditorOpen apijson.Field
|
||||||
Help apijson.Field
|
FileClose apijson.Field
|
||||||
HistoryNext apijson.Field
|
FileDiffToggle apijson.Field
|
||||||
HistoryPrevious apijson.Field
|
FileList apijson.Field
|
||||||
|
FileSearch apijson.Field
|
||||||
InputClear apijson.Field
|
InputClear apijson.Field
|
||||||
InputNewline apijson.Field
|
InputNewline apijson.Field
|
||||||
InputPaste apijson.Field
|
InputPaste apijson.Field
|
||||||
InputSubmit apijson.Field
|
InputSubmit apijson.Field
|
||||||
Leader apijson.Field
|
Leader apijson.Field
|
||||||
|
MessagesCopy apijson.Field
|
||||||
MessagesFirst apijson.Field
|
MessagesFirst apijson.Field
|
||||||
MessagesHalfPageDown apijson.Field
|
MessagesHalfPageDown apijson.Field
|
||||||
MessagesHalfPageUp apijson.Field
|
MessagesHalfPageUp apijson.Field
|
||||||
MessagesLast apijson.Field
|
MessagesLast apijson.Field
|
||||||
|
MessagesLayoutToggle apijson.Field
|
||||||
MessagesNext apijson.Field
|
MessagesNext apijson.Field
|
||||||
MessagesPageDown apijson.Field
|
MessagesPageDown apijson.Field
|
||||||
MessagesPageUp apijson.Field
|
MessagesPageUp apijson.Field
|
||||||
MessagesPrevious apijson.Field
|
MessagesPrevious apijson.Field
|
||||||
|
MessagesRevert apijson.Field
|
||||||
ModelList apijson.Field
|
ModelList apijson.Field
|
||||||
ProjectInit apijson.Field
|
ProjectInit apijson.Field
|
||||||
SessionCompact apijson.Field
|
SessionCompact apijson.Field
|
||||||
@@ -479,6 +574,8 @@ type keybindsJSON struct {
|
|||||||
SessionList apijson.Field
|
SessionList apijson.Field
|
||||||
SessionNew apijson.Field
|
SessionNew apijson.Field
|
||||||
SessionShare apijson.Field
|
SessionShare apijson.Field
|
||||||
|
SessionUnshare apijson.Field
|
||||||
|
SwitchMode apijson.Field
|
||||||
ThemeList apijson.Field
|
ThemeList apijson.Field
|
||||||
ToolDetails apijson.Field
|
ToolDetails apijson.Field
|
||||||
raw string
|
raw string
|
||||||
|
|||||||
+94
-84
@@ -52,10 +52,10 @@ type EventListResponse struct {
|
|||||||
// [EventListResponseEventPermissionUpdatedProperties],
|
// [EventListResponseEventPermissionUpdatedProperties],
|
||||||
// [EventListResponseEventFileEditedProperties],
|
// [EventListResponseEventFileEditedProperties],
|
||||||
// [EventListResponseEventInstallationUpdatedProperties],
|
// [EventListResponseEventInstallationUpdatedProperties],
|
||||||
// [EventListResponseEventStorageWriteProperties],
|
|
||||||
// [EventListResponseEventMessageUpdatedProperties],
|
// [EventListResponseEventMessageUpdatedProperties],
|
||||||
// [EventListResponseEventMessageRemovedProperties],
|
// [EventListResponseEventMessageRemovedProperties],
|
||||||
// [EventListResponseEventMessagePartUpdatedProperties],
|
// [EventListResponseEventMessagePartUpdatedProperties],
|
||||||
|
// [EventListResponseEventStorageWriteProperties],
|
||||||
// [EventListResponseEventSessionUpdatedProperties],
|
// [EventListResponseEventSessionUpdatedProperties],
|
||||||
// [EventListResponseEventSessionDeletedProperties],
|
// [EventListResponseEventSessionDeletedProperties],
|
||||||
// [EventListResponseEventSessionIdleProperties],
|
// [EventListResponseEventSessionIdleProperties],
|
||||||
@@ -96,11 +96,11 @@ func (r *EventListResponse) UnmarshalJSON(data []byte) (err error) {
|
|||||||
// [EventListResponseEventLspClientDiagnostics],
|
// [EventListResponseEventLspClientDiagnostics],
|
||||||
// [EventListResponseEventPermissionUpdated], [EventListResponseEventFileEdited],
|
// [EventListResponseEventPermissionUpdated], [EventListResponseEventFileEdited],
|
||||||
// [EventListResponseEventInstallationUpdated],
|
// [EventListResponseEventInstallationUpdated],
|
||||||
// [EventListResponseEventStorageWrite], [EventListResponseEventMessageUpdated],
|
// [EventListResponseEventMessageUpdated], [EventListResponseEventMessageRemoved],
|
||||||
// [EventListResponseEventMessageRemoved],
|
|
||||||
// [EventListResponseEventMessagePartUpdated],
|
// [EventListResponseEventMessagePartUpdated],
|
||||||
// [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted],
|
// [EventListResponseEventStorageWrite], [EventListResponseEventSessionUpdated],
|
||||||
// [EventListResponseEventSessionIdle], [EventListResponseEventSessionError],
|
// [EventListResponseEventSessionDeleted], [EventListResponseEventSessionIdle],
|
||||||
|
// [EventListResponseEventSessionError],
|
||||||
// [EventListResponseEventFileWatcherUpdated].
|
// [EventListResponseEventFileWatcherUpdated].
|
||||||
func (r EventListResponse) AsUnion() EventListResponseUnion {
|
func (r EventListResponse) AsUnion() EventListResponseUnion {
|
||||||
return r.union
|
return r.union
|
||||||
@@ -109,11 +109,11 @@ func (r EventListResponse) AsUnion() EventListResponseUnion {
|
|||||||
// Union satisfied by [EventListResponseEventLspClientDiagnostics],
|
// Union satisfied by [EventListResponseEventLspClientDiagnostics],
|
||||||
// [EventListResponseEventPermissionUpdated], [EventListResponseEventFileEdited],
|
// [EventListResponseEventPermissionUpdated], [EventListResponseEventFileEdited],
|
||||||
// [EventListResponseEventInstallationUpdated],
|
// [EventListResponseEventInstallationUpdated],
|
||||||
// [EventListResponseEventStorageWrite], [EventListResponseEventMessageUpdated],
|
// [EventListResponseEventMessageUpdated], [EventListResponseEventMessageRemoved],
|
||||||
// [EventListResponseEventMessageRemoved],
|
|
||||||
// [EventListResponseEventMessagePartUpdated],
|
// [EventListResponseEventMessagePartUpdated],
|
||||||
// [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted],
|
// [EventListResponseEventStorageWrite], [EventListResponseEventSessionUpdated],
|
||||||
// [EventListResponseEventSessionIdle], [EventListResponseEventSessionError] or
|
// [EventListResponseEventSessionDeleted], [EventListResponseEventSessionIdle],
|
||||||
|
// [EventListResponseEventSessionError] or
|
||||||
// [EventListResponseEventFileWatcherUpdated].
|
// [EventListResponseEventFileWatcherUpdated].
|
||||||
type EventListResponseUnion interface {
|
type EventListResponseUnion interface {
|
||||||
implementsEventListResponse()
|
implementsEventListResponse()
|
||||||
@@ -143,11 +143,6 @@ func init() {
|
|||||||
Type: reflect.TypeOf(EventListResponseEventInstallationUpdated{}),
|
Type: reflect.TypeOf(EventListResponseEventInstallationUpdated{}),
|
||||||
DiscriminatorValue: "installation.updated",
|
DiscriminatorValue: "installation.updated",
|
||||||
},
|
},
|
||||||
apijson.UnionVariant{
|
|
||||||
TypeFilter: gjson.JSON,
|
|
||||||
Type: reflect.TypeOf(EventListResponseEventStorageWrite{}),
|
|
||||||
DiscriminatorValue: "storage.write",
|
|
||||||
},
|
|
||||||
apijson.UnionVariant{
|
apijson.UnionVariant{
|
||||||
TypeFilter: gjson.JSON,
|
TypeFilter: gjson.JSON,
|
||||||
Type: reflect.TypeOf(EventListResponseEventMessageUpdated{}),
|
Type: reflect.TypeOf(EventListResponseEventMessageUpdated{}),
|
||||||
@@ -163,6 +158,11 @@ func init() {
|
|||||||
Type: reflect.TypeOf(EventListResponseEventMessagePartUpdated{}),
|
Type: reflect.TypeOf(EventListResponseEventMessagePartUpdated{}),
|
||||||
DiscriminatorValue: "message.part.updated",
|
DiscriminatorValue: "message.part.updated",
|
||||||
},
|
},
|
||||||
|
apijson.UnionVariant{
|
||||||
|
TypeFilter: gjson.JSON,
|
||||||
|
Type: reflect.TypeOf(EventListResponseEventStorageWrite{}),
|
||||||
|
DiscriminatorValue: "storage.write",
|
||||||
|
},
|
||||||
apijson.UnionVariant{
|
apijson.UnionVariant{
|
||||||
TypeFilter: gjson.JSON,
|
TypeFilter: gjson.JSON,
|
||||||
Type: reflect.TypeOf(EventListResponseEventSessionUpdated{}),
|
Type: reflect.TypeOf(EventListResponseEventSessionUpdated{}),
|
||||||
@@ -462,68 +462,6 @@ func (r EventListResponseEventInstallationUpdatedType) IsKnown() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
type EventListResponseEventStorageWrite struct {
|
|
||||||
Properties EventListResponseEventStorageWriteProperties `json:"properties,required"`
|
|
||||||
Type EventListResponseEventStorageWriteType `json:"type,required"`
|
|
||||||
JSON eventListResponseEventStorageWriteJSON `json:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// eventListResponseEventStorageWriteJSON contains the JSON metadata for the struct
|
|
||||||
// [EventListResponseEventStorageWrite]
|
|
||||||
type eventListResponseEventStorageWriteJSON struct {
|
|
||||||
Properties apijson.Field
|
|
||||||
Type apijson.Field
|
|
||||||
raw string
|
|
||||||
ExtraFields map[string]apijson.Field
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *EventListResponseEventStorageWrite) UnmarshalJSON(data []byte) (err error) {
|
|
||||||
return apijson.UnmarshalRoot(data, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r eventListResponseEventStorageWriteJSON) RawJSON() string {
|
|
||||||
return r.raw
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r EventListResponseEventStorageWrite) implementsEventListResponse() {}
|
|
||||||
|
|
||||||
type EventListResponseEventStorageWriteProperties struct {
|
|
||||||
Key string `json:"key,required"`
|
|
||||||
Content interface{} `json:"content"`
|
|
||||||
JSON eventListResponseEventStorageWritePropertiesJSON `json:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// eventListResponseEventStorageWritePropertiesJSON contains the JSON metadata for
|
|
||||||
// the struct [EventListResponseEventStorageWriteProperties]
|
|
||||||
type eventListResponseEventStorageWritePropertiesJSON struct {
|
|
||||||
Key apijson.Field
|
|
||||||
Content apijson.Field
|
|
||||||
raw string
|
|
||||||
ExtraFields map[string]apijson.Field
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *EventListResponseEventStorageWriteProperties) UnmarshalJSON(data []byte) (err error) {
|
|
||||||
return apijson.UnmarshalRoot(data, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r eventListResponseEventStorageWritePropertiesJSON) RawJSON() string {
|
|
||||||
return r.raw
|
|
||||||
}
|
|
||||||
|
|
||||||
type EventListResponseEventStorageWriteType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
EventListResponseEventStorageWriteTypeStorageWrite EventListResponseEventStorageWriteType = "storage.write"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (r EventListResponseEventStorageWriteType) IsKnown() bool {
|
|
||||||
switch r {
|
|
||||||
case EventListResponseEventStorageWriteTypeStorageWrite:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
type EventListResponseEventMessageUpdated struct {
|
type EventListResponseEventMessageUpdated struct {
|
||||||
Properties EventListResponseEventMessageUpdatedProperties `json:"properties,required"`
|
Properties EventListResponseEventMessageUpdatedProperties `json:"properties,required"`
|
||||||
Type EventListResponseEventMessageUpdatedType `json:"type,required"`
|
Type EventListResponseEventMessageUpdatedType `json:"type,required"`
|
||||||
@@ -710,6 +648,68 @@ func (r EventListResponseEventMessagePartUpdatedType) IsKnown() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EventListResponseEventStorageWrite struct {
|
||||||
|
Properties EventListResponseEventStorageWriteProperties `json:"properties,required"`
|
||||||
|
Type EventListResponseEventStorageWriteType `json:"type,required"`
|
||||||
|
JSON eventListResponseEventStorageWriteJSON `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// eventListResponseEventStorageWriteJSON contains the JSON metadata for the struct
|
||||||
|
// [EventListResponseEventStorageWrite]
|
||||||
|
type eventListResponseEventStorageWriteJSON struct {
|
||||||
|
Properties apijson.Field
|
||||||
|
Type apijson.Field
|
||||||
|
raw string
|
||||||
|
ExtraFields map[string]apijson.Field
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *EventListResponseEventStorageWrite) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
return apijson.UnmarshalRoot(data, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r eventListResponseEventStorageWriteJSON) RawJSON() string {
|
||||||
|
return r.raw
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r EventListResponseEventStorageWrite) implementsEventListResponse() {}
|
||||||
|
|
||||||
|
type EventListResponseEventStorageWriteProperties struct {
|
||||||
|
Key string `json:"key,required"`
|
||||||
|
Content interface{} `json:"content"`
|
||||||
|
JSON eventListResponseEventStorageWritePropertiesJSON `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// eventListResponseEventStorageWritePropertiesJSON contains the JSON metadata for
|
||||||
|
// the struct [EventListResponseEventStorageWriteProperties]
|
||||||
|
type eventListResponseEventStorageWritePropertiesJSON struct {
|
||||||
|
Key apijson.Field
|
||||||
|
Content apijson.Field
|
||||||
|
raw string
|
||||||
|
ExtraFields map[string]apijson.Field
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *EventListResponseEventStorageWriteProperties) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
return apijson.UnmarshalRoot(data, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r eventListResponseEventStorageWritePropertiesJSON) RawJSON() string {
|
||||||
|
return r.raw
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventListResponseEventStorageWriteType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
EventListResponseEventStorageWriteTypeStorageWrite EventListResponseEventStorageWriteType = "storage.write"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r EventListResponseEventStorageWriteType) IsKnown() bool {
|
||||||
|
switch r {
|
||||||
|
case EventListResponseEventStorageWriteTypeStorageWrite:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
type EventListResponseEventSessionUpdated struct {
|
type EventListResponseEventSessionUpdated struct {
|
||||||
Properties EventListResponseEventSessionUpdatedProperties `json:"properties,required"`
|
Properties EventListResponseEventSessionUpdatedProperties `json:"properties,required"`
|
||||||
Type EventListResponseEventSessionUpdatedType `json:"type,required"`
|
Type EventListResponseEventSessionUpdatedType `json:"type,required"`
|
||||||
@@ -916,14 +916,16 @@ func (r eventListResponseEventSessionErrorJSON) RawJSON() string {
|
|||||||
func (r EventListResponseEventSessionError) implementsEventListResponse() {}
|
func (r EventListResponseEventSessionError) implementsEventListResponse() {}
|
||||||
|
|
||||||
type EventListResponseEventSessionErrorProperties struct {
|
type EventListResponseEventSessionErrorProperties struct {
|
||||||
Error EventListResponseEventSessionErrorPropertiesError `json:"error"`
|
Error EventListResponseEventSessionErrorPropertiesError `json:"error"`
|
||||||
JSON eventListResponseEventSessionErrorPropertiesJSON `json:"-"`
|
SessionID string `json:"sessionID"`
|
||||||
|
JSON eventListResponseEventSessionErrorPropertiesJSON `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// eventListResponseEventSessionErrorPropertiesJSON contains the JSON metadata for
|
// eventListResponseEventSessionErrorPropertiesJSON contains the JSON metadata for
|
||||||
// the struct [EventListResponseEventSessionErrorProperties]
|
// the struct [EventListResponseEventSessionErrorProperties]
|
||||||
type eventListResponseEventSessionErrorPropertiesJSON struct {
|
type eventListResponseEventSessionErrorPropertiesJSON struct {
|
||||||
Error apijson.Field
|
Error apijson.Field
|
||||||
|
SessionID apijson.Field
|
||||||
raw string
|
raw string
|
||||||
ExtraFields map[string]apijson.Field
|
ExtraFields map[string]apijson.Field
|
||||||
}
|
}
|
||||||
@@ -972,13 +974,15 @@ func (r *EventListResponseEventSessionErrorPropertiesError) UnmarshalJSON(data [
|
|||||||
//
|
//
|
||||||
// Possible runtime types of the union are [shared.ProviderAuthError],
|
// Possible runtime types of the union are [shared.ProviderAuthError],
|
||||||
// [shared.UnknownError],
|
// [shared.UnknownError],
|
||||||
// [EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthError].
|
// [EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthError],
|
||||||
|
// [shared.MessageAbortedError].
|
||||||
func (r EventListResponseEventSessionErrorPropertiesError) AsUnion() EventListResponseEventSessionErrorPropertiesErrorUnion {
|
func (r EventListResponseEventSessionErrorPropertiesError) AsUnion() EventListResponseEventSessionErrorPropertiesErrorUnion {
|
||||||
return r.union
|
return r.union
|
||||||
}
|
}
|
||||||
|
|
||||||
// Union satisfied by [shared.ProviderAuthError], [shared.UnknownError] or
|
// Union satisfied by [shared.ProviderAuthError], [shared.UnknownError],
|
||||||
// [EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthError].
|
// [EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthError] or
|
||||||
|
// [shared.MessageAbortedError].
|
||||||
type EventListResponseEventSessionErrorPropertiesErrorUnion interface {
|
type EventListResponseEventSessionErrorPropertiesErrorUnion interface {
|
||||||
ImplementsEventListResponseEventSessionErrorPropertiesError()
|
ImplementsEventListResponseEventSessionErrorPropertiesError()
|
||||||
}
|
}
|
||||||
@@ -1002,6 +1006,11 @@ func init() {
|
|||||||
Type: reflect.TypeOf(EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthError{}),
|
Type: reflect.TypeOf(EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthError{}),
|
||||||
DiscriminatorValue: "MessageOutputLengthError",
|
DiscriminatorValue: "MessageOutputLengthError",
|
||||||
},
|
},
|
||||||
|
apijson.UnionVariant{
|
||||||
|
TypeFilter: gjson.JSON,
|
||||||
|
Type: reflect.TypeOf(shared.MessageAbortedError{}),
|
||||||
|
DiscriminatorValue: "MessageAbortedError",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1052,11 +1061,12 @@ const (
|
|||||||
EventListResponseEventSessionErrorPropertiesErrorNameProviderAuthError EventListResponseEventSessionErrorPropertiesErrorName = "ProviderAuthError"
|
EventListResponseEventSessionErrorPropertiesErrorNameProviderAuthError EventListResponseEventSessionErrorPropertiesErrorName = "ProviderAuthError"
|
||||||
EventListResponseEventSessionErrorPropertiesErrorNameUnknownError EventListResponseEventSessionErrorPropertiesErrorName = "UnknownError"
|
EventListResponseEventSessionErrorPropertiesErrorNameUnknownError EventListResponseEventSessionErrorPropertiesErrorName = "UnknownError"
|
||||||
EventListResponseEventSessionErrorPropertiesErrorNameMessageOutputLengthError EventListResponseEventSessionErrorPropertiesErrorName = "MessageOutputLengthError"
|
EventListResponseEventSessionErrorPropertiesErrorNameMessageOutputLengthError EventListResponseEventSessionErrorPropertiesErrorName = "MessageOutputLengthError"
|
||||||
|
EventListResponseEventSessionErrorPropertiesErrorNameMessageAbortedError EventListResponseEventSessionErrorPropertiesErrorName = "MessageAbortedError"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r EventListResponseEventSessionErrorPropertiesErrorName) IsKnown() bool {
|
func (r EventListResponseEventSessionErrorPropertiesErrorName) IsKnown() bool {
|
||||||
switch r {
|
switch r {
|
||||||
case EventListResponseEventSessionErrorPropertiesErrorNameProviderAuthError, EventListResponseEventSessionErrorPropertiesErrorNameUnknownError, EventListResponseEventSessionErrorPropertiesErrorNameMessageOutputLengthError:
|
case EventListResponseEventSessionErrorPropertiesErrorNameProviderAuthError, EventListResponseEventSessionErrorPropertiesErrorNameUnknownError, EventListResponseEventSessionErrorPropertiesErrorNameMessageOutputLengthError, EventListResponseEventSessionErrorPropertiesErrorNameMessageAbortedError:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -1160,10 +1170,10 @@ const (
|
|||||||
EventListResponseTypePermissionUpdated EventListResponseType = "permission.updated"
|
EventListResponseTypePermissionUpdated EventListResponseType = "permission.updated"
|
||||||
EventListResponseTypeFileEdited EventListResponseType = "file.edited"
|
EventListResponseTypeFileEdited EventListResponseType = "file.edited"
|
||||||
EventListResponseTypeInstallationUpdated EventListResponseType = "installation.updated"
|
EventListResponseTypeInstallationUpdated EventListResponseType = "installation.updated"
|
||||||
EventListResponseTypeStorageWrite EventListResponseType = "storage.write"
|
|
||||||
EventListResponseTypeMessageUpdated EventListResponseType = "message.updated"
|
EventListResponseTypeMessageUpdated EventListResponseType = "message.updated"
|
||||||
EventListResponseTypeMessageRemoved EventListResponseType = "message.removed"
|
EventListResponseTypeMessageRemoved EventListResponseType = "message.removed"
|
||||||
EventListResponseTypeMessagePartUpdated EventListResponseType = "message.part.updated"
|
EventListResponseTypeMessagePartUpdated EventListResponseType = "message.part.updated"
|
||||||
|
EventListResponseTypeStorageWrite EventListResponseType = "storage.write"
|
||||||
EventListResponseTypeSessionUpdated EventListResponseType = "session.updated"
|
EventListResponseTypeSessionUpdated EventListResponseType = "session.updated"
|
||||||
EventListResponseTypeSessionDeleted EventListResponseType = "session.deleted"
|
EventListResponseTypeSessionDeleted EventListResponseType = "session.deleted"
|
||||||
EventListResponseTypeSessionIdle EventListResponseType = "session.idle"
|
EventListResponseTypeSessionIdle EventListResponseType = "session.idle"
|
||||||
@@ -1173,7 +1183,7 @@ const (
|
|||||||
|
|
||||||
func (r EventListResponseType) IsKnown() bool {
|
func (r EventListResponseType) IsKnown() bool {
|
||||||
switch r {
|
switch r {
|
||||||
case EventListResponseTypeLspClientDiagnostics, EventListResponseTypePermissionUpdated, EventListResponseTypeFileEdited, EventListResponseTypeInstallationUpdated, EventListResponseTypeStorageWrite, EventListResponseTypeMessageUpdated, EventListResponseTypeMessageRemoved, EventListResponseTypeMessagePartUpdated, EventListResponseTypeSessionUpdated, EventListResponseTypeSessionDeleted, EventListResponseTypeSessionIdle, EventListResponseTypeSessionError, EventListResponseTypeFileWatcherUpdated:
|
case EventListResponseTypeLspClientDiagnostics, EventListResponseTypePermissionUpdated, EventListResponseTypeFileEdited, EventListResponseTypeInstallationUpdated, EventListResponseTypeMessageUpdated, EventListResponseTypeMessageRemoved, EventListResponseTypeMessagePartUpdated, EventListResponseTypeStorageWrite, EventListResponseTypeSessionUpdated, EventListResponseTypeSessionDeleted, EventListResponseTypeSessionIdle, EventListResponseTypeSessionError, EventListResponseTypeFileWatcherUpdated:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|||||||
+43
-44
@@ -42,13 +42,55 @@ func (r *FileService) Read(ctx context.Context, query FileReadParams, opts ...op
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get file status
|
// Get file status
|
||||||
func (r *FileService) Status(ctx context.Context, opts ...option.RequestOption) (res *[]FileStatusResponse, err error) {
|
func (r *FileService) Status(ctx context.Context, opts ...option.RequestOption) (res *[]File, err error) {
|
||||||
opts = append(r.Options[:], opts...)
|
opts = append(r.Options[:], opts...)
|
||||||
path := "file/status"
|
path := "file/status"
|
||||||
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
|
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
Added int64 `json:"added,required"`
|
||||||
|
Path string `json:"path,required"`
|
||||||
|
Removed int64 `json:"removed,required"`
|
||||||
|
Status FileStatus `json:"status,required"`
|
||||||
|
JSON fileJSON `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// fileJSON contains the JSON metadata for the struct [File]
|
||||||
|
type fileJSON struct {
|
||||||
|
Added apijson.Field
|
||||||
|
Path apijson.Field
|
||||||
|
Removed apijson.Field
|
||||||
|
Status apijson.Field
|
||||||
|
raw string
|
||||||
|
ExtraFields map[string]apijson.Field
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *File) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
return apijson.UnmarshalRoot(data, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r fileJSON) RawJSON() string {
|
||||||
|
return r.raw
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
FileStatusAdded FileStatus = "added"
|
||||||
|
FileStatusDeleted FileStatus = "deleted"
|
||||||
|
FileStatusModified FileStatus = "modified"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r FileStatus) IsKnown() bool {
|
||||||
|
switch r {
|
||||||
|
case FileStatusAdded, FileStatusDeleted, FileStatusModified:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
type FileReadResponse struct {
|
type FileReadResponse struct {
|
||||||
Content string `json:"content,required"`
|
Content string `json:"content,required"`
|
||||||
Type FileReadResponseType `json:"type,required"`
|
Type FileReadResponseType `json:"type,required"`
|
||||||
@@ -87,49 +129,6 @@ func (r FileReadResponseType) IsKnown() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
type FileStatusResponse struct {
|
|
||||||
Added int64 `json:"added,required"`
|
|
||||||
File string `json:"file,required"`
|
|
||||||
Removed int64 `json:"removed,required"`
|
|
||||||
Status FileStatusResponseStatus `json:"status,required"`
|
|
||||||
JSON fileStatusResponseJSON `json:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// fileStatusResponseJSON contains the JSON metadata for the struct
|
|
||||||
// [FileStatusResponse]
|
|
||||||
type fileStatusResponseJSON struct {
|
|
||||||
Added apijson.Field
|
|
||||||
File apijson.Field
|
|
||||||
Removed apijson.Field
|
|
||||||
Status apijson.Field
|
|
||||||
raw string
|
|
||||||
ExtraFields map[string]apijson.Field
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FileStatusResponse) UnmarshalJSON(data []byte) (err error) {
|
|
||||||
return apijson.UnmarshalRoot(data, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r fileStatusResponseJSON) RawJSON() string {
|
|
||||||
return r.raw
|
|
||||||
}
|
|
||||||
|
|
||||||
type FileStatusResponseStatus string
|
|
||||||
|
|
||||||
const (
|
|
||||||
FileStatusResponseStatusAdded FileStatusResponseStatus = "added"
|
|
||||||
FileStatusResponseStatusDeleted FileStatusResponseStatus = "deleted"
|
|
||||||
FileStatusResponseStatusModified FileStatusResponseStatus = "modified"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (r FileStatusResponseStatus) IsKnown() bool {
|
|
||||||
switch r {
|
|
||||||
case FileStatusResponseStatusAdded, FileStatusResponseStatusDeleted, FileStatusResponseStatusModified:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
type FileReadParams struct {
|
type FileReadParams struct {
|
||||||
Path param.Field[string] `query:"path,required"`
|
Path param.Field[string] `query:"path,required"`
|
||||||
}
|
}
|
||||||
|
|||||||
+159
-50
@@ -42,7 +42,7 @@ func (r *FindService) Files(ctx context.Context, query FindFilesParams, opts ...
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find workspace symbols
|
// Find workspace symbols
|
||||||
func (r *FindService) Symbols(ctx context.Context, query FindSymbolsParams, opts ...option.RequestOption) (res *[]FindSymbolsResponse, err error) {
|
func (r *FindService) Symbols(ctx context.Context, query FindSymbolsParams, opts ...option.RequestOption) (res *[]Symbol, err error) {
|
||||||
opts = append(r.Options[:], opts...)
|
opts = append(r.Options[:], opts...)
|
||||||
path := "find/symbol"
|
path := "find/symbol"
|
||||||
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...)
|
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...)
|
||||||
@@ -50,27 +50,24 @@ func (r *FindService) Symbols(ctx context.Context, query FindSymbolsParams, opts
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find text in files
|
// Find text in files
|
||||||
func (r *FindService) Text(ctx context.Context, query FindTextParams, opts ...option.RequestOption) (res *[]FindTextResponse, err error) {
|
func (r *FindService) Text(ctx context.Context, query FindTextParams, opts ...option.RequestOption) (res *[]Match, err error) {
|
||||||
opts = append(r.Options[:], opts...)
|
opts = append(r.Options[:], opts...)
|
||||||
path := "find"
|
path := "find"
|
||||||
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...)
|
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
type FindSymbolsResponse = interface{}
|
type Match struct {
|
||||||
|
AbsoluteOffset float64 `json:"absolute_offset,required"`
|
||||||
type FindTextResponse struct {
|
LineNumber float64 `json:"line_number,required"`
|
||||||
AbsoluteOffset float64 `json:"absolute_offset,required"`
|
Lines MatchLines `json:"lines,required"`
|
||||||
LineNumber float64 `json:"line_number,required"`
|
Path MatchPath `json:"path,required"`
|
||||||
Lines FindTextResponseLines `json:"lines,required"`
|
Submatches []MatchSubmatch `json:"submatches,required"`
|
||||||
Path FindTextResponsePath `json:"path,required"`
|
JSON matchJSON `json:"-"`
|
||||||
Submatches []FindTextResponseSubmatch `json:"submatches,required"`
|
|
||||||
JSON findTextResponseJSON `json:"-"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// findTextResponseJSON contains the JSON metadata for the struct
|
// matchJSON contains the JSON metadata for the struct [Match]
|
||||||
// [FindTextResponse]
|
type matchJSON struct {
|
||||||
type findTextResponseJSON struct {
|
|
||||||
AbsoluteOffset apijson.Field
|
AbsoluteOffset apijson.Field
|
||||||
LineNumber apijson.Field
|
LineNumber apijson.Field
|
||||||
Lines apijson.Field
|
Lines apijson.Field
|
||||||
@@ -80,66 +77,63 @@ type findTextResponseJSON struct {
|
|||||||
ExtraFields map[string]apijson.Field
|
ExtraFields map[string]apijson.Field
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *FindTextResponse) UnmarshalJSON(data []byte) (err error) {
|
func (r *Match) UnmarshalJSON(data []byte) (err error) {
|
||||||
return apijson.UnmarshalRoot(data, r)
|
return apijson.UnmarshalRoot(data, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r findTextResponseJSON) RawJSON() string {
|
func (r matchJSON) RawJSON() string {
|
||||||
return r.raw
|
return r.raw
|
||||||
}
|
}
|
||||||
|
|
||||||
type FindTextResponseLines struct {
|
type MatchLines struct {
|
||||||
Text string `json:"text,required"`
|
Text string `json:"text,required"`
|
||||||
JSON findTextResponseLinesJSON `json:"-"`
|
JSON matchLinesJSON `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// findTextResponseLinesJSON contains the JSON metadata for the struct
|
// matchLinesJSON contains the JSON metadata for the struct [MatchLines]
|
||||||
// [FindTextResponseLines]
|
type matchLinesJSON struct {
|
||||||
type findTextResponseLinesJSON struct {
|
|
||||||
Text apijson.Field
|
Text apijson.Field
|
||||||
raw string
|
raw string
|
||||||
ExtraFields map[string]apijson.Field
|
ExtraFields map[string]apijson.Field
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *FindTextResponseLines) UnmarshalJSON(data []byte) (err error) {
|
func (r *MatchLines) UnmarshalJSON(data []byte) (err error) {
|
||||||
return apijson.UnmarshalRoot(data, r)
|
return apijson.UnmarshalRoot(data, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r findTextResponseLinesJSON) RawJSON() string {
|
func (r matchLinesJSON) RawJSON() string {
|
||||||
return r.raw
|
return r.raw
|
||||||
}
|
}
|
||||||
|
|
||||||
type FindTextResponsePath struct {
|
type MatchPath struct {
|
||||||
Text string `json:"text,required"`
|
Text string `json:"text,required"`
|
||||||
JSON findTextResponsePathJSON `json:"-"`
|
JSON matchPathJSON `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// findTextResponsePathJSON contains the JSON metadata for the struct
|
// matchPathJSON contains the JSON metadata for the struct [MatchPath]
|
||||||
// [FindTextResponsePath]
|
type matchPathJSON struct {
|
||||||
type findTextResponsePathJSON struct {
|
|
||||||
Text apijson.Field
|
Text apijson.Field
|
||||||
raw string
|
raw string
|
||||||
ExtraFields map[string]apijson.Field
|
ExtraFields map[string]apijson.Field
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *FindTextResponsePath) UnmarshalJSON(data []byte) (err error) {
|
func (r *MatchPath) UnmarshalJSON(data []byte) (err error) {
|
||||||
return apijson.UnmarshalRoot(data, r)
|
return apijson.UnmarshalRoot(data, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r findTextResponsePathJSON) RawJSON() string {
|
func (r matchPathJSON) RawJSON() string {
|
||||||
return r.raw
|
return r.raw
|
||||||
}
|
}
|
||||||
|
|
||||||
type FindTextResponseSubmatch struct {
|
type MatchSubmatch struct {
|
||||||
End float64 `json:"end,required"`
|
End float64 `json:"end,required"`
|
||||||
Match FindTextResponseSubmatchesMatch `json:"match,required"`
|
Match MatchSubmatchesMatch `json:"match,required"`
|
||||||
Start float64 `json:"start,required"`
|
Start float64 `json:"start,required"`
|
||||||
JSON findTextResponseSubmatchJSON `json:"-"`
|
JSON matchSubmatchJSON `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// findTextResponseSubmatchJSON contains the JSON metadata for the struct
|
// matchSubmatchJSON contains the JSON metadata for the struct [MatchSubmatch]
|
||||||
// [FindTextResponseSubmatch]
|
type matchSubmatchJSON struct {
|
||||||
type findTextResponseSubmatchJSON struct {
|
|
||||||
End apijson.Field
|
End apijson.Field
|
||||||
Match apijson.Field
|
Match apijson.Field
|
||||||
Start apijson.Field
|
Start apijson.Field
|
||||||
@@ -147,32 +141,147 @@ type findTextResponseSubmatchJSON struct {
|
|||||||
ExtraFields map[string]apijson.Field
|
ExtraFields map[string]apijson.Field
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *FindTextResponseSubmatch) UnmarshalJSON(data []byte) (err error) {
|
func (r *MatchSubmatch) UnmarshalJSON(data []byte) (err error) {
|
||||||
return apijson.UnmarshalRoot(data, r)
|
return apijson.UnmarshalRoot(data, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r findTextResponseSubmatchJSON) RawJSON() string {
|
func (r matchSubmatchJSON) RawJSON() string {
|
||||||
return r.raw
|
return r.raw
|
||||||
}
|
}
|
||||||
|
|
||||||
type FindTextResponseSubmatchesMatch struct {
|
type MatchSubmatchesMatch struct {
|
||||||
Text string `json:"text,required"`
|
Text string `json:"text,required"`
|
||||||
JSON findTextResponseSubmatchesMatchJSON `json:"-"`
|
JSON matchSubmatchesMatchJSON `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// findTextResponseSubmatchesMatchJSON contains the JSON metadata for the struct
|
// matchSubmatchesMatchJSON contains the JSON metadata for the struct
|
||||||
// [FindTextResponseSubmatchesMatch]
|
// [MatchSubmatchesMatch]
|
||||||
type findTextResponseSubmatchesMatchJSON struct {
|
type matchSubmatchesMatchJSON struct {
|
||||||
Text apijson.Field
|
Text apijson.Field
|
||||||
raw string
|
raw string
|
||||||
ExtraFields map[string]apijson.Field
|
ExtraFields map[string]apijson.Field
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *FindTextResponseSubmatchesMatch) UnmarshalJSON(data []byte) (err error) {
|
func (r *MatchSubmatchesMatch) UnmarshalJSON(data []byte) (err error) {
|
||||||
return apijson.UnmarshalRoot(data, r)
|
return apijson.UnmarshalRoot(data, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r findTextResponseSubmatchesMatchJSON) RawJSON() string {
|
func (r matchSubmatchesMatchJSON) RawJSON() string {
|
||||||
|
return r.raw
|
||||||
|
}
|
||||||
|
|
||||||
|
type Symbol struct {
|
||||||
|
Kind float64 `json:"kind,required"`
|
||||||
|
Location SymbolLocation `json:"location,required"`
|
||||||
|
Name string `json:"name,required"`
|
||||||
|
JSON symbolJSON `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// symbolJSON contains the JSON metadata for the struct [Symbol]
|
||||||
|
type symbolJSON struct {
|
||||||
|
Kind apijson.Field
|
||||||
|
Location apijson.Field
|
||||||
|
Name apijson.Field
|
||||||
|
raw string
|
||||||
|
ExtraFields map[string]apijson.Field
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Symbol) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
return apijson.UnmarshalRoot(data, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r symbolJSON) RawJSON() string {
|
||||||
|
return r.raw
|
||||||
|
}
|
||||||
|
|
||||||
|
type SymbolLocation struct {
|
||||||
|
Range SymbolLocationRange `json:"range,required"`
|
||||||
|
Uri string `json:"uri,required"`
|
||||||
|
JSON symbolLocationJSON `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// symbolLocationJSON contains the JSON metadata for the struct [SymbolLocation]
|
||||||
|
type symbolLocationJSON struct {
|
||||||
|
Range apijson.Field
|
||||||
|
Uri apijson.Field
|
||||||
|
raw string
|
||||||
|
ExtraFields map[string]apijson.Field
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SymbolLocation) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
return apijson.UnmarshalRoot(data, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r symbolLocationJSON) RawJSON() string {
|
||||||
|
return r.raw
|
||||||
|
}
|
||||||
|
|
||||||
|
type SymbolLocationRange struct {
|
||||||
|
End SymbolLocationRangeEnd `json:"end,required"`
|
||||||
|
Start SymbolLocationRangeStart `json:"start,required"`
|
||||||
|
JSON symbolLocationRangeJSON `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// symbolLocationRangeJSON contains the JSON metadata for the struct
|
||||||
|
// [SymbolLocationRange]
|
||||||
|
type symbolLocationRangeJSON struct {
|
||||||
|
End apijson.Field
|
||||||
|
Start apijson.Field
|
||||||
|
raw string
|
||||||
|
ExtraFields map[string]apijson.Field
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SymbolLocationRange) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
return apijson.UnmarshalRoot(data, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r symbolLocationRangeJSON) RawJSON() string {
|
||||||
|
return r.raw
|
||||||
|
}
|
||||||
|
|
||||||
|
type SymbolLocationRangeEnd struct {
|
||||||
|
Character float64 `json:"character,required"`
|
||||||
|
Line float64 `json:"line,required"`
|
||||||
|
JSON symbolLocationRangeEndJSON `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// symbolLocationRangeEndJSON contains the JSON metadata for the struct
|
||||||
|
// [SymbolLocationRangeEnd]
|
||||||
|
type symbolLocationRangeEndJSON struct {
|
||||||
|
Character apijson.Field
|
||||||
|
Line apijson.Field
|
||||||
|
raw string
|
||||||
|
ExtraFields map[string]apijson.Field
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SymbolLocationRangeEnd) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
return apijson.UnmarshalRoot(data, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r symbolLocationRangeEndJSON) RawJSON() string {
|
||||||
|
return r.raw
|
||||||
|
}
|
||||||
|
|
||||||
|
type SymbolLocationRangeStart struct {
|
||||||
|
Character float64 `json:"character,required"`
|
||||||
|
Line float64 `json:"line,required"`
|
||||||
|
JSON symbolLocationRangeStartJSON `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// symbolLocationRangeStartJSON contains the JSON metadata for the struct
|
||||||
|
// [SymbolLocationRangeStart]
|
||||||
|
type symbolLocationRangeStartJSON struct {
|
||||||
|
Character apijson.Field
|
||||||
|
Line apijson.Field
|
||||||
|
raw string
|
||||||
|
ExtraFields map[string]apijson.Field
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SymbolLocationRangeStart) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
return apijson.UnmarshalRoot(data, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r symbolLocationRangeStartJSON) RawJSON() string {
|
||||||
return r.raw
|
return r.raw
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ func NewDecoder(res *http.Response) Decoder {
|
|||||||
decoder = t(res.Body)
|
decoder = t(res.Body)
|
||||||
} else {
|
} else {
|
||||||
scn := bufio.NewScanner(res.Body)
|
scn := bufio.NewScanner(res.Body)
|
||||||
scn.Buffer(nil, bufio.MaxScanTokenSize<<4)
|
scn.Buffer(nil, bufio.MaxScanTokenSize<<9)
|
||||||
decoder = &eventStreamDecoder{rc: res.Body, scn: scn}
|
decoder = &eventStreamDecoder{rc: res.Body, scn: scn}
|
||||||
}
|
}
|
||||||
return decoder
|
return decoder
|
||||||
|
|||||||
@@ -60,5 +60,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"release-type": "go",
|
"release-type": "go",
|
||||||
"extra-files": ["internal/version.go", "README.md"]
|
"extra-files": [
|
||||||
|
"internal/version.go",
|
||||||
|
"README.md"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
@@ -5,4 +5,7 @@ set -e
|
|||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
echo "==> Running Go build"
|
echo "==> Running Go build"
|
||||||
go build ./...
|
go build .
|
||||||
|
|
||||||
|
echo "==> Checking tests compile"
|
||||||
|
go test -run=^$ .
|
||||||
|
|||||||
+51
-35
@@ -340,13 +340,14 @@ func (r *AssistantMessageError) UnmarshalJSON(data []byte) (err error) {
|
|||||||
// the specific types for more type safety.
|
// the specific types for more type safety.
|
||||||
//
|
//
|
||||||
// Possible runtime types of the union are [shared.ProviderAuthError],
|
// Possible runtime types of the union are [shared.ProviderAuthError],
|
||||||
// [shared.UnknownError], [AssistantMessageErrorMessageOutputLengthError].
|
// [shared.UnknownError], [AssistantMessageErrorMessageOutputLengthError],
|
||||||
|
// [shared.MessageAbortedError].
|
||||||
func (r AssistantMessageError) AsUnion() AssistantMessageErrorUnion {
|
func (r AssistantMessageError) AsUnion() AssistantMessageErrorUnion {
|
||||||
return r.union
|
return r.union
|
||||||
}
|
}
|
||||||
|
|
||||||
// Union satisfied by [shared.ProviderAuthError], [shared.UnknownError] or
|
// Union satisfied by [shared.ProviderAuthError], [shared.UnknownError],
|
||||||
// [AssistantMessageErrorMessageOutputLengthError].
|
// [AssistantMessageErrorMessageOutputLengthError] or [shared.MessageAbortedError].
|
||||||
type AssistantMessageErrorUnion interface {
|
type AssistantMessageErrorUnion interface {
|
||||||
ImplementsAssistantMessageError()
|
ImplementsAssistantMessageError()
|
||||||
}
|
}
|
||||||
@@ -370,6 +371,11 @@ func init() {
|
|||||||
Type: reflect.TypeOf(AssistantMessageErrorMessageOutputLengthError{}),
|
Type: reflect.TypeOf(AssistantMessageErrorMessageOutputLengthError{}),
|
||||||
DiscriminatorValue: "MessageOutputLengthError",
|
DiscriminatorValue: "MessageOutputLengthError",
|
||||||
},
|
},
|
||||||
|
apijson.UnionVariant{
|
||||||
|
TypeFilter: gjson.JSON,
|
||||||
|
Type: reflect.TypeOf(shared.MessageAbortedError{}),
|
||||||
|
DiscriminatorValue: "MessageAbortedError",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,11 +424,12 @@ const (
|
|||||||
AssistantMessageErrorNameProviderAuthError AssistantMessageErrorName = "ProviderAuthError"
|
AssistantMessageErrorNameProviderAuthError AssistantMessageErrorName = "ProviderAuthError"
|
||||||
AssistantMessageErrorNameUnknownError AssistantMessageErrorName = "UnknownError"
|
AssistantMessageErrorNameUnknownError AssistantMessageErrorName = "UnknownError"
|
||||||
AssistantMessageErrorNameMessageOutputLengthError AssistantMessageErrorName = "MessageOutputLengthError"
|
AssistantMessageErrorNameMessageOutputLengthError AssistantMessageErrorName = "MessageOutputLengthError"
|
||||||
|
AssistantMessageErrorNameMessageAbortedError AssistantMessageErrorName = "MessageAbortedError"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r AssistantMessageErrorName) IsKnown() bool {
|
func (r AssistantMessageErrorName) IsKnown() bool {
|
||||||
switch r {
|
switch r {
|
||||||
case AssistantMessageErrorNameProviderAuthError, AssistantMessageErrorNameUnknownError, AssistantMessageErrorNameMessageOutputLengthError:
|
case AssistantMessageErrorNameProviderAuthError, AssistantMessageErrorNameUnknownError, AssistantMessageErrorNameMessageOutputLengthError, AssistantMessageErrorNameMessageAbortedError:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -432,11 +439,12 @@ type AssistantMessagePart struct {
|
|||||||
Type AssistantMessagePartType `json:"type,required"`
|
Type AssistantMessagePartType `json:"type,required"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
// This field can have the runtime type of [ToolPartState].
|
// This field can have the runtime type of [ToolPartState].
|
||||||
State interface{} `json:"state"`
|
State interface{} `json:"state"`
|
||||||
Text string `json:"text"`
|
Synthetic bool `json:"synthetic"`
|
||||||
Tool string `json:"tool"`
|
Text string `json:"text"`
|
||||||
JSON assistantMessagePartJSON `json:"-"`
|
Tool string `json:"tool"`
|
||||||
union AssistantMessagePartUnion
|
JSON assistantMessagePartJSON `json:"-"`
|
||||||
|
union AssistantMessagePartUnion
|
||||||
}
|
}
|
||||||
|
|
||||||
// assistantMessagePartJSON contains the JSON metadata for the struct
|
// assistantMessagePartJSON contains the JSON metadata for the struct
|
||||||
@@ -445,6 +453,7 @@ type assistantMessagePartJSON struct {
|
|||||||
Type apijson.Field
|
Type apijson.Field
|
||||||
ID apijson.Field
|
ID apijson.Field
|
||||||
State apijson.Field
|
State apijson.Field
|
||||||
|
Synthetic apijson.Field
|
||||||
Text apijson.Field
|
Text apijson.Field
|
||||||
Tool apijson.Field
|
Tool apijson.Field
|
||||||
raw string
|
raw string
|
||||||
@@ -808,15 +817,17 @@ func (r StepStartPartType) IsKnown() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TextPart struct {
|
type TextPart struct {
|
||||||
Text string `json:"text,required"`
|
Text string `json:"text,required"`
|
||||||
Type TextPartType `json:"type,required"`
|
Type TextPartType `json:"type,required"`
|
||||||
JSON textPartJSON `json:"-"`
|
Synthetic bool `json:"synthetic"`
|
||||||
|
JSON textPartJSON `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// textPartJSON contains the JSON metadata for the struct [TextPart]
|
// textPartJSON contains the JSON metadata for the struct [TextPart]
|
||||||
type textPartJSON struct {
|
type textPartJSON struct {
|
||||||
Text apijson.Field
|
Text apijson.Field
|
||||||
Type apijson.Field
|
Type apijson.Field
|
||||||
|
Synthetic apijson.Field
|
||||||
raw string
|
raw string
|
||||||
ExtraFields map[string]apijson.Field
|
ExtraFields map[string]apijson.Field
|
||||||
}
|
}
|
||||||
@@ -848,8 +859,9 @@ func (r TextPartType) IsKnown() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TextPartParam struct {
|
type TextPartParam struct {
|
||||||
Text param.Field[string] `json:"text,required"`
|
Text param.Field[string] `json:"text,required"`
|
||||||
Type param.Field[TextPartType] `json:"type,required"`
|
Type param.Field[TextPartType] `json:"type,required"`
|
||||||
|
Synthetic param.Field[bool] `json:"synthetic"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r TextPartParam) MarshalJSON() (data []byte, err error) {
|
func (r TextPartParam) MarshalJSON() (data []byte, err error) {
|
||||||
@@ -889,7 +901,7 @@ func (r ToolPart) implementsAssistantMessagePart() {}
|
|||||||
type ToolPartState struct {
|
type ToolPartState struct {
|
||||||
Status ToolPartStateStatus `json:"status,required"`
|
Status ToolPartStateStatus `json:"status,required"`
|
||||||
Error string `json:"error"`
|
Error string `json:"error"`
|
||||||
// This field can have the runtime type of [interface{}].
|
// This field can have the runtime type of [interface{}], [map[string]interface{}].
|
||||||
Input interface{} `json:"input"`
|
Input interface{} `json:"input"`
|
||||||
// This field can have the runtime type of [map[string]interface{}].
|
// This field can have the runtime type of [map[string]interface{}].
|
||||||
Metadata interface{} `json:"metadata"`
|
Metadata interface{} `json:"metadata"`
|
||||||
@@ -1002,24 +1014,24 @@ func (r ToolPartType) IsKnown() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ToolStateCompleted struct {
|
type ToolStateCompleted struct {
|
||||||
|
Input map[string]interface{} `json:"input,required"`
|
||||||
Metadata map[string]interface{} `json:"metadata,required"`
|
Metadata map[string]interface{} `json:"metadata,required"`
|
||||||
Output string `json:"output,required"`
|
Output string `json:"output,required"`
|
||||||
Status ToolStateCompletedStatus `json:"status,required"`
|
Status ToolStateCompletedStatus `json:"status,required"`
|
||||||
Time ToolStateCompletedTime `json:"time,required"`
|
Time ToolStateCompletedTime `json:"time,required"`
|
||||||
Title string `json:"title,required"`
|
Title string `json:"title,required"`
|
||||||
Input interface{} `json:"input"`
|
|
||||||
JSON toolStateCompletedJSON `json:"-"`
|
JSON toolStateCompletedJSON `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// toolStateCompletedJSON contains the JSON metadata for the struct
|
// toolStateCompletedJSON contains the JSON metadata for the struct
|
||||||
// [ToolStateCompleted]
|
// [ToolStateCompleted]
|
||||||
type toolStateCompletedJSON struct {
|
type toolStateCompletedJSON struct {
|
||||||
|
Input apijson.Field
|
||||||
Metadata apijson.Field
|
Metadata apijson.Field
|
||||||
Output apijson.Field
|
Output apijson.Field
|
||||||
Status apijson.Field
|
Status apijson.Field
|
||||||
Time apijson.Field
|
Time apijson.Field
|
||||||
Title apijson.Field
|
Title apijson.Field
|
||||||
Input apijson.Field
|
|
||||||
raw string
|
raw string
|
||||||
ExtraFields map[string]apijson.Field
|
ExtraFields map[string]apijson.Field
|
||||||
}
|
}
|
||||||
@@ -1072,19 +1084,19 @@ func (r toolStateCompletedTimeJSON) RawJSON() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ToolStateError struct {
|
type ToolStateError struct {
|
||||||
Error string `json:"error,required"`
|
Error string `json:"error,required"`
|
||||||
Status ToolStateErrorStatus `json:"status,required"`
|
Input map[string]interface{} `json:"input,required"`
|
||||||
Time ToolStateErrorTime `json:"time,required"`
|
Status ToolStateErrorStatus `json:"status,required"`
|
||||||
Input interface{} `json:"input"`
|
Time ToolStateErrorTime `json:"time,required"`
|
||||||
JSON toolStateErrorJSON `json:"-"`
|
JSON toolStateErrorJSON `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// toolStateErrorJSON contains the JSON metadata for the struct [ToolStateError]
|
// toolStateErrorJSON contains the JSON metadata for the struct [ToolStateError]
|
||||||
type toolStateErrorJSON struct {
|
type toolStateErrorJSON struct {
|
||||||
Error apijson.Field
|
Error apijson.Field
|
||||||
|
Input apijson.Field
|
||||||
Status apijson.Field
|
Status apijson.Field
|
||||||
Time apijson.Field
|
Time apijson.Field
|
||||||
Input apijson.Field
|
|
||||||
raw string
|
raw string
|
||||||
ExtraFields map[string]apijson.Field
|
ExtraFields map[string]apijson.Field
|
||||||
}
|
}
|
||||||
@@ -1304,13 +1316,14 @@ func (r userMessageTimeJSON) RawJSON() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UserMessagePart struct {
|
type UserMessagePart struct {
|
||||||
Type UserMessagePartType `json:"type,required"`
|
Type UserMessagePartType `json:"type,required"`
|
||||||
Filename string `json:"filename"`
|
Filename string `json:"filename"`
|
||||||
Mime string `json:"mime"`
|
Mime string `json:"mime"`
|
||||||
Text string `json:"text"`
|
Synthetic bool `json:"synthetic"`
|
||||||
URL string `json:"url"`
|
Text string `json:"text"`
|
||||||
JSON userMessagePartJSON `json:"-"`
|
URL string `json:"url"`
|
||||||
union UserMessagePartUnion
|
JSON userMessagePartJSON `json:"-"`
|
||||||
|
union UserMessagePartUnion
|
||||||
}
|
}
|
||||||
|
|
||||||
// userMessagePartJSON contains the JSON metadata for the struct [UserMessagePart]
|
// userMessagePartJSON contains the JSON metadata for the struct [UserMessagePart]
|
||||||
@@ -1318,6 +1331,7 @@ type userMessagePartJSON struct {
|
|||||||
Type apijson.Field
|
Type apijson.Field
|
||||||
Filename apijson.Field
|
Filename apijson.Field
|
||||||
Mime apijson.Field
|
Mime apijson.Field
|
||||||
|
Synthetic apijson.Field
|
||||||
Text apijson.Field
|
Text apijson.Field
|
||||||
URL apijson.Field
|
URL apijson.Field
|
||||||
raw string
|
raw string
|
||||||
@@ -1383,11 +1397,12 @@ func (r UserMessagePartType) IsKnown() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UserMessagePartParam struct {
|
type UserMessagePartParam struct {
|
||||||
Type param.Field[UserMessagePartType] `json:"type,required"`
|
Type param.Field[UserMessagePartType] `json:"type,required"`
|
||||||
Filename param.Field[string] `json:"filename"`
|
Filename param.Field[string] `json:"filename"`
|
||||||
Mime param.Field[string] `json:"mime"`
|
Mime param.Field[string] `json:"mime"`
|
||||||
Text param.Field[string] `json:"text"`
|
Synthetic param.Field[bool] `json:"synthetic"`
|
||||||
URL param.Field[string] `json:"url"`
|
Text param.Field[string] `json:"text"`
|
||||||
|
URL param.Field[string] `json:"url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r UserMessagePartParam) MarshalJSON() (data []byte, err error) {
|
func (r UserMessagePartParam) MarshalJSON() (data []byte, err error) {
|
||||||
@@ -1402,6 +1417,7 @@ type UserMessagePartUnionParam interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SessionChatParams struct {
|
type SessionChatParams struct {
|
||||||
|
Mode param.Field[string] `json:"mode,required"`
|
||||||
ModelID param.Field[string] `json:"modelID,required"`
|
ModelID param.Field[string] `json:"modelID,required"`
|
||||||
Parts param.Field[[]UserMessagePartUnionParam] `json:"parts,required"`
|
Parts param.Field[[]UserMessagePartUnionParam] `json:"parts,required"`
|
||||||
ProviderID param.Field[string] `json:"providerID,required"`
|
ProviderID param.Field[string] `json:"providerID,required"`
|
||||||
|
|||||||
@@ -117,10 +117,12 @@ func TestSessionChat(t *testing.T) {
|
|||||||
context.TODO(),
|
context.TODO(),
|
||||||
"id",
|
"id",
|
||||||
opencode.SessionChatParams{
|
opencode.SessionChatParams{
|
||||||
|
Mode: opencode.F("mode"),
|
||||||
ModelID: opencode.F("modelID"),
|
ModelID: opencode.F("modelID"),
|
||||||
Parts: opencode.F([]opencode.UserMessagePartUnionParam{opencode.TextPartParam{
|
Parts: opencode.F([]opencode.UserMessagePartUnionParam{opencode.TextPartParam{
|
||||||
Text: opencode.F("text"),
|
Text: opencode.F("text"),
|
||||||
Type: opencode.F(opencode.TextPartTypeText),
|
Type: opencode.F(opencode.TextPartTypeText),
|
||||||
|
Synthetic: opencode.F(true),
|
||||||
}}),
|
}}),
|
||||||
ProviderID: opencode.F("providerID"),
|
ProviderID: opencode.F("providerID"),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,6 +6,47 @@ import (
|
|||||||
"github.com/sst/opencode-sdk-go/internal/apijson"
|
"github.com/sst/opencode-sdk-go/internal/apijson"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type MessageAbortedError struct {
|
||||||
|
Data interface{} `json:"data,required"`
|
||||||
|
Name MessageAbortedErrorName `json:"name,required"`
|
||||||
|
JSON messageAbortedErrorJSON `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// messageAbortedErrorJSON contains the JSON metadata for the struct
|
||||||
|
// [MessageAbortedError]
|
||||||
|
type messageAbortedErrorJSON struct {
|
||||||
|
Data apijson.Field
|
||||||
|
Name apijson.Field
|
||||||
|
raw string
|
||||||
|
ExtraFields map[string]apijson.Field
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MessageAbortedError) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
return apijson.UnmarshalRoot(data, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r messageAbortedErrorJSON) RawJSON() string {
|
||||||
|
return r.raw
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r MessageAbortedError) ImplementsEventListResponseEventSessionErrorPropertiesError() {}
|
||||||
|
|
||||||
|
func (r MessageAbortedError) ImplementsAssistantMessageError() {}
|
||||||
|
|
||||||
|
type MessageAbortedErrorName string
|
||||||
|
|
||||||
|
const (
|
||||||
|
MessageAbortedErrorNameMessageAbortedError MessageAbortedErrorName = "MessageAbortedError"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r MessageAbortedErrorName) IsKnown() bool {
|
||||||
|
switch r {
|
||||||
|
case MessageAbortedErrorNameMessageAbortedError:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
type ProviderAuthError struct {
|
type ProviderAuthError struct {
|
||||||
Data ProviderAuthErrorData `json:"data,required"`
|
Data ProviderAuthErrorData `json:"data,required"`
|
||||||
Name ProviderAuthErrorName `json:"name,required"`
|
Name ProviderAuthErrorName `json:"name,required"`
|
||||||
|
|||||||
@@ -7,15 +7,13 @@ import theme from "toolbeam-docs-theme"
|
|||||||
import config from "./config.mjs"
|
import config from "./config.mjs"
|
||||||
import { rehypeHeadingIds } from "@astrojs/markdown-remark"
|
import { rehypeHeadingIds } from "@astrojs/markdown-remark"
|
||||||
import rehypeAutolinkHeadings from "rehype-autolink-headings"
|
import rehypeAutolinkHeadings from "rehype-autolink-headings"
|
||||||
|
import { spawnSync } from "child_process"
|
||||||
|
|
||||||
const github = "https://github.com/sst/opencode"
|
const github = "https://github.com/sst/opencode"
|
||||||
const stage = process.env.SST_STAGE || "dev"
|
|
||||||
|
|
||||||
// https://astro.build/config
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
site: stage === "production"
|
site: config.url,
|
||||||
? `https://${config.domain}`
|
|
||||||
: `https://${stage}.${config.domain}`,
|
|
||||||
output: "server",
|
output: "server",
|
||||||
adapter: cloudflare({
|
adapter: cloudflare({
|
||||||
imageService: "passthrough",
|
imageService: "passthrough",
|
||||||
@@ -29,7 +27,9 @@ export default defineConfig({
|
|||||||
markdown: {
|
markdown: {
|
||||||
rehypePlugins: [rehypeHeadingIds, [rehypeAutolinkHeadings, { behavior: "wrap" }]],
|
rehypePlugins: [rehypeHeadingIds, [rehypeAutolinkHeadings, { behavior: "wrap" }]],
|
||||||
},
|
},
|
||||||
|
build: {},
|
||||||
integrations: [
|
integrations: [
|
||||||
|
configSchema(),
|
||||||
solidJs(),
|
solidJs(),
|
||||||
starlight({
|
starlight({
|
||||||
title: "opencode",
|
title: "opencode",
|
||||||
@@ -62,12 +62,16 @@ export default defineConfig({
|
|||||||
sidebar: [
|
sidebar: [
|
||||||
"docs",
|
"docs",
|
||||||
"docs/cli",
|
"docs/cli",
|
||||||
|
"docs/share",
|
||||||
|
"docs/modes",
|
||||||
"docs/rules",
|
"docs/rules",
|
||||||
"docs/config",
|
"docs/config",
|
||||||
"docs/models",
|
"docs/models",
|
||||||
"docs/themes",
|
"docs/themes",
|
||||||
"docs/keybinds",
|
"docs/keybinds",
|
||||||
|
"docs/enterprise",
|
||||||
"docs/mcp-servers",
|
"docs/mcp-servers",
|
||||||
|
"docs/troubleshooting",
|
||||||
],
|
],
|
||||||
components: {
|
components: {
|
||||||
Hero: "./src/components/Hero.astro",
|
Hero: "./src/components/Hero.astro",
|
||||||
@@ -81,4 +85,19 @@ export default defineConfig({
|
|||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
redirects: {
|
||||||
|
"/discord": "https://discord.gg/opencode",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function configSchema() {
|
||||||
|
return {
|
||||||
|
name: "configSchema",
|
||||||
|
hooks: {
|
||||||
|
"astro:build:done": async () => {
|
||||||
|
console.log("generating config schema")
|
||||||
|
spawnSync("../opencode/script/schema.ts", ["./dist/config.json"])
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
|
const stage = process.env.SST_STAGE || "dev"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
domain: "opencode.ai",
|
url: stage === "production"
|
||||||
|
? "https://opencode.ai"
|
||||||
|
: `https://${stage}.opencode.ai`,
|
||||||
socialCard: "https://social-cards.sst.dev",
|
socialCard: "https://social-cards.sst.dev",
|
||||||
github: "https://github.com/sst/opencode",
|
github: "https://github.com/sst/opencode",
|
||||||
discord: "https://discord.gg/opencode",
|
discord: "https://opencode.ai/discord",
|
||||||
headerLinks: [
|
headerLinks: [
|
||||||
{ name: "Home", url: "/" },
|
{ name: "Home", url: "/" },
|
||||||
{ name: "Docs", url: "/docs/" },
|
{ name: "Docs", url: "/docs/" },
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
"sharp": "0.32.5",
|
"sharp": "0.32.5",
|
||||||
"shiki": "3.4.2",
|
"shiki": "3.4.2",
|
||||||
"solid-js": "1.9.7",
|
"solid-js": "1.9.7",
|
||||||
"toolbeam-docs-theme": "0.4.1"
|
"toolbeam-docs-theme": "0.4.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"opencode": "workspace:*",
|
"opencode": "workspace:*",
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import { type JSX, splitProps, createResource } from "solid-js"
|
|
||||||
import { codeToHtml } from "shiki"
|
|
||||||
import styles from "./codeblock.module.css"
|
|
||||||
import { transformerNotationDiff } from "@shikijs/transformers"
|
|
||||||
|
|
||||||
interface CodeBlockProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|
||||||
code: string
|
|
||||||
lang?: string
|
|
||||||
}
|
|
||||||
function CodeBlock(props: CodeBlockProps) {
|
|
||||||
const [local, rest] = splitProps(props, ["code", "lang"])
|
|
||||||
|
|
||||||
const [html] = createResource(
|
|
||||||
() => [local.code, local.lang],
|
|
||||||
async ([code, lang]) => {
|
|
||||||
// TODO: For testing delays
|
|
||||||
// await new Promise((resolve) => setTimeout(resolve, 3000))
|
|
||||||
return (await codeToHtml(code || "", {
|
|
||||||
lang: lang || "text",
|
|
||||||
themes: {
|
|
||||||
light: "github-light",
|
|
||||||
dark: "github-dark",
|
|
||||||
},
|
|
||||||
transformers: [transformerNotationDiff()],
|
|
||||||
})) as string
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
return <div innerHTML={html()} class={styles.codeblock} {...rest}></div>
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CodeBlock
|
|
||||||
@@ -13,7 +13,7 @@ const {
|
|||||||
const isDocs = slug.startsWith("docs")
|
const isDocs = slug.startsWith("docs")
|
||||||
|
|
||||||
let encodedTitle = '';
|
let encodedTitle = '';
|
||||||
let ogImage = `https://${config.domain}/social-share.png`;
|
let ogImage = `${config.url}/social-share.png`;
|
||||||
let truncatedDesc = '';
|
let truncatedDesc = '';
|
||||||
|
|
||||||
if (isDocs) {
|
if (isDocs) {
|
||||||
|
|||||||
@@ -1,24 +1,13 @@
|
|||||||
import {
|
import { For, Show, onMount, Suspense, onCleanup, createMemo, createSignal, SuspenseList } from "solid-js"
|
||||||
For,
|
|
||||||
Show,
|
|
||||||
Match,
|
|
||||||
Switch,
|
|
||||||
onMount,
|
|
||||||
Suspense,
|
|
||||||
onCleanup,
|
|
||||||
createMemo,
|
|
||||||
createSignal,
|
|
||||||
SuspenseList,
|
|
||||||
} from "solid-js"
|
|
||||||
import { DateTime } from "luxon"
|
import { DateTime } from "luxon"
|
||||||
import { createStore, reconcile } from "solid-js/store"
|
import { createStore, reconcile } from "solid-js/store"
|
||||||
import { IconOpenAI, IconGemini, IconOpencode, IconAnthropic } from "./icons/custom"
|
import { IconArrowDown } from "./icons"
|
||||||
import { IconSparkles, IconArrowDown } from "./icons"
|
import { IconOpencode } from "./icons/custom"
|
||||||
import styles from "./share.module.css"
|
import styles from "./share.module.css"
|
||||||
import type { MessageV2 } from "opencode/session/message-v2"
|
import type { MessageV2 } from "opencode/session/message-v2"
|
||||||
import type { Message } from "opencode/session/message"
|
import type { Message } from "opencode/session/message"
|
||||||
import type { Session } from "opencode/session/index"
|
import type { Session } from "opencode/session/index"
|
||||||
import { Part } from "./share/part"
|
import { Part, ProviderIcon } from "./share/part"
|
||||||
|
|
||||||
type Status = "disconnected" | "connecting" | "connected" | "error" | "reconnecting"
|
type Status = "disconnected" | "connecting" | "connected" | "error" | "reconnecting"
|
||||||
|
|
||||||
@@ -46,23 +35,6 @@ function getStatusText(status: [Status, string?]): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProviderIcon(props: { provider: string; size?: number }) {
|
|
||||||
const size = props.size || 16
|
|
||||||
return (
|
|
||||||
<Switch fallback={<IconSparkles width={size} height={size} />}>
|
|
||||||
<Match when={props.provider === "openai"}>
|
|
||||||
<IconOpenAI width={size} height={size} />
|
|
||||||
</Match>
|
|
||||||
<Match when={props.provider === "anthropic"}>
|
|
||||||
<IconAnthropic width={size} height={size} />
|
|
||||||
</Match>
|
|
||||||
<Match when={props.provider === "gemini"}>
|
|
||||||
<IconGemini width={size} height={size} />
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Share(props: {
|
export default function Share(props: {
|
||||||
id: string
|
id: string
|
||||||
api: string
|
api: string
|
||||||
@@ -296,7 +268,6 @@ export default function Share(props: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log(result.messages)
|
|
||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -319,7 +290,7 @@ export default function Share(props: {
|
|||||||
{([provider, model]) => (
|
{([provider, model]) => (
|
||||||
<li data-slot="item">
|
<li data-slot="item">
|
||||||
<div data-slot="icon" title={provider}>
|
<div data-slot="icon" title={provider}>
|
||||||
<ProviderIcon provider={provider} />
|
<ProviderIcon model={model} />
|
||||||
</div>
|
</div>
|
||||||
<span data-slot="model">{model}</span>
|
<span data-slot="model">{model}</span>
|
||||||
</li>
|
</li>
|
||||||
@@ -346,42 +317,47 @@ export default function Share(props: {
|
|||||||
<div class={styles.parts}>
|
<div class={styles.parts}>
|
||||||
<SuspenseList revealOrder="forwards">
|
<SuspenseList revealOrder="forwards">
|
||||||
<For each={data().messages}>
|
<For each={data().messages}>
|
||||||
{(msg, msgIndex) => (
|
{(msg, msgIndex) => {
|
||||||
<Suspense>
|
const filteredParts = createMemo(() =>
|
||||||
<For
|
msg.parts.filter((x, index) => {
|
||||||
each={msg.parts.filter((x, index) => {
|
if (x.type === "step-start" && index > 0) return false
|
||||||
if (x.type === "step-start" && index > 0) return false
|
if (x.type === "tool" && x.tool === "todoread") return false
|
||||||
if (x.type === "tool" && x.tool === "todoread") return false
|
if (x.type === "text" && !x.text) return false
|
||||||
if (x.type === "text" && !x.text) return false
|
if (x.type === "tool" && (x.state.status === "pending" || x.state.status === "running"))
|
||||||
if (x.type === "tool" && (x.state.status === "pending" || x.state.status === "running"))
|
return false
|
||||||
return false
|
return true
|
||||||
return true
|
})
|
||||||
})}
|
)
|
||||||
>
|
|
||||||
{(part, partIndex) => {
|
|
||||||
const last = createMemo(
|
|
||||||
() => data().messages.length === msgIndex() + 1 && msg.parts.length === partIndex() + 1,
|
|
||||||
)
|
|
||||||
|
|
||||||
onMount(() => {
|
return (
|
||||||
const hash = window.location.hash.slice(1)
|
<Suspense>
|
||||||
// Wait till all parts are loaded
|
<For each={filteredParts()}>
|
||||||
if (
|
{(part, partIndex) => {
|
||||||
hash !== "" &&
|
const last = createMemo(
|
||||||
!hasScrolledToAnchor &&
|
() =>
|
||||||
msg.parts.length === partIndex() + 1 &&
|
data().messages.length === msgIndex() + 1 && filteredParts().length === partIndex() + 1,
|
||||||
data().messages.length === msgIndex() + 1
|
)
|
||||||
) {
|
|
||||||
hasScrolledToAnchor = true
|
|
||||||
scrollToAnchor(hash)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return <Part last={last()} part={part} index={partIndex()} message={msg} />
|
onMount(() => {
|
||||||
}}
|
const hash = window.location.hash.slice(1)
|
||||||
</For>
|
// Wait till all parts are loaded
|
||||||
</Suspense>
|
if (
|
||||||
)}
|
hash !== "" &&
|
||||||
|
!hasScrolledToAnchor &&
|
||||||
|
filteredParts().length === partIndex() + 1 &&
|
||||||
|
data().messages.length === msgIndex() + 1
|
||||||
|
) {
|
||||||
|
hasScrolledToAnchor = true
|
||||||
|
scrollToAnchor(hash)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return <Part last={last()} part={part} index={partIndex()} message={msg} />
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}}
|
||||||
</For>
|
</For>
|
||||||
</SuspenseList>
|
</SuspenseList>
|
||||||
<div data-section="part" data-part-type="summary">
|
<div data-section="part" data-part-type="summary">
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
.codeblock {
|
|
||||||
pre {
|
|
||||||
--shiki-dark-bg: var(--sl-color-bg-surface) !important;
|
|
||||||
background-color: var(--sl-color-bg-surface) !important;
|
|
||||||
|
|
||||||
span {
|
|
||||||
white-space: break-spaces;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -49,3 +49,12 @@ export function IconOpencode(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
|||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://icones.js.org/collection/ri?s=meta&icon=ri:meta-fill
|
||||||
|
export function IconMeta(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d="M16.92 4.5c-1.851 0-3.298 1.394-4.608 3.165C10.512 5.373 9.007 4.5 7.206 4.5C3.534 4.5.72 9.28.72 14.338c0 3.165 1.531 5.162 4.096 5.162c1.846 0 3.174-.87 5.535-4.997c0 0 .984-1.737 1.66-2.934q.356.574.75 1.238l1.107 1.862c2.156 3.608 3.358 4.831 5.534 4.831c2.5 0 3.89-2.024 3.89-5.255c0-5.297-2.877-9.745-6.372-9.745m-8.37 8.886c-1.913 3-2.575 3.673-3.64 3.673c-1.097 0-1.749-.963-1.749-2.68c0-3.672 1.831-7.427 4.014-7.427c1.182 0 2.17.682 3.683 2.848c-1.437 2.204-2.307 3.586-2.307 3.586m7.224-.377L14.45 10.8a45 45 0 0 0-1.032-1.608c1.193-1.841 2.176-2.759 3.347-2.759c2.43 0 4.375 3.58 4.375 7.976c0 1.676-.549 2.649-1.686 2.649c-1.09 0-1.61-.72-3.68-4.05" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem 0.875rem;
|
gap: 0.5rem 0.875rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
max-width: var(--lg-tool-width);
|
||||||
|
|
||||||
[data-slot="item"] {
|
[data-slot="item"] {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
pre {
|
pre {
|
||||||
--shiki-dark-bg: var(--sl-color-bg-surface) !important;
|
--shiki-dark-bg: var(--sl-color-bg-surface) !important;
|
||||||
|
background-color: var(--sl-color-bg-surface) !important;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { type JSX, splitProps, createResource, Suspense } from "solid-js"
|
|
||||||
import { codeToHtml } from "shiki"
|
import { codeToHtml } from "shiki"
|
||||||
import style from "./content-code.module.css"
|
import { createResource, Suspense } from "solid-js"
|
||||||
import { transformerNotationDiff } from "@shikijs/transformers"
|
import { transformerNotationDiff } from "@shikijs/transformers"
|
||||||
|
import style from "./content-code.module.css"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
code: string
|
code: string
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
.root {
|
||||||
|
background-color: var(--sl-color-bg-surface);
|
||||||
|
padding: 0.5rem calc(0.5rem + 3px);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
align-self: flex-start;
|
||||||
|
max-width: var(--md-tool-width);
|
||||||
|
|
||||||
|
[data-section="content"] {
|
||||||
|
pre {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
span[data-color="red"] {
|
||||||
|
color: var(--sl-color-red);
|
||||||
|
}
|
||||||
|
span[data-color="dimmed"] {
|
||||||
|
color: var(--sl-color-text-dimmed);
|
||||||
|
}
|
||||||
|
span[data-marker="label"] {
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
}
|
||||||
|
span[data-separator] {
|
||||||
|
margin-right: 0.375rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-expanded="true"] {
|
||||||
|
[data-section="content"] {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&[data-expanded="false"] {
|
||||||
|
[data-section="content"] {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 7;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: 2px 0;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import style from "./content-error.module.css"
|
||||||
|
import { type JSX, createSignal } from "solid-js"
|
||||||
|
import { createOverflow } from "./common"
|
||||||
|
|
||||||
|
interface Props extends JSX.HTMLAttributes<HTMLDivElement> {
|
||||||
|
expand?: boolean
|
||||||
|
}
|
||||||
|
export function ContentError(props: Props) {
|
||||||
|
const [expanded, setExpanded] = createSignal(false)
|
||||||
|
const overflow = createOverflow()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={style.root}
|
||||||
|
data-expanded={expanded() || props.expand === true ? true : undefined}
|
||||||
|
>
|
||||||
|
<div data-section="content" ref={overflow.ref}>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
{((!props.expand && overflow.status) || expanded()) && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-element-button-text
|
||||||
|
onClick={() => setExpanded((e) => !e)}
|
||||||
|
>
|
||||||
|
{expanded() ? "Show less" : "Show more"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,31 +1,42 @@
|
|||||||
import { createMemo, createSignal, For, Match, Show, Switch, type JSX, type ParentProps } from "solid-js"
|
import map from "lang-map"
|
||||||
|
import { DateTime } from "luxon"
|
||||||
|
import {
|
||||||
|
For,
|
||||||
|
Show,
|
||||||
|
Match,
|
||||||
|
Switch,
|
||||||
|
type JSX,
|
||||||
|
createMemo,
|
||||||
|
createSignal,
|
||||||
|
type ParentProps
|
||||||
|
} from "solid-js"
|
||||||
import {
|
import {
|
||||||
IconCheckCircle,
|
|
||||||
IconChevronDown,
|
|
||||||
IconChevronRight,
|
|
||||||
IconHashtag,
|
IconHashtag,
|
||||||
IconSparkles,
|
IconSparkles,
|
||||||
IconGlobeAlt,
|
IconGlobeAlt,
|
||||||
IconDocument,
|
IconDocument,
|
||||||
IconQueueList,
|
IconQueueList,
|
||||||
|
IconUserCircle,
|
||||||
IconCommandLine,
|
IconCommandLine,
|
||||||
|
IconCheckCircle,
|
||||||
|
IconChevronDown,
|
||||||
|
IconChevronRight,
|
||||||
IconDocumentPlus,
|
IconDocumentPlus,
|
||||||
IconPencilSquare,
|
IconPencilSquare,
|
||||||
IconRectangleStack,
|
IconRectangleStack,
|
||||||
IconMagnifyingGlass,
|
IconMagnifyingGlass,
|
||||||
IconDocumentMagnifyingGlass,
|
IconDocumentMagnifyingGlass,
|
||||||
} from "../icons"
|
} from "../icons"
|
||||||
import styles from "./part.module.css"
|
import { IconMeta, IconOpenAI, IconGemini, IconAnthropic } from "../icons/custom"
|
||||||
import type { MessageV2 } from "opencode/session/message-v2"
|
|
||||||
import { ContentText } from "./content-text"
|
|
||||||
import { ContentMarkdown } from "./content-markdown"
|
|
||||||
import { DateTime } from "luxon"
|
|
||||||
import CodeBlock from "../CodeBlock"
|
|
||||||
import map from "lang-map"
|
|
||||||
import type { Diagnostic } from "vscode-languageserver-types"
|
|
||||||
|
|
||||||
import { ContentCode } from "./content-code"
|
import { ContentCode } from "./content-code"
|
||||||
import { ContentDiff } from "./content-diff"
|
import { ContentDiff } from "./content-diff"
|
||||||
|
import { ContentText } from "./content-text"
|
||||||
|
import { ContentError } from "./content-error"
|
||||||
|
import { ContentMarkdown } from "./content-markdown"
|
||||||
|
import type { MessageV2 } from "opencode/session/message-v2"
|
||||||
|
import type { Diagnostic } from "vscode-languageserver-types"
|
||||||
|
|
||||||
|
import styles from "./part.module.css"
|
||||||
|
|
||||||
export interface PartProps {
|
export interface PartProps {
|
||||||
index: number
|
index: number
|
||||||
@@ -65,6 +76,15 @@ export function Part(props: PartProps) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Switch>
|
<Switch>
|
||||||
|
<Match when={props.message.role === "user" && props.part.type === "text"}>
|
||||||
|
<IconUserCircle width={18} height={18} />
|
||||||
|
</Match>
|
||||||
|
<Match when={props.message.role === "user" && props.part.type === "file"}>
|
||||||
|
<IconDocument width={18} height={18} />
|
||||||
|
</Match>
|
||||||
|
<Match when={props.part.type === "step-start" && props.message.role === "assistant" && props.message.modelID}>
|
||||||
|
{model => <ProviderIcon model={model()} size={18} />}
|
||||||
|
</Match>
|
||||||
<Match when={props.part.type === "tool" && props.part.tool === "todowrite"}>
|
<Match when={props.part.type === "tool" && props.part.tool === "todowrite"}>
|
||||||
<IconQueueList width={18} height={18} />
|
<IconQueueList width={18} height={18} />
|
||||||
</Match>
|
</Match>
|
||||||
@@ -112,7 +132,8 @@ export function Part(props: PartProps) {
|
|||||||
<div data-component="content">
|
<div data-component="content">
|
||||||
{props.message.role === "user" && props.part.type === "text" && (
|
{props.message.role === "user" && props.part.type === "text" && (
|
||||||
<>
|
<>
|
||||||
<ContentText text={props.part.text} expand={props.last} /> <Spacer />
|
<ContentText text={props.part.text} expand={props.last} />
|
||||||
|
<Spacer />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{props.message.role === "assistant" && props.part.type === "text" && (
|
{props.message.role === "assistant" && props.part.type === "text" && (
|
||||||
@@ -130,12 +151,28 @@ export function Part(props: PartProps) {
|
|||||||
<Spacer />
|
<Spacer />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{props.message.role === "user" && props.part.type === "file" && (
|
||||||
|
<div data-component="tool-title">
|
||||||
|
<span data-slot="name">Read</span>
|
||||||
|
<span data-slot="target" title={props.part.filename}>
|
||||||
|
{props.part.filename}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{props.part.type === "step-start" && props.message.role === "assistant" && (
|
{props.part.type === "step-start" && props.message.role === "assistant" && (
|
||||||
<div data-component="step-start">
|
<div data-component="step-start">
|
||||||
<div data-slot="provider">{props.message.providerID}</div>
|
<div data-slot="provider">{props.message.providerID}</div>
|
||||||
<div data-slot="model">{props.message.modelID}</div>
|
<div data-slot="model">{props.message.modelID}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{props.part.type === "tool" &&
|
||||||
|
props.part.state.status === "error" && (
|
||||||
|
<div data-component="tool">
|
||||||
|
<ContentError>
|
||||||
|
{formatErrorString(props.part.state.error)}
|
||||||
|
</ContentError>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{props.part.type === "tool" &&
|
{props.part.type === "tool" &&
|
||||||
props.part.state.status === "completed" &&
|
props.part.state.status === "completed" &&
|
||||||
props.message.role === "assistant" && (
|
props.message.role === "assistant" && (
|
||||||
@@ -420,11 +457,11 @@ export function WebFetchTool(props: ToolProps) {
|
|||||||
<div data-component="tool-result">
|
<div data-component="tool-result">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={props.state.metadata?.error}>
|
<Match when={props.state.metadata?.error}>
|
||||||
<div data-component="error">{formatErrorString(props.state.output)}</div>
|
<ContentError>{formatErrorString(props.state.output)}</ContentError>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={props.state.output}>
|
<Match when={props.state.output}>
|
||||||
<ResultsButton>
|
<ResultsButton>
|
||||||
<CodeBlock lang={props.state.input.format || "text"} code={props.state.output} />
|
<ContentCode lang={props.state.input.format || "text"} code={props.state.output} />
|
||||||
</ResultsButton>
|
</ResultsButton>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
@@ -447,7 +484,7 @@ export function ReadTool(props: ToolProps) {
|
|||||||
<div data-component="tool-result">
|
<div data-component="tool-result">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={props.state.metadata?.error}>
|
<Match when={props.state.metadata?.error}>
|
||||||
<div data-component="error">{formatErrorString(props.state.output)}</div>
|
<ContentError>{formatErrorString(props.state.output)}</ContentError>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={typeof props.state.metadata?.preview === "string"}>
|
<Match when={typeof props.state.metadata?.preview === "string"}>
|
||||||
<ResultsButton showCopy="Show preview" hideCopy="Hide preview">
|
<ResultsButton showCopy="Show preview" hideCopy="Hide preview">
|
||||||
@@ -483,7 +520,7 @@ export function WriteTool(props: ToolProps) {
|
|||||||
<div data-component="tool-result">
|
<div data-component="tool-result">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={props.state.metadata?.error}>
|
<Match when={props.state.metadata?.error}>
|
||||||
<div data-component="error">{formatErrorString(props.state.output)}</div>
|
<ContentError>{formatErrorString(props.state.output)}</ContentError>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={props.state.input?.content}>
|
<Match when={props.state.input?.content}>
|
||||||
<ResultsButton showCopy="Show contents" hideCopy="Hide contents">
|
<ResultsButton showCopy="Show contents" hideCopy="Hide contents">
|
||||||
@@ -511,7 +548,7 @@ export function EditTool(props: ToolProps) {
|
|||||||
<div data-component="tool-result">
|
<div data-component="tool-result">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={props.state.metadata?.error}>
|
<Match when={props.state.metadata?.error}>
|
||||||
<div data-component="error">{formatErrorString(props.state.metadata?.message || "")}</div>
|
<ContentError>{formatErrorString(props.state.metadata?.message || "")}</ContentError>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={props.state.metadata?.diff}>
|
<Match when={props.state.metadata?.diff}>
|
||||||
<div data-component="diff">
|
<div data-component="diff">
|
||||||
@@ -662,3 +699,35 @@ function flattenToolArgs(obj: any, prefix: string = ""): Array<[string, any]> {
|
|||||||
|
|
||||||
return entries
|
return entries
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getProvider(model: string) {
|
||||||
|
const lowerModel = model.toLowerCase()
|
||||||
|
|
||||||
|
if (/claude|anthropic/.test(lowerModel)) return "anthropic"
|
||||||
|
if (/gpt|o[1-4]|codex|openai/.test(lowerModel)) return "openai"
|
||||||
|
if (/gemini|palm|bard|google/.test(lowerModel)) return "gemini"
|
||||||
|
if (/llama|meta/.test(lowerModel)) return "meta"
|
||||||
|
|
||||||
|
return "any"
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProviderIcon(props: { model: string; size?: number }) {
|
||||||
|
const provider = getProvider(props.model)
|
||||||
|
const size = props.size || 16
|
||||||
|
return (
|
||||||
|
<Switch fallback={<IconSparkles width={size} height={size} />}>
|
||||||
|
<Match when={provider === "openai"}>
|
||||||
|
<IconOpenAI width={size} height={size} />
|
||||||
|
</Match>
|
||||||
|
<Match when={provider === "anthropic"}>
|
||||||
|
<IconAnthropic width={size} height={size} />
|
||||||
|
</Match>
|
||||||
|
<Match when={provider === "gemini"}>
|
||||||
|
<IconGemini width={size} height={size} />
|
||||||
|
</Match>
|
||||||
|
<Match when={provider === "meta"}>
|
||||||
|
<IconMeta width={size} height={size} />
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -127,3 +127,6 @@ The opencode CLI takes the following flags.
|
|||||||
| `--help` | `-h` | Display help |
|
| `--help` | `-h` | Display help |
|
||||||
| `--version` | | Print version number |
|
| `--version` | | Print version number |
|
||||||
| `--print-logs` | | Print logs to stderr |
|
| `--print-logs` | | Print logs to stderr |
|
||||||
|
| `--prompt` | `-p` | Prompt to use |
|
||||||
|
| `--model` | `-m` | Model to use in the form of provider/model |
|
||||||
|
| `--mode` | | Mode to use |
|
||||||
|
|||||||
@@ -3,12 +3,9 @@ title: Config
|
|||||||
description: Using the opencode JSON config.
|
description: Using the opencode JSON config.
|
||||||
---
|
---
|
||||||
|
|
||||||
You can configure opencode using a JSON config file that can be placed in:
|
You can configure opencode using a JSON config file.
|
||||||
|
|
||||||
- Globally under `~/.config/opencode/config.json`.
|
```json title="opencode config"
|
||||||
- Your project root under `opencode.json`. This is safe to be checked into Git and uses the same schema as the global one.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
{
|
||||||
"$schema": "https://opencode.ai/config.json",
|
"$schema": "https://opencode.ai/config.json",
|
||||||
"theme": "opencode",
|
"theme": "opencode",
|
||||||
@@ -18,10 +15,24 @@ You can configure opencode using a JSON config file that can be placed in:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
In most cases, you'll want to use the global config for things like themes, providers, or keybinds. Having a config per project is useful if you are using different providers for your company.
|
This can be used to configure opencode globally or for a specific project.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Global
|
||||||
|
|
||||||
|
Place your global opencode config in `~/.config/opencode/config.json`. You'll want to use the global config for things like themes, providers, or keybinds.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Per project
|
||||||
|
|
||||||
|
You can also add a `opencode.json` in your project. This is useful for configuring providers or modes specific to your project.
|
||||||
|
|
||||||
When opencode starts up, it looks for a config file in the current directory or traverse up to the nearest Git directory.
|
When opencode starts up, it looks for a config file in the current directory or traverse up to the nearest Git directory.
|
||||||
|
|
||||||
|
This is also safe to be checked into Git and uses the same schema as the global one.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Schema
|
## Schema
|
||||||
@@ -32,6 +43,25 @@ Your editor should be able to validate and autocomplete based on the schema.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Modes
|
||||||
|
|
||||||
|
opencode comes with two built-in modes: _build_, the default with all tools enabled. And _plan_, restricted mode with file modification tools disabled. You can override these built-in modes or define your own custom modes with the `mode` option.
|
||||||
|
|
||||||
|
```json title="opencode.json"
|
||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"mode": {
|
||||||
|
"build": { },
|
||||||
|
"plan": { },
|
||||||
|
"my-custom-mode": { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
[Learn more here](/docs/modes).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Models
|
### Models
|
||||||
|
|
||||||
You can configure the providers and models you want to use in your opencode config through the `provider` and `model` options.
|
You can configure the providers and models you want to use in your opencode config through the `provider` and `model` options.
|
||||||
@@ -44,7 +74,7 @@ You can configure the providers and models you want to use in your opencode conf
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
[Learn more here](/docs/models).
|
You can also configure [local models](/docs/models#local). [Learn more](/docs/models).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -63,6 +93,36 @@ You can configure the theme you want to use in your opencode config through the
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
|
||||||
|
Logs are written to:
|
||||||
|
|
||||||
|
- **macOS/Linux**: `~/.local/share/opencode/log/`
|
||||||
|
- **Windows**: `%APPDATA%\opencode\log\`
|
||||||
|
|
||||||
|
You can configure the minimum log level through the `log_level` option.
|
||||||
|
|
||||||
|
```json title="opencode.json"
|
||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"log_level": "INFO"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
With the following options:
|
||||||
|
|
||||||
|
| Level | Description |
|
||||||
|
| ------- | ---------------------------------------- |
|
||||||
|
| `DEBUG` | All messages including debug information |
|
||||||
|
| `INFO` | Informational messages and above |
|
||||||
|
| `WARN` | Warnings and errors only |
|
||||||
|
| `ERROR` | Errors only |
|
||||||
|
|
||||||
|
The **default** log level is `INFO`. If you are running opencode locally in
|
||||||
|
development mode it's set to `DEBUG`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Keybinds
|
### Keybinds
|
||||||
|
|
||||||
You can customize your keybinds through the `keybinds` option.
|
You can customize your keybinds through the `keybinds` option.
|
||||||
@@ -109,3 +169,58 @@ The `disabled_providers` option accepts an array of provider IDs. When a provide
|
|||||||
- It won't be loaded even if environment variables are set
|
- It won't be loaded even if environment variables are set
|
||||||
- It won't be loaded even if API keys are configured through `opencode auth login`
|
- It won't be loaded even if API keys are configured through `opencode auth login`
|
||||||
- The provider's models won't appear in the model selection list
|
- The provider's models won't appear in the model selection list
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Variables
|
||||||
|
|
||||||
|
You can use variable substitution in your config files to reference environment variables and file contents.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Env vars
|
||||||
|
|
||||||
|
Use `{env:VARIABLE_NAME}` to substitute environment variables:
|
||||||
|
|
||||||
|
```json title="opencode.json"
|
||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"model": "{env:OPENCODE_MODEL}",
|
||||||
|
"provider": {
|
||||||
|
"anthropic": {
|
||||||
|
"api_key": "{env:ANTHROPIC_API_KEY}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If the environment variable is not set, it will be replaced with an empty string.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Files
|
||||||
|
|
||||||
|
Use `{file:path/to/file}` to substitute the contents of a file:
|
||||||
|
|
||||||
|
```json title="opencode.json"
|
||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"instructions": ["{file:./custom-instructions.md}"],
|
||||||
|
"provider": {
|
||||||
|
"openai": {
|
||||||
|
"api_key": "{file:~/.secrets/openai-key}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
File paths can be:
|
||||||
|
|
||||||
|
- Relative to the config file directory
|
||||||
|
- Or absolute paths starting with `/` or `~`
|
||||||
|
|
||||||
|
These are useful for:
|
||||||
|
|
||||||
|
- Keeping sensitive data like API keys in separate files.
|
||||||
|
- Including large instruction files without cluttering your config.
|
||||||
|
- Sharing common configuration snippets across multiple config files.
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user