run formatter

This commit is contained in:
Dax Raad 2025-05-31 14:41:00 -04:00
parent 6df19f1828
commit 3b746162d2
52 changed files with 1376 additions and 1390 deletions

View file

@ -83,6 +83,7 @@ You can configure OpenCode using environment variables:
| `AZURE_OPENAI_ENDPOINT` | For Azure OpenAI models |
| `AZURE_OPENAI_API_KEY` | For Azure OpenAI models (optional when using Entra ID) |
| `AZURE_OPENAI_API_VERSION` | For Azure OpenAI models |
### Configuration File Structure
```json
@ -205,7 +206,7 @@ To use bedrock models with OpenCode you need three things.
1. Valid AWS credentials (the env vars: `AWS_SECRET_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_REGION`)
2. Access to the corresponding model in AWS Bedrock in your region.
a. You can request access in the AWS console on the Bedrock -> "Model access" page.
a. You can request access in the AWS console on the Bedrock -> "Model access" page.
3. A correct configuration file. You don't need the `providers` key. Instead you have to prefix your models per agent with `bedrock.` and then a valid model. For now only Claude 3.7 is supported.
```json
@ -226,10 +227,10 @@ To use bedrock models with OpenCode you need three things.
"maxTokens": 80,
"reasoningEffort": ""
}
},
}
}
```
## Interactive Mode Usage
```bash
@ -295,26 +296,26 @@ These flags are mutually exclusive - you can use either `--allowedTools` or `--e
OpenCode supports the following output formats in non-interactive mode:
| Format | Description |
| ------ | -------------------------------------- |
| `text` | Plain text output (default) |
| `json` | Output wrapped in a JSON object |
| Format | Description |
| ------ | ------------------------------- |
| `text` | Plain text output (default) |
| `json` | Output wrapped in a JSON object |
The output format is implemented as a strongly-typed `OutputFormat` in the codebase, ensuring type safety and validation when processing outputs.
## Command-line Flags
| Flag | Short | Description |
| ----------------- | ----- | ---------------------------------------------------------- |
| `--help` | `-h` | Display help information |
| `--debug` | `-d` | Enable debug mode |
| `--cwd` | `-c` | Set current working directory |
| `--prompt` | `-p` | Run a single prompt in non-interactive mode |
| `--output-format` | `-f` | Output format for non-interactive mode (text, json) |
| `--quiet` | `-q` | Hide spinner in non-interactive mode |
| `--verbose` | | Display logs to stderr in non-interactive mode |
| `--allowedTools` | | Restrict the agent to only use specified tools |
| `--excludedTools` | | Prevent the agent from using specified tools |
| Flag | Short | Description |
| ----------------- | ----- | --------------------------------------------------- |
| `--help` | `-h` | Display help information |
| `--debug` | `-d` | Enable debug mode |
| `--cwd` | `-c` | Set current working directory |
| `--prompt` | `-p` | Run a single prompt in non-interactive mode |
| `--output-format` | `-f` | Output format for non-interactive mode (text, json) |
| `--quiet` | `-q` | Hide spinner in non-interactive mode |
| `--verbose` | | Display logs to stderr in non-interactive mode |
| `--allowedTools` | | Restrict the agent to only use specified tools |
| `--excludedTools` | | Prevent the agent from using specified tools |
## Keyboard Shortcuts
@ -483,6 +484,7 @@ You don't need to define all colors. Any undefined colors will fall back to the
### Shell Configuration
OpenCode allows you to configure the shell used by the `bash` tool. By default, it uses:
1. The shell specified in the config file (if provided)
2. The shell from the `$SHELL` environment variable (if available)
3. Falls back to `/bin/bash` if neither of the above is available
@ -577,6 +579,7 @@ RUN grep -R "$SEARCH_PATTERN" $DIRECTORY
```
When you run a command with arguments, OpenCode will prompt you to enter values for each unique placeholder. Named arguments provide several benefits:
- Clear identification of what each argument represents
- Ability to use the same argument multiple times
- Better organization for commands with multiple inputs

View file

@ -30,7 +30,6 @@
"env-paths": "3.0.0",
"hono": "4.7.10",
"hono-openapi": "0.4.8",
"jsdom": "26.1.0",
"remeda": "2.22.3",
"ts-lsp-client": "1.0.3",
"turndown": "7.2.0",
@ -42,7 +41,6 @@
"devDependencies": {
"@tsconfig/bun": "1.0.7",
"@types/bun": "latest",
"@types/jsdom": "21.1.7",
"@types/turndown": "5.0.5",
"typescript": "catalog:",
},
@ -97,8 +95,6 @@
"@apidevtools/json-schema-ref-parser": ["@apidevtools/json-schema-ref-parser@11.9.3", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.0" } }, "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ=="],
"@asamuzakjp/css-color": ["@asamuzakjp/css-color@3.2.0", "", { "dependencies": { "@csstools/css-calc": "^2.1.3", "@csstools/css-color-parser": "^3.0.9", "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "lru-cache": "^10.4.3" } }, "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw=="],
"@astrojs/compiler": ["@astrojs/compiler@2.12.0", "", {}, "sha512-7bCjW6tVDpUurQLeKBUN9tZ5kSv5qYrGmcn0sG0IwacL7isR2ZbyyA3AdZ4uxsuUFOS2SlgReTH7wkxO6zpqWA=="],
"@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="],
@ -157,16 +153,6 @@
"@cloudflare/workers-types": ["@cloudflare/workers-types@4.20250522.0", "", {}, "sha512-9RIffHobc35JWeddzBguGgPa4wLDr5x5F94+0/qy7LiV6pTBQ/M5qGEN9VA16IDT3EUpYI0WKh6VpcmeVEtVtw=="],
"@csstools/color-helpers": ["@csstools/color-helpers@5.0.2", "", {}, "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA=="],
"@csstools/css-calc": ["@csstools/css-calc@2.1.4", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ=="],
"@csstools/css-color-parser": ["@csstools/css-color-parser@3.0.10", "", { "dependencies": { "@csstools/color-helpers": "^5.0.2", "@csstools/css-calc": "^2.1.4" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg=="],
"@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@3.0.5", "", { "peerDependencies": { "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ=="],
"@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="],
"@ctrl/tinycolor": ["@ctrl/tinycolor@4.1.0", "", {}, "sha512-WyOx8cJQ+FQus4Mm4uPIZA64gbk3Wxh0so5Lcii0aJifqwoVOlfFtorjLE0Hen4OYyHZMXDWqMmaQemBhgxFRQ=="],
"@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="],
@ -407,8 +393,6 @@
"@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="],
"@types/jsdom": ["@types/jsdom@21.1.7", "", { "dependencies": { "@types/node": "*", "@types/tough-cookie": "*", "parse5": "^7.0.0" } }, "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/luxon": ["@types/luxon@3.6.2", "", {}, "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw=="],
@ -425,8 +409,6 @@
"@types/sax": ["@types/sax@1.2.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A=="],
"@types/tough-cookie": ["@types/tough-cookie@4.0.5", "", {}, "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA=="],
"@types/turndown": ["@types/turndown@5.0.5", "", {}, "sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w=="],
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
@ -439,8 +421,6 @@
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
"agent-base": ["agent-base@7.1.3", "", {}, "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="],
"ai": ["ai@5.0.0-alpha.7", "", { "dependencies": { "@ai-sdk/gateway": "1.0.0-alpha.7", "@ai-sdk/provider": "2.0.0-alpha.7", "@ai-sdk/provider-utils": "3.0.0-alpha.7", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-ShCk3frIMdVtK9knvWKiFS7N6Vwnf8mLMv670+T//W9oqfoetSVPBhTF6Dy+oDM/bjVSsBf1BuYImLDvHICOIQ=="],
"ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
@ -601,12 +581,8 @@
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
"cssstyle": ["cssstyle@4.3.1", "", { "dependencies": { "@asamuzakjp/css-color": "^3.1.2", "rrweb-cssom": "^0.8.0" } }, "sha512-ZgW+Jgdd7i52AaLYCriF8Mxqft0gD/R9i9wi6RWBhs1pqdPEzPjym7rvRKi397WmQFf3SlyUsszhw+VVCbx79Q=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"data-urls": ["data-urls@5.0.0", "", { "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0" } }, "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg=="],
"dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="],
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
@ -827,8 +803,6 @@
"hono-openapi": ["hono-openapi@0.4.8", "", { "dependencies": { "json-schema-walker": "^2.0.0" }, "peerDependencies": { "@hono/arktype-validator": "^2.0.0", "@hono/effect-validator": "^1.2.0", "@hono/typebox-validator": "^0.2.0 || ^0.3.0", "@hono/valibot-validator": "^0.5.1", "@hono/zod-validator": "^0.4.1", "@sinclair/typebox": "^0.34.9", "@valibot/to-json-schema": "^1.0.0-beta.3", "arktype": "^2.0.0", "effect": "^3.11.3", "hono": "^4.6.13", "openapi-types": "^12.1.3", "valibot": "^1.0.0-beta.9", "zod": "^3.23.8", "zod-openapi": "^4.0.0" }, "optionalPeers": ["@hono/arktype-validator", "@hono/effect-validator", "@hono/typebox-validator", "@hono/valibot-validator", "@hono/zod-validator", "@sinclair/typebox", "@valibot/to-json-schema", "arktype", "effect", "hono", "valibot", "zod", "zod-openapi"] }, "sha512-LYr5xdtD49M7hEAduV1PftOMzuT8ZNvkyWfh1DThkLsIr4RkvDb12UxgIiFbwrJB6FLtFXLoOZL9x4IeDk2+VA=="],
"html-encoding-sniffer": ["html-encoding-sniffer@4.0.0", "", { "dependencies": { "whatwg-encoding": "^3.1.1" } }, "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ=="],
"html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="],
"html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="],
@ -841,10 +815,6 @@
"http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"i18next": ["i18next@23.16.8", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg=="],
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
@ -887,8 +857,6 @@
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
"is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="],
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
"is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
@ -911,8 +879,6 @@
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
"jsdom": ["jsdom@26.1.0", "", { "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", "decimal.js": "^10.5.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "nwsapi": "^2.2.16", "parse5": "^7.2.1", "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^5.1.1", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.1.1", "ws": "^8.18.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json-rpc-2.0": ["json-rpc-2.0@1.7.0", "", {}, "sha512-asnLgC1qD5ytP+fvBP8uL0rvj+l8P6iYICbzZ8dVxCpESffVjzA7KkYkbKCIbavs7cllwH1ZUaNtJwphdeRqpg=="],
@ -1105,8 +1071,6 @@
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
"nwsapi": ["nwsapi@2.2.20", "", {}, "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"object-hash": ["object-hash@2.2.0", "", {}, "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="],
@ -1283,8 +1247,6 @@
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
"rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
@ -1295,8 +1257,6 @@
"sax": ["sax@1.2.1", "", {}, "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA=="],
"saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="],
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="],
@ -1393,8 +1353,6 @@
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
"tar-fs": ["tar-fs@3.0.9", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-XF4w9Xp+ZQgifKakjZYmFdkLoSWd34VGKcsTCwlNWM7QG3ZbaxnTsaBwnjFZqHRf/rROxaR8rXnbtwdvaDI+lA=="],
"tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="],
@ -1409,19 +1367,13 @@
"tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
"tldts": ["tldts@6.1.86", "", { "dependencies": { "tldts-core": "^6.1.86" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ=="],
"tldts-core": ["tldts-core@6.1.86", "", {}, "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA=="],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"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.2.4", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-W5mdbcgRpTBDFyEdcU81USs3MFZoXMInpSznc/AFZCwqz8atk4iBNDIlhvihpGHY54Nf5crKmZwJjxVojkHFvA=="],
"tough-cookie": ["tough-cookie@5.1.2", "", { "dependencies": { "tldts": "^6.1.32" } }, "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A=="],
"tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="],
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
@ -1519,17 +1471,11 @@
"vscode-languageserver-types": ["vscode-languageserver-types@3.17.3", "", {}, "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA=="],
"w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="],
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
"webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="],
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
"whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="],
"whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="],
"whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="],
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
"which-pm-runs": ["which-pm-runs@1.1.0", "", {}, "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA=="],
@ -1541,16 +1487,10 @@
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"ws": ["ws@8.18.2", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ=="],
"xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="],
"xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="],
"xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="],
"xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="],
"xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="],
"yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
@ -1573,8 +1513,6 @@
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
"@asamuzakjp/css-color/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"@astrojs/mdx/@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.2", "", { "dependencies": { "@astrojs/internal-helpers": "0.6.1", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.1.0", "js-yaml": "^4.1.0", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.2.1", "smol-toml": "^1.3.1", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "vfile": "^6.0.3" } }, "sha512-bO35JbWpVvyKRl7cmSJD822e8YA8ThR/YbUsciWNA7yTcqpIAL2hJDToWP5KcZBWxGT6IOdOkHSXARSNZc4l/Q=="],
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
@ -1611,8 +1549,6 @@
"mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
"node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
"opencontrol/hono": ["hono@4.7.4", "", {}, "sha512-Pst8FuGqz3L7tFF+u9Pu70eI0xa5S3LPUmrNd5Jm8nTHze9FxLTK9Kaj5g/k4UcwuJSXTP65SyHOPLrffpcAJg=="],
"opencontrol/zod-to-json-schema": ["zod-to-json-schema@3.24.3", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A=="],
@ -1639,8 +1575,6 @@
"token-types/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"tr46/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"unstorage/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"vscode-languageserver-protocol/vscode-jsonrpc": ["vscode-jsonrpc@8.1.0", "", {}, "sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw=="],
@ -1659,10 +1593,6 @@
"bl/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
"node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
"pino-pretty/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"prebuild-install/tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],

View file

@ -6,20 +6,20 @@
import "sst"
declare module "sst" {
export interface Resource {
"Web": {
"type": "sst.cloudflare.StaticSite"
"url": string
Web: {
type: "sst.cloudflare.StaticSite"
url: string
}
}
}
// cloudflare
import * as cloudflare from "@cloudflare/workers-types";
// cloudflare
import * as cloudflare from "@cloudflare/workers-types"
declare module "sst" {
export interface Resource {
"Api": cloudflare.Service
"Bucket": cloudflare.R2Bucket
Api: cloudflare.Service
Bucket: cloudflare.R2Bucket
}
}
import "sst"
export {}
export {}

View file

@ -14,7 +14,6 @@
"devDependencies": {
"@tsconfig/bun": "1.0.7",
"@types/bun": "latest",
"@types/jsdom": "21.1.7",
"@types/turndown": "5.0.5",
"typescript": "catalog:"
},
@ -28,7 +27,6 @@
"env-paths": "3.0.0",
"hono": "4.7.10",
"hono-openapi": "0.4.8",
"jsdom": "26.1.0",
"remeda": "2.22.3",
"ts-lsp-client": "1.0.3",
"turndown": "7.2.0",

View file

@ -28,7 +28,7 @@ for (const [os, arch] of targets) {
console.log(`building ${os}-${arch}`)
const name = `${pkg.name}-${os}-${arch}`
await $`mkdir -p dist/${name}/bin`
await $`GOOS=${os} GOARCH=${GOARCH[arch]} go build -o ../opencode/dist/${name}/bin/tui ../tui/main.go`.cwd(
await $`GOOS=${os} GOARCH=${GOARCH[arch]} go build -ldflags="-s -w -X github.com/sst/opencode/internal/version.Version=${version}" -o ../opencode/dist/${name}/bin/tui ../tui/main.go`.cwd(
"../tui",
)
await $`bun build --compile --minify --target=bun-${os}-${arch} --outfile=dist/${name}/bin/opencode ./src/index.ts ./dist/${name}/bin/tui`

View file

@ -1,40 +1,40 @@
import fs from "fs/promises";
import { AppPath } from "./path";
import { Log } from "../util/log";
import { Context } from "../util/context";
import fs from "fs/promises"
import { AppPath } from "./path"
import { Log } from "../util/log"
import { Context } from "../util/context"
export namespace App {
const log = Log.create({ service: "app" });
const log = Log.create({ service: "app" })
export type Info = Awaited<ReturnType<typeof create>>;
export type Info = Awaited<ReturnType<typeof create>>
const ctx = Context.create<Info>("app");
const ctx = Context.create<Info>("app")
async function create(input: { directory: string }) {
const dataDir = AppPath.data(input.directory);
await fs.mkdir(dataDir, { recursive: true });
await Log.file(input.directory);
const dataDir = AppPath.data(input.directory)
await fs.mkdir(dataDir, { recursive: true })
await Log.file(input.directory)
log.info("created", { path: dataDir });
log.info("created", { path: dataDir })
const services = new Map<
any,
{
state: any;
shutdown?: (input: any) => Promise<void>;
state: any
shutdown?: (input: any) => Promise<void>
}
>();
>()
const result = {
get services() {
return services;
return services
},
get root() {
return input.directory;
return input.directory
},
};
}
return result;
return result
}
export function state<State>(
@ -43,36 +43,36 @@ export namespace App {
shutdown?: (state: Awaited<State>) => Promise<void>,
) {
return () => {
const app = ctx.use();
const services = app.services;
const app = ctx.use()
const services = app.services
if (!services.has(key)) {
log.info("registering service", { name: key });
log.info("registering service", { name: key })
services.set(key, {
state: init(app),
shutdown: shutdown,
});
})
}
return services.get(key)?.state as State;
};
return services.get(key)?.state as State
}
}
export async function use() {
return ctx.use();
return ctx.use()
}
export async function provide<T extends (app: Info) => any>(
input: { directory: string },
cb: T,
) {
const app = await create(input);
const app = await create(input)
return ctx.provide(app, async () => {
const result = await cb(app);
const result = await cb(app)
for (const [key, entry] of app.services.entries()) {
log.info("shutdown", { name: key });
await entry.shutdown?.(await entry.state);
log.info("shutdown", { name: key })
await entry.shutdown?.(await entry.state)
}
return result;
});
return result
})
}
}

View file

@ -1,11 +1,11 @@
import path from "path";
import path from "path"
export namespace AppPath {
export function data(input: string) {
return path.join(input, ".opencode");
return path.join(input, ".opencode")
}
export function storage(input: string) {
return path.join(data(input), "storage");
return path.join(data(input), "storage")
}
}

View file

@ -1,7 +1,7 @@
import path from "path";
import { Log } from "../util/log";
import path from "path"
import { Log } from "../util/log"
export namespace BunProc {
const log = Log.create({ service: "bun" });
const log = Log.create({ service: "bun" })
export function run(
cmd: string[],
@ -10,11 +10,11 @@ export namespace BunProc {
const root =
process.argv0 !== "bun"
? path.resolve(process.cwd(), process.argv0)
: process.argv0;
: process.argv0
log.info("running", {
cmd: [root, ...cmd],
options,
});
})
const result = Bun.spawnSync([root, ...cmd], {
...options,
argv0: "bun",
@ -22,7 +22,11 @@ export namespace BunProc {
...process.env,
...options?.env,
},
});
return result;
})
if (result.exitCode !== 0) {
console.error(result.stderr?.toString("utf8") ?? "")
throw new Error(`Command failed with exit code ${result.exitCode}`)
}
return result
}
}

View file

@ -1,22 +1,22 @@
import { z, type ZodType } from "zod";
import { App } from "../app/app";
import { Log } from "../util/log";
import { z, type ZodType } from "zod"
import { App } from "../app/app"
import { Log } from "../util/log"
export namespace Bus {
const log = Log.create({ service: "bus" });
type Subscription = (event: any) => void;
const log = Log.create({ service: "bus" })
type Subscription = (event: any) => void
const state = App.state("bus", () => {
const subscriptions = new Map<any, Subscription[]>();
const subscriptions = new Map<any, Subscription[]>()
return {
subscriptions,
};
});
}
})
export type EventDefinition = ReturnType<typeof event>;
export type EventDefinition = ReturnType<typeof event>
const registry = new Map<string, EventDefinition>();
const registry = new Map<string, EventDefinition>()
export function event<Type extends string, Properties extends ZodType>(
type: Type,
@ -25,9 +25,9 @@ export namespace Bus {
const result = {
type,
properties,
};
registry.set(type, result);
return result;
}
registry.set(type, result)
return result
}
export function payloads() {
@ -46,7 +46,7 @@ export namespace Bus {
}),
)
.toArray() as any,
);
)
}
export function publish<Definition extends EventDefinition>(
@ -56,14 +56,14 @@ export namespace Bus {
const payload = {
type: def.type,
properties,
};
}
log.info("publishing", {
type: def.type,
});
})
for (const key of [def.type, "*"]) {
const match = state().subscriptions.get(key);
const match = state().subscriptions.get(key)
for (const sub of match ?? []) {
sub(payload);
sub(payload)
}
}
}
@ -71,31 +71,31 @@ export namespace Bus {
export function subscribe<Definition extends EventDefinition>(
def: Definition,
callback: (event: {
type: Definition["type"];
properties: z.infer<Definition["properties"]>;
type: Definition["type"]
properties: z.infer<Definition["properties"]>
}) => void,
) {
return raw(def.type, callback);
return raw(def.type, callback)
}
export function subscribeAll(callback: (event: any) => void) {
return raw("*", callback);
return raw("*", callback)
}
function raw(type: string, callback: (event: any) => void) {
log.info("subscribing", { type });
const subscriptions = state().subscriptions;
let match = subscriptions.get(type) ?? [];
match.push(callback);
subscriptions.set(type, match);
log.info("subscribing", { type })
const subscriptions = state().subscriptions
let match = subscriptions.get(type) ?? []
match.push(callback)
subscriptions.set(type, match)
return () => {
log.info("unsubscribing", { type });
const match = subscriptions.get(type);
if (!match) return;
const index = match.indexOf(callback);
if (index === -1) return;
match.splice(index, 1);
};
log.info("unsubscribing", { type })
const match = subscriptions.get(type)
if (!match) return
const index = match.indexOf(callback)
if (index === -1) return
match.splice(index, 1)
}
}
}

View file

@ -1,51 +1,51 @@
import path from "path";
import { Log } from "../util/log";
import { z } from "zod";
import { App } from "../app/app";
import { Provider } from "../provider/provider";
import path from "path"
import { Log } from "../util/log"
import { z } from "zod"
import { App } from "../app/app"
import { Provider } from "../provider/provider"
export namespace Config {
const log = Log.create({ service: "config" });
const log = Log.create({ service: "config" })
export const state = App.state("config", async (app) => {
const result = await load(app.root);
return result;
});
const result = await load(app.root)
return result
})
export const Info = z
.object({
providers: Provider.Info.array().optional(),
})
.strict();
.strict()
export type Info = z.output<typeof Info>;
export type Info = z.output<typeof Info>
export function get() {
return state();
return state()
}
async function load(directory: string) {
let result: Info = {};
let result: Info = {}
for (const file of ["opencode.jsonc", "opencode.json"]) {
const resolved = path.join(directory, file);
log.info("searching", { path: resolved });
const resolved = path.join(directory, file)
log.info("searching", { path: resolved })
try {
result = await import(path.join(directory, file)).then((mod) =>
Info.parse(mod.default),
);
log.info("found", { path: resolved });
break;
)
log.info("found", { path: resolved })
break
} catch (e) {
if (e instanceof z.ZodError) {
for (const issue of e.issues) {
log.info(issue.message);
log.info(issue.message)
}
throw e;
throw e
}
continue;
continue
}
}
log.info("loaded", result);
return result;
log.info("loaded", result)
return result
}
}

View file

@ -1,20 +1,20 @@
import envpaths from "env-paths";
import fs from "fs/promises";
import envpaths from "env-paths"
import fs from "fs/promises"
const paths = envpaths("opencode", {
suffix: "",
});
})
await Promise.all([
fs.mkdir(paths.config, { recursive: true }),
fs.mkdir(paths.cache, { recursive: true }),
]);
])
export namespace Global {
export function config() {
return paths.config;
return paths.config
}
export function cache() {
return paths.cache;
return paths.cache
}
}

View file

@ -1,28 +1,28 @@
import { z } from "zod";
import { randomBytes } from "crypto";
import { z } from "zod"
import { randomBytes } from "crypto"
export namespace Identifier {
const prefixes = {
session: "ses",
message: "msg",
} as const;
} as const
export function schema(prefix: keyof typeof prefixes) {
return z.string().startsWith(prefixes[prefix]);
return z.string().startsWith(prefixes[prefix])
}
const LENGTH = 26;
const LENGTH = 26
// State for monotonic ID generation
let lastTimestamp = 0;
let counter = 0;
let lastTimestamp = 0
let counter = 0
export function ascending(prefix: keyof typeof prefixes, given?: string) {
return generateID(prefix, false, given);
return generateID(prefix, false, given)
}
export function descending(prefix: keyof typeof prefixes, given?: string) {
return generateID(prefix, true, given);
return generateID(prefix, true, given)
}
function generateID(
@ -31,44 +31,44 @@ export namespace Identifier {
given?: string,
): string {
if (!given) {
return generateNewID(prefix, descending);
return generateNewID(prefix, descending)
}
if (!given.startsWith(prefixes[prefix])) {
throw new Error(`ID ${given} does not start with ${prefixes[prefix]}`);
throw new Error(`ID ${given} does not start with ${prefixes[prefix]}`)
}
return given;
return given
}
function generateNewID(
prefix: keyof typeof prefixes,
descending: boolean,
): string {
const currentTimestamp = Date.now();
const currentTimestamp = Date.now()
if (currentTimestamp !== lastTimestamp) {
lastTimestamp = currentTimestamp;
counter = 0;
lastTimestamp = currentTimestamp
counter = 0
}
counter++;
counter++
let now = BigInt(currentTimestamp) * BigInt(0x1000) + BigInt(counter);
let now = BigInt(currentTimestamp) * BigInt(0x1000) + BigInt(counter)
now = descending ? ~now : now;
now = descending ? ~now : now
const timeBytes = Buffer.alloc(6);
const timeBytes = Buffer.alloc(6)
for (let i = 0; i < 6; i++) {
timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff));
timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff))
}
const randLength = (LENGTH - 12) / 2;
const random = randomBytes(randLength);
const randLength = (LENGTH - 12) / 2
const random = randomBytes(randLength)
return (
prefixes[prefix] +
"_" +
timeBytes.toString("hex") +
random.toString("hex")
);
)
}
}

View file

@ -27,6 +27,7 @@ cli.command("", "Start the opencode in interactive mode").action(async () => {
if (!(await file.exists())) {
console.log("installing tui binary...")
await Bun.write(file, blob, { mode: 0o755 })
await fs.chmod(binary, 0o755)
}
cwd = process.cwd()
cmd = [binary]

View file

@ -115,7 +115,7 @@ export namespace LLM {
provider.id,
)
if (!(await Bun.file(path.join(dir, "package.json")).exists())) {
BunProc.run(["add", "--exact", `@ai-sdk/${provider.id}@alpha`], {
BunProc.run(["add", `@ai-sdk/${provider.id}@alpha`], {
cwd: Global.cache(),
})
}

View file

@ -1,23 +1,23 @@
import { spawn } from "child_process";
import path from "path";
import { spawn } from "child_process"
import path from "path"
import {
createMessageConnection,
StreamMessageReader,
StreamMessageWriter,
} from "vscode-jsonrpc/node";
import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types";
import { App } from "../app/app";
import { Log } from "../util/log";
import { LANGUAGE_EXTENSIONS } from "./language";
import { Bus } from "../bus";
import z from "zod";
} from "vscode-jsonrpc/node"
import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types"
import { App } from "../app/app"
import { Log } from "../util/log"
import { LANGUAGE_EXTENSIONS } from "./language"
import { Bus } from "../bus"
import z from "zod"
export namespace LSPClient {
const log = Log.create({ service: "lsp.client" });
const log = Log.create({ service: "lsp.client" })
export type Info = Awaited<ReturnType<typeof create>>;
export type Info = Awaited<ReturnType<typeof create>>
export type Diagnostic = VSCodeDiagnostic;
export type Diagnostic = VSCodeDiagnostic
export const Event = {
Diagnostics: Bus.event(
@ -27,36 +27,36 @@ export namespace LSPClient {
path: z.string(),
}),
),
};
}
export async function create(input: { cmd: string[]; serverID: string }) {
log.info("starting client", input);
log.info("starting client", input)
const app = await App.use();
const [command, ...args] = input.cmd;
const app = await App.use()
const [command, ...args] = input.cmd
const server = spawn(command, args, {
stdio: ["pipe", "pipe", "pipe"],
cwd: app.root,
});
})
const connection = createMessageConnection(
new StreamMessageReader(server.stdout),
new StreamMessageWriter(server.stdin),
);
)
const diagnostics = new Map<string, Diagnostic[]>();
const diagnostics = new Map<string, Diagnostic[]>()
connection.onNotification("textDocument/publishDiagnostics", (params) => {
const path = new URL(params.uri).pathname;
const path = new URL(params.uri).pathname
log.info("textDocument/publishDiagnostics", {
path,
});
const exists = diagnostics.has(path);
diagnostics.set(path, params.diagnostics);
})
const exists = diagnostics.has(path)
diagnostics.set(path, params.diagnostics)
// servers seem to send one blank publishDiagnostics event before the first real one
if (!exists && !params.diagnostics.length) return;
Bus.publish(Event.Diagnostics, { path, serverID: input.serverID });
});
connection.listen();
if (!exists && !params.diagnostics.length) return
Bus.publish(Event.Diagnostics, { path, serverID: input.serverID })
})
connection.listen()
await connection.sendRequest("initialize", {
processId: server.pid,
@ -116,29 +116,29 @@ export namespace LSPClient {
},
window: {},
},
});
await connection.sendNotification("initialized", {});
log.info("initialized");
})
await connection.sendNotification("initialized", {})
log.info("initialized")
const files = new Set<string>();
const files = new Set<string>()
const result = {
get clientID() {
return input.serverID;
return input.serverID
},
get connection() {
return connection;
return connection
},
notify: {
async open(input: { path: string }) {
const file = Bun.file(input.path);
const text = await file.text();
const opened = files.has(input.path);
const file = Bun.file(input.path)
const text = await file.text()
const opened = files.has(input.path)
if (!opened) {
log.info("textDocument/didOpen", input);
diagnostics.delete(input.path);
const extension = path.extname(input.path);
const languageId = LANGUAGE_EXTENSIONS[extension] ?? "plaintext";
log.info("textDocument/didOpen", input)
diagnostics.delete(input.path)
const extension = path.extname(input.path)
const languageId = LANGUAGE_EXTENSIONS[extension] ?? "plaintext"
await connection.sendNotification("textDocument/didOpen", {
textDocument: {
uri: `file://` + input.path,
@ -146,13 +146,13 @@ export namespace LSPClient {
version: Date.now(),
text,
},
});
files.add(input.path);
return;
})
files.add(input.path)
return
}
log.info("textDocument/didChange", input);
diagnostics.delete(input.path);
log.info("textDocument/didChange", input)
diagnostics.delete(input.path)
await connection.sendNotification("textDocument/didChange", {
textDocument: {
uri: `file://` + input.path,
@ -163,16 +163,16 @@ export namespace LSPClient {
text,
},
],
});
})
},
},
get diagnostics() {
return diagnostics;
return diagnostics
},
async waitForDiagnostics(input: { path: string }) {
log.info("waiting for diagnostics", input);
let unsub: () => void;
let timeout: NodeJS.Timeout;
log.info("waiting for diagnostics", input)
let unsub: () => void
let timeout: NodeJS.Timeout
return await Promise.race([
new Promise<void>(async (resolve) => {
unsub = Bus.subscribe(Event.Diagnostics, (event) => {
@ -180,29 +180,29 @@ export namespace LSPClient {
event.properties.path === input.path &&
event.properties.serverID === result.clientID
) {
log.info("got diagnostics", input);
clearTimeout(timeout);
unsub?.();
resolve();
log.info("got diagnostics", input)
clearTimeout(timeout)
unsub?.()
resolve()
}
});
})
}),
new Promise<void>((resolve) => {
timeout = setTimeout(() => {
log.info("timed out refreshing diagnostics", input);
unsub?.();
resolve();
}, 5000);
log.info("timed out refreshing diagnostics", input)
unsub?.()
resolve()
}, 5000)
}),
]);
])
},
async shutdown() {
log.info("shutting down");
connection.end();
connection.dispose();
log.info("shutting down")
connection.end()
connection.dispose()
},
};
}
return result;
return result
}
}

View file

@ -1,64 +1,64 @@
import { App } from "../app/app";
import { Log } from "../util/log";
import { LSPClient } from "./client";
import path from "path";
import { App } from "../app/app"
import { Log } from "../util/log"
import { LSPClient } from "./client"
import path from "path"
export namespace LSP {
const log = Log.create({ service: "lsp" });
const log = Log.create({ service: "lsp" })
const state = App.state(
"lsp",
async () => {
log.info("initializing");
const clients = new Map<string, LSPClient.Info>();
log.info("initializing")
const clients = new Map<string, LSPClient.Info>()
return {
clients,
};
}
},
async (state) => {
for (const client of state.clients.values()) {
await client.shutdown();
await client.shutdown()
}
},
);
)
export async function file(input: string) {
const extension = path.parse(input).ext;
const s = await state();
const matches = AUTO.filter((x) => x.extensions.includes(extension));
const extension = path.parse(input).ext
const s = await state()
const matches = AUTO.filter((x) => x.extensions.includes(extension))
for (const match of matches) {
const existing = s.clients.get(match.id);
if (existing) continue;
const existing = s.clients.get(match.id)
if (existing) continue
const client = await LSPClient.create({
cmd: match.command,
serverID: match.id,
});
s.clients.set(match.id, client);
})
s.clients.set(match.id, client)
}
await run(async (client) => {
const wait = client.waitForDiagnostics({ path: input });
await client.notify.open({ path: input });
return wait;
});
const wait = client.waitForDiagnostics({ path: input })
await client.notify.open({ path: input })
return wait
})
}
export async function diagnostics() {
const results: Record<string, LSPClient.Diagnostic[]> = {};
const results: Record<string, LSPClient.Diagnostic[]> = {}
for (const result of await run(async (client) => client.diagnostics)) {
for (const [path, diagnostics] of result.entries()) {
const arr = results[path] || [];
arr.push(...diagnostics);
results[path] = arr;
const arr = results[path] || []
arr.push(...diagnostics)
results[path] = arr
}
}
return results;
return results
}
export async function hover(input: {
file: string;
line: number;
character: number;
file: string
line: number
character: number
}) {
return run((client) => {
return client.connection.sendRequest("textDocument/hover", {
@ -69,23 +69,23 @@ export namespace LSP {
line: input.line,
character: input.character,
},
});
});
})
})
}
async function run<T>(
input: (client: LSPClient.Info) => Promise<T>,
): Promise<T[]> {
const clients = await state().then((x) => [...x.clients.values()]);
const tasks = clients.map((x) => input(x));
return Promise.all(tasks);
const clients = await state().then((x) => [...x.clients.values()])
const tasks = clients.map((x) => input(x))
return Promise.all(tasks)
}
const AUTO: {
id: string;
command: string[];
extensions: string[];
install?: () => Promise<void>;
id: string
command: string[]
extensions: string[]
install?: () => Promise<void>
}[] = [
{
id: "typescript",
@ -110,7 +110,7 @@ export namespace LSP {
extensions: [".go"],
},
*/
];
]
export namespace Diagnostic {
export function pretty(diagnostic: LSPClient.Diagnostic) {
@ -119,13 +119,13 @@ export namespace LSP {
2: "WARN",
3: "INFO",
4: "HINT",
};
}
const severity = severityMap[diagnostic.severity || 1];
const line = diagnostic.range.start.line + 1;
const col = diagnostic.range.start.character + 1;
const severity = severityMap[diagnostic.severity || 1]
const line = diagnostic.range.start.line + 1
const col = diagnostic.range.start.character + 1
return `${severity} [${line}:${col}] ${diagnostic.message}`;
return `${severity} [${line}:${col}] ${diagnostic.message}`
}
}
}

View file

@ -86,4 +86,4 @@ export const LANGUAGE_EXTENSIONS: Record<string, string> = {
".yml": "yaml",
".mjs": "javascript",
".cjs": "javascript",
} as const;
} as const

View file

@ -1,4 +1,4 @@
import z from "zod";
import z from "zod"
export namespace Provider {
export const Model = z
@ -18,8 +18,8 @@ export namespace Provider {
})
.openapi({
ref: "Provider.Model",
});
export type Model = z.output<typeof Model>;
})
export type Model = z.output<typeof Model>
export const Info = z
.object({
@ -30,6 +30,6 @@ export namespace Provider {
})
.openapi({
ref: "Provider.Info",
});
export type Info = z.output<typeof Info>;
})
export type Info = z.output<typeof Info>
}

View file

@ -1,23 +1,23 @@
import { Log } from "../util/log";
import { Bus } from "../bus";
import { describeRoute, generateSpecs, openAPISpecs } from "hono-openapi";
import { Hono } from "hono";
import { streamSSE } from "hono/streaming";
import { Session } from "../session/session";
import { resolver, validator as zValidator } from "hono-openapi/zod";
import { z } from "zod";
import { LLM } from "../llm/llm";
import { Message } from "../session/message";
import { Provider } from "../provider/provider";
import { Log } from "../util/log"
import { Bus } from "../bus"
import { describeRoute, generateSpecs, openAPISpecs } from "hono-openapi"
import { Hono } from "hono"
import { streamSSE } from "hono/streaming"
import { Session } from "../session/session"
import { resolver, validator as zValidator } from "hono-openapi/zod"
import { z } from "zod"
import { LLM } from "../llm/llm"
import { Message } from "../session/message"
import { Provider } from "../provider/provider"
export namespace Server {
const log = Log.create({ service: "server" });
const PORT = 16713;
const log = Log.create({ service: "server" })
const PORT = 16713
export type App = ReturnType<typeof app>;
export type App = ReturnType<typeof app>
function app() {
const app = new Hono();
const app = new Hono()
const result = app
.get(
@ -53,24 +53,24 @@ export namespace Server {
},
}),
async (c) => {
log.info("event connected");
log.info("event connected")
return streamSSE(c, async (stream) => {
stream.writeSSE({
data: JSON.stringify({}),
});
})
const unsub = Bus.subscribeAll(async (event) => {
await stream.writeSSE({
data: JSON.stringify(event),
});
});
})
})
await new Promise<void>((resolve) => {
stream.onAbort(() => {
unsub();
resolve();
log.info("event disconnected");
});
});
});
unsub()
resolve()
log.info("event disconnected")
})
})
})
},
)
.post(
@ -89,8 +89,8 @@ export namespace Server {
},
}),
async (c) => {
const session = await Session.create();
return c.json(session);
const session = await Session.create()
return c.json(session)
},
)
.post(
@ -115,10 +115,10 @@ export namespace Server {
}),
),
async (c) => {
const body = c.req.valid("json");
await Session.share(body.sessionID);
const session = await Session.get(body.sessionID);
return c.json(session);
const body = c.req.valid("json")
await Session.share(body.sessionID)
const session = await Session.get(body.sessionID)
return c.json(session)
},
)
.post(
@ -143,10 +143,8 @@ export namespace Server {
}),
),
async (c) => {
const messages = await Session.messages(
c.req.valid("json").sessionID,
);
return c.json(messages);
const messages = await Session.messages(c.req.valid("json").sessionID)
return c.json(messages)
},
)
.post(
@ -165,8 +163,8 @@ export namespace Server {
},
}),
async (c) => {
const sessions = await Array.fromAsync(Session.list());
return c.json(sessions);
const sessions = await Array.fromAsync(Session.list())
return c.json(sessions)
},
)
.post(
@ -191,8 +189,8 @@ export namespace Server {
}),
),
async (c) => {
const body = c.req.valid("json");
return c.json(Session.abort(body.sessionID));
const body = c.req.valid("json")
return c.json(Session.abort(body.sessionID))
},
)
.post(
@ -219,9 +217,9 @@ export namespace Server {
}),
),
async (c) => {
const body = c.req.valid("json");
await Session.summarize(body);
return c.json(true);
const body = c.req.valid("json")
await Session.summarize(body)
return c.json(true)
},
)
.post(
@ -249,9 +247,9 @@ export namespace Server {
}),
),
async (c) => {
const body = c.req.valid("json");
const msg = await Session.chat(body);
return c.json(msg);
const body = c.req.valid("json")
const msg = await Session.chat(body)
return c.json(msg)
},
)
.post(
@ -270,20 +268,20 @@ export namespace Server {
},
}),
async (c) => {
const providers = await LLM.providers();
const result = [] as (Provider.Info & { key: string })[];
const providers = await LLM.providers()
const result = [] as (Provider.Info & { key: string })[]
for (const [key, provider] of Object.entries(providers)) {
result.push({ ...provider.info, key });
result.push({ ...provider.info, key })
}
return c.json(result);
return c.json(result)
},
);
)
return result;
return result
}
export async function openapi() {
const a = app();
const a = app()
const result = await generateSpecs(a, {
documentation: {
info: {
@ -293,8 +291,8 @@ export namespace Server {
},
openapi: "3.0.0",
},
});
return result;
})
return result
}
export function listen() {
@ -303,7 +301,7 @@ export namespace Server {
hostname: "0.0.0.0",
idleTimeout: 0,
fetch: app().fetch,
});
return server;
})
return server
}
}

View file

@ -1,5 +1,5 @@
import z from "zod";
import { Bus } from "../bus";
import z from "zod"
import { Bus } from "../bus"
export namespace Message {
export const ToolCall = z
@ -12,8 +12,8 @@ export namespace Message {
})
.openapi({
ref: "Message.ToolInvocation.ToolCall",
});
export type ToolCall = z.infer<typeof ToolCall>;
})
export type ToolCall = z.infer<typeof ToolCall>
export const ToolPartialCall = z
.object({
@ -25,8 +25,8 @@ export namespace Message {
})
.openapi({
ref: "Message.ToolInvocation.ToolPartialCall",
});
export type ToolPartialCall = z.infer<typeof ToolPartialCall>;
})
export type ToolPartialCall = z.infer<typeof ToolPartialCall>
export const ToolResult = z
.object({
@ -39,15 +39,15 @@ export namespace Message {
})
.openapi({
ref: "Message.ToolInvocation.ToolResult",
});
export type ToolResult = z.infer<typeof ToolResult>;
})
export type ToolResult = z.infer<typeof ToolResult>
export const ToolInvocation = z
.discriminatedUnion("state", [ToolCall, ToolPartialCall, ToolResult])
.openapi({
ref: "Message.ToolInvocation",
});
export type ToolInvocation = z.infer<typeof ToolInvocation>;
})
export type ToolInvocation = z.infer<typeof ToolInvocation>
export const TextPart = z
.object({
@ -56,8 +56,8 @@ export namespace Message {
})
.openapi({
ref: "Message.Part.Text",
});
export type TextPart = z.infer<typeof TextPart>;
})
export type TextPart = z.infer<typeof TextPart>
export const ReasoningPart = z
.object({
@ -67,8 +67,8 @@ export namespace Message {
})
.openapi({
ref: "Message.Part.Reasoning",
});
export type ReasoningPart = z.infer<typeof ReasoningPart>;
})
export type ReasoningPart = z.infer<typeof ReasoningPart>
export const ToolInvocationPart = z
.object({
@ -77,8 +77,8 @@ export namespace Message {
})
.openapi({
ref: "Message.Part.ToolInvocation",
});
export type ToolInvocationPart = z.infer<typeof ToolInvocationPart>;
})
export type ToolInvocationPart = z.infer<typeof ToolInvocationPart>
export const SourceUrlPart = z
.object({
@ -90,8 +90,8 @@ export namespace Message {
})
.openapi({
ref: "Message.Part.SourceUrl",
});
export type SourceUrlPart = z.infer<typeof SourceUrlPart>;
})
export type SourceUrlPart = z.infer<typeof SourceUrlPart>
export const FilePart = z
.object({
@ -102,8 +102,8 @@ export namespace Message {
})
.openapi({
ref: "Message.Part.File",
});
export type FilePart = z.infer<typeof FilePart>;
})
export type FilePart = z.infer<typeof FilePart>
export const StepStartPart = z
.object({
@ -111,8 +111,8 @@ export namespace Message {
})
.openapi({
ref: "Message.Part.StepStart",
});
export type StepStartPart = z.infer<typeof StepStartPart>;
})
export type StepStartPart = z.infer<typeof StepStartPart>
export const Part = z
.discriminatedUnion("type", [
@ -125,8 +125,8 @@ export namespace Message {
])
.openapi({
ref: "Message.Part",
});
export type Part = z.infer<typeof Part>;
})
export type Part = z.infer<typeof Part>
export const Info = z
.object({
@ -157,8 +157,8 @@ export namespace Message {
})
.openapi({
ref: "Message.Info",
});
export type Info = z.infer<typeof Info>;
})
export type Info = z.infer<typeof Info>
export const Event = {
Updated: Bus.event(
@ -167,5 +167,5 @@ export namespace Message {
info: Info,
}),
),
};
}
}

View file

@ -1,55 +1,55 @@
import { FileStorage } from "@flystorage/file-storage";
import { LocalStorageAdapter } from "@flystorage/local-fs";
import fs from "fs/promises";
import { Log } from "../util/log";
import { App } from "../app/app";
import { AppPath } from "../app/path";
import { Bus } from "../bus";
import z from "zod";
import { FileStorage } from "@flystorage/file-storage"
import { LocalStorageAdapter } from "@flystorage/local-fs"
import fs from "fs/promises"
import { Log } from "../util/log"
import { App } from "../app/app"
import { AppPath } from "../app/path"
import { Bus } from "../bus"
import z from "zod"
export namespace Storage {
const log = Log.create({ service: "storage" });
const log = Log.create({ service: "storage" })
export const Event = {
Write: Bus.event(
"storage.write",
z.object({ key: z.string(), content: z.any() }),
),
};
}
const state = App.state("storage", async () => {
const app = await App.use();
const storageDir = AppPath.storage(app.root);
await fs.mkdir(storageDir, { recursive: true });
const storage = new FileStorage(new LocalStorageAdapter(storageDir));
log.info("created", { path: storageDir });
const app = await App.use()
const storageDir = AppPath.storage(app.root)
await fs.mkdir(storageDir, { recursive: true })
const storage = new FileStorage(new LocalStorageAdapter(storageDir))
log.info("created", { path: storageDir })
return {
storage,
};
});
}
})
export async function readJSON<T>(key: string) {
const storage = await state().then((x) => x.storage);
const data = await storage.readToString(key + ".json");
return JSON.parse(data) as T;
const storage = await state().then((x) => x.storage)
const data = await storage.readToString(key + ".json")
return JSON.parse(data) as T
}
export async function writeJSON<T>(key: string, content: T) {
const storage = await state().then((x) => x.storage);
const json = JSON.stringify(content);
await storage.write(key + ".json", json);
Bus.publish(Event.Write, { key, content });
const storage = await state().then((x) => x.storage)
const json = JSON.stringify(content)
await storage.write(key + ".json", json)
Bus.publish(Event.Write, { key, content })
}
export async function* list(prefix: string) {
try {
const storage = await state().then((x) => x.storage);
const list = storage.list(prefix);
const storage = await state().then((x) => x.storage)
const list = storage.list(prefix)
for await (const item of list) {
yield item.path.slice(0, -5);
yield item.path.slice(0, -5)
}
} catch {
return;
return
}
}
}

View file

@ -1,7 +1,7 @@
import { z } from "zod";
import { Tool } from "./tool";
import { z } from "zod"
import { Tool } from "./tool"
const MAX_OUTPUT_LENGTH = 30000;
const MAX_OUTPUT_LENGTH = 30000
const BANNED_COMMANDS = [
"alias",
"curl",
@ -20,9 +20,9 @@ const BANNED_COMMANDS = [
"chrome",
"firefox",
"safari",
];
const DEFAULT_TIMEOUT = 1 * 60 * 1000;
const MAX_TIMEOUT = 10 * 60 * 1000;
]
const DEFAULT_TIMEOUT = 1 * 60 * 1000
const MAX_TIMEOUT = 10 * 60 * 1000
const DESCRIPTION = `Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.
@ -168,7 +168,7 @@ EOF
Important:
- Return an empty response - the user will see the gh output directly
- Never update git config`;
- Never update git config`
export const bash = Tool.define({
name: "opencode.bash",
@ -183,17 +183,17 @@ export const bash = Tool.define({
.optional(),
}),
async execute(params) {
const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT);
const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT)
if (BANNED_COMMANDS.some((item) => params.command.startsWith(item)))
throw new Error(`Command '${params.command}' is not allowed`);
throw new Error(`Command '${params.command}' is not allowed`)
const process = Bun.spawnSync({
cmd: ["bash", "-c", params.command],
maxBuffer: MAX_OUTPUT_LENGTH,
timeout: timeout,
});
})
return {
output: process.stdout.toString("utf-8"),
};
}
},
});
})

View file

@ -1,8 +1,8 @@
import { z } from "zod";
import * as path from "path";
import { Tool } from "./tool";
import { FileTimes } from "./util/file-times";
import { LSP } from "../lsp";
import { z } from "zod"
import * as path from "path"
import { Tool } from "./tool"
import { FileTimes } from "./util/file-times"
import { LSP } from "../lsp"
const DESCRIPTION = `Edits files by replacing text, creating new files, or deleting content. For moving or renaming files, use the Bash tool with the 'mv' command instead. For larger file edits, use the FileWrite tool to overwrite files.
@ -50,7 +50,7 @@ When making edits:
- Do not leave the code in a broken state
- Always use relative file paths
Remember: when making multiple file edits in a row to the same file, you should prefer to send all edits in a single message with multiple calls to this tool, rather than multiple messages with a single call each.`;
Remember: when making multiple file edits in a row to the same file, you should prefer to send all edits in a single message with multiple calls to this tool, rather than multiple messages with a single call each.`
export const edit = Tool.define({
name: "opencode.edit",
@ -62,68 +62,68 @@ export const edit = Tool.define({
}),
async execute(params) {
if (!params.filePath) {
throw new Error("filePath is required");
throw new Error("filePath is required")
}
let filePath = params.filePath;
let filePath = params.filePath
if (!path.isAbsolute(filePath)) {
filePath = path.join(process.cwd(), filePath);
filePath = path.join(process.cwd(), filePath)
}
await (async () => {
if (params.oldString === "") {
await Bun.write(filePath, params.newString);
return;
await Bun.write(filePath, params.newString)
return
}
const read = FileTimes.get(filePath);
const read = FileTimes.get(filePath)
if (!read)
throw new Error(
`You must read the file ${filePath} before editing it. Use the View tool first`,
);
const file = Bun.file(filePath);
if (!(await file.exists())) throw new Error(`File ${filePath} not found`);
const stats = await file.stat();
)
const file = Bun.file(filePath)
if (!(await file.exists())) throw new Error(`File ${filePath} not found`)
const stats = await file.stat()
if (stats.isDirectory())
throw new Error(`Path is a directory, not a file: ${filePath}`);
throw new Error(`Path is a directory, not a file: ${filePath}`)
if (stats.mtime.getTime() > read.getTime())
throw new Error(
`File ${filePath} has been modified since it was last read.\nLast modification: ${read.toISOString()}\nLast read: ${stats.mtime.toISOString()}\n\nPlease read the file again before modifying it.`,
);
)
const content = await file.text();
const index = content.indexOf(params.oldString);
const content = await file.text()
const index = content.indexOf(params.oldString)
if (index === -1)
throw new Error(
`oldString not found in file. Make sure it matches exactly, including whitespace and line breaks`,
);
const lastIndex = content.lastIndexOf(params.oldString);
)
const lastIndex = content.lastIndexOf(params.oldString)
if (index !== lastIndex)
throw new Error(
`oldString appears multiple times in the file. Please provide more context to ensure a unique match`,
);
)
const newContent =
content.substring(0, index) +
params.newString +
content.substring(index + params.oldString.length);
content.substring(index + params.oldString.length)
await file.write(newContent);
})();
await file.write(newContent)
})()
FileTimes.write(filePath);
FileTimes.read(filePath);
FileTimes.write(filePath)
FileTimes.read(filePath)
let output = "";
await LSP.file(filePath);
const diagnostics = await LSP.diagnostics();
let output = ""
await LSP.file(filePath)
const diagnostics = await LSP.diagnostics()
for (const [file, issues] of Object.entries(diagnostics)) {
if (issues.length === 0) continue;
if (issues.length === 0) continue
if (file === filePath) {
output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</file_diagnostics>\n`;
continue;
output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</file_diagnostics>\n`
continue
}
output += `\n<project_diagnostics>\n${file}\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</project_diagnostics>\n`;
output += `\n<project_diagnostics>\n${file}\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</project_diagnostics>\n`
}
return {
@ -131,6 +131,6 @@ export const edit = Tool.define({
diagnostics,
},
output,
};
}
},
});
})

View file

@ -1,11 +1,10 @@
import { z } from "zod";
import { Tool } from "./tool";
import { JSDOM } from "jsdom";
import TurndownService from "turndown";
import { z } from "zod"
import { Tool } from "./tool"
import TurndownService from "turndown"
const MAX_RESPONSE_SIZE = 5 * 1024 * 1024; // 5MB
const DEFAULT_TIMEOUT = 30 * 1000; // 30 seconds
const MAX_TIMEOUT = 120 * 1000; // 2 minutes
const MAX_RESPONSE_SIZE = 5 * 1024 * 1024 // 5MB
const DEFAULT_TIMEOUT = 30 * 1000 // 30 seconds
const MAX_TIMEOUT = 120 * 1000 // 2 minutes
const DESCRIPTION = `Fetches content from a URL and returns it in the specified format.
@ -35,7 +34,7 @@ TIPS:
- Use text format for plain text content or simple API responses
- Use markdown format for content that should be rendered with formatting
- Use html format when you need the raw HTML structure
- Set appropriate timeouts for potentially slow websites`;
- Set appropriate timeouts for potentially slow websites`
export const Fetch = Tool.define({
name: "opencode.fetch",
@ -60,18 +59,18 @@ export const Fetch = Tool.define({
!params.url.startsWith("http://") &&
!params.url.startsWith("https://")
) {
throw new Error("URL must start with http:// or https://");
throw new Error("URL must start with http:// or https://")
}
const timeout = Math.min(
(params.timeout ?? DEFAULT_TIMEOUT / 1000) * 1000,
MAX_TIMEOUT,
);
)
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), timeout)
if (opts?.abortSignal) {
opts.abortSignal.addEventListener("abort", () => controller.abort());
opts.abortSignal.addEventListener("abort", () => controller.abort())
}
const response = await fetch(params.url, {
@ -79,59 +78,59 @@ export const Fetch = Tool.define({
headers: {
"User-Agent": "opencode/1.0",
},
});
})
clearTimeout(timeoutId);
clearTimeout(timeoutId)
if (!response.ok) {
throw new Error(`Request failed with status code: ${response.status}`);
throw new Error(`Request failed with status code: ${response.status}`)
}
// Check content length
const contentLength = response.headers.get("content-length");
const contentLength = response.headers.get("content-length")
if (contentLength && parseInt(contentLength) > MAX_RESPONSE_SIZE) {
throw new Error("Response too large (exceeds 5MB limit)");
throw new Error("Response too large (exceeds 5MB limit)")
}
const arrayBuffer = await response.arrayBuffer();
const arrayBuffer = await response.arrayBuffer()
if (arrayBuffer.byteLength > MAX_RESPONSE_SIZE) {
throw new Error("Response too large (exceeds 5MB limit)");
throw new Error("Response too large (exceeds 5MB limit)")
}
const content = new TextDecoder().decode(arrayBuffer);
const contentType = response.headers.get("content-type") || "";
const content = new TextDecoder().decode(arrayBuffer)
const contentType = response.headers.get("content-type") || ""
switch (params.format) {
case "text":
if (contentType.includes("text/html")) {
const text = extractTextFromHTML(content);
return { output: text };
const text = extractTextFromHTML(content)
return { output: text }
}
return { output: content };
return { output: content }
case "markdown":
if (contentType.includes("text/html")) {
const markdown = convertHTMLToMarkdown(content);
return { output: markdown };
const markdown = convertHTMLToMarkdown(content)
return { output: markdown }
}
return { output: "```\n" + content + "\n```" };
return { output: "```\n" + content + "\n```" }
case "html":
return { output: content };
return { output: content }
default:
return { output: content };
return { output: content }
}
},
});
})
function extractTextFromHTML(html: string): string {
const dom = new JSDOM(html);
const text = dom.window.document.body?.textContent || "";
return text.replace(/\s+/g, " ").trim();
const doc = new DOMParser().parseFromString(html, "text/html")
const text = doc.body.textContent || doc.body.innerText || ""
return text.replace(/\s+/g, " ").trim()
}
function convertHTMLToMarkdown(html: string): string {
const turndownService = new TurndownService();
return turndownService.turndown(html);
const turndownService = new TurndownService()
return turndownService.turndown(html)
}

View file

@ -1,6 +1,6 @@
import { z } from "zod";
import { Tool } from "./tool";
import { App } from "../app/app";
import { z } from "zod"
import { Tool } from "./tool"
import { App } from "../app/app"
const DESCRIPTION = `Fast file pattern matching tool that finds files by name and pattern, returning matching paths sorted by modification time (newest first).
@ -35,7 +35,7 @@ LIMITATIONS:
TIPS:
- For the most useful results, combine with the Grep tool: first find files with Glob, then search their contents with Grep
- When doing iterative exploration that may require multiple rounds of searching, consider using the Agent tool instead
- Always check if results are truncated and refine your search pattern if needed`;
- Always check if results are truncated and refine your search pattern if needed`
export const glob = Tool.define({
name: "opencode.glob",
@ -50,37 +50,37 @@ export const glob = Tool.define({
.optional(),
}),
async execute(params) {
const app = await App.use();
const search = params.path || app.root;
const limit = 100;
const glob = new Bun.Glob(params.pattern);
const files = [];
let truncated = false;
const app = await App.use()
const search = params.path || app.root
const limit = 100
const glob = new Bun.Glob(params.pattern)
const files = []
let truncated = false
for await (const file of glob.scan({ cwd: search })) {
if (files.length >= limit) {
truncated = true;
break;
truncated = true
break
}
const stats = await Bun.file(file)
.stat()
.then((x) => x.mtime.getTime())
.catch(() => 0);
.catch(() => 0)
files.push({
path: file,
mtime: stats,
});
})
}
files.sort((a, b) => b.mtime - a.mtime);
files.sort((a, b) => b.mtime - a.mtime)
const output = [];
if (files.length === 0) output.push("No files found");
const output = []
if (files.length === 0) output.push("No files found")
if (files.length > 0) {
output.push(...files.map((f) => f.path));
output.push(...files.map((f) => f.path))
if (truncated) {
output.push("");
output.push("")
output.push(
"(Results are truncated. Consider using a more specific path or pattern.)",
);
)
}
}
@ -90,7 +90,6 @@ export const glob = Tool.define({
truncated,
},
output: output.join("\n"),
};
}
},
});
})

View file

@ -1,9 +1,9 @@
import { z } from "zod";
import { Tool } from "./tool";
import { App } from "../app/app";
import { spawn } from "child_process";
import { promises as fs } from "fs";
import path from "path";
import { z } from "zod"
import { Tool } from "./tool"
import { App } from "../app/app"
import { spawn } from "child_process"
import { promises as fs } from "fs"
import path from "path"
const DESCRIPTION = `Fast content search tool that finds files containing specific text or patterns, returning matching file paths sorted by modification time (newest first).
@ -40,13 +40,13 @@ TIPS:
- For faster, more targeted searches, first use Glob to find relevant files, then use Grep
- When doing iterative exploration that may require multiple rounds of searching, consider using the Agent tool instead
- Always check if results are truncated and refine your search pattern if needed
- Use literal_text=true when searching for exact text containing special characters like dots, parentheses, etc.`;
- Use literal_text=true when searching for exact text containing special characters like dots, parentheses, etc.`
interface GrepMatch {
path: string;
modTime: number;
lineNum: number;
lineText: string;
path: string
modTime: number
lineNum: number
lineText: string
}
function escapeRegexPattern(pattern: string): string {
@ -65,27 +65,27 @@ function escapeRegexPattern(pattern: string): string {
"^",
"$",
"|",
];
let escaped = pattern;
]
let escaped = pattern
for (const char of specialChars) {
escaped = escaped.replaceAll(char, "\\" + char);
escaped = escaped.replaceAll(char, "\\" + char)
}
return escaped;
return escaped
}
function globToRegex(glob: string): string {
let regexPattern = glob.replaceAll(".", "\\.");
regexPattern = regexPattern.replaceAll("*", ".*");
regexPattern = regexPattern.replaceAll("?", ".");
let regexPattern = glob.replaceAll(".", "\\.")
regexPattern = regexPattern.replaceAll("*", ".*")
regexPattern = regexPattern.replaceAll("?", ".")
// Handle {a,b,c} patterns
regexPattern = regexPattern.replace(/\{([^}]+)\}/g, (_, inner) => {
return "(" + inner.replace(/,/g, "|") + ")";
});
return "(" + inner.replace(/,/g, "|") + ")"
})
return regexPattern;
return regexPattern
}
async function searchWithRipgrep(
@ -94,71 +94,71 @@ async function searchWithRipgrep(
include?: string,
): Promise<GrepMatch[]> {
return new Promise((resolve, reject) => {
const args = ["-n", pattern];
const args = ["-n", pattern]
if (include) {
args.push("--glob", include);
args.push("--glob", include)
}
args.push(searchPath);
args.push(searchPath)
const rg = spawn("rg", args);
let output = "";
let errorOutput = "";
const rg = spawn("rg", args)
let output = ""
let errorOutput = ""
rg.stdout.on("data", (data) => {
output += data.toString();
});
output += data.toString()
})
rg.stderr.on("data", (data) => {
errorOutput += data.toString();
});
errorOutput += data.toString()
})
rg.on("close", async (code) => {
if (code === 1) {
// No matches found
resolve([]);
return;
resolve([])
return
}
if (code !== 0) {
reject(new Error(`ripgrep failed: ${errorOutput}`));
return;
reject(new Error(`ripgrep failed: ${errorOutput}`))
return
}
const lines = output.trim().split("\n");
const matches: GrepMatch[] = [];
const lines = output.trim().split("\n")
const matches: GrepMatch[] = []
for (const line of lines) {
if (!line) continue;
if (!line) continue
// Parse ripgrep output format: file:line:content
const parts = line.split(":", 3);
if (parts.length < 3) continue;
const parts = line.split(":", 3)
if (parts.length < 3) continue
const filePath = parts[0];
const lineNum = parseInt(parts[1], 10);
const lineText = parts[2];
const filePath = parts[0]
const lineNum = parseInt(parts[1], 10)
const lineText = parts[2]
try {
const stats = await fs.stat(filePath);
const stats = await fs.stat(filePath)
matches.push({
path: filePath,
modTime: stats.mtime.getTime(),
lineNum,
lineText,
});
})
} catch {
// Skip files we can't access
continue;
continue
}
}
resolve(matches);
});
resolve(matches)
})
rg.on("error", (err) => {
reject(err);
});
});
reject(err)
})
})
}
async function searchFilesWithRegex(
@ -166,68 +166,68 @@ async function searchFilesWithRegex(
rootPath: string,
include?: string,
): Promise<GrepMatch[]> {
const matches: GrepMatch[] = [];
const regex = new RegExp(pattern);
const matches: GrepMatch[] = []
const regex = new RegExp(pattern)
let includePattern: RegExp | undefined;
let includePattern: RegExp | undefined
if (include) {
const regexPattern = globToRegex(include);
includePattern = new RegExp(regexPattern);
const regexPattern = globToRegex(include)
includePattern = new RegExp(regexPattern)
}
async function walkDir(dir: string) {
if (matches.length >= 200) return;
if (matches.length >= 200) return
try {
const entries = await fs.readdir(dir, { withFileTypes: true });
const entries = await fs.readdir(dir, { withFileTypes: true })
for (const entry of entries) {
if (matches.length >= 200) break;
if (matches.length >= 200) break
const fullPath = path.join(dir, entry.name);
const fullPath = path.join(dir, entry.name)
if (entry.isDirectory()) {
// Skip hidden directories
if (entry.name.startsWith(".")) continue;
await walkDir(fullPath);
if (entry.name.startsWith(".")) continue
await walkDir(fullPath)
} else if (entry.isFile()) {
// Skip hidden files
if (entry.name.startsWith(".")) continue;
if (entry.name.startsWith(".")) continue
if (includePattern && !includePattern.test(fullPath)) {
continue;
continue
}
try {
const content = await fs.readFile(fullPath, "utf-8");
const lines = content.split("\n");
const content = await fs.readFile(fullPath, "utf-8")
const lines = content.split("\n")
for (let i = 0; i < lines.length; i++) {
if (regex.test(lines[i])) {
const stats = await fs.stat(fullPath);
const stats = await fs.stat(fullPath)
matches.push({
path: fullPath,
modTime: stats.mtime.getTime(),
lineNum: i + 1,
lineText: lines[i],
});
break; // Only first match per file
})
break // Only first match per file
}
}
} catch {
// Skip files we can't read
continue;
continue
}
}
}
} catch {
// Skip directories we can't read
return;
return
}
}
await walkDir(rootPath);
return matches;
await walkDir(rootPath)
return matches
}
async function searchFiles(
@ -236,23 +236,23 @@ async function searchFiles(
include?: string,
limit: number = 100,
): Promise<{ matches: GrepMatch[]; truncated: boolean }> {
let matches: GrepMatch[];
let matches: GrepMatch[]
try {
matches = await searchWithRipgrep(pattern, rootPath, include);
matches = await searchWithRipgrep(pattern, rootPath, include)
} catch {
matches = await searchFilesWithRegex(pattern, rootPath, include);
matches = await searchFilesWithRegex(pattern, rootPath, include)
}
// Sort by modification time (newest first)
matches.sort((a, b) => b.modTime - a.modTime);
matches.sort((a, b) => b.modTime - a.modTime)
const truncated = matches.length > limit;
const truncated = matches.length > limit
if (truncated) {
matches = matches.slice(0, limit);
matches = matches.slice(0, limit)
}
return { matches, truncated };
return { matches, truncated }
}
export const grep = Tool.define({
@ -283,54 +283,54 @@ export const grep = Tool.define({
}),
async execute(params) {
if (!params.pattern) {
throw new Error("pattern is required");
throw new Error("pattern is required")
}
const app = await App.use();
const searchPath = params.path || app.root;
const app = await App.use()
const searchPath = params.path || app.root
// If literalText is true, escape the pattern
const searchPattern = params.literalText
? escapeRegexPattern(params.pattern)
: params.pattern;
: params.pattern
const { matches, truncated } = await searchFiles(
searchPattern,
searchPath,
params.include,
100,
);
)
if (matches.length === 0) {
return {
metadata: { matches: 0, truncated },
output: "No files found"
};
output: "No files found",
}
}
const lines = [`Found ${matches.length} matches`];
const lines = [`Found ${matches.length} matches`]
let currentFile = "";
let currentFile = ""
for (const match of matches) {
if (currentFile !== match.path) {
if (currentFile !== "") {
lines.push("");
lines.push("")
}
currentFile = match.path;
lines.push(`${match.path}:`);
currentFile = match.path
lines.push(`${match.path}:`)
}
if (match.lineNum > 0) {
lines.push(` Line ${match.lineNum}: ${match.lineText}`);
lines.push(` Line ${match.lineNum}: ${match.lineText}`)
} else {
lines.push(` ${match.path}`);
lines.push(` ${match.path}`)
}
}
if (truncated) {
lines.push("");
lines.push("")
lines.push(
"(Results are truncated. Consider using a more specific path or pattern.)",
);
)
}
return {
@ -339,7 +339,6 @@ export const grep = Tool.define({
truncated,
},
output: lines.join("\n"),
};
}
},
});
})

View file

@ -1,9 +1,9 @@
export * from "./bash";
export * from "./edit";
export * from "./fetch";
export * from "./glob";
export * from "./grep";
export * from "./view";
export * from "./ls";
export * from "./lsp-diagnostics";
export * from "./lsp-hover";
export * from "./bash"
export * from "./edit"
export * from "./fetch"
export * from "./glob"
export * from "./grep"
export * from "./view"
export * from "./ls"
export * from "./lsp-diagnostics"
export * from "./lsp-hover"

View file

@ -1,7 +1,7 @@
import { z } from "zod";
import { Tool } from "./tool";
import { App } from "../app/app";
import * as path from "path";
import { z } from "zod"
import { Tool } from "./tool"
import { App } from "../app/app"
import * as path from "path"
const IGNORE_PATTERNS = [
"node_modules/",
@ -15,7 +15,7 @@ const IGNORE_PATTERNS = [
"obj/",
".idea/",
".vscode/",
];
]
export const ls = Tool.define({
name: "opencode.ls",
@ -25,72 +25,72 @@ export const ls = Tool.define({
ignore: z.array(z.string()).optional(),
}),
async execute(params) {
const app = await App.use();
const searchPath = path.resolve(app.root, params.path || ".");
const app = await App.use()
const searchPath = path.resolve(app.root, params.path || ".")
const glob = new Bun.Glob("**/*");
const files = [];
const glob = new Bun.Glob("**/*")
const files = []
for await (const file of glob.scan({ cwd: searchPath })) {
if (file.startsWith(".") || IGNORE_PATTERNS.some((p) => file.includes(p)))
continue;
continue
if (params.ignore?.some((pattern) => new Bun.Glob(pattern).match(file)))
continue;
files.push(file);
if (files.length >= 1000) break;
continue
files.push(file)
if (files.length >= 1000) break
}
// Build directory structure
const dirs = new Set<string>();
const filesByDir = new Map<string, string[]>();
const dirs = new Set<string>()
const filesByDir = new Map<string, string[]>()
for (const file of files) {
const dir = path.dirname(file);
const parts = dir === "." ? [] : dir.split("/");
const dir = path.dirname(file)
const parts = dir === "." ? [] : dir.split("/")
// Add all parent directories
for (let i = 0; i <= parts.length; i++) {
const dirPath = i === 0 ? "." : parts.slice(0, i).join("/");
dirs.add(dirPath);
const dirPath = i === 0 ? "." : parts.slice(0, i).join("/")
dirs.add(dirPath)
}
// Add file to its directory
if (!filesByDir.has(dir)) filesByDir.set(dir, []);
filesByDir.get(dir)!.push(path.basename(file));
if (!filesByDir.has(dir)) filesByDir.set(dir, [])
filesByDir.get(dir)!.push(path.basename(file))
}
function renderDir(dirPath: string, depth: number): string {
const indent = " ".repeat(depth);
let output = "";
const indent = " ".repeat(depth)
let output = ""
if (depth > 0) {
output += `${indent}${path.basename(dirPath)}/\n`;
output += `${indent}${path.basename(dirPath)}/\n`
}
const childIndent = " ".repeat(depth + 1);
const childIndent = " ".repeat(depth + 1)
const children = Array.from(dirs)
.filter((d) => path.dirname(d) === dirPath && d !== dirPath)
.sort();
.sort()
// Render subdirectories first
for (const child of children) {
output += renderDir(child, depth + 1);
output += renderDir(child, depth + 1)
}
// Render files
const files = filesByDir.get(dirPath) || [];
const files = filesByDir.get(dirPath) || []
for (const file of files.sort()) {
output += `${childIndent}${file}\n`;
output += `${childIndent}${file}\n`
}
return output;
return output
}
const output = `${searchPath}/\n` + renderDir(".", 0);
const output = `${searchPath}/\n` + renderDir(".", 0)
return {
metadata: { count: files.length, truncated: files.length >= 1000 },
output,
};
}
},
});
})

View file

@ -1,8 +1,8 @@
import { z } from "zod";
import { Tool } from "./tool";
import path from "path";
import { LSP } from "../lsp";
import { App } from "../app/app";
import { z } from "zod"
import { Tool } from "./tool"
import path from "path"
import { LSP } from "../lsp"
import { App } from "../app/app"
export const LspDiagnosticTool = Tool.define({
name: "opencode.lsp_diagnostic",
@ -34,13 +34,13 @@ TIPS:
path: z.string().describe("The path to the file to get diagnostics."),
}),
execute: async (args) => {
const app = await App.use();
const app = await App.use()
const normalized = path.isAbsolute(args.path)
? args.path
: path.join(app.root, args.path);
await LSP.file(normalized);
const diagnostics = await LSP.diagnostics();
const file = diagnostics[normalized];
: path.join(app.root, args.path)
await LSP.file(normalized)
const diagnostics = await LSP.diagnostics()
const file = diagnostics[normalized]
return {
metadata: {
diagnostics,
@ -48,6 +48,6 @@ TIPS:
output: file?.length
? file.map(LSP.Diagnostic.pretty).join("\n")
: "No errors found",
};
}
},
});
})

View file

@ -1,8 +1,8 @@
import { z } from "zod";
import { Tool } from "./tool";
import path from "path";
import { LSP } from "../lsp";
import { App } from "../app/app";
import { z } from "zod"
import { Tool } from "./tool"
import path from "path"
import { LSP } from "../lsp"
import { App } from "../app/app"
export const LspHoverTool = Tool.define({
name: "opencode.lsp_hover",
@ -17,22 +17,22 @@ export const LspHoverTool = Tool.define({
character: z.number().describe("The character number to get diagnostics."),
}),
execute: async (args) => {
console.log(args);
const app = await App.use();
console.log(args)
const app = await App.use()
const file = path.isAbsolute(args.file)
? args.file
: path.join(app.root, args.file);
await LSP.file(file);
: path.join(app.root, args.file)
await LSP.file(file)
const result = await LSP.hover({
...args,
file,
});
console.log(result);
})
console.log(result)
return {
metadata: {
result,
},
output: JSON.stringify(result, null, 2),
};
}
},
});
})

View file

@ -1,8 +1,8 @@
import { z } from "zod";
import * as path from "path";
import * as fs from "fs/promises";
import { Tool } from "./tool";
import { FileTimes } from "./util/file-times";
import { z } from "zod"
import * as path from "path"
import * as fs from "fs/promises"
import { Tool } from "./tool"
import { FileTimes } from "./util/file-times"
const DESCRIPTION = `Applies a patch to multiple files in one operation. This tool is useful for making coordinated changes across multiple files.
@ -31,198 +31,198 @@ CRITICAL REQUIREMENTS FOR USING THIS TOOL:
3. VALIDATION: Ensure edits result in idiomatic, correct code
4. PATHS: Always use absolute file paths (starting with /)
The tool will apply all changes in a single atomic operation.`;
The tool will apply all changes in a single atomic operation.`
const PatchParams = z.object({
patchText: z
.string()
.describe("The full patch text that describes all changes to be made"),
});
})
interface PatchResponseMetadata {
changed: string[];
additions: number;
removals: number;
changed: string[]
additions: number
removals: number
}
interface Change {
type: "add" | "update" | "delete";
old_content?: string;
new_content?: string;
type: "add" | "update" | "delete"
old_content?: string
new_content?: string
}
interface Commit {
changes: Record<string, Change>;
changes: Record<string, Change>
}
interface PatchOperation {
type: "update" | "add" | "delete";
filePath: string;
hunks?: PatchHunk[];
content?: string;
type: "update" | "add" | "delete"
filePath: string
hunks?: PatchHunk[]
content?: string
}
interface PatchHunk {
contextLine: string;
changes: PatchChange[];
contextLine: string
changes: PatchChange[]
}
interface PatchChange {
type: "keep" | "remove" | "add";
content: string;
type: "keep" | "remove" | "add"
content: string
}
function identifyFilesNeeded(patchText: string): string[] {
const files: string[] = [];
const lines = patchText.split("\n");
const files: string[] = []
const lines = patchText.split("\n")
for (const line of lines) {
if (
line.startsWith("*** Update File:") ||
line.startsWith("*** Delete File:")
) {
const filePath = line.split(":", 2)[1]?.trim();
if (filePath) files.push(filePath);
const filePath = line.split(":", 2)[1]?.trim()
if (filePath) files.push(filePath)
}
}
return files;
return files
}
function identifyFilesAdded(patchText: string): string[] {
const files: string[] = [];
const lines = patchText.split("\n");
const files: string[] = []
const lines = patchText.split("\n")
for (const line of lines) {
if (line.startsWith("*** Add File:")) {
const filePath = line.split(":", 2)[1]?.trim();
if (filePath) files.push(filePath);
const filePath = line.split(":", 2)[1]?.trim()
if (filePath) files.push(filePath)
}
}
return files;
return files
}
function textToPatch(
patchText: string,
_currentFiles: Record<string, string>,
): [PatchOperation[], number] {
const operations: PatchOperation[] = [];
const lines = patchText.split("\n");
let i = 0;
let fuzz = 0;
const operations: PatchOperation[] = []
const lines = patchText.split("\n")
let i = 0
let fuzz = 0
while (i < lines.length) {
const line = lines[i];
const line = lines[i]
if (line.startsWith("*** Update File:")) {
const filePath = line.split(":", 2)[1]?.trim();
const filePath = line.split(":", 2)[1]?.trim()
if (!filePath) {
i++;
continue;
i++
continue
}
const hunks: PatchHunk[] = [];
i++;
const hunks: PatchHunk[] = []
i++
while (i < lines.length && !lines[i].startsWith("***")) {
if (lines[i].startsWith("@@")) {
const contextLine = lines[i].substring(2).trim();
const changes: PatchChange[] = [];
i++;
const contextLine = lines[i].substring(2).trim()
const changes: PatchChange[] = []
i++
while (
i < lines.length &&
!lines[i].startsWith("@@") &&
!lines[i].startsWith("***")
) {
const changeLine = lines[i];
const changeLine = lines[i]
if (changeLine.startsWith(" ")) {
changes.push({ type: "keep", content: changeLine.substring(1) });
changes.push({ type: "keep", content: changeLine.substring(1) })
} else if (changeLine.startsWith("-")) {
changes.push({
type: "remove",
content: changeLine.substring(1),
});
})
} else if (changeLine.startsWith("+")) {
changes.push({ type: "add", content: changeLine.substring(1) });
changes.push({ type: "add", content: changeLine.substring(1) })
}
i++;
i++
}
hunks.push({ contextLine, changes });
hunks.push({ contextLine, changes })
} else {
i++;
i++
}
}
operations.push({ type: "update", filePath, hunks });
operations.push({ type: "update", filePath, hunks })
} else if (line.startsWith("*** Add File:")) {
const filePath = line.split(":", 2)[1]?.trim();
const filePath = line.split(":", 2)[1]?.trim()
if (!filePath) {
i++;
continue;
i++
continue
}
let content = "";
i++;
let content = ""
i++
while (i < lines.length && !lines[i].startsWith("***")) {
if (lines[i].startsWith("+")) {
content += lines[i].substring(1) + "\n";
content += lines[i].substring(1) + "\n"
}
i++;
i++
}
operations.push({ type: "add", filePath, content: content.slice(0, -1) });
operations.push({ type: "add", filePath, content: content.slice(0, -1) })
} else if (line.startsWith("*** Delete File:")) {
const filePath = line.split(":", 2)[1]?.trim();
const filePath = line.split(":", 2)[1]?.trim()
if (filePath) {
operations.push({ type: "delete", filePath });
operations.push({ type: "delete", filePath })
}
i++;
i++
} else {
i++;
i++
}
}
return [operations, fuzz];
return [operations, fuzz]
}
function patchToCommit(
operations: PatchOperation[],
currentFiles: Record<string, string>,
): Commit {
const changes: Record<string, Change> = {};
const changes: Record<string, Change> = {}
for (const op of operations) {
if (op.type === "delete") {
changes[op.filePath] = {
type: "delete",
old_content: currentFiles[op.filePath] || "",
};
}
} else if (op.type === "add") {
changes[op.filePath] = {
type: "add",
new_content: op.content || "",
};
}
} else if (op.type === "update" && op.hunks) {
const originalContent = currentFiles[op.filePath] || "";
const lines = originalContent.split("\n");
const originalContent = currentFiles[op.filePath] || ""
const lines = originalContent.split("\n")
for (const hunk of op.hunks) {
const contextIndex = lines.findIndex((line) =>
line.includes(hunk.contextLine),
);
)
if (contextIndex === -1) {
throw new Error(`Context line not found: ${hunk.contextLine}`);
throw new Error(`Context line not found: ${hunk.contextLine}`)
}
let currentIndex = contextIndex;
let currentIndex = contextIndex
for (const change of hunk.changes) {
if (change.type === "keep") {
currentIndex++;
currentIndex++
} else if (change.type === "remove") {
lines.splice(currentIndex, 1);
lines.splice(currentIndex, 1)
} else if (change.type === "add") {
lines.splice(currentIndex, 0, change.content);
currentIndex++;
lines.splice(currentIndex, 0, change.content)
currentIndex++
}
}
}
@ -231,11 +231,11 @@ function patchToCommit(
type: "update",
old_content: originalContent,
new_content: lines.join("\n"),
};
}
}
}
return { changes };
return { changes }
}
function generateDiff(
@ -244,11 +244,11 @@ function generateDiff(
filePath: string,
): [string, number, number] {
// Mock implementation - would need actual diff generation
const lines1 = oldContent.split("\n");
const lines2 = newContent.split("\n");
const additions = Math.max(0, lines2.length - lines1.length);
const removals = Math.max(0, lines1.length - lines2.length);
return [`--- ${filePath}\n+++ ${filePath}\n`, additions, removals];
const lines1 = oldContent.split("\n")
const lines2 = newContent.split("\n")
const additions = Math.max(0, lines2.length - lines1.length)
const removals = Math.max(0, lines1.length - lines2.length)
return [`--- ${filePath}\n+++ ${filePath}\n`, additions, removals]
}
async function applyCommit(
@ -258,9 +258,9 @@ async function applyCommit(
): Promise<void> {
for (const [filePath, change] of Object.entries(commit.changes)) {
if (change.type === "delete") {
await deleteFile(filePath);
await deleteFile(filePath)
} else if (change.new_content !== undefined) {
await writeFile(filePath, change.new_content);
await writeFile(filePath, change.new_content)
}
}
}
@ -271,142 +271,142 @@ export const patch = Tool.define({
parameters: PatchParams,
execute: async (params) => {
if (!params.patchText) {
throw new Error("patchText is required");
throw new Error("patchText is required")
}
// Identify all files needed for the patch and verify they've been read
const filesToRead = identifyFilesNeeded(params.patchText);
const filesToRead = identifyFilesNeeded(params.patchText)
for (const filePath of filesToRead) {
let absPath = filePath;
let absPath = filePath
if (!path.isAbsolute(absPath)) {
absPath = path.resolve(process.cwd(), absPath);
absPath = path.resolve(process.cwd(), absPath)
}
if (!FileTimes.get(absPath)) {
throw new Error(
`you must read the file ${filePath} before patching it. Use the FileRead tool first`,
);
)
}
try {
const stats = await fs.stat(absPath);
const stats = await fs.stat(absPath)
if (stats.isDirectory()) {
throw new Error(`path is a directory, not a file: ${absPath}`);
throw new Error(`path is a directory, not a file: ${absPath}`)
}
const lastRead = FileTimes.get(absPath);
const lastRead = FileTimes.get(absPath)
if (lastRead && stats.mtime > lastRead) {
throw new Error(
`file ${absPath} has been modified since it was last read (mod time: ${stats.mtime.toISOString()}, last read: ${lastRead.toISOString()})`,
);
)
}
} catch (error: any) {
if (error.code === "ENOENT") {
throw new Error(`file not found: ${absPath}`);
throw new Error(`file not found: ${absPath}`)
}
throw new Error(`failed to access file: ${error.message}`);
throw new Error(`failed to access file: ${error.message}`)
}
}
// Check for new files to ensure they don't already exist
const filesToAdd = identifyFilesAdded(params.patchText);
const filesToAdd = identifyFilesAdded(params.patchText)
for (const filePath of filesToAdd) {
let absPath = filePath;
let absPath = filePath
if (!path.isAbsolute(absPath)) {
absPath = path.resolve(process.cwd(), absPath);
absPath = path.resolve(process.cwd(), absPath)
}
try {
await fs.stat(absPath);
throw new Error(`file already exists and cannot be added: ${absPath}`);
await fs.stat(absPath)
throw new Error(`file already exists and cannot be added: ${absPath}`)
} catch (error: any) {
if (error.code !== "ENOENT") {
throw new Error(`failed to check file: ${error.message}`);
throw new Error(`failed to check file: ${error.message}`)
}
}
}
// Load all required files
const currentFiles: Record<string, string> = {};
const currentFiles: Record<string, string> = {}
for (const filePath of filesToRead) {
let absPath = filePath;
let absPath = filePath
if (!path.isAbsolute(absPath)) {
absPath = path.resolve(process.cwd(), absPath);
absPath = path.resolve(process.cwd(), absPath)
}
try {
const content = await fs.readFile(absPath, "utf-8");
currentFiles[filePath] = content;
const content = await fs.readFile(absPath, "utf-8")
currentFiles[filePath] = content
} catch (error: any) {
throw new Error(`failed to read file ${absPath}: ${error.message}`);
throw new Error(`failed to read file ${absPath}: ${error.message}`)
}
}
// Process the patch
const [patch, fuzz] = textToPatch(params.patchText, currentFiles);
const [patch, fuzz] = textToPatch(params.patchText, currentFiles)
if (fuzz > 3) {
throw new Error(
`patch contains fuzzy matches (fuzz level: ${fuzz}). Please make your context lines more precise`,
);
)
}
// Convert patch to commit
const commit = patchToCommit(patch, currentFiles);
const commit = patchToCommit(patch, currentFiles)
// Apply the changes to the filesystem
await applyCommit(
commit,
async (filePath: string, content: string) => {
let absPath = filePath;
let absPath = filePath
if (!path.isAbsolute(absPath)) {
absPath = path.resolve(process.cwd(), absPath);
absPath = path.resolve(process.cwd(), absPath)
}
// Create parent directories if needed
const dir = path.dirname(absPath);
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(absPath, content, "utf-8");
const dir = path.dirname(absPath)
await fs.mkdir(dir, { recursive: true })
await fs.writeFile(absPath, content, "utf-8")
},
async (filePath: string) => {
let absPath = filePath;
let absPath = filePath
if (!path.isAbsolute(absPath)) {
absPath = path.resolve(process.cwd(), absPath);
absPath = path.resolve(process.cwd(), absPath)
}
await fs.unlink(absPath);
await fs.unlink(absPath)
},
);
)
// Calculate statistics
const changedFiles: string[] = [];
let totalAdditions = 0;
let totalRemovals = 0;
const changedFiles: string[] = []
let totalAdditions = 0
let totalRemovals = 0
for (const [filePath, change] of Object.entries(commit.changes)) {
let absPath = filePath;
let absPath = filePath
if (!path.isAbsolute(absPath)) {
absPath = path.resolve(process.cwd(), absPath);
absPath = path.resolve(process.cwd(), absPath)
}
changedFiles.push(absPath);
changedFiles.push(absPath)
const oldContent = change.old_content || "";
const newContent = change.new_content || "";
const oldContent = change.old_content || ""
const newContent = change.new_content || ""
// Calculate diff statistics
const [, additions, removals] = generateDiff(
oldContent,
newContent,
filePath,
);
totalAdditions += additions;
totalRemovals += removals;
)
totalAdditions += additions
totalRemovals += removals
// Record file operations
FileTimes.write(absPath);
FileTimes.read(absPath);
FileTimes.write(absPath)
FileTimes.read(absPath)
}
const result = `Patch applied successfully. ${changedFiles.length} files changed, ${totalAdditions} additions, ${totalRemovals} removals`;
const output = result;
const result = `Patch applied successfully. ${changedFiles.length} files changed, ${totalAdditions} additions, ${totalRemovals} removals`
const output = result
return {
metadata: {
@ -415,6 +415,6 @@ export const patch = Tool.define({
removals: totalRemovals,
} satisfies PatchResponseMetadata,
output,
};
}
},
});
})

View file

@ -1,17 +1,17 @@
import { tool, type Tool as AITool } from "ai";
import { Log } from "../util/log";
import { tool, type Tool as AITool } from "ai"
import { Log } from "../util/log"
const log = Log.create({ service: "tool" });
const log = Log.create({ service: "tool" })
export namespace Tool {
export interface Metadata<
Properties extends Record<string, any> = Record<string, any>,
> {
properties: Properties;
properties: Properties
time: {
start: number;
end: number;
};
start: number
end: number
}
}
export function define<
Params,
@ -19,7 +19,7 @@ export namespace Tool {
Name extends string,
>(
input: AITool<Params, Output> & {
name: Name;
name: Name
},
) {
return tool({
@ -29,33 +29,33 @@ export namespace Tool {
id: opts.toolCallId,
name: input.name,
...params,
});
})
try {
const start = Date.now();
const result = await input.execute!(params, opts);
const start = Date.now()
const result = await input.execute!(params, opts)
const metadata: Metadata<Output["metadata"]> = {
...result.metadata,
time: {
start,
end: Date.now(),
},
};
}
return {
metadata,
output: result.output,
};
}
} catch (e: any) {
log.error("error", {
msg: e.toString(),
});
})
return {
metadata: {
error: true,
},
output: "An error occurred: " + e.toString(),
};
}
}
},
});
})
}
}

View file

@ -1,20 +1,20 @@
import { App } from "../../app/app";
import { App } from "../../app/app"
export namespace FileTimes {
export const state = App.state("tool.filetimes", () => ({
read: new Map<string, Date>(),
write: new Map<string, Date>(),
}));
}))
export function read(filePath: string) {
state().read.set(filePath, new Date());
state().read.set(filePath, new Date())
}
export function write(filePath: string) {
state().write.set(filePath, new Date());
state().write.set(filePath, new Date())
}
export function get(filePath: string): Date | null {
return state().read.get(filePath) || null;
return state().read.get(filePath) || null
}
}

View file

@ -1,13 +1,13 @@
import { z } from "zod";
import * as fs from "fs";
import * as path from "path";
import { Tool } from "./tool";
import { LSP } from "../lsp";
import { FileTimes } from "./util/file-times";
import { z } from "zod"
import * as fs from "fs"
import * as path from "path"
import { Tool } from "./tool"
import { LSP } from "../lsp"
import { FileTimes } from "./util/file-times"
const MAX_READ_SIZE = 250 * 1024;
const DEFAULT_READ_LIMIT = 2000;
const MAX_LINE_LENGTH = 2000;
const MAX_READ_SIZE = 250 * 1024
const DEFAULT_READ_LIMIT = 2000
const MAX_LINE_LENGTH = 2000
const DESCRIPTION = `File viewing tool that reads and displays the contents of files with line numbers, allowing you to examine code, logs, or text data.
@ -38,7 +38,7 @@ LIMITATIONS:
TIPS:
- Use with Glob tool to first find files you want to view
- For code exploration, first use Grep to find relevant files, then View to examine them
- When viewing large files, use the offset parameter to read specific sections`;
- When viewing large files, use the offset parameter to read specific sections`
export const view = Tool.define({
name: "opencode.view",
@ -55,17 +55,17 @@ export const view = Tool.define({
.optional(),
}),
async execute(params) {
let filePath = params.filePath;
let filePath = params.filePath
if (!path.isAbsolute(filePath)) {
filePath = path.join(process.cwd(), filePath);
filePath = path.join(process.cwd(), filePath)
}
const file = Bun.file(filePath);
const file = Bun.file(filePath)
if (!(await file.exists())) {
const dir = path.dirname(filePath);
const base = path.basename(filePath);
const dir = path.dirname(filePath)
const base = path.basename(filePath)
const dirEntries = fs.readdirSync(dir);
const dirEntries = fs.readdirSync(dir)
const suggestions = dirEntries
.filter(
(entry) =>
@ -73,80 +73,80 @@ export const view = Tool.define({
base.toLowerCase().includes(entry.toLowerCase()),
)
.map((entry) => path.join(dir, entry))
.slice(0, 3);
.slice(0, 3)
if (suggestions.length > 0) {
throw new Error(
`File not found: ${filePath}\n\nDid you mean one of these?\n${suggestions.join("\n")}`,
);
)
}
throw new Error(`File not found: ${filePath}`);
throw new Error(`File not found: ${filePath}`)
}
const stats = await file.stat();
const stats = await file.stat()
if (stats.size > MAX_READ_SIZE)
throw new Error(
`File is too large (${stats.size} bytes). Maximum size is ${MAX_READ_SIZE} bytes`,
);
const limit = params.limit ?? DEFAULT_READ_LIMIT;
const offset = params.offset || 0;
const isImage = isImageFile(filePath);
)
const limit = params.limit ?? DEFAULT_READ_LIMIT
const offset = params.offset || 0
const isImage = isImageFile(filePath)
if (isImage)
throw new Error(
`This is an image file of type: ${isImage}\nUse a different tool to process images`,
);
const lines = await file.text().then((text) => text.split("\n"));
)
const lines = await file.text().then((text) => text.split("\n"))
const raw = lines.slice(offset, offset + limit).map((line) => {
return line.length > MAX_LINE_LENGTH
? line.substring(0, MAX_LINE_LENGTH) + "..."
: line;
});
: line
})
const content = raw.map((line, index) => {
return `${(index + offset + 1).toString().padStart(5, "0")}| ${line}`;
});
const preview = raw.slice(0, 20).join("\n");
return `${(index + offset + 1).toString().padStart(5, "0")}| ${line}`
})
const preview = raw.slice(0, 20).join("\n")
let output = "<file>\n";
output += content.join("\n");
let output = "<file>\n"
output += content.join("\n")
if (lines.length > offset + content.length) {
output += `\n\n(File has more lines. Use 'offset' parameter to read beyond line ${
offset + content.length
})`;
})`
}
output += "\n</file>";
output += "\n</file>"
// just warms the lsp client
LSP.file(filePath);
FileTimes.read(filePath);
LSP.file(filePath)
FileTimes.read(filePath)
return {
output,
metadata: {
preview,
},
};
}
},
});
})
function isImageFile(filePath: string): string | false {
const ext = path.extname(filePath).toLowerCase();
const ext = path.extname(filePath).toLowerCase()
switch (ext) {
case ".jpg":
case ".jpeg":
return "JPEG";
return "JPEG"
case ".png":
return "PNG";
return "PNG"
case ".gif":
return "GIF";
return "GIF"
case ".bmp":
return "BMP";
return "BMP"
case ".svg":
return "SVG";
return "SVG"
case ".webp":
return "WebP";
return "WebP"
default:
return false;
return false
}
}

View file

@ -1,25 +1,25 @@
import { AsyncLocalStorage } from "async_hooks";
import { AsyncLocalStorage } from "async_hooks"
export namespace Context {
export class NotFound extends Error {
constructor(public readonly name: string) {
super(`No context found for ${name}`);
super(`No context found for ${name}`)
}
}
export function create<T>(name: string) {
const storage = new AsyncLocalStorage<T>();
const storage = new AsyncLocalStorage<T>()
return {
use() {
const result = storage.getStore();
const result = storage.getStore()
if (!result) {
throw new NotFound(name);
throw new NotFound(name)
}
return result;
return result
},
provide<R>(value: T, fn: () => R) {
return storage.run<R>(value, fn);
return storage.run<R>(value, fn)
},
};
}
}
}

View file

@ -1,37 +1,37 @@
import path from "path";
import { AppPath } from "../app/path";
import fs from "fs/promises";
import path from "path"
import { AppPath } from "../app/path"
import fs from "fs/promises"
export namespace Log {
const write = {
out: (msg: string) => {
process.stdout.write(msg);
process.stdout.write(msg)
},
err: (msg: string) => {
process.stderr.write(msg);
process.stderr.write(msg)
},
};
}
export async function file(directory: string) {
const outPath = path.join(AppPath.data(directory), "opencode.out.log");
const errPath = path.join(AppPath.data(directory), "opencode.err.log");
await fs.truncate(outPath).catch(() => {});
await fs.truncate(errPath).catch(() => {});
const out = Bun.file(outPath);
const err = Bun.file(errPath);
const outWriter = out.writer();
const errWriter = err.writer();
const outPath = path.join(AppPath.data(directory), "opencode.out.log")
const errPath = path.join(AppPath.data(directory), "opencode.err.log")
await fs.truncate(outPath).catch(() => {})
await fs.truncate(errPath).catch(() => {})
const out = Bun.file(outPath)
const err = Bun.file(errPath)
const outWriter = out.writer()
const errWriter = err.writer()
write["out"] = (msg) => {
outWriter.write(msg);
outWriter.flush();
};
outWriter.write(msg)
outWriter.flush()
}
write["err"] = (msg) => {
errWriter.write(msg);
errWriter.flush();
};
errWriter.write(msg)
errWriter.flush()
}
}
export function create(tags?: Record<string, any>) {
tags = tags || {};
tags = tags || {}
function build(message: any, extra?: Record<string, any>) {
const prefix = Object.entries({
@ -40,25 +40,28 @@ export namespace Log {
})
.filter(([_, value]) => value !== undefined && value !== null)
.map(([key, value]) => `${key}=${value}`)
.join(" ");
return [new Date().toISOString(), prefix, message].filter(Boolean).join(" ") + "\n";
.join(" ")
return (
[new Date().toISOString(), prefix, message].filter(Boolean).join(" ") +
"\n"
)
}
const result = {
info(message?: any, extra?: Record<string, any>) {
write.out(build(message, extra));
write.out(build(message, extra))
},
error(message?: any, extra?: Record<string, any>) {
write.err(build(message, extra));
write.err(build(message, extra))
},
tag(key: string, value: string) {
if (tags) tags[key] = value;
return result;
if (tags) tags[key] = value
return result
},
clone() {
return Log.create({ ...tags });
return Log.create({ ...tags })
},
};
}
return result;
return result
}
}

View file

@ -1,5 +1,5 @@
export const foo: string = "42";
export const foo: string = "42"
export function dummyFunction(): void {
console.log("This is a dummy function");
console.log("This is a dummy function")
}

View file

@ -1,7 +1,7 @@
import { describe, expect, test } from "bun:test";
import { App } from "../../src/app/app";
import { glob } from "../../src/tool/glob";
import { ls } from "../../src/tool/ls";
import { describe, expect, test } from "bun:test"
import { App } from "../../src/app/app"
import { glob } from "../../src/tool/glob"
import { ls } from "../../src/tool/ls"
describe("tool.glob", () => {
test("truncate", async () => {
@ -14,10 +14,10 @@ describe("tool.glob", () => {
toolCallId: "test",
messages: [],
},
);
expect(result.metadata.truncated).toBe(true);
});
});
)
expect(result.metadata.truncated).toBe(true)
})
})
test("basic", async () => {
await App.provide({ directory: process.cwd() }, async () => {
let result = await glob.execute(
@ -28,14 +28,14 @@ describe("tool.glob", () => {
toolCallId: "test",
messages: [],
},
);
)
expect(result.metadata).toMatchObject({
truncated: false,
count: 2,
});
});
});
});
})
})
})
})
describe("tool.ls", () => {
test("basic", async () => {
@ -48,8 +48,8 @@ describe("tool.ls", () => {
toolCallId: "test",
messages: [],
},
);
});
expect(result.output).toMatchSnapshot();
});
});
)
})
expect(result.output).toMatchSnapshot()
})
})

View file

@ -1,69 +1,69 @@
// @ts-check
import { defineConfig } from "astro/config";
import starlight from "@astrojs/starlight";
import solidJs from "@astrojs/solid-js";
import theme from "toolbeam-docs-theme";
import { rehypeHeadingIds } from "@astrojs/markdown-remark";
import rehypeAutolinkHeadings from "rehype-autolink-headings";
import { defineConfig } from "astro/config"
import starlight from "@astrojs/starlight"
import solidJs from "@astrojs/solid-js"
import theme from "toolbeam-docs-theme"
import { rehypeHeadingIds } from "@astrojs/markdown-remark"
import rehypeAutolinkHeadings from "rehype-autolink-headings"
const discord = "https://discord.gg/sst";
const github = "https://github.com/sst/opencode";
const discord = "https://discord.gg/sst"
const github = "https://github.com/sst/opencode"
// https://astro.build/config
export default defineConfig({
devToolbar: {
enabled: false,
},
markdown: {
rehypePlugins: [
rehypeHeadingIds,
[rehypeAutolinkHeadings, { behavior: "wrap" }],
],
},
integrations: [
solidJs(),
starlight({
title: "OpenCode",
expressiveCode: { themes: ["github-light", "github-dark"] },
social: [
{ icon: "discord", label: "Discord", href: discord },
{ icon: "github", label: "GitHub", href: github },
],
editLink: {
baseUrl: `${github}/edit/master/www/`,
},
markdown: {
headingLinks: false,
},
customCss: [
"./src/styles/custom.css",
],
logo: {
light: "./src/assets/logo-light.svg",
dark: "./src/assets/logo-dark.svg",
replacesTitle: true,
},
sidebar: [
"docs",
"docs/cli",
"docs/config",
"docs/models",
"docs/themes",
"docs/shortcuts",
"docs/lsp-servers",
"docs/mcp-servers",
],
components: {
Hero: "./src/components/Hero.astro",
Header: "./src/components/Header.astro",
},
plugins: [theme({
// Optionally, add your own header links
headerLinks: [
{ name: "Home", url: "/" },
{ name: "Docs", url: "/docs/" },
],
})],
}),
],
});
devToolbar: {
enabled: false,
},
markdown: {
rehypePlugins: [
rehypeHeadingIds,
[rehypeAutolinkHeadings, { behavior: "wrap" }],
],
},
integrations: [
solidJs(),
starlight({
title: "OpenCode",
expressiveCode: { themes: ["github-light", "github-dark"] },
social: [
{ icon: "discord", label: "Discord", href: discord },
{ icon: "github", label: "GitHub", href: github },
],
editLink: {
baseUrl: `${github}/edit/master/www/`,
},
markdown: {
headingLinks: false,
},
customCss: ["./src/styles/custom.css"],
logo: {
light: "./src/assets/logo-light.svg",
dark: "./src/assets/logo-dark.svg",
replacesTitle: true,
},
sidebar: [
"docs",
"docs/cli",
"docs/config",
"docs/models",
"docs/themes",
"docs/shortcuts",
"docs/lsp-servers",
"docs/mcp-servers",
],
components: {
Hero: "./src/components/Hero.astro",
Header: "./src/components/Header.astro",
},
plugins: [
theme({
// Optionally, add your own header links
headerLinks: [
{ name: "Home", url: "/" },
{ name: "Docs", url: "/docs/" },
],
}),
],
}),
],
})

View file

@ -6,7 +6,7 @@ import {
createResource,
} from "solid-js"
import { codeToHtml } from "shiki"
import { transformerNotationDiff } from '@shikijs/transformers'
import { transformerNotationDiff } from "@shikijs/transformers"
interface CodeBlockProps extends JSX.HTMLAttributes<HTMLDivElement> {
code: string
@ -20,12 +20,10 @@ function CodeBlock(props: CodeBlockProps) {
return (await codeToHtml(local.code, {
lang: local.lang || "text",
themes: {
light: 'github-light',
dark: 'github-dark',
light: "github-light",
dark: "github-dark",
},
transformers: [
transformerNotationDiff(),
],
transformers: [transformerNotationDiff()],
})) as string
})
@ -39,9 +37,7 @@ function CodeBlock(props: CodeBlockProps) {
}
})
return (
<div ref={containerRef} {...rest}></div>
)
return <div ref={containerRef} {...rest}></div>
}
export default CodeBlock

View file

@ -31,11 +31,7 @@ const DiffView: Component<DiffViewProps> = (props) => {
diffRows.push({
left: chunk.removed ? line : chunk.added ? "" : line,
right: chunk.added ? line : chunk.removed ? "" : line,
type: chunk.added
? "added"
: chunk.removed
? "removed"
: "unchanged",
type: chunk.added ? "added" : chunk.removed ? "removed" : "unchanged",
})
}
}

View file

@ -12,11 +12,7 @@ import {
createSignal,
} from "solid-js"
import { DateTime } from "luxon"
import {
IconOpenAI,
IconGemini,
IconAnthropic,
} from "./icons/custom"
import { IconOpenAI, IconGemini, IconAnthropic } from "./icons/custom"
import {
IconCpuChip,
IconSparkles,
@ -31,8 +27,12 @@ import styles from "./share.module.css"
import { type UIMessage } from "ai"
import { createStore, reconcile } from "solid-js/store"
type Status = "disconnected" | "connecting" | "connected" | "error" | "reconnecting"
type Status =
| "disconnected"
| "connecting"
| "connected"
| "error"
| "reconnecting"
type SessionMessage = UIMessage<{
time: {
@ -40,23 +40,26 @@ type SessionMessage = UIMessage<{
completed?: number
}
assistant?: {
modelID: string;
providerID: string;
cost: number;
modelID: string
providerID: string
cost: number
tokens: {
input: number;
output: number;
reasoning: number;
};
};
sessionID: string
tool: Record<string, {
properties: Record<string, any>
time: {
start: number
end: number
input: number
output: number
reasoning: number
}
}>
}
sessionID: string
tool: Record<
string,
{
properties: Record<string, any>
time: {
start: number
end: number
}
}
>
}>
type SessionInfo = {
@ -65,48 +68,47 @@ type SessionInfo = {
}
function getFileType(path: string) {
return path.split('.').pop()
return path.split(".").pop()
}
// Converts `{a:{b:{c:1}}` to `[['a.b.c', 1]]`
function flattenToolArgs(obj: any, prefix: string = ""): Array<[string, any]> {
const entries: Array<[string, any]> = [];
const entries: Array<[string, any]> = []
for (const [key, value] of Object.entries(obj)) {
const path = prefix ? `${prefix}.${key}` : key;
const path = prefix ? `${prefix}.${key}` : key
if (
value !== null &&
typeof value === "object" &&
!Array.isArray(value)
) {
entries.push(...flattenToolArgs(value, path));
}
else {
entries.push([path, value]);
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
entries.push(...flattenToolArgs(value, path))
} else {
entries.push([path, value])
}
}
return entries;
return entries
}
function getStatusText(status: [Status, string?]): string {
switch (status[0]) {
case "connected": return "Connected"
case "connecting": return "Connecting..."
case "disconnected": return "Disconnected"
case "reconnecting": return "Reconnecting..."
case "error": return status[1] || "Error"
default: return "Unknown"
case "connected":
return "Connected"
case "connecting":
return "Connecting..."
case "disconnected":
return "Disconnected"
case "reconnecting":
return "Reconnecting..."
case "error":
return status[1] || "Error"
default:
return "Unknown"
}
}
function ProviderIcon(props: { provider: string, size?: number }) {
function ProviderIcon(props: { provider: string; size?: number }) {
const size = props.size || 16
return (
<Switch fallback={
<IconSparkles width={size} height={size} />
}>
<Switch fallback={<IconSparkles width={size} height={size} />}>
<Match when={props.provider === "openai"}>
<IconOpenAI width={size} height={size} />
</Match>
@ -132,15 +134,11 @@ function ResultsButton(props: ResultsButtonProps) {
data-element-button-more
{...rest}
>
<span>
{local.results ? "Hide results" : "Show results"}
</span>
<span>{local.results ? "Hide results" : "Show results"}</span>
<span data-button-icon>
<Show
when={local.results}
fallback={
<IconChevronRight width={10} height={10} />
}
fallback={<IconChevronRight width={10} height={10} />}
>
<IconChevronDown width={10} height={10} />
</Show>
@ -187,16 +185,16 @@ function TextPart(props: TextPartProps) {
data-expanded={expanded() || local.expand === true}
{...rest}
>
<pre ref={el => (preEl = el)}>{local.text}</pre>
{overflowed() &&
<pre ref={(el) => (preEl = el)}>{local.text}</pre>
{overflowed() && (
<button
type="button"
data-element-button-text
onClick={() => setExpanded(e => !e)}
onClick={() => setExpanded((e) => !e)}
>
{expanded() ? "Show less" : "Show more"}
</button>
}
)}
</div>
)
}
@ -205,13 +203,13 @@ function PartFooter(props: { time: number }) {
return (
<span
data-part-footer
title={
DateTime.fromMillis(props.time).toLocaleString(
DateTime.DATETIME_FULL_WITH_SECONDS
)
}
title={DateTime.fromMillis(props.time).toLocaleString(
DateTime.DATETIME_FULL_WITH_SECONDS,
)}
>
{DateTime.fromMillis(props.time).toLocaleString(DateTime.TIME_WITH_SECONDS)}
{DateTime.fromMillis(props.time).toLocaleString(
DateTime.TIME_WITH_SECONDS,
)}
</span>
)
}
@ -226,8 +224,12 @@ export default function Share(props: { api: string }) {
}>({
messages: {},
})
const messages = createMemo(() => Object.values(store.messages).toSorted((a, b) => a.id?.localeCompare(b.id)))
const [connectionStatus, setConnectionStatus] = createSignal<[Status, string?]>(["disconnected", "Disconnected"])
const messages = createMemo(() =>
Object.values(store.messages).toSorted((a, b) => a.id?.localeCompare(b.id)),
)
const [connectionStatus, setConnectionStatus] = createSignal<
[Status, string?]
>(["disconnected", "Disconnected"])
onMount(() => {
const apiUrl = props.api
@ -326,7 +328,10 @@ export default function Share(props: { api: string }) {
const result: string[][] = []
for (const msg of messages()) {
if (msg.role === "assistant" && msg.metadata?.assistant) {
result.push([msg.metadata.assistant.providerID, msg.metadata.assistant.modelID])
result.push([
msg.metadata.assistant.providerID,
msg.metadata.assistant.modelID,
])
}
}
return result
@ -339,7 +344,7 @@ export default function Share(props: { api: string }) {
input: 0,
output: 0,
reasoning: 0,
}
},
}
for (const msg of messages()) {
const assistant = msg.metadata?.assistant
@ -366,39 +371,39 @@ export default function Share(props: { api: string }) {
<ul data-section="stats">
<li>
<span data-element-label>Cost</span>
{metrics().cost !== undefined ?
{metrics().cost !== undefined ? (
<span>${metrics().cost.toFixed(2)}</span>
:
) : (
<span data-placeholder>&mdash;</span>
}
)}
</li>
<li>
<span data-element-label>Input Tokens</span>
{metrics().tokens.input ?
{metrics().tokens.input ? (
<span>{metrics().tokens.input}</span>
:
) : (
<span data-placeholder>&mdash;</span>
}
)}
</li>
<li>
<span data-element-label>Output Tokens</span>
{metrics().tokens.output ?
{metrics().tokens.output ? (
<span>{metrics().tokens.output}</span>
:
) : (
<span data-placeholder>&mdash;</span>
}
)}
</li>
<li>
<span data-element-label>Reasoning Tokens</span>
{metrics().tokens.reasoning ?
{metrics().tokens.reasoning ? (
<span>{metrics().tokens.reasoning}</span>
:
) : (
<span data-placeholder>&mdash;</span>
}
)}
</li>
</ul>
<ul data-section="stats" data-section-models>
{models().length > 0 ?
{models().length > 0 ? (
<For each={Array.from(models())}>
{([provider, model]) => (
<li>
@ -409,27 +414,29 @@ export default function Share(props: { api: string }) {
</li>
)}
</For>
:
) : (
<li>
<span data-element-label>Models</span>
<span data-placeholder>&mdash;</span>
</li>
}
)}
</ul>
<div data-section="date">
{messages().length > 0 && messages()[0].metadata?.time.created ?
<span title={
DateTime.fromMillis(
messages()[0].metadata?.time.created || 0
).toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS)
}>
{messages().length > 0 && messages()[0].metadata?.time.created ? (
<span
title={DateTime.fromMillis(
messages()[0].metadata?.time.created || 0,
).toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS)}
>
{DateTime.fromMillis(
messages()[0].metadata?.time.created || 0
messages()[0].metadata?.time.created || 0,
).toLocaleString(DateTime.DATE_MED)}
</span>
:
<span data-element-label data-placeholder>Started at &mdash;</span>
}
) : (
<span data-element-label data-placeholder>
Started at &mdash;
</span>
)}
</div>
</div>
</div>
@ -444,27 +451,32 @@ export default function Share(props: { api: string }) {
{(msg, msgIndex) => (
<For each={msg.parts}>
{(part, partIndex) => {
if (part.type === "step-start" && (partIndex() > 0 || !msg.metadata?.assistant)) return null
if (
part.type === "step-start" &&
(partIndex() > 0 || !msg.metadata?.assistant)
)
return null
const [results, showResults] = createSignal(false)
const isLastPart = createMemo(() =>
(messages().length === msgIndex() + 1)
&& (msg.parts.length === partIndex() + 1)
const isLastPart = createMemo(
() =>
messages().length === msgIndex() + 1 &&
msg.parts.length === partIndex() + 1,
)
const time = msg.metadata?.time.completed
|| msg.metadata?.time.created
|| 0
const time =
msg.metadata?.time.completed ||
msg.metadata?.time.created ||
0
return (
<Switch>
{ /* User text */}
<Match when={
msg.role === "user" && part.type === "text" && part
}>
{part =>
<div
data-section="part"
data-part-type="user-text"
>
{/* User text */}
<Match
when={
msg.role === "user" && part.type === "text" && part
}
>
{(part) => (
<div data-section="part" data-part-type="user-text">
<div data-section="decoration">
<div>
<IconUserCircle width={18} height={18} />
@ -480,21 +492,22 @@ export default function Share(props: { api: string }) {
<PartFooter time={time} />
</div>
</div>
}
)}
</Match>
{ /* AI text */}
<Match when={
msg.role === "assistant"
&& part.type === "text"
&& part
}>
{part =>
<div
data-section="part"
data-part-type="ai-text"
>
{/* AI text */}
<Match
when={
msg.role === "assistant" &&
part.type === "text" &&
part
}
>
{(part) => (
<div data-section="part" data-part-type="ai-text">
<div data-section="decoration">
<div><IconSparkles width={18} height={18} /></div>
<div>
<IconSparkles width={18} height={18} />
</div>
<div></div>
</div>
<div data-section="content">
@ -505,19 +518,18 @@ export default function Share(props: { api: string }) {
<PartFooter time={time} />
</div>
</div>
}
)}
</Match>
{ /* AI model */}
<Match when={
msg.role === "assistant"
&& part.type === "step-start"
&& msg.metadata?.assistant
}>
{assistant =>
<div
data-section="part"
data-part-type="ai-model"
>
{/* AI model */}
<Match
when={
msg.role === "assistant" &&
part.type === "step-start" &&
msg.metadata?.assistant
}
>
{(assistant) => (
<div data-section="part" data-part-type="ai-model">
<div data-section="decoration">
<div>
<ProviderIcon
@ -542,15 +554,17 @@ export default function Share(props: { api: string }) {
</div>
</div>
</div>
}
)}
</Match>
{ /* System text */}
<Match when={
msg.role === "system"
&& part.type === "text"
&& part
}>
{part =>
{/* System text */}
<Match
when={
msg.role === "system" &&
part.type === "text" &&
part
}
>
{(part) => (
<div
data-section="part"
data-part-type="system-text"
@ -575,16 +589,18 @@ export default function Share(props: { api: string }) {
<PartFooter time={time} />
</div>
</div>
}
)}
</Match>
{ /* Edit tool */}
<Match when={
msg.role === "assistant"
&& part.type === "tool-invocation"
&& part.toolInvocation.toolName === "edit"
&& part
}>
{part => {
{/* Edit tool */}
<Match
when={
msg.role === "assistant" &&
part.type === "tool-invocation" &&
part.toolInvocation.toolName === "edit" &&
part
}
>
{(part) => {
const args = part().toolInvocation.args
const filePath = args.filePath
return (
@ -618,20 +634,25 @@ export default function Share(props: { api: string }) {
)
}}
</Match>
{ /* Tool call */}
<Match when={
msg.role === "assistant"
&& part.type === "tool-invocation"
&& part
}>
{part =>
{/* Tool call */}
<Match
when={
msg.role === "assistant" &&
part.type === "tool-invocation" &&
part
}
>
{(part) => (
<div
data-section="part"
data-part-type="tool-fallback"
>
<div data-section="decoration">
<div>
<IconWrenchScrewdriver width={18} height={18} />
<IconWrenchScrewdriver
width={18}
height={18}
/>
</div>
<div></div>
</div>
@ -641,27 +662,32 @@ export default function Share(props: { api: string }) {
{part().toolInvocation.toolName}
</span>
<div data-part-tool-args>
<For each={
flattenToolArgs(part().toolInvocation.args)
}>
{([name, value]) =>
<For
each={flattenToolArgs(
part().toolInvocation.args,
)}
>
{([name, value]) => (
<>
<div></div>
<div>{name}</div>
<div>{value}</div>
</>
}
)}
</For>
</div>
<Switch>
<Match when={
part().toolInvocation.state === "result"
&& part().toolInvocation.result
}>
<Match
when={
part().toolInvocation.state ===
"result" &&
part().toolInvocation.result
}
>
<div data-part-tool-result>
<ResultsButton
results={results()}
onClick={() => showResults(e => !e)}
onClick={() => showResults((e) => !e)}
/>
<Show when={results()}>
<TextPart
@ -673,9 +699,11 @@ export default function Share(props: { api: string }) {
</Show>
</div>
</Match>
<Match when={
part().toolInvocation.state === "call"
}>
<Match
when={
part().toolInvocation.state === "call"
}
>
<TextPart
data-size="sm"
data-color="dimmed"
@ -687,20 +715,27 @@ export default function Share(props: { api: string }) {
<PartFooter time={time} />
</div>
</div>
}
)}
</Match>
{ /* Fallback */}
{/* Fallback */}
<Match when={true}>
<div
data-section="part"
data-part-type="fallback"
>
<div data-section="part" data-part-type="fallback">
<div data-section="decoration">
<div>
<Switch fallback={
<IconWrenchScrewdriver width={16} height={16} />
}>
<Match when={msg.role === "assistant" && part.type !== "tool-invocation"}>
<Switch
fallback={
<IconWrenchScrewdriver
width={16}
height={16}
/>
}
>
<Match
when={
msg.role === "assistant" &&
part.type !== "tool-invocation"
}
>
<IconSparkles width={18} height={18} />
</Match>
<Match when={msg.role === "system"}>
@ -718,7 +753,9 @@ export default function Share(props: { api: string }) {
<span data-element-label data-part-title>
{part.type}
</span>
<TextPart text={JSON.stringify(part, null, 2)} />
<TextPart
text={JSON.stringify(part, null, 2)}
/>
</div>
<PartFooter time={time} />
</div>
@ -767,6 +804,6 @@ export default function Share(props: { api: string }) {
</Show>
</div>
</div>
</main >
</main>
)
}

View file

@ -7,7 +7,7 @@
}
.column {
display: flex;
display: flex;
flex-direction: column;
overflow-x: auto;
min-width: 0;

View file

@ -3,20 +3,35 @@ import { type JSX } from "solid-js"
// https://icones.js.org/collection/ri?s=openai&icon=ri:openai-fill
export function IconOpenAI(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M20.562 10.188c.25-.688.313-1.376.25-2.063c-.062-.687-.312-1.375-.625-2c-.562-.937-1.375-1.687-2.312-2.125c-1-.437-2.063-.562-3.125-.312c-.5-.5-1.063-.938-1.688-1.25S11.687 2 11 2a5.17 5.17 0 0 0-3 .938c-.875.624-1.5 1.5-1.813 2.5c-.75.187-1.375.5-2 .875c-.562.437-1 1-1.375 1.562c-.562.938-.75 2-.625 3.063a5.44 5.44 0 0 0 1.25 2.874a4.7 4.7 0 0 0-.25 2.063c.063.688.313 1.375.625 2c.563.938 1.375 1.688 2.313 2.125c1 .438 2.062.563 3.125.313c.5.5 1.062.937 1.687 1.25S12.312 22 13 22a5.17 5.17 0 0 0 3-.937c.875-.625 1.5-1.5 1.812-2.5a4.54 4.54 0 0 0 1.938-.875c.562-.438 1.062-.938 1.375-1.563c.562-.937.75-2 .625-3.062c-.125-1.063-.5-2.063-1.188-2.876m-7.5 10.5c-1 0-1.75-.313-2.437-.875c0 0 .062-.063.125-.063l4-2.312a.5.5 0 0 0 .25-.25a.57.57 0 0 0 .062-.313V11.25l1.688 1v4.625a3.685 3.685 0 0 1-3.688 3.813M5 17.25c-.438-.75-.625-1.625-.438-2.5c0 0 .063.063.125.063l4 2.312a.56.56 0 0 0 .313.063c.125 0 .25 0 .312-.063l4.875-2.812v1.937l-4.062 2.375A3.7 3.7 0 0 1 7.312 19c-1-.25-1.812-.875-2.312-1.75M3.937 8.563a3.8 3.8 0 0 1 1.938-1.626v4.751c0 .124 0 .25.062.312a.5.5 0 0 0 .25.25l4.875 2.813l-1.687 1l-4-2.313a3.7 3.7 0 0 1-1.75-2.25c-.25-.937-.188-2.062.312-2.937M17.75 11.75l-4.875-2.812l1.687-1l4 2.312c.625.375 1.125.875 1.438 1.5s.5 1.313.437 2.063a3.7 3.7 0 0 1-.75 1.937c-.437.563-1 1-1.687 1.25v-4.75c0-.125 0-.25-.063-.312c0 0-.062-.126-.187-.188m1.687-2.5s-.062-.062-.125-.062l-4-2.313c-.125-.062-.187-.062-.312-.062s-.25 0-.313.062L9.812 9.688V7.75l4.063-2.375c.625-.375 1.312-.5 2.062-.5c.688 0 1.375.25 2 .688c.563.437 1.063 1 1.313 1.625s.312 1.375.187 2.062m-10.5 3.5l-1.687-1V7.063c0-.688.187-1.438.562-2C8.187 4.438 8.75 4 9.375 3.688a3.37 3.37 0 0 1 2.062-.313c.688.063 1.375.375 1.938.813c0 0-.063.062-.125.062l-4 2.313a.5.5 0 0 0-.25.25c-.063.125-.063.187-.063.312zm.875-2L12 9.5l2.187 1.25v2.5L12 14.5l-2.188-1.25z" /></svg>
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M20.562 10.188c.25-.688.313-1.376.25-2.063c-.062-.687-.312-1.375-.625-2c-.562-.937-1.375-1.687-2.312-2.125c-1-.437-2.063-.562-3.125-.312c-.5-.5-1.063-.938-1.688-1.25S11.687 2 11 2a5.17 5.17 0 0 0-3 .938c-.875.624-1.5 1.5-1.813 2.5c-.75.187-1.375.5-2 .875c-.562.437-1 1-1.375 1.562c-.562.938-.75 2-.625 3.063a5.44 5.44 0 0 0 1.25 2.874a4.7 4.7 0 0 0-.25 2.063c.063.688.313 1.375.625 2c.563.938 1.375 1.688 2.313 2.125c1 .438 2.062.563 3.125.313c.5.5 1.062.937 1.687 1.25S12.312 22 13 22a5.17 5.17 0 0 0 3-.937c.875-.625 1.5-1.5 1.812-2.5a4.54 4.54 0 0 0 1.938-.875c.562-.438 1.062-.938 1.375-1.563c.562-.937.75-2 .625-3.062c-.125-1.063-.5-2.063-1.188-2.876m-7.5 10.5c-1 0-1.75-.313-2.437-.875c0 0 .062-.063.125-.063l4-2.312a.5.5 0 0 0 .25-.25a.57.57 0 0 0 .062-.313V11.25l1.688 1v4.625a3.685 3.685 0 0 1-3.688 3.813M5 17.25c-.438-.75-.625-1.625-.438-2.5c0 0 .063.063.125.063l4 2.312a.56.56 0 0 0 .313.063c.125 0 .25 0 .312-.063l4.875-2.812v1.937l-4.062 2.375A3.7 3.7 0 0 1 7.312 19c-1-.25-1.812-.875-2.312-1.75M3.937 8.563a3.8 3.8 0 0 1 1.938-1.626v4.751c0 .124 0 .25.062.312a.5.5 0 0 0 .25.25l4.875 2.813l-1.687 1l-4-2.313a3.7 3.7 0 0 1-1.75-2.25c-.25-.937-.188-2.062.312-2.937M17.75 11.75l-4.875-2.812l1.687-1l4 2.312c.625.375 1.125.875 1.438 1.5s.5 1.313.437 2.063a3.7 3.7 0 0 1-.75 1.937c-.437.563-1 1-1.687 1.25v-4.75c0-.125 0-.25-.063-.312c0 0-.062-.126-.187-.188m1.687-2.5s-.062-.062-.125-.062l-4-2.313c-.125-.062-.187-.062-.312-.062s-.25 0-.313.062L9.812 9.688V7.75l4.063-2.375c.625-.375 1.312-.5 2.062-.5c.688 0 1.375.25 2 .688c.563.437 1.063 1 1.313 1.625s.312 1.375.187 2.062m-10.5 3.5l-1.687-1V7.063c0-.688.187-1.438.562-2C8.187 4.438 8.75 4 9.375 3.688a3.37 3.37 0 0 1 2.062-.313c.688.063 1.375.375 1.938.813c0 0-.063.062-.125.062l-4 2.313a.5.5 0 0 0-.25.25c-.063.125-.063.187-.063.312zm.875-2L12 9.5l2.187 1.25v2.5L12 14.5l-2.188-1.25z"
/>
</svg>
)
}
// https://icones.js.org/collection/ri?s=anthropic&icon=ri:anthropic-fill
export function IconAnthropic(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M16.765 5h-3.308l5.923 15h3.23zM7.226 5L1.38 20h3.308l1.307-3.154h6.154l1.23 3.077h3.309L10.688 5zm-.308 9.077l2-5.308l2.077 5.308z" /></svg>
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M16.765 5h-3.308l5.923 15h3.23zM7.226 5L1.38 20h3.308l1.307-3.154h6.154l1.23 3.077h3.309L10.688 5zm-.308 9.077l2-5.308l2.077 5.308z"
/>
</svg>
)
}
// https://icones.js.org/collection/ri?s=gemini&icon=ri:gemini-fill
export function IconGemini(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M24 12.024c-6.437.388-11.59 5.539-11.977 11.976h-.047C11.588 17.563 6.436 12.412 0 12.024v-.047C6.437 11.588 11.588 6.437 11.976 0h.047c.388 6.437 5.54 11.588 11.977 11.977z" /></svg>
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M24 12.024c-6.437.388-11.59 5.539-11.977 11.976h-.047C11.588 17.563 6.436 12.412 0 12.024v-.047C6.437 11.588 11.588 6.437 11.976 0h.047c.388 6.437 5.54 11.588 11.977 11.977z"
/>
</svg>
)
}

View file

@ -21,7 +21,7 @@ export function IconAcademicCap(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconAdjustmentsHorizontal(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -42,7 +42,7 @@ export function IconAdjustmentsHorizontal(
)
}
export function IconAdjustmentsVertical(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -63,7 +63,7 @@ export function IconAdjustmentsVertical(
)
}
export function IconArchiveBoxArrowDown(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -84,7 +84,7 @@ export function IconArchiveBoxArrowDown(
)
}
export function IconArchiveBoxXMark(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -124,7 +124,7 @@ export function IconArchiveBox(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconArrowDownCircle(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -164,7 +164,7 @@ export function IconArrowDownLeft(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconArrowDownOnSquareStack(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -185,7 +185,7 @@ export function IconArrowDownOnSquareStack(
)
}
export function IconArrowDownOnSquare(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -263,7 +263,7 @@ export function IconArrowDown(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconArrowLeftCircle(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -284,7 +284,7 @@ export function IconArrowLeftCircle(
)
}
export function IconArrowLeftOnRectangle(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -400,7 +400,7 @@ export function IconArrowLongUp(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconArrowPathRoundedSquare(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -440,7 +440,7 @@ export function IconArrowPath(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconArrowRightCircle(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -461,7 +461,7 @@ export function IconArrowRightCircle(
)
}
export function IconArrowRightOnRectangle(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -539,7 +539,7 @@ export function IconArrowSmallLeft(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconArrowSmallRight(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -579,7 +579,7 @@ export function IconArrowSmallUp(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconArrowTopRightOnSquare(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -600,7 +600,7 @@ export function IconArrowTopRightOnSquare(
)
}
export function IconArrowTrendingDown(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -621,7 +621,7 @@ export function IconArrowTrendingDown(
)
}
export function IconArrowTrendingUp(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -680,7 +680,7 @@ export function IconArrowUpLeft(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconArrowUpOnSquareStack(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -701,7 +701,7 @@ export function IconArrowUpOnSquareStack(
)
}
export function IconArrowUpOnSquare(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -817,7 +817,7 @@ export function IconArrowUturnLeft(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconArrowUturnRight(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -857,7 +857,7 @@ export function IconArrowUturnUp(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconArrowsPointingIn(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -878,7 +878,7 @@ export function IconArrowsPointingIn(
)
}
export function IconArrowsPointingOut(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -899,7 +899,7 @@ export function IconArrowsPointingOut(
)
}
export function IconArrowsRightLeft(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -1040,7 +1040,7 @@ export function IconBars2(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconBars3BottomLeft(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -1061,7 +1061,7 @@ export function IconBars3BottomLeft(
)
}
export function IconBars3BottomRight(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -1082,7 +1082,7 @@ export function IconBars3BottomRight(
)
}
export function IconBars3CenterLeft(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -1507,7 +1507,7 @@ export function IconBugAnt(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconBuildingLibrary(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -1528,7 +1528,7 @@ export function IconBuildingLibrary(
)
}
export function IconBuildingOffice2(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -1568,7 +1568,7 @@ export function IconBuildingOffice(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconBuildingStorefront(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -1776,7 +1776,7 @@ export function IconChartPie(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconChatBubbleBottomCenterText(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -1797,7 +1797,7 @@ export function IconChatBubbleBottomCenterText(
)
}
export function IconChatBubbleBottomCenter(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -1818,7 +1818,7 @@ export function IconChatBubbleBottomCenter(
)
}
export function IconChatBubbleLeftEllipsis(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -1839,7 +1839,7 @@ export function IconChatBubbleLeftEllipsis(
)
}
export function IconChatBubbleLeftRight(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -1879,7 +1879,7 @@ export function IconChatBubbleLeft(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconChatBubbleOvalLeftEllipsis(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -1900,7 +1900,7 @@ export function IconChatBubbleOvalLeftEllipsis(
)
}
export function IconChatBubbleOvalLeft(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -1978,7 +1978,7 @@ export function IconCheck(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconChevronDoubleDown(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -1999,7 +1999,7 @@ export function IconChevronDoubleDown(
)
}
export function IconChevronDoubleLeft(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -2020,7 +2020,7 @@ export function IconChevronDoubleLeft(
)
}
export function IconChevronDoubleRight(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -2041,7 +2041,7 @@ export function IconChevronDoubleRight(
)
}
export function IconChevronDoubleUp(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -2176,7 +2176,7 @@ export function IconCircleStack(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconClipboardDocumentCheck(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -2197,7 +2197,7 @@ export function IconClipboardDocumentCheck(
)
}
export function IconClipboardDocumentList(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -2218,7 +2218,7 @@ export function IconClipboardDocumentList(
)
}
export function IconClipboardDocument(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -2334,7 +2334,7 @@ export function IconCloud(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconCodeBracketSquare(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -2464,7 +2464,7 @@ export function IconCommandLine(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconComputerDesktop(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -2523,7 +2523,7 @@ export function IconCreditCard(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconCubeTransparent(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -2563,7 +2563,7 @@ export function IconCube(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconCurrencyBangladeshi(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -2679,7 +2679,7 @@ export function IconCurrencyYen(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconCursorArrowRays(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -2700,7 +2700,7 @@ export function IconCursorArrowRays(
)
}
export function IconCursorArrowRipple(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -2721,7 +2721,7 @@ export function IconCursorArrowRipple(
)
}
export function IconDevicePhoneMobile(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -2761,7 +2761,7 @@ export function IconDeviceTablet(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconDocumentArrowDown(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -2782,7 +2782,7 @@ export function IconDocumentArrowDown(
)
}
export function IconDocumentArrowUp(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -2803,7 +2803,7 @@ export function IconDocumentArrowUp(
)
}
export function IconDocumentChartBar(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -2843,7 +2843,7 @@ export function IconDocumentCheck(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconDocumentDuplicate(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -2864,7 +2864,7 @@ export function IconDocumentDuplicate(
)
}
export function IconDocumentMagnifyingGlass(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -2961,7 +2961,7 @@ export function IconDocument(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconEllipsisHorizontalCircle(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -2982,7 +2982,7 @@ export function IconEllipsisHorizontalCircle(
)
}
export function IconEllipsisHorizontal(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -3017,7 +3017,7 @@ export function IconEllipsisHorizontal(
)
}
export function IconEllipsisVertical(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -3103,7 +3103,7 @@ export function IconEnvelopeSolid(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconExclamationCircle(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -3124,7 +3124,7 @@ export function IconExclamationCircle(
)
}
export function IconExclamationTriangle(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -3330,7 +3330,7 @@ export function IconFlag(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconFolderArrowDown(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -3567,7 +3567,7 @@ export function IconGlobeAmericas(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconGlobeAsiaAustralia(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -3588,7 +3588,7 @@ export function IconGlobeAsiaAustralia(
)
}
export function IconGlobeEuropeAfrica(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -3818,7 +3818,7 @@ export function IconInbox(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconInformationCircle(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -3991,7 +3991,7 @@ export function IconLockOpen(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconMagnifyingGlassCircle(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -4012,7 +4012,7 @@ export function IconMagnifyingGlassCircle(
)
}
export function IconMagnifyingGlassMinus(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -4033,7 +4033,7 @@ export function IconMagnifyingGlassMinus(
)
}
export function IconMagnifyingGlassPlus(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -4054,7 +4054,7 @@ export function IconMagnifyingGlassPlus(
)
}
export function IconMagnifyingGlass(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -4424,7 +4424,7 @@ export function IconPencil(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconPhoneArrowDownLeft(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -4445,7 +4445,7 @@ export function IconPhoneArrowDownLeft(
)
}
export function IconPhoneArrowUpRight(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -4663,7 +4663,7 @@ export function IconPower(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconPresentationChartBar(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -4684,7 +4684,7 @@ export function IconPresentationChartBar(
)
}
export function IconPresentationChartLine(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -4832,7 +4832,7 @@ export function IconQrCode(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconQuestionMarkCircle(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -5133,7 +5133,7 @@ export function IconShieldCheck(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconShieldExclamation(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -5780,7 +5780,7 @@ export function IconVariable(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconVideoCameraSlash(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -5838,7 +5838,7 @@ export function IconViewColumns(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconViewfinderCircle(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg
@ -5916,7 +5916,7 @@ export function IconWindow(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconWrenchScrewdriver(
props: JSX.SvgSVGAttributes<SVGSVGElement>
props: JSX.SvgSVGAttributes<SVGSVGElement>,
) {
return (
<svg

View file

@ -84,11 +84,21 @@
span:first-child {
color: var(--sl-color-divider);
&[data-status="connected"] { color: var(--sl-color-green); }
&[data-status="connecting"] { color: var(--sl-color-orange); }
&[data-status="disconnected"] { color: var(--sl-color-divider); }
&[data-status="reconnecting"] { color: var(--sl-color-orange); }
&[data-status="error"] { color: var(--sl-color-red); }
&[data-status="connected"] {
color: var(--sl-color-green);
}
&[data-status="connecting"] {
color: var(--sl-color-orange);
}
&[data-status="disconnected"] {
color: var(--sl-color-divider);
}
&[data-status="reconnecting"] {
color: var(--sl-color-orange);
}
&[data-status="error"] {
color: var(--sl-color-red);
}
}
}
@ -106,7 +116,7 @@
font-size: 0.875rem;
span[data-placeholder] {
color: var(--sl-color-text-dimmed);
color: var(--sl-color-text-dimmed);
}
}
}
@ -215,16 +225,15 @@
max-width: 100%;
gap: 0.25rem 0.375rem;
& > div:nth-child(3n+1) {
& > div:nth-child(3n + 1) {
width: 8px;
height: 2px;
border-radius: 1px;
background: var(--sl-color-divider);
}
& > div:nth-child(3n+2),
& > div:nth-child(3n+3) {
& > div:nth-child(3n + 2),
& > div:nth-child(3n + 3) {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@ -232,7 +241,7 @@
line-height: 1.5;
}
& > div:nth-child(3n+3) {
& > div:nth-child(3n + 3) {
padding-left: 0.125rem;
color: var(--sl-color-text-dimmed);
}

View file

@ -1,7 +1,7 @@
import { defineCollection } from 'astro:content';
import { docsLoader } from '@astrojs/starlight/loaders';
import { docsSchema } from '@astrojs/starlight/schema';
import { defineCollection } from "astro:content"
import { docsLoader } from "@astrojs/starlight/loaders"
import { docsSchema } from "@astrojs/starlight/schema"
export const collections = {
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
};
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
}

View file

@ -20,21 +20,21 @@ Or start with a specific working directory.
opencode -c /path/to/project
```
## Flags
## Flags
The OpenCode CLI takes the following flags.
| Flag | Short | Description |
| -- | -- | -- |
| `--help` | `-h` | Display help |
| `--debug` | `-d` | Enable debug mode |
| `--cwd` | `-c` | Set current working directory |
| `--prompt` | `-p` | Run a single prompt in non-interactive mode |
| Flag | Short | Description |
| ----------------- | ----- | -------------------------------------------------------- |
| `--help` | `-h` | Display help |
| `--debug` | `-d` | Enable debug mode |
| `--cwd` | `-c` | Set current working directory |
| `--prompt` | `-p` | Run a single prompt in non-interactive mode |
| `--output-format` | `-f` | Output format for non-interactive mode, `text` or `json` |
| `--quiet` | `-q` | Hide spinner in non-interactive mode |
| `--verbose` | | Display logs to stderr in non-interactive mode |
| `--allowedTools` | | Restrict the agent to only use specified tools |
| `--excludedTools` | | Prevent the agent from using specified tools |
| `--quiet` | `-q` | Hide spinner in non-interactive mode |
| `--verbose` | | Display logs to stderr in non-interactive mode |
| `--allowedTools` | | Restrict the agent to only use specified tools |
| `--excludedTools` | | Prevent the agent from using specified tools |
## Non-interactive

View file

@ -73,16 +73,15 @@ The config file has the following structure.
For the providers, you can also specify the keys using environment variables.
| Environment Variable | Models |
| -------------------------- | ----------- |
| `ANTHROPIC_API_KEY` | Claude |
| `OPENAI_API_KEY` | OpenAI |
| `GEMINI_API_KEY` | Google Gemini |
| `GROQ_API_KEY` | Groq |
| `AWS_ACCESS_KEY_ID` | Amazon Bedrock |
| `AWS_SECRET_ACCESS_KEY` | Amazon Bedrock |
| `AWS_REGION` | Amazon Bedrock |
| `AZURE_OPENAI_ENDPOINT` | Azure OpenAI |
| Environment Variable | Models |
| -------------------------- | ------------------------------------------ |
| `ANTHROPIC_API_KEY` | Claude |
| `OPENAI_API_KEY` | OpenAI |
| `GEMINI_API_KEY` | Google Gemini |
| `GROQ_API_KEY` | Groq |
| `AWS_ACCESS_KEY_ID` | Amazon Bedrock |
| `AWS_SECRET_ACCESS_KEY` | Amazon Bedrock |
| `AWS_REGION` | Amazon Bedrock |
| `AZURE_OPENAI_ENDPOINT` | Azure OpenAI |
| `AZURE_OPENAI_API_KEY` | Azure OpenAI, optional when using Entra ID |
| `AZURE_OPENAI_API_VERSION` | Azure OpenAI |
| `AZURE_OPENAI_API_VERSION` | Azure OpenAI |

View file

@ -55,14 +55,14 @@ You can create your own custom theme by setting the `theme: custom` and providin
You can define any of the following color keys in your `customTheme`.
| Type | Color keys |
| --- | --- |
| Base colors | `primary`, `secondary`, `accent` |
| Status colors | `error`, `warning`, `success`, `info` |
| Text colors | `text`, `textMuted`, `textEmphasized` |
| Type | Color keys |
| ----------------- | ------------------------------------------------------- |
| Base colors | `primary`, `secondary`, `accent` |
| Status colors | `error`, `warning`, `success`, `info` |
| Text colors | `text`, `textMuted`, `textEmphasized` |
| Background colors | `background`, `backgroundSecondary`, `backgroundDarker` |
| Border colors | `borderNormal`, `borderFocused`, `borderDim` |
| Diff view colors | `diffAdded`, `diffRemoved`, `diffContext`, etc. |
| Border colors | `borderNormal`, `borderFocused`, `borderDim` |
| Diff view colors | `diffAdded`, `diffRemoved`, `diffContext`, etc. |
You don't need to define all the color keys. Any undefined colors will fall back to the default `opencode` theme colors.

View file

@ -6,4 +6,4 @@
/// <reference path="../../sst-env.d.ts" />
import "sst"
export {}
export {}

18
sst-env.d.ts vendored
View file

@ -5,20 +5,20 @@
declare module "sst" {
export interface Resource {
"Api": {
"type": "sst.cloudflare.Worker"
"url": string
Api: {
type: "sst.cloudflare.Worker"
url: string
}
"Bucket": {
"type": "sst.cloudflare.Bucket"
Bucket: {
type: "sst.cloudflare.Bucket"
}
"Web": {
"type": "sst.cloudflare.StaticSite"
"url": string
Web: {
type: "sst.cloudflare.StaticSite"
url: string
}
}
}
/// <reference path="sst-env.d.ts" />
import "sst"
export {}
export {}