Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d3b2fb821 | ||
|
|
fa991920bc | ||
|
|
5e79e3d7a5 | ||
|
|
966015c9ae | ||
|
|
61f057337a | ||
|
|
0b261054a2 | ||
|
|
e2e481cbb5 | ||
|
|
5140e83012 | ||
|
|
100d6212be | ||
|
|
f0e19a6542 | ||
|
|
00c4d4f9f8 | ||
|
|
6e6fe6e013 | ||
|
|
d05b60291e | ||
|
|
5162361372 | ||
|
|
d271b9f75b | ||
|
|
333569bed3 | ||
|
|
09b89fdb23 | ||
|
|
0e8c3359d1 | ||
|
|
37e0a7050f | ||
|
|
774dcb6980 | ||
|
|
28bc49ad17 | ||
|
|
dc1947838c | ||
|
|
3ea2daaa4c | ||
|
|
137e964131 | ||
|
|
8efbe497fd | ||
|
|
119d2d966c | ||
|
|
194415e785 | ||
|
|
1684042fb6 | ||
|
|
59f0004d34 | ||
|
|
da35a64fa1 | ||
|
|
460338ca53 | ||
|
|
53c18a64b4 | ||
|
|
b8144c5654 | ||
|
|
9081e17fcc | ||
|
|
ef3fd5900f | ||
|
|
453d690c11 | ||
|
|
c45be6a645 | ||
|
|
7b9b177088 | ||
|
|
3cee5b0470 | ||
|
|
9246d1c901 | ||
|
|
cc12abc83e | ||
|
|
4f7e4a9436 | ||
|
|
eee396f903 | ||
|
|
0d2f8e175a | ||
|
|
4df40e0d9b | ||
|
|
b72e17a8b7 | ||
|
|
61160dc220 | ||
|
|
98734ff28c | ||
|
|
9991352663 | ||
|
|
91c4da5dbd | ||
|
|
2fd0e7dd6b | ||
|
|
d50b7ad481 | ||
|
|
df95c49401 | ||
|
|
8b73c52f00 | ||
|
|
5603098d17 | ||
|
|
f436a50125 | ||
|
|
e19e977591 | ||
|
|
addbe295b1 | ||
|
|
9a573dedc6 | ||
|
|
9ea0d71e8d | ||
|
|
b1a3599017 | ||
|
|
7b0329f67f | ||
|
|
311b9c74dd | ||
|
|
f7e8dd2ff8 | ||
|
|
40b1dd7ef2 | ||
|
|
261e76e0a3 | ||
|
|
a300bfaccb | ||
|
|
41dba0db08 | ||
|
|
6674c6083a | ||
|
|
f6afa2c6bb | ||
|
|
b2fb0508ea | ||
|
|
93f4252bb1 | ||
|
|
46ab9c16dd | ||
|
|
d869df4fee | ||
|
|
b99d4650ec | ||
|
|
261bb7f110 | ||
|
|
0515fbb260 | ||
|
|
88211d8c5b | ||
|
|
a812f95b9d | ||
|
|
3728a12bee | ||
|
|
af07e51213 | ||
|
|
3113788c92 | ||
|
|
efb5fe6d4e | ||
|
|
54dd6c644d | ||
|
|
39ad8f2667 | ||
|
|
c4a2c84e53 | ||
|
|
44fe012812 | ||
|
|
f5e7f079ea | ||
|
|
15a8936806 | ||
|
|
4e4cff49c0 | ||
|
|
5540503bee | ||
|
|
193718034b | ||
|
|
72108c0296 | ||
|
|
ec1c9f8cd1 | ||
|
|
a85b0a370e | ||
|
|
e7784d2864 | ||
|
|
97c4815444 | ||
|
|
7d1a1663c8 | ||
|
|
24c0ce6e53 | ||
|
|
4cdc86612c | ||
|
|
f1f3f8d12c | ||
|
|
e78d3b54bf | ||
|
|
f8a7cd372d | ||
|
|
f48eac638d | ||
|
|
e1f12f93eb | ||
|
|
7ca8334a8b | ||
|
|
f1a2b2eba4 | ||
|
|
4b132656df | ||
|
|
26bab00dab | ||
|
|
568c04753e | ||
|
|
4a06e164d2 | ||
|
|
c57b52c300 | ||
|
|
0b8f48f17f | ||
|
|
3862184ccb | ||
|
|
8619c50976 | ||
|
|
bb6b56b72a | ||
|
|
1252b65166 | ||
|
|
6840276dad | ||
|
|
bd8c3cd0f1 | ||
|
|
e5e9b3e3c0 | ||
|
|
1e8a681de9 | ||
|
|
a834bedc17 | ||
|
|
6a3392385e | ||
|
|
6a00e063c4 | ||
|
|
73a0ce2b7d | ||
|
|
4d1afd01fa | ||
|
|
801d5f47bd | ||
|
|
b6caae9708 | ||
|
|
183ca64ef9 | ||
|
|
8c32cfe829 | ||
|
|
73dcc88da1 | ||
|
|
14bded65dc | ||
|
|
87d1d3fb62 | ||
|
|
e054454109 | ||
|
|
a6142cf975 | ||
|
|
69332e5fa3 | ||
|
|
20201ba3c4 | ||
|
|
658067186a | ||
|
|
ac777b77cf | ||
|
|
5944ae2023 | ||
|
|
2f10961ba8 | ||
|
|
fae97978a3 | ||
|
|
3423415e49 | ||
|
|
1d0bfc2b2a | ||
|
|
bd46cf0f86 | ||
|
|
d4157d9a96 | ||
|
|
6e4ef585d8 | ||
|
|
e05c3b7a76 | ||
|
|
f99904bc1c | ||
|
|
b796d6763f | ||
|
|
c1250abdf8 | ||
|
|
ebe51534a1 | ||
|
|
b8bbee4718 | ||
|
|
8f852b396f | ||
|
|
ae4d089c06 | ||
|
|
5110fbdaf9 | ||
|
|
e6ddb474fc | ||
|
|
0dc71774ce | ||
|
|
b470466e30 | ||
|
|
d1f9311931 | ||
|
|
1c58023df9 | ||
|
|
4e0aa58b7e | ||
|
|
23ee34b35f | ||
|
|
674c9a5220 | ||
|
|
54c86ed43a | ||
|
|
676d75ee75 | ||
|
|
70dc0a12f2 | ||
|
|
d579c5e8aa | ||
|
|
ee91f31313 | ||
|
|
57b3051024 | ||
|
|
ae5cf3cc23 | ||
|
|
68e1b3c46c | ||
|
|
2d68814abc | ||
|
|
a5da5127fa | ||
|
|
b5a4439704 | ||
|
|
9c5616521d | ||
|
|
3fe163416d | ||
|
|
d054f88130 | ||
|
|
b929b4f4b9 | ||
|
|
4c0c83b02d | ||
|
|
d6d45bdc63 | ||
|
|
13a83721b0 | ||
|
|
f0edffbae9 | ||
|
|
8131bee49a | ||
|
|
b5f44ae13f | ||
|
|
0d23f2a7fd | ||
|
|
ac096d84ad | ||
|
|
fcaf0e6dbf | ||
|
|
19e259d90d | ||
|
|
2c9fd1e776 | ||
|
|
63996c4189 | ||
|
|
c7bb7ce4de | ||
|
|
c8eb1b24c3 | ||
|
|
b9f894f1e9 | ||
|
|
7c0d10a4ce | ||
|
|
06af406146 | ||
|
|
0e3458b112 | ||
|
|
2d15c683e0 | ||
|
|
3c94d26570 | ||
|
|
1a553e525f | ||
|
|
3c4e966216 | ||
|
|
0721620ed8 | ||
|
|
9fc6734f32 | ||
|
|
e1733a423d | ||
|
|
d42e3db7e0 | ||
|
|
cdb26f6d83 | ||
|
|
fe05edaa79 | ||
|
|
7d174767b0 | ||
|
|
c5eefd1752 | ||
|
|
77a6b3bdd6 | ||
|
|
7effff56c0 | ||
|
|
e30fba0d3c | ||
|
|
7fbb2ca9a6 | ||
|
|
230d0a1510 | ||
|
|
46ff2c0ae0 | ||
|
|
b8a89dab0f | ||
|
|
7351e12886 | ||
|
|
38879dee2d | ||
|
|
c4ff8dd205 | ||
|
|
0e035b3115 | ||
|
|
b855511d9a | ||
|
|
783faf554d | ||
|
|
bfd4269d7d | ||
|
|
25f78b053b | ||
|
|
87f260ee17 | ||
|
|
12931a869d | ||
|
|
f759e1804d | ||
|
|
c9b4564d36 | ||
|
|
d097c546db | ||
|
|
adb54521b4 | ||
|
|
2ea0399aa7 | ||
|
|
fa1266263d | ||
|
|
fe109c921e | ||
|
|
37bb8895fe | ||
|
|
89b95be4de | ||
|
|
eaf295bac7 | ||
|
|
27d3cec477 | ||
|
|
574d494c3c | ||
|
|
0239761f31 | ||
|
|
a53f9165e9 | ||
|
|
ffc231bd8b | ||
|
|
3cf4ef56fb | ||
|
|
c738e26438 | ||
|
|
9c6aa82ac1 | ||
|
|
ef74d97491 | ||
|
|
af892e5432 | ||
|
|
d7aca6230d | ||
|
|
0f9c2c5c27 | ||
|
|
6a261dedb4 | ||
|
|
ec928d88b5 | ||
|
|
59a5f120c0 | ||
|
|
ce07f80b19 | ||
|
|
168fd9b2e3 |
@@ -1,37 +0,0 @@
|
|||||||
name: build
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- dev
|
|
||||||
|
|
||||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
packages: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- run: git fetch --force --tags
|
|
||||||
|
|
||||||
- uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version: ">=1.23.2"
|
|
||||||
cache: true
|
|
||||||
cache-dependency-path: go.sum
|
|
||||||
|
|
||||||
- run: go mod download
|
|
||||||
|
|
||||||
- uses: goreleaser/goreleaser-action@v6
|
|
||||||
with:
|
|
||||||
distribution: goreleaser
|
|
||||||
version: latest
|
|
||||||
args: build --snapshot --clean
|
|
||||||
@@ -3,7 +3,8 @@ name: deploy
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- dontlook
|
- dev
|
||||||
|
- production
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||||
@@ -20,6 +21,6 @@ jobs:
|
|||||||
|
|
||||||
- run: bun install
|
- run: bun install
|
||||||
|
|
||||||
- run: bun sst deploy --stage=dev
|
- run: bun sst deploy --stage=${{ github.ref_name }}
|
||||||
env:
|
env:
|
||||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- dontlook
|
- dev
|
||||||
tags:
|
tags:
|
||||||
- "*"
|
- "*"
|
||||||
|
|
||||||
|
|||||||
@@ -3,3 +3,5 @@ node_modules
|
|||||||
.opencode
|
.opencode
|
||||||
.sst
|
.sst
|
||||||
.env
|
.env
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2025 OpenCode
|
Copyright (c) 2025 opencode
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -1,46 +1,87 @@
|
|||||||
```
|
<p align="center">
|
||||||
█▀▀█ █▀▀█ █▀▀ █▀▀▄ █▀▀ █▀▀█ █▀▀▄ █▀▀
|
<a href="https://opencode.ai">
|
||||||
█░░█ █░░█ █▀▀ █░░█ █░░ █░░█ █░░█ █▀▀
|
<picture>
|
||||||
▀▀▀▀ █▀▀▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀▀
|
<source srcset="packages/web/src/assets/logo-dark.svg" media="(prefers-color-scheme: dark)">
|
||||||
```
|
<source srcset="packages/web/src/assets/logo-light.svg" media="(prefers-color-scheme: light)">
|
||||||
|
<img src="packages/web/src/assets/logo-light.svg" alt="opencode logo">
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p align="center">AI coding agent, built for the terminal.</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://opencode.ai/docs"><img alt="View docs" src="https://img.shields.io/badge/view-docs-blue?style=flat-square" /></a>
|
||||||
|
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
|
||||||
|
<a href="https://github.com/sst/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/sst/opencode/publish.yml?style=flat-square&branch=dev" /></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
[](https://github.com/sst/opencode)
|
[](https://opencode.ai)
|
||||||
|
|
||||||
AI coding agent, built for the terminal.
|
---
|
||||||
|
|
||||||
Note: version 0.1.x is a full rewrite and we do not have proper documentation for it yet. Should have this out week of June 17th 2025
|
### Installation
|
||||||
|
|
||||||
## Installation
|
```bash
|
||||||
|
# YOLO
|
||||||
If you have a previous version of opencode < 0.1.x installed you might have to remove it first.
|
|
||||||
|
|
||||||
### Curl
|
|
||||||
|
|
||||||
```
|
|
||||||
curl -fsSL https://opencode.ai/install | bash
|
curl -fsSL https://opencode.ai/install | bash
|
||||||
|
|
||||||
|
# Package managers
|
||||||
|
npm i -g opencode-ai@latest # or bun/pnpm/yarn
|
||||||
|
brew install sst/tap/opencode # macOS
|
||||||
|
paru -S opencode-bin # Arch Linux
|
||||||
```
|
```
|
||||||
|
|
||||||
### NPM
|
> **Note:** Remove versions older than 0.1.x before installing
|
||||||
|
|
||||||
```
|
### Documentation
|
||||||
npm i -g opencode-ai@latest
|
|
||||||
bun i -g opencode-ai@latest
|
For more info on how to configure opencode [**head over to our docs**](https://opencode.ai/docs).
|
||||||
pnpm i -g opencode-ai@latest
|
|
||||||
yarn global add opencode-ai@latest
|
### Contributing
|
||||||
|
|
||||||
|
For any new features we'd appreciate it if you could open an issue first to discuss what you'd like to implement. We're pretty responsive there and it'll save you from working on something that we don't end up using. No need to do this for simpler fixes.
|
||||||
|
|
||||||
|
To run opencode locally you need.
|
||||||
|
|
||||||
|
- Bun
|
||||||
|
- Golang 1.24.x
|
||||||
|
|
||||||
|
And run.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ bun install
|
||||||
|
$ bun run packages/opencode/src/index.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
### Brew
|
#### Development Notes
|
||||||
|
|
||||||
```
|
**API Client Generation**: After making changes to the TypeScript API endpoints in `packages/opencode/src/server/server.ts`, you need to regenerate the Go client and OpenAPI specification:
|
||||||
brew install sst/tap/opencode
|
|
||||||
|
```bash
|
||||||
|
$ cd packages/tui
|
||||||
|
$ go generate ./pkg/client/
|
||||||
```
|
```
|
||||||
|
|
||||||
### AUR
|
This updates the generated Go client code that the TUI uses to communicate with the backend server.
|
||||||
|
|
||||||
```
|
### FAQ
|
||||||
paru -S opencode-bin
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
#### How is this different than Claude Code?
|
||||||
|
|
||||||
We are working on proper keybinds - right now it's the various function keys press F1 to see them
|
It's very similar to Claude Code in terms of capability. Here are the key differences:
|
||||||
|
|
||||||
|
- 100% open source
|
||||||
|
- Not coupled to any provider. Although Anthropic is recommended, opencode can be used with OpenAI, Google or even local models. As models evolve the gaps between them will close and pricing will drop so being provider agnostic is important.
|
||||||
|
- A focus on TUI. opencode is built by neovim users and the creators of [terminal.shop](https://terminal.shop); we are going to push the limits of what's possible in the terminal.
|
||||||
|
- A client/server architecture. This for example can allow opencode to run on your computer, while you can drive it remotely from a mobile app. Meaning that the TUI frontend is just one of the possible clients.
|
||||||
|
|
||||||
|
#### What about Windows support?
|
||||||
|
|
||||||
|
There are some minor problems blocking opencode from working on windows. We are working on on them now. You'll need to use WSL for now.
|
||||||
|
|
||||||
|
#### What's the other repo?
|
||||||
|
|
||||||
|
The other confusingly named repo has no relation to this one. You can [read the story behind it here](https://x.com/thdxr/status/1933561254481666466).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Join our community** [YouTube](https://www.youtube.com/c/sst-dev) | [X.com](https://x.com/SST_dev)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "3.5.3",
|
"prettier": "3.5.3",
|
||||||
"sst": "3.17.4",
|
"sst": "3.17.6",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
@@ -43,13 +43,17 @@
|
|||||||
"yargs": "18.0.0",
|
"yargs": "18.0.0",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
"zod-openapi": "4.2.4",
|
"zod-openapi": "4.2.4",
|
||||||
|
"zod-validation-error": "3.5.2",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@ai-sdk/amazon-bedrock": "2.2.10",
|
||||||
|
"@ai-sdk/anthropic": "1.2.12",
|
||||||
"@tsconfig/bun": "1.0.7",
|
"@tsconfig/bun": "1.0.7",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"@types/turndown": "5.0.5",
|
"@types/turndown": "5.0.5",
|
||||||
"@types/yargs": "17.0.33",
|
"@types/yargs": "17.0.33",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
|
"zod-to-json-schema": "3.24.5",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
@@ -67,16 +71,18 @@
|
|||||||
"astro": "5.7.13",
|
"astro": "5.7.13",
|
||||||
"diff": "8.0.2",
|
"diff": "8.0.2",
|
||||||
"js-base64": "3.7.7",
|
"js-base64": "3.7.7",
|
||||||
|
"lang-map": "0.4.0",
|
||||||
"luxon": "3.6.1",
|
"luxon": "3.6.1",
|
||||||
"marked": "15.0.12",
|
"marked": "15.0.12",
|
||||||
"rehype-autolink-headings": "7.1.0",
|
"rehype-autolink-headings": "7.1.0",
|
||||||
"sharp": "0.32.5",
|
"sharp": "0.32.5",
|
||||||
"shiki": "3.4.2",
|
"shiki": "3.4.2",
|
||||||
"solid-js": "1.9.7",
|
"solid-js": "1.9.7",
|
||||||
"toolbeam-docs-theme": "0.2.4",
|
"toolbeam-docs-theme": "0.3.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
|
"opencode": "workspace:*",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -85,21 +91,30 @@
|
|||||||
"sharp",
|
"sharp",
|
||||||
"esbuild",
|
"esbuild",
|
||||||
],
|
],
|
||||||
|
"patchedDependencies": {
|
||||||
|
"ai@4.3.16": "patches/ai@4.3.16.patch",
|
||||||
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"zod": "3.24.2",
|
"zod": "3.24.2",
|
||||||
},
|
},
|
||||||
"catalog": {
|
"catalog": {
|
||||||
"@types/node": "22.13.9",
|
"@types/node": "22.13.9",
|
||||||
"ai": "5.0.0-alpha.7",
|
"ai": "4.3.16",
|
||||||
"typescript": "5.8.2",
|
"typescript": "5.8.2",
|
||||||
"zod": "3.24.2",
|
"zod": "3.24.2",
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
"@ai-sdk/gateway": ["@ai-sdk/gateway@1.0.0-alpha.7", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-alpha.7", "@ai-sdk/provider-utils": "3.0.0-alpha.7" }, "peerDependencies": { "zod": "^3.24.0" } }, "sha512-gz1V165eiJnQIexfLyKm11vimrmQ3zdcJhPpjeLFmDU9wrvZwLuklfZ0WgfYSb+EjiP1cKypwt6JSGvWkfKIAQ=="],
|
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@2.2.10", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-icLGO7Q0NinnHIPgT+y1QjHVwH4HwV+brWbvM+FfCG2Afpa89PyKa3Ret91kGjZpBgM/xnj1B7K5eM+rRlsXQA=="],
|
||||||
|
|
||||||
"@ai-sdk/provider": ["@ai-sdk/provider@2.0.0-alpha.7", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-lhdrARU3SSmt5p/GNNK7VhazvZpKSCIOjpHUfX7f5jIhVGi/vvlxP1rD6Go57nn1MtuGKNqL04AebSRFDQsQbw=="],
|
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@1.2.12", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-YSzjlko7JvuiyQFmI9RN1tNZdEiZxc+6xld/0tq/VkJaHpEzGAb1yiNxxvmYVcjvfu/PcvCxAAYXmTYQQ63IHQ=="],
|
||||||
|
|
||||||
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0-alpha.7", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-alpha.7", "@standard-schema/spec": "^1.0.0", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-AYkT3jskmo7Lwzijo/yHKD1jC+UZizsROO8ULTg9aJZUwR4ABZzAxh4NxDIEy4TWRfBGufp+/9ICHAn6pkU71w=="],
|
"@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
|
||||||
|
|
||||||
|
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="],
|
||||||
|
|
||||||
|
"@ai-sdk/react": ["@ai-sdk/react@1.2.12", "", { "dependencies": { "@ai-sdk/provider-utils": "2.2.8", "@ai-sdk/ui-utils": "1.2.11", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["zod"] }, "sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g=="],
|
||||||
|
|
||||||
|
"@ai-sdk/ui-utils": ["@ai-sdk/ui-utils@1.2.11", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w=="],
|
||||||
|
|
||||||
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
|
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
|
||||||
|
|
||||||
@@ -127,6 +142,12 @@
|
|||||||
|
|
||||||
"@astrojs/underscore-redirects": ["@astrojs/underscore-redirects@0.6.1", "", {}, "sha512-4bMLrs2KW+8/vHEE5Ffv2HbxCbbgXO+2N6MpoCsMXUlUoi7pgEEx8kbkzMXJ2dZtWF3gvwm9lvgjnFeanC2LGg=="],
|
"@astrojs/underscore-redirects": ["@astrojs/underscore-redirects@0.6.1", "", {}, "sha512-4bMLrs2KW+8/vHEE5Ffv2HbxCbbgXO+2N6MpoCsMXUlUoi7pgEEx8kbkzMXJ2dZtWF3gvwm9lvgjnFeanC2LGg=="],
|
||||||
|
|
||||||
|
"@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="],
|
||||||
|
|
||||||
|
"@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="],
|
||||||
|
|
||||||
|
"@aws-sdk/types": ["@aws-sdk/types@3.821.0", "", { "dependencies": { "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-Znroqdai1a90TlxGaJ+FK1lwC0fHpo97Xjsp5UKGR5JODYm7f9+/fF17ebO1KdoBr/Rm0UIFiF5VmI8ts9F1eA=="],
|
||||||
|
|
||||||
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
"@babel/compat-data": ["@babel/compat-data@7.27.3", "", {}, "sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw=="],
|
"@babel/compat-data": ["@babel/compat-data@7.27.3", "", {}, "sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw=="],
|
||||||
@@ -407,6 +428,18 @@
|
|||||||
|
|
||||||
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
|
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
|
||||||
|
|
||||||
|
"@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.0.4", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.3.1", "@smithy/util-hex-encoding": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-7XoWfZqWb/QoR/rAU4VSi0mWnO2vu9/ltS6JZ5ZSZv0eovLVfDfu0/AX4ub33RsJTOth3TiFWSHS5YdztvFnig=="],
|
||||||
|
|
||||||
|
"@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw=="],
|
||||||
|
|
||||||
|
"@smithy/types": ["@smithy/types@4.3.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA=="],
|
||||||
|
|
||||||
|
"@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.0.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug=="],
|
||||||
|
|
||||||
|
"@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw=="],
|
||||||
|
|
||||||
|
"@smithy/util-utf8": ["@smithy/util-utf8@4.0.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow=="],
|
||||||
|
|
||||||
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
|
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
|
||||||
|
|
||||||
"@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="],
|
"@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="],
|
||||||
@@ -425,10 +458,12 @@
|
|||||||
|
|
||||||
"@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="],
|
"@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.2.16", "", { "dependencies": { "bun-types": "1.2.16" } }, "sha512-1aCZJ/6nSiViw339RsaNhkNoEloLaPzZhxMOYEa7OzRzO41IGg5n/7I43/ZIAW/c+Q6cT12Vf7fOZOoVIzb5BQ=="],
|
"@types/bun": ["@types/bun@1.2.17", "", { "dependencies": { "bun-types": "1.2.17" } }, "sha512-l/BYs/JYt+cXA/0+wUhulYJB6a6p//GTPiJ7nV+QHa8iiId4HZmnu/3J/SowP5g0rTiERY2kfGKXEK5Ehltx4Q=="],
|
||||||
|
|
||||||
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
|
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
|
||||||
|
|
||||||
|
"@types/diff-match-patch": ["@types/diff-match-patch@1.0.36", "", {}, "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg=="],
|
||||||
|
|
||||||
"@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
|
"@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
|
||||||
|
|
||||||
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
|
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
|
||||||
@@ -473,7 +508,7 @@
|
|||||||
|
|
||||||
"acorn-walk": ["acorn-walk@8.3.2", "", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="],
|
"acorn-walk": ["acorn-walk@8.3.2", "", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="],
|
||||||
|
|
||||||
"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=="],
|
"ai": ["ai@4.3.16", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "@ai-sdk/react": "1.2.12", "@ai-sdk/ui-utils": "1.2.11", "@opentelemetry/api": "1.9.0", "jsondiffpatch": "0.6.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["react"] }, "sha512-KUDwlThJ5tr2Vw0A1ZkbDKNME3wzWhuVfAOwIvFUzl1TPVDFAXDFTXio3p+jaKneB+dKNCvFFlolYmmgHttG1g=="],
|
||||||
|
|
||||||
"ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
|
"ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
|
||||||
|
|
||||||
@@ -561,7 +596,7 @@
|
|||||||
|
|
||||||
"buffer": ["buffer@4.9.2", "", { "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4", "isarray": "^1.0.0" } }, "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg=="],
|
"buffer": ["buffer@4.9.2", "", { "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4", "isarray": "^1.0.0" } }, "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.2.16", "", { "dependencies": { "@types/node": "*" } }, "sha512-ciXLrHV4PXax9vHvUrkvun9VPVGOVwbbbBF/Ev1cXz12lyEZMoJpIJABOfPcN9gDJRaiKF9MVbSygLg4NXu3/A=="],
|
"bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="],
|
||||||
|
|
||||||
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
||||||
|
|
||||||
@@ -685,6 +720,8 @@
|
|||||||
|
|
||||||
"diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="],
|
"diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="],
|
||||||
|
|
||||||
|
"diff-match-patch": ["diff-match-patch@1.0.5", "", {}, "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="],
|
||||||
|
|
||||||
"direction": ["direction@2.0.1", "", { "bin": { "direction": "cli.js" } }, "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA=="],
|
"direction": ["direction@2.0.1", "", { "bin": { "direction": "cli.js" } }, "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA=="],
|
||||||
|
|
||||||
"dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
|
"dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
|
||||||
@@ -969,10 +1006,16 @@
|
|||||||
|
|
||||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||||
|
|
||||||
|
"jsondiffpatch": ["jsondiffpatch@0.6.0", "", { "dependencies": { "@types/diff-match-patch": "^1.0.36", "chalk": "^5.3.0", "diff-match-patch": "^1.0.5" }, "bin": { "jsondiffpatch": "bin/jsondiffpatch.js" } }, "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ=="],
|
||||||
|
|
||||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||||
|
|
||||||
"klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="],
|
"klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="],
|
||||||
|
|
||||||
|
"lang-map": ["lang-map@0.4.0", "", { "dependencies": { "language-map": "^1.1.0" } }, "sha512-oiSqZIEUnWdFeDNsp4HId4tAxdFbx5iMBOwA3666Fn2L8Khj8NiD9xRvMsGmKXopPVkaDFtSv3CJOmXFUB0Hcg=="],
|
||||||
|
|
||||||
|
"language-map": ["language-map@1.5.0", "", {}, "sha512-n7gFZpe+DwEAX9cXVTw43i3wiudWDDtSn28RmdnS/HCPr284dQI/SztsamWanRr75oSlKSaGbV2nmWCTzGCoVg=="],
|
||||||
|
|
||||||
"leven": ["leven@2.1.0", "", {}, "sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA=="],
|
"leven": ["leven@2.1.0", "", {}, "sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA=="],
|
||||||
|
|
||||||
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
|
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
|
||||||
@@ -1273,6 +1316,8 @@
|
|||||||
|
|
||||||
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
|
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
|
||||||
|
|
||||||
|
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
|
||||||
|
|
||||||
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||||
|
|
||||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||||
@@ -1353,6 +1398,8 @@
|
|||||||
|
|
||||||
"sax": ["sax@1.2.1", "", {}, "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA=="],
|
"sax": ["sax@1.2.1", "", {}, "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA=="],
|
||||||
|
|
||||||
|
"secure-json-parse": ["secure-json-parse@2.7.0", "", {}, "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="],
|
||||||
|
|
||||||
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
"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=="],
|
"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=="],
|
||||||
@@ -1405,23 +1452,23 @@
|
|||||||
|
|
||||||
"split2": ["split2@3.2.2", "", { "dependencies": { "readable-stream": "^3.0.0" } }, "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg=="],
|
"split2": ["split2@3.2.2", "", { "dependencies": { "readable-stream": "^3.0.0" } }, "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg=="],
|
||||||
|
|
||||||
"sst": ["sst@3.17.4", "", { "dependencies": { "aws-sdk": "2.1692.0", "aws4fetch": "1.0.18", "jose": "5.2.3", "opencontrol": "0.0.6", "openid-client": "5.6.4" }, "optionalDependencies": { "sst-darwin-arm64": "3.17.4", "sst-darwin-x64": "3.17.4", "sst-linux-arm64": "3.17.4", "sst-linux-x64": "3.17.4", "sst-linux-x86": "3.17.4", "sst-win32-arm64": "3.17.4", "sst-win32-x64": "3.17.4", "sst-win32-x86": "3.17.4" }, "bin": { "sst": "bin/sst.mjs" } }, "sha512-WpAws1ASJIilKC9/DGBhZ5wk2I4gtlzHXKpuwPC25bHWjqllv1jZiehIYhhN0PpV2pV8xCvqzyN8Gdm3J4EWQg=="],
|
"sst": ["sst@3.17.6", "", { "dependencies": { "aws-sdk": "2.1692.0", "aws4fetch": "1.0.18", "jose": "5.2.3", "opencontrol": "0.0.6", "openid-client": "5.6.4" }, "optionalDependencies": { "sst-darwin-arm64": "3.17.6", "sst-darwin-x64": "3.17.6", "sst-linux-arm64": "3.17.6", "sst-linux-x64": "3.17.6", "sst-linux-x86": "3.17.6", "sst-win32-arm64": "3.17.6", "sst-win32-x64": "3.17.6", "sst-win32-x86": "3.17.6" }, "bin": { "sst": "bin/sst.mjs" } }, "sha512-p+AcqwfYQUdkxeRjCikQoTMviPCBiGoU7M0vcV6GDVmVis8hzhVw4EFfHTafZC+aWfy1Ke2UQi66vZlEVWuEqA=="],
|
||||||
|
|
||||||
"sst-darwin-arm64": ["sst-darwin-arm64@3.17.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-IJansQWlPdiaQNsJw3FQ+Q/ZXN1hzrq2Q31xG4l2HhA1doj1C3y+6s57vu4cTRDFo2OwBlC4+zlQBJHsOYGhrA=="],
|
"sst-darwin-arm64": ["sst-darwin-arm64@3.17.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-6tb7KlcPR7PTi3ofQv8dX/n6Jf7pNP9VfrnYL4HBWnWrcYaZeJ5MWobILfIJ/y2jHgoqmg9e5C3266Eds0JQyw=="],
|
||||||
|
|
||||||
"sst-darwin-x64": ["sst-darwin-x64@3.17.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-mHd26/AtaQ79ajqzsutRhgEjkCxX+bXgW4KJIN0AGT3110fo2OL0x2UXmfX+sxSWOFHvJQsjFjFm4CLtQSxyBg=="],
|
"sst-darwin-x64": ["sst-darwin-x64@3.17.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-lFakq6/EgTuBSjbl8Kry4pfgAPEIyn6o7ZkyRz3hz5331wUaX88yfjs3tL9JQ8Ey6jBUYxwhP/Q1n7fzIG046g=="],
|
||||||
|
|
||||||
"sst-linux-arm64": ["sst-linux-arm64@3.17.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-unaNWOY3oEI/jUUG47/2Gbreaoi/D/rLsTPeKyYEWhWEBWCojns7LfMQs1bgW0qjBGmazB2IJD4NVYhYqYQxqQ=="],
|
"sst-linux-arm64": ["sst-linux-arm64@3.17.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-SdTxXMbTEdiwOqp37w31kXv97vHqSx3oK9h/76lKg7V9k5JxPJ6JMefPLhoKWwK0Zh6AndY2zo2oRoEv4SIaDw=="],
|
||||||
|
|
||||||
"sst-linux-x64": ["sst-linux-x64@3.17.4", "", { "os": "linux", "cpu": "x64" }, "sha512-zoErI6dVoRxWcmoVVrzNJWKEqfUF/MyQInEkGROGY2YsFFzOM5RD5Dsdm9q6oDGwx+NxFAhQWc8/8C+OmoW1nA=="],
|
"sst-linux-x64": ["sst-linux-x64@3.17.6", "", { "os": "linux", "cpu": "x64" }, "sha512-qneh7uWDiTUYx8X1Y3h2YVw3SJ0ybBBlRrVybIvCM09JqQ8+qq/XjKXGzA/3/EF0Jr7Ug8cARSn9CwxhdQGN7Q=="],
|
||||||
|
|
||||||
"sst-linux-x86": ["sst-linux-x86@3.17.4", "", { "os": "linux", "cpu": "none" }, "sha512-7ZHS2rxzxVAxMFW3u5+GMRGGACaBMuLht8JYxqruD8mFVqk9UaPQgrFKIHGKWHLBJLVnF2AdwmlHOcEKP+UJWA=="],
|
"sst-linux-x86": ["sst-linux-x86@3.17.6", "", { "os": "linux", "cpu": "none" }, "sha512-pU3D5OeqnmfxGqN31DxuwWnc1OayxhkErnITHhZ39D0MTiwbIgCapH26FuLW8B08/uxJWG8djUlOboCRhSBvWA=="],
|
||||||
|
|
||||||
"sst-win32-arm64": ["sst-win32-arm64@3.17.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-q4cedr6WD3NqeQkDvmAsIgMgPIjziIWy81wA3ZmnY6UT0jFgFus23ppLIi6F4BFJfOygvAP2PeGrRR3o8giclw=="],
|
"sst-win32-arm64": ["sst-win32-arm64@3.17.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-Rr3RTYWAsH9sM9CbM/sAZCk7dB1OsSAljjJuuHMvdSAYW3RDpXEza0PBJGxnBID2eOrpswEchzMPL2d8LtL7oA=="],
|
||||||
|
|
||||||
"sst-win32-x64": ["sst-win32-x64@3.17.4", "", { "os": "win32", "cpu": "x64" }, "sha512-sSQL041YCusZ8/0ynYGe9DCmPYVZOFsemXKUA9tX4IGSDqXae1FN0Sj7HQ17JyY24UUirY1zR7LFk+7KrP6wiA=="],
|
"sst-win32-x64": ["sst-win32-x64@3.17.6", "", { "os": "win32", "cpu": "x64" }, "sha512-yZ3roxwI0Wve9PFzdrrF1kfzCmIMFCCoa8qKeXY7LxCJ4QQIqHbCOccLK1Wv/MIU/mcZHWXTQVCLHw77uaa0GQ=="],
|
||||||
|
|
||||||
"sst-win32-x86": ["sst-win32-x86@3.17.4", "", { "os": "win32", "cpu": "none" }, "sha512-WhjsD2dkA2fbQ03CgwIJb+2p0osll2PTXlr7HC3L+H8wG2DgLFPjoE+6N8n6r2dVMVaDzuNwy/7J8hRB29blaw=="],
|
"sst-win32-x86": ["sst-win32-x86@3.17.6", "", { "os": "win32", "cpu": "none" }, "sha512-zV7TJWPJN9PmIXr15iXFSs0tbGsa52oBR3+xiKrUj2qj9XsZe7HBFwskRnHyiFq0durZY9kk9ZtoVlpuUuzr1g=="],
|
||||||
|
|
||||||
"stacktracey": ["stacktracey@2.1.8", "", { "dependencies": { "as-table": "^1.0.36", "get-source": "^2.0.12" } }, "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw=="],
|
"stacktracey": ["stacktracey@2.1.8", "", { "dependencies": { "as-table": "^1.0.36", "get-source": "^2.0.12" } }, "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw=="],
|
||||||
|
|
||||||
@@ -1453,6 +1500,8 @@
|
|||||||
|
|
||||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||||
|
|
||||||
|
"swr": ["swr@2.3.3", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A=="],
|
||||||
|
|
||||||
"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-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=="],
|
"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=="],
|
||||||
@@ -1461,6 +1510,8 @@
|
|||||||
|
|
||||||
"thread-stream": ["thread-stream@0.15.2", "", { "dependencies": { "real-require": "^0.1.0" } }, "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA=="],
|
"thread-stream": ["thread-stream@0.15.2", "", { "dependencies": { "real-require": "^0.1.0" } }, "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA=="],
|
||||||
|
|
||||||
|
"throttleit": ["throttleit@2.1.0", "", {}, "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw=="],
|
||||||
|
|
||||||
"tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
|
"tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
|
||||||
|
|
||||||
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
|
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
|
||||||
@@ -1471,7 +1522,7 @@
|
|||||||
|
|
||||||
"token-types": ["token-types@6.0.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA=="],
|
"token-types": ["token-types@6.0.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA=="],
|
||||||
|
|
||||||
"toolbeam-docs-theme": ["toolbeam-docs-theme@0.2.4", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-W5mdbcgRpTBDFyEdcU81USs3MFZoXMInpSznc/AFZCwqz8atk4iBNDIlhvihpGHY54Nf5crKmZwJjxVojkHFvA=="],
|
"toolbeam-docs-theme": ["toolbeam-docs-theme@0.3.0", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-qlBkKRp8HVYV7p7jaG9lT2lvQY7c8b9czZ0tnsJUrN2TBTtEyFJymCdkhhpZNC9U4oGZ7lLk0glRJHrndWvVsg=="],
|
||||||
|
|
||||||
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||||
|
|
||||||
@@ -1545,6 +1596,8 @@
|
|||||||
|
|
||||||
"url": ["url@0.10.3", "", { "dependencies": { "punycode": "1.3.2", "querystring": "0.2.0" } }, "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ=="],
|
"url": ["url@0.10.3", "", { "dependencies": { "punycode": "1.3.2", "querystring": "0.2.0" } }, "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ=="],
|
||||||
|
|
||||||
|
"use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="],
|
||||||
|
|
||||||
"util": ["util@0.12.5", "", { "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } }, "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA=="],
|
"util": ["util@0.12.5", "", { "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } }, "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA=="],
|
||||||
|
|
||||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||||
@@ -1629,12 +1682,24 @@
|
|||||||
|
|
||||||
"zod-to-ts": ["zod-to-ts@1.2.0", "", { "peerDependencies": { "typescript": "^4.9.4 || ^5.0.2", "zod": "^3" } }, "sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA=="],
|
"zod-to-ts": ["zod-to-ts@1.2.0", "", { "peerDependencies": { "typescript": "^4.9.4 || ^5.0.2", "zod": "^3" } }, "sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA=="],
|
||||||
|
|
||||||
|
"zod-validation-error": ["zod-validation-error@3.5.2", "", { "peerDependencies": { "zod": "^3.25.0" } }, "sha512-mdi7YOLtram5dzJ5aDtm1AG9+mxRma1iaMrZdYIpFO7epdKBUwLHIxTF8CPDeCQ828zAXYtizrKlEJAtzgfgrw=="],
|
||||||
|
|
||||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||||
|
|
||||||
|
"@ai-sdk/amazon-bedrock/aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="],
|
||||||
|
|
||||||
"@ampproject/remapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
"@ampproject/remapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
||||||
|
|
||||||
"@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=="],
|
"@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=="],
|
||||||
|
|
||||||
|
"@aws-crypto/crc32/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
|
||||||
|
|
||||||
|
"@aws-crypto/util/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"@aws-sdk/types/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
"@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
"@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
||||||
@@ -1655,6 +1720,18 @@
|
|||||||
|
|
||||||
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||||
|
|
||||||
|
"@smithy/eventstream-codec/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"@smithy/is-array-buffer/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"@smithy/types/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"@smithy/util-buffer-from/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"@smithy/util-hex-encoding/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"@smithy/util-utf8/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
"@swc/helpers/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"@swc/helpers/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
@@ -1725,6 +1802,8 @@
|
|||||||
|
|
||||||
"@astrojs/mdx/@astrojs/markdown-remark/@astrojs/prism": ["@astrojs/prism@3.3.0", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ=="],
|
"@astrojs/mdx/@astrojs/markdown-remark/@astrojs/prism": ["@astrojs/prism@3.3.0", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ=="],
|
||||||
|
|
||||||
|
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
|
||||||
|
|
||||||
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||||
|
|
||||||
"ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
"ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
@@ -1791,6 +1870,8 @@
|
|||||||
|
|
||||||
"wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="],
|
"wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="],
|
||||||
|
|
||||||
|
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
||||||
|
|
||||||
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
"args/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
|
"args/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
|
||||||
|
|||||||
+3
-13
@@ -23,25 +23,15 @@ export const api = new sst.cloudflare.Worker("Api", {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
args.migrations = {
|
args.migrations = {
|
||||||
oldTag: "v1",
|
// Note: when releasing the next tag, make sure all stages use tag v2
|
||||||
newTag: "v1",
|
oldTag: $app.stage === "production" ? "" : "v1",
|
||||||
|
newTag: $app.stage === "production" ? "" : "v1",
|
||||||
//newSqliteClasses: ["SyncServer"],
|
//newSqliteClasses: ["SyncServer"],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// new sst.cloudflare.StaticSite("Web", {
|
|
||||||
// path: "packages/web",
|
|
||||||
// domain,
|
|
||||||
// environment: {
|
|
||||||
// VITE_API_URL: api.url,
|
|
||||||
// },
|
|
||||||
// build: {
|
|
||||||
// command: "bun run build",
|
|
||||||
// output: "dist",
|
|
||||||
// },
|
|
||||||
// })
|
|
||||||
new sst.cloudflare.x.Astro("Web", {
|
new sst.cloudflare.x.Astro("Web", {
|
||||||
domain,
|
domain,
|
||||||
path: "packages/web",
|
path: "packages/web",
|
||||||
|
|||||||
@@ -107,7 +107,9 @@ add_to_path() {
|
|||||||
local config_file=$1
|
local config_file=$1
|
||||||
local command=$2
|
local command=$2
|
||||||
|
|
||||||
if [[ -w $config_file ]]; then
|
if grep -Fxq "$command" "$config_file"; then
|
||||||
|
print_message info "Command already exists in $config_file, skipping write."
|
||||||
|
elif [[ -w $config_file ]]; then
|
||||||
echo -e "\n# opencode" >> "$config_file"
|
echo -e "\n# opencode" >> "$config_file"
|
||||||
echo "$command" >> "$config_file"
|
echo "$command" >> "$config_file"
|
||||||
print_message info "Successfully added ${ORANGE}opencode ${GREEN}to \$PATH in $config_file"
|
print_message info "Successfully added ${ORANGE}opencode ${GREEN}to \$PATH in $config_file"
|
||||||
@@ -173,6 +175,7 @@ if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
|
|||||||
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
|
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
|
export PATH=$INSTALL_DIR:$PATH
|
||||||
print_message warning "Manually add the directory to $config_file (or similar):"
|
print_message warning "Manually add the directory to $config_file (or similar):"
|
||||||
print_message info " export PATH=$INSTALL_DIR:\$PATH"
|
print_message info " export PATH=$INSTALL_DIR:\$PATH"
|
||||||
;;
|
;;
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"mcp": {
|
||||||
|
"sentry": {
|
||||||
|
"type": "remote",
|
||||||
|
"url": "https://mcp.sentry.dev/sse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-4
@@ -6,7 +6,7 @@
|
|||||||
"packageManager": "bun@1.2.14",
|
"packageManager": "bun@1.2.14",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "bun run --filter='*' typecheck",
|
"typecheck": "bun run --filter='*' typecheck",
|
||||||
"dev": "sst dev"
|
"postinstall": "./scripts/hooks"
|
||||||
},
|
},
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"packages": [
|
"packages": [
|
||||||
@@ -16,12 +16,12 @@
|
|||||||
"typescript": "5.8.2",
|
"typescript": "5.8.2",
|
||||||
"@types/node": "22.13.9",
|
"@types/node": "22.13.9",
|
||||||
"zod": "3.24.2",
|
"zod": "3.24.2",
|
||||||
"ai": "5.0.0-alpha.7"
|
"ai": "4.3.16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "3.5.3",
|
"prettier": "3.5.3",
|
||||||
"sst": "3.17.4"
|
"sst": "3.17.6"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -38,5 +38,8 @@
|
|||||||
"esbuild",
|
"esbuild",
|
||||||
"protobufjs",
|
"protobufjs",
|
||||||
"sharp"
|
"sharp"
|
||||||
]
|
],
|
||||||
|
"patchedDependencies": {
|
||||||
|
"ai@4.3.16": "patches/ai@4.3.16.patch"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ export class SyncServer extends DurableObject<Env> {
|
|||||||
this.ctx.acceptWebSocket(server)
|
this.ctx.acceptWebSocket(server)
|
||||||
|
|
||||||
const data = await this.ctx.storage.list()
|
const data = await this.ctx.storage.list()
|
||||||
for (const [key, content] of data.entries()) {
|
Array.from(data.entries())
|
||||||
server.send(JSON.stringify({ key, content }))
|
.filter(([key, _]) => key.startsWith("session/"))
|
||||||
}
|
.map(([key, content]) => server.send(JSON.stringify({ key, content })))
|
||||||
|
|
||||||
return new Response(null, {
|
return new Response(null, {
|
||||||
status: 101,
|
status: 101,
|
||||||
@@ -71,11 +71,9 @@ export class SyncServer extends DurableObject<Env> {
|
|||||||
|
|
||||||
public async getData() {
|
public async getData() {
|
||||||
const data = await this.ctx.storage.list()
|
const data = await this.ctx.storage.list()
|
||||||
const messages = []
|
return Array.from(data.entries())
|
||||||
for (const [key, content] of data.entries()) {
|
.filter(([key, _]) => key.startsWith("session/"))
|
||||||
messages.push({ key, content })
|
.map(([key, content]) => ({ key, content }))
|
||||||
}
|
|
||||||
return messages
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getSecret() {
|
private async getSecret() {
|
||||||
@@ -122,7 +120,7 @@ export default {
|
|||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
secret,
|
secret,
|
||||||
url: "https://dev.opencode.ai/s/" + short,
|
url: "https://opencode.ai/s/" + short,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# OpenCode Agent Guidelines
|
# opencode agent guidelines
|
||||||
|
|
||||||
## Build/Test Commands
|
## Build/Test Commands
|
||||||
|
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
- **Typecheck**: `bun run typecheck` (npm run typecheck)
|
- **Typecheck**: `bun run typecheck` (npm run typecheck)
|
||||||
- **Test**: `bun test` (runs all tests)
|
- **Test**: `bun test` (runs all tests)
|
||||||
- **Single test**: `bun test test/tool/tool.test.ts` (specific test file)
|
- **Single test**: `bun test test/tool/tool.test.ts` (specific test file)
|
||||||
|
- **API Client Generation**: `cd packages/tui && go generate ./pkg/client/` (after changes to server endpoints)
|
||||||
|
|
||||||
## Code Style
|
## Code Style
|
||||||
|
|
||||||
@@ -16,9 +17,19 @@
|
|||||||
- **Naming**: camelCase for variables/functions, PascalCase for classes/namespaces
|
- **Naming**: camelCase for variables/functions, PascalCase for classes/namespaces
|
||||||
- **Error handling**: Use Result patterns, avoid throwing exceptions in tools
|
- **Error handling**: Use Result patterns, avoid throwing exceptions in tools
|
||||||
- **File structure**: Namespace-based organization (e.g., `Tool.define()`, `Session.create()`)
|
- **File structure**: Namespace-based organization (e.g., `Tool.define()`, `Session.create()`)
|
||||||
|
|
||||||
|
## IMPORTANT
|
||||||
|
|
||||||
|
- Try to keep things in one function unless composable or reusable
|
||||||
- DO NOT do unnecessary destructuring of variables
|
- DO NOT do unnecessary destructuring of variables
|
||||||
- DO NOT use else statements unless necessary
|
- DO NOT use `else` statements unless necessary
|
||||||
- DO NOT use try catch if it can be avoided
|
- DO NOT use `try`/`catch` if it can be avoided
|
||||||
|
- AVOID `try`/`catch` where possible
|
||||||
|
- AVOID `else` statements
|
||||||
|
- AVOID using `any` type
|
||||||
|
- AVOID `let` statements
|
||||||
|
- PREFER single word variable names where possible
|
||||||
|
- Use as many bun apis as possible like Bun.file()
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -27,4 +38,4 @@
|
|||||||
- **Validation**: All inputs validated with Zod schemas
|
- **Validation**: All inputs validated with Zod schemas
|
||||||
- **Logging**: Use `Log.create({ service: "name" })` pattern
|
- **Logging**: Use `Log.create({ service: "name" })` pattern
|
||||||
- **Storage**: Use `Storage` namespace for persistence
|
- **Storage**: Use `Storage` namespace for persistence
|
||||||
|
- **API Client**: Go TUI communicates with TypeScript server via generated client. When adding/modifying server endpoints in `packages/opencode/src/server/server.ts`, run `cd packages/tui && go generate ./pkg/client/` to update the Go client code and OpenAPI spec.
|
||||||
|
|||||||
@@ -0,0 +1,302 @@
|
|||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"$schema": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "JSON schema reference for configuration validation"
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Theme name to use for the interface"
|
||||||
|
},
|
||||||
|
"keybinds": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"leader": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Leader key for keybind combinations"
|
||||||
|
},
|
||||||
|
"help": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Show help dialog"
|
||||||
|
},
|
||||||
|
"editor_open": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Open external editor"
|
||||||
|
},
|
||||||
|
"session_new": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Create a new session"
|
||||||
|
},
|
||||||
|
"session_list": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "List all sessions"
|
||||||
|
},
|
||||||
|
"session_share": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Share current session"
|
||||||
|
},
|
||||||
|
"session_interrupt": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Interrupt current session"
|
||||||
|
},
|
||||||
|
"session_compact": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Toggle compact mode for session"
|
||||||
|
},
|
||||||
|
"tool_details": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Show tool details"
|
||||||
|
},
|
||||||
|
"model_list": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "List available models"
|
||||||
|
},
|
||||||
|
"theme_list": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "List available themes"
|
||||||
|
},
|
||||||
|
"project_init": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Initialize project configuration"
|
||||||
|
},
|
||||||
|
"input_clear": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Clear input field"
|
||||||
|
},
|
||||||
|
"input_paste": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Paste from clipboard"
|
||||||
|
},
|
||||||
|
"input_submit": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Submit input"
|
||||||
|
},
|
||||||
|
"input_newline": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Insert newline in input"
|
||||||
|
},
|
||||||
|
"history_previous": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Navigate to previous history item"
|
||||||
|
},
|
||||||
|
"history_next": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Navigate to next history item"
|
||||||
|
},
|
||||||
|
"messages_page_up": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Scroll messages up by one page"
|
||||||
|
},
|
||||||
|
"messages_page_down": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Scroll messages down by one page"
|
||||||
|
},
|
||||||
|
"messages_half_page_up": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Scroll messages up by half page"
|
||||||
|
},
|
||||||
|
"messages_half_page_down": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Scroll messages down by half page"
|
||||||
|
},
|
||||||
|
"messages_previous": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Navigate to previous message"
|
||||||
|
},
|
||||||
|
"messages_next": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Navigate to next message"
|
||||||
|
},
|
||||||
|
"messages_first": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Navigate to first message"
|
||||||
|
},
|
||||||
|
"messages_last": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Navigate to last message"
|
||||||
|
},
|
||||||
|
"app_exit": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Exit the application"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"description": "Custom keybind configurations"
|
||||||
|
},
|
||||||
|
"autoshare": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Share newly created sessions automatically"
|
||||||
|
},
|
||||||
|
"autoupdate": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Automatically update to the latest version"
|
||||||
|
},
|
||||||
|
"disabled_providers": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Disable providers that are loaded automatically"
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Model to use in the format of provider/model, eg anthropic/claude-2"
|
||||||
|
},
|
||||||
|
"provider": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"api": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"npm": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"models": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"attachment": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"reasoning": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"temperature": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"cost": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"input": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"output": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"cache_read": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"cache_write": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"input",
|
||||||
|
"output"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"limit": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"context": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"output": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"context",
|
||||||
|
"output"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"models"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"description": "Custom provider configurations and model overrides"
|
||||||
|
},
|
||||||
|
"mcp": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "local",
|
||||||
|
"description": "Type of MCP server connection"
|
||||||
|
},
|
||||||
|
"command": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Command and arguments to run the MCP server"
|
||||||
|
},
|
||||||
|
"environment": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Environment variables to set when running the MCP server"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"type",
|
||||||
|
"command"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "remote",
|
||||||
|
"description": "Type of MCP server connection"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "URL of the remote MCP server"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"type",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "MCP (Model Context Protocol) server configurations"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||||
|
}
|
||||||
@@ -5,20 +5,21 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit",
|
||||||
|
"dev": "bun run ./src/index.ts"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
"./*": [
|
"./*": "./src/*.ts"
|
||||||
"./src/*.ts",
|
|
||||||
"./src/*/index.ts"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@ai-sdk/amazon-bedrock": "2.2.10",
|
||||||
|
"@ai-sdk/anthropic": "1.2.12",
|
||||||
"@tsconfig/bun": "1.0.7",
|
"@tsconfig/bun": "1.0.7",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"@types/turndown": "5.0.5",
|
"@types/turndown": "5.0.5",
|
||||||
"@types/yargs": "17.0.33",
|
"@types/yargs": "17.0.33",
|
||||||
"typescript": "catalog:"
|
"typescript": "catalog:",
|
||||||
|
"zod-to-json-schema": "3.24.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/prompts": "0.11.0",
|
"@clack/prompts": "0.11.0",
|
||||||
@@ -42,6 +43,7 @@
|
|||||||
"xdg-basedir": "5.1.0",
|
"xdg-basedir": "5.1.0",
|
||||||
"yargs": "18.0.0",
|
"yargs": "18.0.0",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
"zod-openapi": "4.2.4"
|
"zod-openapi": "4.2.4",
|
||||||
|
"zod-validation-error": "3.5.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,9 +80,9 @@ function main() {
|
|||||||
|
|
||||||
// Create symlink to the actual binary
|
// Create symlink to the actual binary
|
||||||
fs.symlinkSync(binaryPath, binScript)
|
fs.symlinkSync(binaryPath, binScript)
|
||||||
console.log(`OpenCode binary symlinked: ${binScript} -> ${binaryPath}`)
|
console.log(`opencode binary symlinked: ${binScript} -> ${binaryPath}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to create OpenCode binary symlink:", error.message)
|
console.error("Failed to create opencode binary symlink:", error.message)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,7 @@ const targets = [
|
|||||||
["linux", "x64"],
|
["linux", "x64"],
|
||||||
["darwin", "x64"],
|
["darwin", "x64"],
|
||||||
["darwin", "arm64"],
|
["darwin", "arm64"],
|
||||||
["windows", "x64"],
|
// ["windows", "x64"],
|
||||||
]
|
]
|
||||||
|
|
||||||
await $`rm -rf dist`
|
await $`rm -rf dist`
|
||||||
@@ -64,7 +64,7 @@ for (const [os, arch] of targets) {
|
|||||||
|
|
||||||
await $`mkdir -p ./dist/${pkg.name}`
|
await $`mkdir -p ./dist/${pkg.name}`
|
||||||
await $`cp -r ./bin ./dist/${pkg.name}/bin`
|
await $`cp -r ./bin ./dist/${pkg.name}/bin`
|
||||||
await $`cp ./script/postinstall.js ./dist/${pkg.name}/postinstall.js`
|
await $`cp ./script/postinstall.mjs ./dist/${pkg.name}/postinstall.mjs`
|
||||||
await Bun.file(`./dist/${pkg.name}/package.json`).write(
|
await Bun.file(`./dist/${pkg.name}/package.json`).write(
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
{
|
{
|
||||||
@@ -73,7 +73,7 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write(
|
|||||||
[pkg.name]: `./bin/${pkg.name}`,
|
[pkg.name]: `./bin/${pkg.name}`,
|
||||||
},
|
},
|
||||||
scripts: {
|
scripts: {
|
||||||
postinstall: "node ./postinstall.js",
|
postinstall: "node ./postinstall.mjs",
|
||||||
},
|
},
|
||||||
version,
|
version,
|
||||||
optionalDependencies,
|
optionalDependencies,
|
||||||
@@ -108,9 +108,10 @@ if (!snapshot) {
|
|||||||
.filter((x: string) => {
|
.filter((x: string) => {
|
||||||
const lower = x.toLowerCase()
|
const lower = x.toLowerCase()
|
||||||
return (
|
return (
|
||||||
!lower.includes("chore:") &&
|
!lower.includes("ignore:") &&
|
||||||
!lower.includes("ci:") &&
|
!lower.includes("ci:") &&
|
||||||
!lower.includes("docs:")
|
!lower.includes("docs:") &&
|
||||||
|
!lower.includes("doc:")
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.join("\n")
|
.join("\n")
|
||||||
|
|||||||
Executable
+8
@@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
import "zod-openapi/extend"
|
||||||
|
import { Config } from "../src/config/config"
|
||||||
|
import { zodToJsonSchema } from "zod-to-json-schema"
|
||||||
|
|
||||||
|
const result = zodToJsonSchema(Config.Info)
|
||||||
|
await Bun.write("config.schema.json", JSON.stringify(result, null, 2))
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import "zod-openapi/extend"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { Context } from "../util/context"
|
import { Context } from "../util/context"
|
||||||
import { Filesystem } from "../util/filesystem"
|
import { Filesystem } from "../util/filesystem"
|
||||||
@@ -18,6 +19,7 @@ export namespace App {
|
|||||||
data: z.string(),
|
data: z.string(),
|
||||||
root: z.string(),
|
root: z.string(),
|
||||||
cwd: z.string(),
|
cwd: z.string(),
|
||||||
|
state: z.string(),
|
||||||
}),
|
}),
|
||||||
time: z.object({
|
time: z.object({
|
||||||
initialized: z.number().optional(),
|
initialized: z.number().optional(),
|
||||||
@@ -32,7 +34,7 @@ export namespace App {
|
|||||||
|
|
||||||
const APP_JSON = "app.json"
|
const APP_JSON = "app.json"
|
||||||
|
|
||||||
async function create(input: { cwd: string; version: string }) {
|
async function create(input: { cwd: string }) {
|
||||||
log.info("creating", {
|
log.info("creating", {
|
||||||
cwd: input.cwd,
|
cwd: input.cwd,
|
||||||
})
|
})
|
||||||
@@ -49,9 +51,7 @@ export namespace App {
|
|||||||
const stateFile = Bun.file(path.join(data, APP_JSON))
|
const stateFile = Bun.file(path.join(data, APP_JSON))
|
||||||
const state = (await stateFile.json().catch(() => ({}))) as {
|
const state = (await stateFile.json().catch(() => ({}))) as {
|
||||||
initialized: number
|
initialized: number
|
||||||
version: string
|
|
||||||
}
|
}
|
||||||
state.version = input.version
|
|
||||||
await stateFile.write(JSON.stringify(state))
|
await stateFile.write(JSON.stringify(state))
|
||||||
|
|
||||||
const services = new Map<
|
const services = new Map<
|
||||||
@@ -70,13 +70,13 @@ export namespace App {
|
|||||||
git: git !== undefined,
|
git: git !== undefined,
|
||||||
path: {
|
path: {
|
||||||
config: Global.Path.config,
|
config: Global.Path.config,
|
||||||
|
state: Global.Path.state,
|
||||||
data,
|
data,
|
||||||
root: git ?? input.cwd,
|
root: git ?? input.cwd,
|
||||||
cwd: input.cwd,
|
cwd: input.cwd,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const result = {
|
const result = {
|
||||||
version: input.version,
|
|
||||||
services,
|
services,
|
||||||
info,
|
info,
|
||||||
}
|
}
|
||||||
@@ -108,7 +108,7 @@ export namespace App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function provide<T>(
|
export async function provide<T>(
|
||||||
input: { cwd: string; version: string },
|
input: { cwd: string },
|
||||||
cb: (app: Info) => Promise<T>,
|
cb: (app: Info) => Promise<T>,
|
||||||
) {
|
) {
|
||||||
const app = await create(input)
|
const app = await create(input)
|
||||||
@@ -124,12 +124,11 @@ export namespace App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function initialize() {
|
export async function initialize() {
|
||||||
const { info, version } = ctx.use()
|
const { info } = ctx.use()
|
||||||
info.time.initialized = Date.now()
|
info.time.initialized = Date.now()
|
||||||
await Bun.write(
|
await Bun.write(
|
||||||
path.join(info.path.data, APP_JSON),
|
path.join(info.path.data, APP_JSON),
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
version,
|
|
||||||
initialized: Date.now(),
|
initialized: Date.now(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { generatePKCE } from "@openauthjs/openauth/pkce"
|
import { generatePKCE } from "@openauthjs/openauth/pkce"
|
||||||
import fs from "fs/promises"
|
|
||||||
import { Auth } from "./index"
|
import { Auth } from "./index"
|
||||||
|
|
||||||
export namespace AuthAnthropic {
|
export namespace AuthAnthropic {
|
||||||
@@ -9,7 +8,7 @@ export namespace AuthAnthropic {
|
|||||||
const pkce = await generatePKCE()
|
const pkce = await generatePKCE()
|
||||||
const url = new URL("https://claude.ai/oauth/authorize", import.meta.url)
|
const url = new URL("https://claude.ai/oauth/authorize", import.meta.url)
|
||||||
url.searchParams.set("code", "true")
|
url.searchParams.set("code", "true")
|
||||||
url.searchParams.set("client_id", "9d1c250a-e61b-44d9-88ed-5944d1962f5e")
|
url.searchParams.set("client_id", CLIENT_ID)
|
||||||
url.searchParams.set("response_type", "code")
|
url.searchParams.set("response_type", "code")
|
||||||
url.searchParams.set(
|
url.searchParams.set(
|
||||||
"redirect_uri",
|
"redirect_uri",
|
||||||
@@ -39,7 +38,7 @@ export namespace AuthAnthropic {
|
|||||||
code: splits[0],
|
code: splits[0],
|
||||||
state: splits[1],
|
state: splits[1],
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
client_id: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
|
client_id: CLIENT_ID,
|
||||||
redirect_uri: "https://console.anthropic.com/oauth/code/callback",
|
redirect_uri: "https://console.anthropic.com/oauth/code/callback",
|
||||||
code_verifier: verifier,
|
code_verifier: verifier,
|
||||||
}),
|
}),
|
||||||
@@ -49,6 +48,7 @@ export namespace AuthAnthropic {
|
|||||||
await Auth.set("anthropic", {
|
await Auth.set("anthropic", {
|
||||||
type: "oauth",
|
type: "oauth",
|
||||||
refresh: json.refresh_token as string,
|
refresh: json.refresh_token as string,
|
||||||
|
access: json.access_token as string,
|
||||||
expires: Date.now() + json.expires_in * 1000,
|
expires: Date.now() + json.expires_in * 1000,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -56,6 +56,7 @@ export namespace AuthAnthropic {
|
|||||||
export async function access() {
|
export async function access() {
|
||||||
const info = await Auth.get("anthropic")
|
const info = await Auth.get("anthropic")
|
||||||
if (!info || info.type !== "oauth") return
|
if (!info || info.type !== "oauth") return
|
||||||
|
if (info.access && info.expires > Date.now()) return info.access
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
"https://console.anthropic.com/v1/oauth/token",
|
"https://console.anthropic.com/v1/oauth/token",
|
||||||
{
|
{
|
||||||
@@ -75,6 +76,7 @@ export namespace AuthAnthropic {
|
|||||||
await Auth.set("anthropic", {
|
await Auth.set("anthropic", {
|
||||||
type: "oauth",
|
type: "oauth",
|
||||||
refresh: json.refresh_token as string,
|
refresh: json.refresh_token as string,
|
||||||
|
access: json.access_token as string,
|
||||||
expires: Date.now() + json.expires_in * 1000,
|
expires: Date.now() + json.expires_in * 1000,
|
||||||
})
|
})
|
||||||
return json.access_token as string
|
return json.access_token as string
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
import { Auth } from "./index"
|
||||||
|
import { NamedError } from "../util/error"
|
||||||
|
|
||||||
|
export namespace AuthGithubCopilot {
|
||||||
|
const CLIENT_ID = "Iv1.b507a08c87ecfe98"
|
||||||
|
const DEVICE_CODE_URL = "https://github.com/login/device/code"
|
||||||
|
const ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token"
|
||||||
|
const COPILOT_API_KEY_URL = "https://api.github.com/copilot_internal/v2/token"
|
||||||
|
|
||||||
|
interface DeviceCodeResponse {
|
||||||
|
device_code: string
|
||||||
|
user_code: string
|
||||||
|
verification_uri: string
|
||||||
|
expires_in: number
|
||||||
|
interval: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AccessTokenResponse {
|
||||||
|
access_token?: string
|
||||||
|
error?: string
|
||||||
|
error_description?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CopilotTokenResponse {
|
||||||
|
token: string
|
||||||
|
expires_at: number
|
||||||
|
refresh_in: number
|
||||||
|
endpoints: {
|
||||||
|
api: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function authorize() {
|
||||||
|
const deviceResponse = await fetch(DEVICE_CODE_URL, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"User-Agent": "GitHubCopilotChat/0.26.7",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
client_id: CLIENT_ID,
|
||||||
|
scope: "read:user",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
const deviceData: DeviceCodeResponse = await deviceResponse.json()
|
||||||
|
return {
|
||||||
|
device: deviceData.device_code,
|
||||||
|
user: deviceData.user_code,
|
||||||
|
verification: deviceData.verification_uri,
|
||||||
|
interval: deviceData.interval || 5,
|
||||||
|
expiry: deviceData.expires_in,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function poll(device_code: string) {
|
||||||
|
const response = await fetch(ACCESS_TOKEN_URL, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"User-Agent": "GitHubCopilotChat/0.26.7",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
client_id: CLIENT_ID,
|
||||||
|
device_code,
|
||||||
|
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) return "failed"
|
||||||
|
|
||||||
|
const data: AccessTokenResponse = await response.json()
|
||||||
|
|
||||||
|
if (data.access_token) {
|
||||||
|
// Store the GitHub OAuth token
|
||||||
|
await Auth.set("github-copilot", {
|
||||||
|
type: "oauth",
|
||||||
|
refresh: data.access_token,
|
||||||
|
access: "",
|
||||||
|
expires: 0,
|
||||||
|
})
|
||||||
|
return "complete"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.error === "authorization_pending") return "pending"
|
||||||
|
|
||||||
|
if (data.error) return "failed"
|
||||||
|
|
||||||
|
return "pending"
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function access() {
|
||||||
|
const info = await Auth.get("github-copilot")
|
||||||
|
if (!info || info.type !== "oauth") return
|
||||||
|
if (info.access && info.expires > Date.now()) return info.access
|
||||||
|
|
||||||
|
// Get new Copilot API token
|
||||||
|
const response = await fetch(COPILOT_API_KEY_URL, {
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
Authorization: `Bearer ${info.refresh}`,
|
||||||
|
"User-Agent": "GitHubCopilotChat/0.26.7",
|
||||||
|
"Editor-Version": "vscode/1.99.3",
|
||||||
|
"Editor-Plugin-Version": "copilot-chat/0.26.7",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) return
|
||||||
|
|
||||||
|
const tokenData: CopilotTokenResponse = await response.json()
|
||||||
|
|
||||||
|
// Store the Copilot API token
|
||||||
|
await Auth.set("github-copilot", {
|
||||||
|
type: "oauth",
|
||||||
|
refresh: info.refresh,
|
||||||
|
access: tokenData.token,
|
||||||
|
expires: tokenData.expires_at * 1000,
|
||||||
|
})
|
||||||
|
|
||||||
|
return tokenData.token
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeviceCodeError = NamedError.create(
|
||||||
|
"DeviceCodeError",
|
||||||
|
z.object({}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const TokenExchangeError = NamedError.create(
|
||||||
|
"TokenExchangeError",
|
||||||
|
z.object({
|
||||||
|
message: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const AuthenticationError = NamedError.create(
|
||||||
|
"AuthenticationError",
|
||||||
|
z.object({
|
||||||
|
message: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const CopilotTokenError = NamedError.create(
|
||||||
|
"CopilotTokenError",
|
||||||
|
z.object({
|
||||||
|
message: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ export namespace Auth {
|
|||||||
export const Oauth = z.object({
|
export const Oauth = z.object({
|
||||||
type: z.literal("oauth"),
|
type: z.literal("oauth"),
|
||||||
refresh: z.string(),
|
refresh: z.string(),
|
||||||
|
access: z.string(),
|
||||||
expires: z.number(),
|
expires: z.number(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -43,16 +43,26 @@ export namespace BunProc {
|
|||||||
version: z.string(),
|
version: z.string(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export async function install(pkg: string, version = "latest") {
|
export async function install(pkg: string, version = "latest") {
|
||||||
const dir = path.join(Global.Path.cache, `node_modules`, pkg)
|
const mod = path.join(Global.Path.cache, "node_modules", pkg)
|
||||||
if (!(await Bun.file(path.join(dir, "package.json")).exists())) {
|
const pkgjson = Bun.file(path.join(Global.Path.cache, "package.json"))
|
||||||
log.info("installing", { pkg })
|
const parsed = await pkgjson.json().catch(() => ({
|
||||||
await BunProc.run(["add", `${pkg}@${version}`], {
|
dependencies: {},
|
||||||
cwd: Global.Path.cache,
|
}))
|
||||||
}).catch(() => {
|
if (parsed.dependencies[pkg] === version) return mod
|
||||||
throw new InstallFailedError({ pkg, version })
|
parsed.dependencies[pkg] = version
|
||||||
})
|
await Bun.write(pkgjson, JSON.stringify(parsed, null, 2))
|
||||||
}
|
await BunProc.run(["install"], {
|
||||||
return dir
|
cwd: Global.Path.cache,
|
||||||
|
}).catch((e) => {
|
||||||
|
new InstallFailedError(
|
||||||
|
{ pkg, version },
|
||||||
|
{
|
||||||
|
cause: e,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
return mod
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { AuthAnthropic } from "../../auth/anthropic"
|
import { AuthAnthropic } from "../../auth/anthropic"
|
||||||
|
import { AuthGithubCopilot } from "../../auth/github-copilot"
|
||||||
import { Auth } from "../../auth"
|
import { Auth } from "../../auth"
|
||||||
import { cmd } from "./cmd"
|
import { cmd } from "./cmd"
|
||||||
import * as prompts from "@clack/prompts"
|
import * as prompts from "@clack/prompts"
|
||||||
import open from "open"
|
import open from "open"
|
||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
import { ModelsDev } from "../../provider/models"
|
import { ModelsDev } from "../../provider/models"
|
||||||
import { map, pipe, sort, sortBy, values } from "remeda"
|
import { map, pipe, sortBy, values } from "remeda"
|
||||||
|
|
||||||
export const AuthCommand = cmd({
|
export const AuthCommand = cmd({
|
||||||
command: "auth",
|
command: "auth",
|
||||||
@@ -16,7 +17,7 @@ export const AuthCommand = cmd({
|
|||||||
.command(AuthLogoutCommand)
|
.command(AuthLogoutCommand)
|
||||||
.command(AuthListCommand)
|
.command(AuthListCommand)
|
||||||
.demandCommand(),
|
.demandCommand(),
|
||||||
async handler(args) {},
|
async handler() { },
|
||||||
})
|
})
|
||||||
|
|
||||||
export const AuthListCommand = cmd({
|
export const AuthListCommand = cmd({
|
||||||
@@ -40,15 +41,16 @@ export const AuthListCommand = cmd({
|
|||||||
|
|
||||||
export const AuthLoginCommand = cmd({
|
export const AuthLoginCommand = cmd({
|
||||||
command: "login",
|
command: "login",
|
||||||
describe: "login to a provider",
|
describe: "log in to a provider",
|
||||||
async handler() {
|
async handler() {
|
||||||
UI.empty()
|
UI.empty()
|
||||||
prompts.intro("Add credential")
|
prompts.intro("Add credential")
|
||||||
const providers = await ModelsDev.get()
|
const providers = await ModelsDev.get()
|
||||||
const priority: Record<string, number> = {
|
const priority: Record<string, number> = {
|
||||||
anthropic: 0,
|
anthropic: 0,
|
||||||
openai: 1,
|
"github-copilot": 1,
|
||||||
google: 2,
|
openai: 2,
|
||||||
|
google: 3,
|
||||||
}
|
}
|
||||||
let provider = await prompts.select({
|
let provider = await prompts.select({
|
||||||
message: "Select provider",
|
message: "Select provider",
|
||||||
@@ -78,9 +80,16 @@ export const AuthLoginCommand = cmd({
|
|||||||
|
|
||||||
if (provider === "other") {
|
if (provider === "other") {
|
||||||
provider = await prompts.text({
|
provider = await prompts.text({
|
||||||
message: "Enter provider - must match @ai-sdk/<provider>",
|
message: "Enter provider id",
|
||||||
|
validate: (x) =>
|
||||||
|
x.match(/^[a-z-]+$/) ? undefined : "a-z and hyphens only",
|
||||||
})
|
})
|
||||||
if (prompts.isCancel(provider)) throw new UI.CancelledError()
|
if (prompts.isCancel(provider)) throw new UI.CancelledError()
|
||||||
|
provider = provider.replace(/^@ai-sdk\//, "")
|
||||||
|
if (prompts.isCancel(provider)) throw new UI.CancelledError()
|
||||||
|
prompts.log.warn(
|
||||||
|
`This only stores a credential for ${provider} - you will need configure it in opencode.json, check the docs for examples.`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (provider === "amazon-bedrock") {
|
if (provider === "amazon-bedrock") {
|
||||||
@@ -111,8 +120,14 @@ export const AuthLoginCommand = cmd({
|
|||||||
// some weird bug where program exits without this
|
// some weird bug where program exits without this
|
||||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||||
const { url, verifier } = await AuthAnthropic.authorize()
|
const { url, verifier } = await AuthAnthropic.authorize()
|
||||||
prompts.note("Opening browser...")
|
prompts.note("Trying to open browser...")
|
||||||
await open(url)
|
try {
|
||||||
|
await open(url)
|
||||||
|
} catch (e) {
|
||||||
|
prompts.log.error(
|
||||||
|
"Failed to open browser perhaps you are running without a display or X server, please open the following URL in your browser:",
|
||||||
|
)
|
||||||
|
}
|
||||||
prompts.log.info(url)
|
prompts.log.info(url)
|
||||||
|
|
||||||
const code = await prompts.text({
|
const code = await prompts.text({
|
||||||
@@ -133,6 +148,37 @@ export const AuthLoginCommand = cmd({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (provider === "github-copilot") {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||||
|
const deviceInfo = await AuthGithubCopilot.authorize()
|
||||||
|
|
||||||
|
prompts.note(
|
||||||
|
`Please visit: ${deviceInfo.verification}\nEnter code: ${deviceInfo.user}`,
|
||||||
|
)
|
||||||
|
|
||||||
|
const spinner = prompts.spinner()
|
||||||
|
spinner.start("Waiting for authorization...")
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
await new Promise((resolve) =>
|
||||||
|
setTimeout(resolve, deviceInfo.interval * 1000),
|
||||||
|
)
|
||||||
|
const status = await AuthGithubCopilot.poll(deviceInfo.device)
|
||||||
|
if (status === "pending") continue
|
||||||
|
if (status === "complete") {
|
||||||
|
spinner.stop("Login successful")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (status === "failed") {
|
||||||
|
spinner.stop("Failed to authorize", 1)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prompts.outro("Done")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const key = await prompts.password({
|
const key = await prompts.password({
|
||||||
message: "Enter your API key",
|
message: "Enter your API key",
|
||||||
validate: (x) => (x.length > 0 ? undefined : "Required"),
|
validate: (x) => (x.length > 0 ? undefined : "Required"),
|
||||||
@@ -149,7 +195,7 @@ export const AuthLoginCommand = cmd({
|
|||||||
|
|
||||||
export const AuthLogoutCommand = cmd({
|
export const AuthLogoutCommand = cmd({
|
||||||
command: "logout",
|
command: "logout",
|
||||||
describe: "logout from a configured provider",
|
describe: "log out from a configured provider",
|
||||||
async handler() {
|
async handler() {
|
||||||
UI.empty()
|
UI.empty()
|
||||||
const credentials = await Auth.all().then((x) => Object.entries(x))
|
const credentials = await Auth.all().then((x) => Object.entries(x))
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import { AuthAnthropic } from "../../auth/anthropic"
|
|
||||||
import { UI } from "../ui"
|
|
||||||
|
|
||||||
// Example: https://claude.ai/oauth/authorize?code=true&client_id=9d1c250a-e61b-44d9-88ed-5944d1962f5e&response_type=code&redirect_uri=https%3A%2F%2Fconsole.anthropic.com%2Foauth%2Fcode%2Fcallback&scope=org%3Acreate_api_key+user%3Aprofile+user%3Ainference&code_challenge=MdFtFgFap23AWDSN0oa3-eaKjQRFE4CaEhXx8M9fHZg&code_challenge_method=S256&state=rKLtaDzm88GSwekyEqdi0wXX-YqIr13tSzYymSzpvfs
|
|
||||||
|
|
||||||
export const LoginAnthropicCommand = {
|
|
||||||
command: "anthropic",
|
|
||||||
describe: "Login to Anthropic",
|
|
||||||
handler: async () => {
|
|
||||||
const { url, verifier } = await AuthAnthropic.authorize()
|
|
||||||
|
|
||||||
UI.println("Login to Anthropic")
|
|
||||||
UI.println("Open the following URL in your browser:")
|
|
||||||
UI.println(url)
|
|
||||||
UI.println("")
|
|
||||||
|
|
||||||
const code = await UI.input("Paste the authorization code here: ")
|
|
||||||
await AuthAnthropic.exchange(code, verifier)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -6,68 +6,102 @@ import { Session } from "../../session"
|
|||||||
import { Share } from "../../share/share"
|
import { Share } from "../../share/share"
|
||||||
import { Message } from "../../session/message"
|
import { Message } from "../../session/message"
|
||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
import { VERSION } from "../version"
|
import { cmd } from "./cmd"
|
||||||
|
import { Flag } from "../../flag/flag"
|
||||||
const COLOR = [
|
import { Config } from "../../config/config"
|
||||||
UI.Style.TEXT_SUCCESS_BOLD,
|
|
||||||
UI.Style.TEXT_INFO_BOLD,
|
|
||||||
UI.Style.TEXT_HIGHLIGHT_BOLD,
|
|
||||||
UI.Style.TEXT_WARNING_BOLD,
|
|
||||||
]
|
|
||||||
|
|
||||||
const TOOL: Record<string, [string, string]> = {
|
const TOOL: Record<string, [string, string]> = {
|
||||||
opencode_todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
|
todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
|
||||||
opencode_todoread: ["Todo", UI.Style.TEXT_WARNING_BOLD],
|
todoread: ["Todo", UI.Style.TEXT_WARNING_BOLD],
|
||||||
opencode_bash: ["Bash", UI.Style.TEXT_DANGER_BOLD],
|
bash: ["Bash", UI.Style.TEXT_DANGER_BOLD],
|
||||||
opencode_edit: ["Edit", UI.Style.TEXT_SUCCESS_BOLD],
|
edit: ["Edit", UI.Style.TEXT_SUCCESS_BOLD],
|
||||||
opencode_glob: ["Glob", UI.Style.TEXT_INFO_BOLD],
|
glob: ["Glob", UI.Style.TEXT_INFO_BOLD],
|
||||||
opencode_grep: ["Grep", UI.Style.TEXT_INFO_BOLD],
|
grep: ["Grep", UI.Style.TEXT_INFO_BOLD],
|
||||||
opencode_list: ["List", UI.Style.TEXT_INFO_BOLD],
|
list: ["List", UI.Style.TEXT_INFO_BOLD],
|
||||||
opencode_read: ["Read", UI.Style.TEXT_HIGHLIGHT_BOLD],
|
read: ["Read", UI.Style.TEXT_HIGHLIGHT_BOLD],
|
||||||
opencode_write: ["Write", UI.Style.TEXT_SUCCESS_BOLD],
|
write: ["Write", UI.Style.TEXT_SUCCESS_BOLD],
|
||||||
|
websearch: ["Search", UI.Style.TEXT_DIM_BOLD],
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RunCommand = {
|
export const RunCommand = cmd({
|
||||||
command: "run [message..]",
|
command: "run [message..]",
|
||||||
describe: "Run OpenCode with a message",
|
describe: "run opencode with a message",
|
||||||
builder: (yargs: Argv) => {
|
builder: (yargs: Argv) => {
|
||||||
return yargs
|
return yargs
|
||||||
.positional("message", {
|
.positional("message", {
|
||||||
describe: "Message to send",
|
describe: "message to send",
|
||||||
type: "string",
|
type: "string",
|
||||||
array: true,
|
array: true,
|
||||||
default: [],
|
default: [],
|
||||||
})
|
})
|
||||||
|
.option("continue", {
|
||||||
|
alias: ["c"],
|
||||||
|
describe: "continue the last session",
|
||||||
|
type: "boolean",
|
||||||
|
})
|
||||||
.option("session", {
|
.option("session", {
|
||||||
describe: "Session ID to continue",
|
alias: ["s"],
|
||||||
|
describe: "session id to continue",
|
||||||
type: "string",
|
type: "string",
|
||||||
})
|
})
|
||||||
|
.option("share", {
|
||||||
|
type: "boolean",
|
||||||
|
describe: "share the session",
|
||||||
|
})
|
||||||
|
.option("model", {
|
||||||
|
type: "string",
|
||||||
|
alias: ["m"],
|
||||||
|
describe: "model to use in the format of provider/model",
|
||||||
|
})
|
||||||
},
|
},
|
||||||
handler: async (args: {
|
handler: async (args) => {
|
||||||
message: string[]
|
|
||||||
session?: string
|
|
||||||
printLogs?: boolean
|
|
||||||
}) => {
|
|
||||||
const message = args.message.join(" ")
|
const message = args.message.join(" ")
|
||||||
await App.provide(
|
await App.provide(
|
||||||
{
|
{
|
||||||
cwd: process.cwd(),
|
cwd: process.cwd(),
|
||||||
version: VERSION,
|
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
await Share.init()
|
await Share.init()
|
||||||
const session = args.session
|
const session = await (async () => {
|
||||||
? await Session.get(args.session)
|
if (args.continue) {
|
||||||
: await Session.create()
|
const first = await Session.list().next()
|
||||||
|
if (first.done) return
|
||||||
|
return first.value
|
||||||
|
}
|
||||||
|
|
||||||
UI.println(UI.Style.TEXT_HIGHLIGHT_BOLD + "◍ OpenCode", VERSION)
|
if (args.session) return Session.get(args.session)
|
||||||
|
|
||||||
|
return Session.create()
|
||||||
|
})()
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
UI.error("Session not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
UI.empty()
|
||||||
|
UI.println(UI.logo())
|
||||||
UI.empty()
|
UI.empty()
|
||||||
UI.println(UI.Style.TEXT_NORMAL_BOLD + "> ", message)
|
UI.println(UI.Style.TEXT_NORMAL_BOLD + "> ", message)
|
||||||
UI.empty()
|
UI.empty()
|
||||||
UI.println(
|
|
||||||
UI.Style.TEXT_INFO_BOLD +
|
const cfg = await Config.get()
|
||||||
"~ https://dev.opencode.ai/s/" +
|
if (cfg.autoshare || Flag.OPENCODE_AUTO_SHARE || args.share) {
|
||||||
|
await Session.share(session.id)
|
||||||
|
UI.println(
|
||||||
|
UI.Style.TEXT_INFO_BOLD +
|
||||||
|
"~ https://opencode.ai/s/" +
|
||||||
session.id.slice(-8),
|
session.id.slice(-8),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
UI.empty()
|
||||||
|
|
||||||
|
const { providerID, modelID } = args.model
|
||||||
|
? Provider.parseModel(args.model)
|
||||||
|
: await Provider.defaultModel()
|
||||||
|
UI.println(
|
||||||
|
UI.Style.TEXT_NORMAL_BOLD + "@ ",
|
||||||
|
UI.Style.TEXT_NORMAL + `${providerID}/${modelID}`,
|
||||||
)
|
)
|
||||||
UI.empty()
|
UI.empty()
|
||||||
|
|
||||||
@@ -75,8 +109,8 @@ export const RunCommand = {
|
|||||||
UI.println(
|
UI.println(
|
||||||
color + `|`,
|
color + `|`,
|
||||||
UI.Style.TEXT_NORMAL +
|
UI.Style.TEXT_NORMAL +
|
||||||
UI.Style.TEXT_DIM +
|
UI.Style.TEXT_DIM +
|
||||||
` ${type.padEnd(7, " ")}`,
|
` ${type.padEnd(7, " ")}`,
|
||||||
"",
|
"",
|
||||||
UI.Style.TEXT_NORMAL + title,
|
UI.Style.TEXT_NORMAL + title,
|
||||||
)
|
)
|
||||||
@@ -113,8 +147,6 @@ export const RunCommand = {
|
|||||||
printEvent(UI.Style.TEXT_NORMAL_BOLD, "Text", part.text)
|
printEvent(UI.Style.TEXT_NORMAL_BOLD, "Text", part.text)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const { providerID, modelID } = await Provider.defaultModel()
|
|
||||||
await Session.chat({
|
await Session.chat({
|
||||||
sessionID: session.id,
|
sessionID: session.id,
|
||||||
providerID,
|
providerID,
|
||||||
@@ -130,4 +162,4 @@ export const RunCommand = {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { App } from "../../app/app"
|
import { App } from "../../app/app"
|
||||||
import { LSP } from "../../lsp"
|
import { LSP } from "../../lsp"
|
||||||
import { VERSION } from "../version"
|
|
||||||
import { cmd } from "./cmd"
|
import { cmd } from "./cmd"
|
||||||
|
|
||||||
export const ScrapCommand = cmd({
|
export const ScrapCommand = cmd({
|
||||||
@@ -8,9 +7,12 @@ export const ScrapCommand = cmd({
|
|||||||
builder: (yargs) =>
|
builder: (yargs) =>
|
||||||
yargs.positional("file", { type: "string", demandOption: true }),
|
yargs.positional("file", { type: "string", demandOption: true }),
|
||||||
async handler(args) {
|
async handler(args) {
|
||||||
await App.provide({ cwd: process.cwd(), version: VERSION }, async (app) => {
|
await App.provide(
|
||||||
await LSP.touchFile(args.file, true)
|
{ cwd: process.cwd() },
|
||||||
console.log(await LSP.diagnostics())
|
async () => {
|
||||||
})
|
await LSP.touchFile(args.file, true)
|
||||||
|
console.log(await LSP.diagnostics())
|
||||||
|
},
|
||||||
|
)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import type { Argv } from "yargs"
|
||||||
|
import { UI } from "../ui"
|
||||||
|
import * as prompts from "@clack/prompts"
|
||||||
|
import { Installation } from "../../installation"
|
||||||
|
|
||||||
|
export const UpgradeCommand = {
|
||||||
|
command: "upgrade [target]",
|
||||||
|
describe: "upgrade opencode to the latest or a specific version",
|
||||||
|
builder: (yargs: Argv) => {
|
||||||
|
return yargs
|
||||||
|
.positional("target", {
|
||||||
|
describe: "version to upgrade to, for ex '0.1.48' or 'v0.1.48'",
|
||||||
|
type: "string",
|
||||||
|
})
|
||||||
|
.option("method", {
|
||||||
|
alias: "m",
|
||||||
|
describe: "installation method to use",
|
||||||
|
type: "string",
|
||||||
|
choices: ["curl", "npm", "pnpm", "bun", "brew"],
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handler: async (args: { target?: string; method?: string }) => {
|
||||||
|
UI.empty()
|
||||||
|
UI.println(UI.logo(" "))
|
||||||
|
UI.empty()
|
||||||
|
prompts.intro("Upgrade")
|
||||||
|
const detectedMethod = await Installation.method()
|
||||||
|
const method = (args.method as Installation.Method) ?? detectedMethod
|
||||||
|
if (method === "unknown") {
|
||||||
|
prompts.log.error(
|
||||||
|
`opencode is installed to ${process.execPath} and seems to be managed by a package manager`,
|
||||||
|
)
|
||||||
|
prompts.outro("Done")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
prompts.log.info("Using method: " + method)
|
||||||
|
const target = args.target ?? (await Installation.latest())
|
||||||
|
prompts.log.info(`From ${Installation.VERSION} → ${target}`)
|
||||||
|
const spinner = prompts.spinner()
|
||||||
|
spinner.start("Upgrading...")
|
||||||
|
const err = await Installation.upgrade(method, target).catch((err) => err)
|
||||||
|
if (err) {
|
||||||
|
spinner.stop("Upgrade failed")
|
||||||
|
if (err instanceof Installation.UpgradeFailedError)
|
||||||
|
prompts.log.error(err.data.stderr)
|
||||||
|
else if (err instanceof Error) prompts.log.error(err.message)
|
||||||
|
prompts.outro("Done")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
spinner.stop("Upgrade complete")
|
||||||
|
prompts.outro("Done")
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { Config } from "../config/config"
|
||||||
|
import { MCP } from "../mcp"
|
||||||
|
|
||||||
|
export function FormatError(input: unknown) {
|
||||||
|
if (MCP.Failed.isInstance(input))
|
||||||
|
return `MCP server "${input.data.name}" failed. Note, opencode does not support MCP authentication yet.`
|
||||||
|
if (Config.JsonError.isInstance(input))
|
||||||
|
return `Config file at ${input.data.path} is not valid JSON`
|
||||||
|
if (Config.InvalidError.isInstance(input))
|
||||||
|
return [
|
||||||
|
`Config file at ${input.data.path} is invalid`,
|
||||||
|
...(input.data.issues?.map(
|
||||||
|
(issue) => "↳ " + issue.message + " " + issue.path.join("."),
|
||||||
|
) ?? []),
|
||||||
|
].join("\n")
|
||||||
|
}
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
import { createCli, type TrpcCliMeta } from "trpc-cli"
|
|
||||||
import { initTRPC } from "@trpc/server"
|
|
||||||
import { z } from "zod"
|
|
||||||
import { Server } from "../server/server"
|
|
||||||
import { AuthAnthropic } from "../auth/anthropic"
|
|
||||||
import { UI } from "./ui"
|
|
||||||
import { App } from "../app/app"
|
|
||||||
import { Bus } from "../bus"
|
|
||||||
import { Provider } from "../provider/provider"
|
|
||||||
import { Session } from "../session"
|
|
||||||
import { Share } from "../share/share"
|
|
||||||
import { Message } from "../session/message"
|
|
||||||
import { VERSION } from "./version"
|
|
||||||
import { LSP } from "../lsp"
|
|
||||||
import fs from "fs/promises"
|
|
||||||
import path from "path"
|
|
||||||
|
|
||||||
const t = initTRPC.meta<TrpcCliMeta>().create()
|
|
||||||
|
|
||||||
export const router = t.router({
|
|
||||||
generate: t.procedure
|
|
||||||
.meta({
|
|
||||||
description: "Generate OpenAPI and event specs",
|
|
||||||
})
|
|
||||||
.input(z.object({}))
|
|
||||||
.mutation(async () => {
|
|
||||||
const specs = await Server.openapi()
|
|
||||||
const dir = "gen"
|
|
||||||
await fs.rmdir(dir, { recursive: true }).catch(() => {})
|
|
||||||
await fs.mkdir(dir, { recursive: true })
|
|
||||||
await Bun.write(
|
|
||||||
path.join(dir, "openapi.json"),
|
|
||||||
JSON.stringify(specs, null, 2),
|
|
||||||
)
|
|
||||||
return "Generated OpenAPI specs in gen/ directory"
|
|
||||||
}),
|
|
||||||
|
|
||||||
run: t.procedure
|
|
||||||
.meta({
|
|
||||||
description: "Run OpenCode with a message",
|
|
||||||
})
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
message: z.array(z.string()).default([]).describe("Message to send"),
|
|
||||||
session: z.string().optional().describe("Session ID to continue"),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.mutation(
|
|
||||||
async ({ input }: { input: { message: string[]; session?: string } }) => {
|
|
||||||
const message = input.message.join(" ")
|
|
||||||
await App.provide(
|
|
||||||
{
|
|
||||||
cwd: process.cwd(),
|
|
||||||
version: "0.0.0",
|
|
||||||
},
|
|
||||||
async () => {
|
|
||||||
await Share.init()
|
|
||||||
const session = input.session
|
|
||||||
? await Session.get(input.session)
|
|
||||||
: await Session.create()
|
|
||||||
|
|
||||||
UI.println(UI.Style.TEXT_HIGHLIGHT_BOLD + "◍ OpenCode", VERSION)
|
|
||||||
UI.empty()
|
|
||||||
UI.println(UI.Style.TEXT_NORMAL_BOLD + "> ", message)
|
|
||||||
UI.empty()
|
|
||||||
UI.println(
|
|
||||||
UI.Style.TEXT_INFO_BOLD +
|
|
||||||
"~ https://dev.opencode.ai/s?id=" +
|
|
||||||
session.id.slice(-8),
|
|
||||||
)
|
|
||||||
UI.empty()
|
|
||||||
|
|
||||||
function printEvent(color: string, type: string, title: string) {
|
|
||||||
UI.println(
|
|
||||||
color + `|`,
|
|
||||||
UI.Style.TEXT_NORMAL +
|
|
||||||
UI.Style.TEXT_DIM +
|
|
||||||
` ${type.padEnd(7, " ")}`,
|
|
||||||
"",
|
|
||||||
UI.Style.TEXT_NORMAL + title,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Bus.subscribe(Message.Event.PartUpdated, async (message) => {
|
|
||||||
const part = message.properties.part
|
|
||||||
if (
|
|
||||||
part.type === "tool-invocation" &&
|
|
||||||
part.toolInvocation.state === "result"
|
|
||||||
) {
|
|
||||||
if (part.toolInvocation.toolName === "opencode_todowrite")
|
|
||||||
return
|
|
||||||
|
|
||||||
const args = part.toolInvocation.args as any
|
|
||||||
const tool = part.toolInvocation.toolName
|
|
||||||
|
|
||||||
if (tool === "opencode_edit")
|
|
||||||
printEvent(UI.Style.TEXT_SUCCESS_BOLD, "Edit", args.filePath)
|
|
||||||
if (tool === "opencode_bash")
|
|
||||||
printEvent(
|
|
||||||
UI.Style.TEXT_WARNING_BOLD,
|
|
||||||
"Execute",
|
|
||||||
args.command,
|
|
||||||
)
|
|
||||||
if (tool === "opencode_read")
|
|
||||||
printEvent(UI.Style.TEXT_INFO_BOLD, "Read", args.filePath)
|
|
||||||
if (tool === "opencode_write")
|
|
||||||
printEvent(
|
|
||||||
UI.Style.TEXT_SUCCESS_BOLD,
|
|
||||||
"Create",
|
|
||||||
args.filePath,
|
|
||||||
)
|
|
||||||
if (tool === "opencode_list")
|
|
||||||
printEvent(UI.Style.TEXT_INFO_BOLD, "List", args.path)
|
|
||||||
if (tool === "opencode_glob")
|
|
||||||
printEvent(
|
|
||||||
UI.Style.TEXT_INFO_BOLD,
|
|
||||||
"Glob",
|
|
||||||
args.pattern + (args.path ? " in " + args.path : ""),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (part.type === "text") {
|
|
||||||
if (part.text.includes("\n")) {
|
|
||||||
UI.empty()
|
|
||||||
UI.println(part.text)
|
|
||||||
UI.empty()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
printEvent(UI.Style.TEXT_NORMAL_BOLD, "Text", part.text)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const { providerID, modelID } = await Provider.defaultModel()
|
|
||||||
await Session.chat({
|
|
||||||
sessionID: session.id,
|
|
||||||
providerID,
|
|
||||||
modelID,
|
|
||||||
parts: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: message,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
UI.empty()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
return "Session completed"
|
|
||||||
},
|
|
||||||
),
|
|
||||||
|
|
||||||
scrap: t.procedure
|
|
||||||
.meta({
|
|
||||||
description: "Test command for scraping files",
|
|
||||||
})
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
file: z.string().describe("File to process"),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.mutation(async ({ input }: { input: { file: string } }) => {
|
|
||||||
await App.provide({ cwd: process.cwd(), version: VERSION }, async () => {
|
|
||||||
await LSP.touchFile(input.file, true)
|
|
||||||
await LSP.diagnostics()
|
|
||||||
})
|
|
||||||
return `Processed file: ${input.file}`
|
|
||||||
}),
|
|
||||||
|
|
||||||
login: t.router({
|
|
||||||
anthropic: t.procedure
|
|
||||||
.meta({
|
|
||||||
description: "Login to Anthropic",
|
|
||||||
})
|
|
||||||
.input(z.object({}))
|
|
||||||
.mutation(async () => {
|
|
||||||
const { url, verifier } = await AuthAnthropic.authorize()
|
|
||||||
|
|
||||||
UI.println("Login to Anthropic")
|
|
||||||
UI.println("Open the following URL in your browser:")
|
|
||||||
UI.println(url)
|
|
||||||
UI.println("")
|
|
||||||
|
|
||||||
const code = await UI.input("Paste the authorization code here: ")
|
|
||||||
await AuthAnthropic.exchange(code, verifier)
|
|
||||||
return "Successfully logged in to Anthropic"
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
export function createOpenCodeCli() {
|
|
||||||
return createCli({ router })
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -3,9 +3,9 @@ import { NamedError } from "../util/error"
|
|||||||
|
|
||||||
export namespace UI {
|
export namespace UI {
|
||||||
const LOGO = [
|
const LOGO = [
|
||||||
`█▀▀█ █▀▀█ █▀▀ █▀▀▄ █▀▀ █▀▀█ █▀▀▄ █▀▀`,
|
[`█▀▀█ █▀▀█ █▀▀ █▀▀▄ `, `█▀▀ █▀▀█ █▀▀▄ █▀▀`],
|
||||||
`█░░█ █░░█ █▀▀ █░░█ █░░ █░░█ █░░█ █▀▀`,
|
[`█░░█ █░░█ █▀▀ █░░█ `, `█░░ █░░█ █░░█ █▀▀`],
|
||||||
`▀▀▀▀ █▀▀▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀▀`,
|
[`▀▀▀▀ █▀▀▀ ▀▀▀ ▀ ▀ `, `▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀▀`],
|
||||||
]
|
]
|
||||||
|
|
||||||
export const CancelledError = NamedError.create("UICancelledError", z.void())
|
export const CancelledError = NamedError.create("UICancelledError", z.void())
|
||||||
@@ -48,12 +48,10 @@ export namespace UI {
|
|||||||
const result = []
|
const result = []
|
||||||
for (const row of LOGO) {
|
for (const row of LOGO) {
|
||||||
if (pad) result.push(pad)
|
if (pad) result.push(pad)
|
||||||
for (let i = 0; i < row.length; i++) {
|
result.push(Bun.color("gray", "ansi"))
|
||||||
const color =
|
result.push(row[0])
|
||||||
i > 18 ? Bun.color("white", "ansi") : Bun.color("gray", "ansi")
|
result.push("\x1b[0m")
|
||||||
const char = row[i]
|
result.push(row[1])
|
||||||
result.push(color + char)
|
|
||||||
}
|
|
||||||
result.push("\n")
|
result.push("\n")
|
||||||
}
|
}
|
||||||
return result.join("").trimEnd()
|
return result.join("").trimEnd()
|
||||||
@@ -73,4 +71,8 @@ export namespace UI {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function error(message: string) {
|
||||||
|
println(Style.TEXT_DANGER_BOLD + "Error: " + Style.TEXT_NORMAL + message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
declare global {
|
|
||||||
const OPENCODE_VERSION: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const VERSION =
|
|
||||||
typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "dev"
|
|
||||||
@@ -1,55 +1,159 @@
|
|||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
|
import path from "path"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { App } from "../app/app"
|
import { App } from "../app/app"
|
||||||
import { Filesystem } from "../util/filesystem"
|
import { Filesystem } from "../util/filesystem"
|
||||||
import { ModelsDev } from "../provider/models"
|
import { ModelsDev } from "../provider/models"
|
||||||
|
import { mergeDeep } from "remeda"
|
||||||
|
import { Global } from "../global"
|
||||||
|
import fs from "fs/promises"
|
||||||
|
import { lazy } from "../util/lazy"
|
||||||
|
import { NamedError } from "../util/error"
|
||||||
|
|
||||||
export namespace Config {
|
export namespace Config {
|
||||||
const log = Log.create({ service: "config" })
|
const log = Log.create({ service: "config" })
|
||||||
|
|
||||||
export const state = App.state("config", async (app) => {
|
export const state = App.state("config", async (app) => {
|
||||||
let result: Info = {}
|
let result = await global()
|
||||||
for (const file of ["opencode.jsonc", "opencode.json"]) {
|
for (const file of ["opencode.jsonc", "opencode.json"]) {
|
||||||
const [resolved] = await Filesystem.findUp(
|
const found = await Filesystem.findUp(file, app.path.cwd, app.path.root)
|
||||||
file,
|
for (const resolved of found.toReversed()) {
|
||||||
app.path.cwd,
|
result = mergeDeep(result, await load(resolved))
|
||||||
app.path.root,
|
|
||||||
)
|
|
||||||
if (!resolved) continue
|
|
||||||
try {
|
|
||||||
result = await import(resolved).then((mod) => Info.parse(mod.default))
|
|
||||||
log.info("found", { path: resolved })
|
|
||||||
break
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof z.ZodError) {
|
|
||||||
for (const issue of e.issues) {
|
|
||||||
log.info(issue.message)
|
|
||||||
}
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.info("loaded", result)
|
log.info("loaded", result)
|
||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
|
|
||||||
export const McpLocal = z.object({
|
export const McpLocal = z
|
||||||
type: z.literal("local"),
|
.object({
|
||||||
command: z.string().array(),
|
type: z.literal("local").describe("Type of MCP server connection"),
|
||||||
environment: z.record(z.string(), z.string()).optional(),
|
command: z
|
||||||
})
|
.string()
|
||||||
|
.array()
|
||||||
|
.describe("Command and arguments to run the MCP server"),
|
||||||
|
environment: z
|
||||||
|
.record(z.string(), z.string())
|
||||||
|
.optional()
|
||||||
|
.describe("Environment variables to set when running the MCP server"),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
.openapi({
|
||||||
|
ref: "Config.McpLocal",
|
||||||
|
})
|
||||||
|
|
||||||
export const McpRemote = z.object({
|
export const McpRemote = z
|
||||||
type: z.literal("remote"),
|
.object({
|
||||||
url: z.string(),
|
type: z.literal("remote").describe("Type of MCP server connection"),
|
||||||
})
|
url: z.string().describe("URL of the remote MCP server"),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
.openapi({
|
||||||
|
ref: "Config.McpRemote",
|
||||||
|
})
|
||||||
|
|
||||||
export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote])
|
export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote])
|
||||||
export type Mcp = z.infer<typeof Mcp>
|
export type Mcp = z.infer<typeof Mcp>
|
||||||
|
|
||||||
|
export const Keybinds = z
|
||||||
|
.object({
|
||||||
|
leader: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Leader key for keybind combinations"),
|
||||||
|
help: z.string().optional().describe("Show help dialog"),
|
||||||
|
editor_open: z.string().optional().describe("Open external editor"),
|
||||||
|
session_new: z.string().optional().describe("Create a new session"),
|
||||||
|
session_list: z.string().optional().describe("List all sessions"),
|
||||||
|
session_share: z.string().optional().describe("Share current session"),
|
||||||
|
session_interrupt: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Interrupt current session"),
|
||||||
|
session_compact: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Toggle compact mode for session"),
|
||||||
|
tool_details: z.string().optional().describe("Show tool details"),
|
||||||
|
model_list: z.string().optional().describe("List available models"),
|
||||||
|
theme_list: z.string().optional().describe("List available themes"),
|
||||||
|
project_init: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Initialize project configuration"),
|
||||||
|
input_clear: z.string().optional().describe("Clear input field"),
|
||||||
|
input_paste: z.string().optional().describe("Paste from clipboard"),
|
||||||
|
input_submit: z.string().optional().describe("Submit input"),
|
||||||
|
input_newline: z.string().optional().describe("Insert newline in input"),
|
||||||
|
history_previous: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Navigate to previous history item"),
|
||||||
|
history_next: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Navigate to next history item"),
|
||||||
|
messages_page_up: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Scroll messages up by one page"),
|
||||||
|
messages_page_down: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Scroll messages down by one page"),
|
||||||
|
messages_half_page_up: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Scroll messages up by half page"),
|
||||||
|
messages_half_page_down: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Scroll messages down by half page"),
|
||||||
|
messages_previous: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Navigate to previous message"),
|
||||||
|
messages_next: z.string().optional().describe("Navigate to next message"),
|
||||||
|
messages_first: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Navigate to first message"),
|
||||||
|
messages_last: z.string().optional().describe("Navigate to last message"),
|
||||||
|
app_exit: z.string().optional().describe("Exit the application"),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
.openapi({
|
||||||
|
ref: "Config.Keybinds",
|
||||||
|
})
|
||||||
export const Info = z
|
export const Info = z
|
||||||
.object({
|
.object({
|
||||||
|
$schema: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("JSON schema reference for configuration validation"),
|
||||||
|
theme: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Theme name to use for the interface"),
|
||||||
|
keybinds: Keybinds.optional().describe("Custom keybind configurations"),
|
||||||
|
autoshare: z
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.describe("Share newly created sessions automatically"),
|
||||||
|
autoupdate: z
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.describe("Automatically update to the latest version"),
|
||||||
|
disabled_providers: z
|
||||||
|
.array(z.string())
|
||||||
|
.optional()
|
||||||
|
.describe("Disable providers that are loaded automatically"),
|
||||||
|
model: z
|
||||||
|
.string()
|
||||||
|
.describe(
|
||||||
|
"Model to use in the format of provider/model, eg anthropic/claude-2",
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
provider: z
|
provider: z
|
||||||
.record(
|
.record(
|
||||||
ModelsDev.Provider.partial().extend({
|
ModelsDev.Provider.partial().extend({
|
||||||
@@ -57,18 +161,72 @@ export namespace Config {
|
|||||||
options: z.record(z.any()).optional(),
|
options: z.record(z.any()).optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional()
|
||||||
tool: z
|
.describe("Custom provider configurations and model overrides"),
|
||||||
.object({
|
mcp: z
|
||||||
provider: z.record(z.string(), z.string().array()).optional(),
|
.record(z.string(), Mcp)
|
||||||
})
|
.optional()
|
||||||
.optional(),
|
.describe("MCP (Model Context Protocol) server configurations"),
|
||||||
mcp: z.record(z.string(), Mcp).optional(),
|
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
|
.openapi({
|
||||||
|
ref: "Config.Info",
|
||||||
|
})
|
||||||
|
|
||||||
export type Info = z.output<typeof Info>
|
export type Info = z.output<typeof Info>
|
||||||
|
|
||||||
|
export const global = lazy(async () => {
|
||||||
|
let result = await load(path.join(Global.Path.config, "config.json"))
|
||||||
|
|
||||||
|
await import(path.join(Global.Path.config, "config"), {
|
||||||
|
with: {
|
||||||
|
type: "toml",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(async (mod) => {
|
||||||
|
const { provider, model, ...rest } = mod.default
|
||||||
|
if (provider && model) result.model = `${provider}/${model}`
|
||||||
|
result["$schema"] = "https://opencode.ai/config.json"
|
||||||
|
result = mergeDeep(result, rest)
|
||||||
|
await Bun.write(
|
||||||
|
path.join(Global.Path.config, "config.json"),
|
||||||
|
JSON.stringify(result, null, 2),
|
||||||
|
)
|
||||||
|
await fs.unlink(path.join(Global.Path.config, "config"))
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
async function load(path: string) {
|
||||||
|
const data = await Bun.file(path)
|
||||||
|
.json()
|
||||||
|
.catch((err) => {
|
||||||
|
if (err.code === "ENOENT") return {}
|
||||||
|
throw new JsonError({ path }, { cause: err })
|
||||||
|
})
|
||||||
|
|
||||||
|
const parsed = Info.safeParse(data)
|
||||||
|
if (parsed.success) return parsed.data
|
||||||
|
throw new InvalidError({ path, issues: parsed.error.issues })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const JsonError = NamedError.create(
|
||||||
|
"ConfigJsonError",
|
||||||
|
z.object({
|
||||||
|
path: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const InvalidError = NamedError.create(
|
||||||
|
"ConfigInvalidError",
|
||||||
|
z.object({
|
||||||
|
path: z.string(),
|
||||||
|
issues: z.custom<z.ZodIssue[]>().optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
export function get() {
|
export function get() {
|
||||||
return state()
|
return state()
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-12
@@ -1,4 +1,3 @@
|
|||||||
import { App } from "../app/app"
|
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
@@ -6,6 +5,7 @@ import { z } from "zod"
|
|||||||
import { NamedError } from "../util/error"
|
import { NamedError } from "../util/error"
|
||||||
import { lazy } from "../util/lazy"
|
import { lazy } from "../util/lazy"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
|
import { $ } from "bun"
|
||||||
|
|
||||||
export namespace Fzf {
|
export namespace Fzf {
|
||||||
const log = Log.create({ service: "fzf" })
|
const log = Log.create({ service: "fzf" })
|
||||||
@@ -116,19 +116,23 @@ export namespace Fzf {
|
|||||||
return filepath
|
return filepath
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function search(cwd: string, query: string) {
|
export async function search(input: {
|
||||||
const process = Bun.spawn({
|
cwd: string
|
||||||
cwd,
|
query: string
|
||||||
stdin: "inherit",
|
limit?: number
|
||||||
stdout: "pipe",
|
}) {
|
||||||
stderr: "pipe",
|
const results = await $`${await filepath()} --filter=${input.query}`
|
||||||
cmd: [await filepath(), "--filter", query],
|
.quiet()
|
||||||
})
|
.throws(false)
|
||||||
await process.exited
|
.cwd(input.cwd)
|
||||||
const stdout = await Bun.readableStreamToText(process.stdout)
|
.text()
|
||||||
return stdout
|
const split = results
|
||||||
.trim()
|
.trim()
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.filter((line) => line.length > 0)
|
.filter((line) => line.length > 0)
|
||||||
|
log.info("results", {
|
||||||
|
count: split.length,
|
||||||
|
})
|
||||||
|
return split
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-1
@@ -1,10 +1,11 @@
|
|||||||
import { App } from "../app/app"
|
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { NamedError } from "../util/error"
|
import { NamedError } from "../util/error"
|
||||||
import { lazy } from "../util/lazy"
|
import { lazy } from "../util/lazy"
|
||||||
|
import { $ } from "bun"
|
||||||
|
import { Fzf } from "./fzf"
|
||||||
|
|
||||||
export namespace Ripgrep {
|
export namespace Ripgrep {
|
||||||
const PLATFORM = {
|
const PLATFORM = {
|
||||||
@@ -111,4 +112,18 @@ export namespace Ripgrep {
|
|||||||
const { filepath } = await state()
|
const { filepath } = await state()
|
||||||
return filepath
|
return filepath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function files(input: {
|
||||||
|
cwd: string
|
||||||
|
query?: string
|
||||||
|
limit?: number
|
||||||
|
}) {
|
||||||
|
const commands = [`${await filepath()} --files --hidden --glob='!.git/*'`]
|
||||||
|
if (input.query)
|
||||||
|
commands.push(`${await Fzf.filepath()} --filter=${input.query}`)
|
||||||
|
if (input.limit) commands.push(`head -n ${input.limit}`)
|
||||||
|
const joined = commands.join(" | ")
|
||||||
|
const result = await $`${{ raw: joined }}`.cwd(input.cwd).text()
|
||||||
|
return result.split("\n").filter(Boolean)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
export namespace File {
|
|
||||||
const glob = new Bun.Glob("**/*")
|
|
||||||
export async function search(path: string, query: string) {
|
|
||||||
for await (const entry of glob.scan({
|
|
||||||
cwd: path,
|
|
||||||
onlyFiles: true,
|
|
||||||
})) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export namespace Flag {
|
||||||
|
export const OPENCODE_AUTO_SHARE = truthy("OPENCODE_AUTO_SHARE")
|
||||||
|
|
||||||
|
function truthy(key: string) {
|
||||||
|
const value = process.env[key]?.toLowerCase()
|
||||||
|
return value === "true" || value === "1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import { xdgData, xdgCache, xdgConfig } from "xdg-basedir"
|
import { xdgData, xdgCache, xdgConfig, xdgState } from "xdg-basedir"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
|
||||||
const app = "opencode"
|
const app = "opencode"
|
||||||
@@ -7,18 +7,23 @@ const app = "opencode"
|
|||||||
const data = path.join(xdgData!, app)
|
const data = path.join(xdgData!, app)
|
||||||
const cache = path.join(xdgCache!, app)
|
const cache = path.join(xdgCache!, app)
|
||||||
const config = path.join(xdgConfig!, app)
|
const config = path.join(xdgConfig!, app)
|
||||||
|
const state = path.join(xdgState!, app)
|
||||||
await Promise.all([
|
|
||||||
fs.mkdir(data, { recursive: true }),
|
|
||||||
fs.mkdir(config, { recursive: true }),
|
|
||||||
fs.mkdir(cache, { recursive: true }),
|
|
||||||
])
|
|
||||||
|
|
||||||
export namespace Global {
|
export namespace Global {
|
||||||
export const Path = {
|
export const Path = {
|
||||||
data,
|
data,
|
||||||
bin: path.join(data, "bin"),
|
bin: path.join(data, "bin"),
|
||||||
|
providers: path.join(config, "providers"),
|
||||||
cache,
|
cache,
|
||||||
config,
|
config,
|
||||||
|
state,
|
||||||
} as const
|
} as const
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
fs.mkdir(Global.Path.data, { recursive: true }),
|
||||||
|
fs.mkdir(Global.Path.config, { recursive: true }),
|
||||||
|
fs.mkdir(Global.Path.cache, { recursive: true }),
|
||||||
|
fs.mkdir(Global.Path.providers, { recursive: true }),
|
||||||
|
fs.mkdir(Global.Path.state, { recursive: true }),
|
||||||
|
])
|
||||||
|
|||||||
@@ -41,6 +41,17 @@ export namespace Identifier {
|
|||||||
return given
|
return given
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function randomBase62(length: number): string {
|
||||||
|
const chars =
|
||||||
|
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||||
|
let result = ""
|
||||||
|
const bytes = randomBytes(length)
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
result += chars[bytes[i] % 62]
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
function generateNewID(
|
function generateNewID(
|
||||||
prefix: keyof typeof prefixes,
|
prefix: keyof typeof prefixes,
|
||||||
descending: boolean,
|
descending: boolean,
|
||||||
@@ -62,14 +73,11 @@ export namespace Identifier {
|
|||||||
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)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
prefixes[prefix] +
|
prefixes[prefix] +
|
||||||
"_" +
|
"_" +
|
||||||
timeBytes.toString("hex") +
|
timeBytes.toString("hex") +
|
||||||
random.toString("hex")
|
randomBase62(LENGTH - 12)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,40 +3,47 @@ import { App } from "./app/app"
|
|||||||
import { Server } from "./server/server"
|
import { Server } from "./server/server"
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
|
||||||
import { Share } from "./share/share"
|
import { Share } from "./share/share"
|
||||||
|
|
||||||
import { Global } from "./global"
|
import { Global } from "./global"
|
||||||
|
|
||||||
import yargs from "yargs"
|
import yargs from "yargs"
|
||||||
import { hideBin } from "yargs/helpers"
|
import { hideBin } from "yargs/helpers"
|
||||||
import { RunCommand } from "./cli/cmd/run"
|
import { RunCommand } from "./cli/cmd/run"
|
||||||
import { GenerateCommand } from "./cli/cmd/generate"
|
import { GenerateCommand } from "./cli/cmd/generate"
|
||||||
import { VERSION } from "./cli/version"
|
|
||||||
import { ScrapCommand } from "./cli/cmd/scrap"
|
import { ScrapCommand } from "./cli/cmd/scrap"
|
||||||
import { Log } from "./util/log"
|
import { Log } from "./util/log"
|
||||||
import { AuthCommand, AuthLoginCommand } from "./cli/cmd/auth"
|
import { AuthCommand, AuthLoginCommand } from "./cli/cmd/auth"
|
||||||
|
import { UpgradeCommand } from "./cli/cmd/upgrade"
|
||||||
import { Provider } from "./provider/provider"
|
import { Provider } from "./provider/provider"
|
||||||
import { UI } from "./cli/ui"
|
import { UI } from "./cli/ui"
|
||||||
|
import { Installation } from "./installation"
|
||||||
|
import { Bus } from "./bus"
|
||||||
|
import { Config } from "./config/config"
|
||||||
|
import { NamedError } from "./util/error"
|
||||||
|
import { FormatError } from "./cli/error"
|
||||||
|
|
||||||
|
const cancel = new AbortController()
|
||||||
|
|
||||||
const cli = yargs(hideBin(process.argv))
|
const cli = yargs(hideBin(process.argv))
|
||||||
.scriptName("opencode")
|
.scriptName("opencode")
|
||||||
.version(VERSION)
|
.help("help", "show help")
|
||||||
|
.alias("help", "h")
|
||||||
|
.version("version", "show version number", Installation.VERSION)
|
||||||
|
.alias("version", "v")
|
||||||
.option("print-logs", {
|
.option("print-logs", {
|
||||||
describe: "Print logs to stderr",
|
describe: "print logs to stderr",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
})
|
})
|
||||||
.middleware(async () => {
|
.middleware(async () => {
|
||||||
await Log.init({ print: process.argv.includes("--print-logs") })
|
await Log.init({ print: process.argv.includes("--print-logs") })
|
||||||
Log.Default.info("opencode", {
|
Log.Default.info("opencode", {
|
||||||
version: VERSION,
|
version: Installation.VERSION,
|
||||||
args: process.argv.slice(2),
|
args: process.argv.slice(2),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.usage("\n" + UI.logo())
|
.usage("\n" + UI.logo())
|
||||||
.command({
|
.command({
|
||||||
command: "$0 [project]",
|
command: "$0 [project]",
|
||||||
describe: "Start OpenCode TUI",
|
describe: "start opencode tui",
|
||||||
builder: (yargs) =>
|
builder: (yargs) =>
|
||||||
yargs.positional("project", {
|
yargs.positional("project", {
|
||||||
type: "string",
|
type: "string",
|
||||||
@@ -46,51 +53,66 @@ const cli = yargs(hideBin(process.argv))
|
|||||||
while (true) {
|
while (true) {
|
||||||
const cwd = args.project ? path.resolve(args.project) : process.cwd()
|
const cwd = args.project ? path.resolve(args.project) : process.cwd()
|
||||||
process.chdir(cwd)
|
process.chdir(cwd)
|
||||||
const result = await App.provide(
|
const result = await App.provide({ cwd }, async (app) => {
|
||||||
{ cwd, version: VERSION },
|
const providers = await Provider.list()
|
||||||
async () => {
|
if (Object.keys(providers).length === 0) {
|
||||||
const providers = await Provider.list()
|
return "needs_provider"
|
||||||
if (Object.keys(providers).length === 0) {
|
}
|
||||||
return "needs_provider"
|
|
||||||
|
await Share.init()
|
||||||
|
const server = Server.listen()
|
||||||
|
|
||||||
|
let cmd = ["go", "run", "./main.go"]
|
||||||
|
let cwd = new URL("../../tui/cmd/opencode", import.meta.url).pathname
|
||||||
|
if (Bun.embeddedFiles.length > 0) {
|
||||||
|
const blob = Bun.embeddedFiles[0] as File
|
||||||
|
const binary = path.join(Global.Path.cache, "tui", blob.name)
|
||||||
|
const file = Bun.file(binary)
|
||||||
|
if (!(await file.exists())) {
|
||||||
|
await Bun.write(file, blob, { mode: 0o755 })
|
||||||
|
await fs.chmod(binary, 0o755)
|
||||||
}
|
}
|
||||||
|
cwd = process.cwd()
|
||||||
|
cmd = [binary]
|
||||||
|
}
|
||||||
|
const proc = Bun.spawn({
|
||||||
|
cmd: [...cmd, ...process.argv.slice(2)],
|
||||||
|
signal: cancel.signal,
|
||||||
|
cwd,
|
||||||
|
stdout: "inherit",
|
||||||
|
stderr: "inherit",
|
||||||
|
stdin: "inherit",
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
OPENCODE_SERVER: server.url.toString(),
|
||||||
|
OPENCODE_APP_INFO: JSON.stringify(app),
|
||||||
|
},
|
||||||
|
onExit: () => {
|
||||||
|
server.stop()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
await Share.init()
|
; (async () => {
|
||||||
const server = Server.listen()
|
if (Installation.VERSION === "dev") return
|
||||||
|
if (Installation.isSnapshot()) return
|
||||||
|
const config = await Config.global()
|
||||||
|
if (config.autoupdate === false) return
|
||||||
|
const latest = await Installation.latest()
|
||||||
|
if (Installation.VERSION === latest) return
|
||||||
|
const method = await Installation.method()
|
||||||
|
if (method === "unknown") return
|
||||||
|
await Installation.upgrade(method, latest)
|
||||||
|
.then(() => {
|
||||||
|
Bus.publish(Installation.Event.Updated, { version: latest })
|
||||||
|
})
|
||||||
|
.catch(() => { })
|
||||||
|
})()
|
||||||
|
|
||||||
let cmd = ["go", "run", "./main.go"]
|
await proc.exited
|
||||||
let cwd = new URL("../../tui/cmd/opencode", import.meta.url)
|
server.stop()
|
||||||
.pathname
|
|
||||||
if (Bun.embeddedFiles.length > 0) {
|
|
||||||
const blob = Bun.embeddedFiles[0] as File
|
|
||||||
const binary = path.join(Global.Path.cache, "tui", blob.name)
|
|
||||||
const file = Bun.file(binary)
|
|
||||||
if (!(await file.exists())) {
|
|
||||||
await Bun.write(file, blob, { mode: 0o755 })
|
|
||||||
await fs.chmod(binary, 0o755)
|
|
||||||
}
|
|
||||||
cwd = process.cwd()
|
|
||||||
cmd = [binary]
|
|
||||||
}
|
|
||||||
const proc = Bun.spawn({
|
|
||||||
cmd,
|
|
||||||
cwd,
|
|
||||||
stdout: "inherit",
|
|
||||||
stderr: "inherit",
|
|
||||||
stdin: "inherit",
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
OPENCODE_SERVER: server.url.toString(),
|
|
||||||
},
|
|
||||||
onExit: () => {
|
|
||||||
server.stop()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
await proc.exited
|
|
||||||
await server.stop()
|
|
||||||
|
|
||||||
return "done"
|
return "done"
|
||||||
},
|
})
|
||||||
)
|
|
||||||
if (result === "done") break
|
if (result === "done") break
|
||||||
if (result === "needs_provider") {
|
if (result === "needs_provider") {
|
||||||
UI.empty()
|
UI.empty()
|
||||||
@@ -105,21 +127,41 @@ const cli = yargs(hideBin(process.argv))
|
|||||||
.command(GenerateCommand)
|
.command(GenerateCommand)
|
||||||
.command(ScrapCommand)
|
.command(ScrapCommand)
|
||||||
.command(AuthCommand)
|
.command(AuthCommand)
|
||||||
.fail((msg, err) => {
|
.command(UpgradeCommand)
|
||||||
|
.fail((msg) => {
|
||||||
if (
|
if (
|
||||||
msg.startsWith("Unknown argument") ||
|
msg.startsWith("Unknown argument") ||
|
||||||
msg.startsWith("Not enough non-option arguments")
|
msg.startsWith("Not enough non-option arguments")
|
||||||
) {
|
) {
|
||||||
cli.showHelp("log")
|
cli.showHelp("log")
|
||||||
}
|
}
|
||||||
Log.Default.error(msg, {
|
|
||||||
err,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await cli.parse()
|
await cli.parse()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.Default.error(e)
|
const data: Record<string, any> = {}
|
||||||
|
if (e instanceof NamedError) {
|
||||||
|
const obj = e.toObject()
|
||||||
|
Object.assign(data, {
|
||||||
|
...obj.data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (e instanceof Error) {
|
||||||
|
Object.assign(data, {
|
||||||
|
name: e.name,
|
||||||
|
message: e.message,
|
||||||
|
cause: e.cause?.toString(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Log.Default.error("fatal", data)
|
||||||
|
const formatted = FormatError(e)
|
||||||
|
if (formatted) UI.error(formatted)
|
||||||
|
if (!formatted)
|
||||||
|
UI.error(
|
||||||
|
"Unexpected error, check log file at " + Log.file() + " for more details",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cancel.abort()
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import path from "path"
|
||||||
|
import { $ } from "bun"
|
||||||
|
import { z } from "zod"
|
||||||
|
import { NamedError } from "../util/error"
|
||||||
|
import { Bus } from "../bus"
|
||||||
|
import { Log } from "../util/log"
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
const OPENCODE_VERSION: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace Installation {
|
||||||
|
const log = Log.create({ service: "installation" })
|
||||||
|
|
||||||
|
export type Method = Awaited<ReturnType<typeof method>>
|
||||||
|
|
||||||
|
export const Event = {
|
||||||
|
Updated: Bus.event(
|
||||||
|
"installation.updated",
|
||||||
|
z.object({
|
||||||
|
version: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Info = z
|
||||||
|
.object({
|
||||||
|
version: z.string(),
|
||||||
|
latest: z.string(),
|
||||||
|
})
|
||||||
|
.openapi({
|
||||||
|
ref: "InstallationInfo",
|
||||||
|
})
|
||||||
|
export type Info = z.infer<typeof Info>
|
||||||
|
|
||||||
|
export async function info() {
|
||||||
|
return {
|
||||||
|
version: VERSION,
|
||||||
|
latest: await latest(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSnapshot() {
|
||||||
|
return VERSION.startsWith("0.0.0")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDev() {
|
||||||
|
return VERSION === "dev"
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function method() {
|
||||||
|
if (process.execPath.includes(path.join(".opencode", "bin"))) return "curl"
|
||||||
|
const exec = process.execPath.toLowerCase()
|
||||||
|
|
||||||
|
const checks = [
|
||||||
|
{
|
||||||
|
name: "npm" as const,
|
||||||
|
command: () => $`npm list -g --depth=0`.throws(false).text(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "yarn" as const,
|
||||||
|
command: () => $`yarn global list`.throws(false).text(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pnpm" as const,
|
||||||
|
command: () => $`pnpm list -g --depth=0`.throws(false).text(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bun" as const,
|
||||||
|
command: () => $`bun pm ls -g`.throws(false).text(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "brew" as const,
|
||||||
|
command: () => $`brew list --formula opencode-ai`.throws(false).text(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
checks.sort((a, b) => {
|
||||||
|
const aMatches = exec.includes(a.name)
|
||||||
|
const bMatches = exec.includes(b.name)
|
||||||
|
if (aMatches && !bMatches) return -1
|
||||||
|
if (!aMatches && bMatches) return 1
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const check of checks) {
|
||||||
|
const output = await check.command()
|
||||||
|
if (output.includes("opencode-ai")) {
|
||||||
|
return check.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UpgradeFailedError = NamedError.create(
|
||||||
|
"UpgradeFailedError",
|
||||||
|
z.object({
|
||||||
|
stderr: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export async function upgrade(method: Method, target: string) {
|
||||||
|
const cmd = (() => {
|
||||||
|
switch (method) {
|
||||||
|
case "curl":
|
||||||
|
return $`curl -fsSL https://opencode.ai/install | bash`.env({
|
||||||
|
...process.env,
|
||||||
|
VERSION: target,
|
||||||
|
})
|
||||||
|
case "npm":
|
||||||
|
return $`npm install -g opencode-ai@${target}`
|
||||||
|
case "pnpm":
|
||||||
|
return $`pnpm install -g opencode-ai@${target}`
|
||||||
|
case "bun":
|
||||||
|
return $`bun install -g opencode-ai@${target}`
|
||||||
|
case "brew":
|
||||||
|
return $`brew install sst/tap/opencode`
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown method: ${method}`)
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
const result = await cmd.quiet().throws(false)
|
||||||
|
log.info("upgraded", {
|
||||||
|
method,
|
||||||
|
target,
|
||||||
|
stdout: result.stdout.toString(),
|
||||||
|
stderr: result.stderr.toString(),
|
||||||
|
})
|
||||||
|
if (result.exitCode !== 0)
|
||||||
|
throw new UpgradeFailedError({
|
||||||
|
stderr: result.stderr.toString("utf8"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VERSION =
|
||||||
|
typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "dev"
|
||||||
|
|
||||||
|
export async function latest() {
|
||||||
|
return fetch("https://api.github.com/repos/sst/opencode/releases/latest")
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => data.tag_name.slice(1) as string)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import { LANGUAGE_EXTENSIONS } from "./language"
|
|||||||
import { Bus } from "../bus"
|
import { Bus } from "../bus"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import type { LSPServer } from "./server"
|
import type { LSPServer } from "./server"
|
||||||
|
import { NamedError } from "../util/error"
|
||||||
|
|
||||||
export namespace LSPClient {
|
export namespace LSPClient {
|
||||||
const log = Log.create({ service: "lsp.client" })
|
const log = Log.create({ service: "lsp.client" })
|
||||||
@@ -19,6 +20,13 @@ export namespace LSPClient {
|
|||||||
|
|
||||||
export type Diagnostic = VSCodeDiagnostic
|
export type Diagnostic = VSCodeDiagnostic
|
||||||
|
|
||||||
|
export const InitializeError = NamedError.create(
|
||||||
|
"LSPInitializeError",
|
||||||
|
z.object({
|
||||||
|
serverID: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
export const Event = {
|
export const Event = {
|
||||||
Diagnostics: Bus.event(
|
Diagnostics: Bus.event(
|
||||||
"lsp.client.diagnostics",
|
"lsp.client.diagnostics",
|
||||||
@@ -52,32 +60,40 @@ export namespace LSPClient {
|
|||||||
})
|
})
|
||||||
connection.listen()
|
connection.listen()
|
||||||
|
|
||||||
await connection.sendRequest("initialize", {
|
log.info("sending initialize", { id: serverID })
|
||||||
processId: server.process.pid,
|
await Promise.race([
|
||||||
workspaceFolders: [
|
connection.sendRequest("initialize", {
|
||||||
{
|
processId: server.process.pid,
|
||||||
name: "workspace",
|
workspaceFolders: [
|
||||||
uri: "file://" + app.path.cwd,
|
{
|
||||||
},
|
name: "workspace",
|
||||||
],
|
uri: "file://" + app.path.cwd,
|
||||||
initializationOptions: {
|
|
||||||
...server.initialization,
|
|
||||||
},
|
|
||||||
capabilities: {
|
|
||||||
workspace: {
|
|
||||||
configuration: true,
|
|
||||||
},
|
|
||||||
textDocument: {
|
|
||||||
synchronization: {
|
|
||||||
didOpen: true,
|
|
||||||
didChange: true,
|
|
||||||
},
|
},
|
||||||
publishDiagnostics: {
|
],
|
||||||
versionSupport: true,
|
initializationOptions: {
|
||||||
|
...server.initialization,
|
||||||
|
},
|
||||||
|
capabilities: {
|
||||||
|
workspace: {
|
||||||
|
configuration: true,
|
||||||
|
},
|
||||||
|
textDocument: {
|
||||||
|
synchronization: {
|
||||||
|
didOpen: true,
|
||||||
|
didChange: true,
|
||||||
|
},
|
||||||
|
publishDiagnostics: {
|
||||||
|
versionSupport: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
})
|
new Promise((_, reject) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
reject(new InitializeError({ serverID }))
|
||||||
|
}, 5_000)
|
||||||
|
}),
|
||||||
|
])
|
||||||
await connection.sendNotification("initialized", {})
|
await connection.sendNotification("initialized", {})
|
||||||
log.info("initialized")
|
log.info("initialized")
|
||||||
|
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ export namespace LSP {
|
|||||||
async () => {
|
async () => {
|
||||||
log.info("initializing")
|
log.info("initializing")
|
||||||
const clients = new Map<string, LSPClient.Info>()
|
const clients = new Map<string, LSPClient.Info>()
|
||||||
|
const skip = new Set<string>()
|
||||||
return {
|
return {
|
||||||
clients,
|
clients,
|
||||||
|
skip,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async (state) => {
|
async (state) => {
|
||||||
@@ -31,11 +32,19 @@ export namespace LSP {
|
|||||||
x.extensions.includes(extension),
|
x.extensions.includes(extension),
|
||||||
)
|
)
|
||||||
for (const match of matches) {
|
for (const match of matches) {
|
||||||
|
if (s.skip.has(match.id)) continue
|
||||||
const existing = s.clients.get(match.id)
|
const existing = s.clients.get(match.id)
|
||||||
if (existing) continue
|
if (existing) continue
|
||||||
const handle = await match.spawn(App.info())
|
const handle = await match.spawn(App.info())
|
||||||
if (!handle) continue
|
if (!handle) {
|
||||||
const client = await LSPClient.create(match.id, handle)
|
s.skip.add(match.id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const client = await LSPClient.create(match.id, handle).catch(() => {})
|
||||||
|
if (!client) {
|
||||||
|
s.skip.add(match.id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
s.clients.set(match.id, client)
|
s.clients.set(match.id, client)
|
||||||
}
|
}
|
||||||
if (waitForDiagnostics) {
|
if (waitForDiagnostics) {
|
||||||
|
|||||||
@@ -70,6 +70,9 @@ export namespace LSPServer {
|
|||||||
const proc = Bun.spawn({
|
const proc = Bun.spawn({
|
||||||
cmd: ["go", "install", "golang.org/x/tools/gopls@latest"],
|
cmd: ["go", "install", "golang.org/x/tools/gopls@latest"],
|
||||||
env: { ...process.env, GOBIN: Global.Path.bin },
|
env: { ...process.env, GOBIN: Global.Path.bin },
|
||||||
|
stdout: "pipe",
|
||||||
|
stderr: "pipe",
|
||||||
|
stdin: "pipe",
|
||||||
})
|
})
|
||||||
const exit = await proc.exited
|
const exit = await proc.exited
|
||||||
if (exit !== 0) {
|
if (exit !== 0) {
|
||||||
|
|||||||
@@ -2,8 +2,22 @@ import { experimental_createMCPClient, type Tool } from "ai"
|
|||||||
import { Experimental_StdioMCPTransport } from "ai/mcp-stdio"
|
import { Experimental_StdioMCPTransport } from "ai/mcp-stdio"
|
||||||
import { App } from "../app/app"
|
import { App } from "../app/app"
|
||||||
import { Config } from "../config/config"
|
import { Config } from "../config/config"
|
||||||
|
import { Log } from "../util/log"
|
||||||
|
import { NamedError } from "../util/error"
|
||||||
|
import { z } from "zod"
|
||||||
|
import { Session } from "../session"
|
||||||
|
import { Bus } from "../bus"
|
||||||
|
|
||||||
export namespace MCP {
|
export namespace MCP {
|
||||||
|
const log = Log.create({ service: "mcp" })
|
||||||
|
|
||||||
|
export const Failed = NamedError.create(
|
||||||
|
"MCPFailed",
|
||||||
|
z.object({
|
||||||
|
name: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
const state = App.state(
|
const state = App.state(
|
||||||
"mcp",
|
"mcp",
|
||||||
async () => {
|
async () => {
|
||||||
@@ -12,27 +26,56 @@ export namespace MCP {
|
|||||||
[name: string]: Awaited<ReturnType<typeof experimental_createMCPClient>>
|
[name: string]: Awaited<ReturnType<typeof experimental_createMCPClient>>
|
||||||
} = {}
|
} = {}
|
||||||
for (const [key, mcp] of Object.entries(cfg.mcp ?? {})) {
|
for (const [key, mcp] of Object.entries(cfg.mcp ?? {})) {
|
||||||
|
log.info("found", { key, type: mcp.type })
|
||||||
if (mcp.type === "remote") {
|
if (mcp.type === "remote") {
|
||||||
clients[key] = await experimental_createMCPClient({
|
const client = await experimental_createMCPClient({
|
||||||
name: key,
|
name: key,
|
||||||
transport: {
|
transport: {
|
||||||
type: "sse",
|
type: "sse",
|
||||||
url: mcp.url,
|
url: mcp.url,
|
||||||
},
|
},
|
||||||
})
|
}).catch(() => {})
|
||||||
|
if (!client) {
|
||||||
|
Bus.publish(Session.Event.Error, {
|
||||||
|
error: {
|
||||||
|
name: "UnknownError",
|
||||||
|
data: {
|
||||||
|
message: `MCP server ${key} failed to start`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
clients[key] = client
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mcp.type === "local") {
|
if (mcp.type === "local") {
|
||||||
const [cmd, ...args] = mcp.command
|
const [cmd, ...args] = mcp.command
|
||||||
clients[key] = await experimental_createMCPClient({
|
const client = await experimental_createMCPClient({
|
||||||
name: key,
|
name: key,
|
||||||
transport: new Experimental_StdioMCPTransport({
|
transport: new Experimental_StdioMCPTransport({
|
||||||
stderr: "ignore",
|
stderr: "ignore",
|
||||||
command: cmd,
|
command: cmd,
|
||||||
args,
|
args,
|
||||||
env: mcp.environment,
|
env: {
|
||||||
|
...process.env,
|
||||||
|
...(cmd === "opencode" ? { BUN_BE_BUN: "1" } : {}),
|
||||||
|
...mcp.environment,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
})
|
}).catch(() => {})
|
||||||
|
if (!client) {
|
||||||
|
Bus.publish(Session.Event.Error, {
|
||||||
|
error: {
|
||||||
|
name: "UnknownError",
|
||||||
|
data: {
|
||||||
|
message: `MCP server ${key} failed to start`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
clients[key] = client
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ export namespace Permission {
|
|||||||
title: Info["title"]
|
title: Info["title"]
|
||||||
metadata: Info["metadata"]
|
metadata: Info["metadata"]
|
||||||
}) {
|
}) {
|
||||||
|
return
|
||||||
const { pending, approved } = state()
|
const { pending, approved } = state()
|
||||||
log.info("asking", {
|
log.info("asking", {
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
|
|||||||
@@ -13,17 +13,19 @@ export namespace ModelsDev {
|
|||||||
attachment: z.boolean(),
|
attachment: z.boolean(),
|
||||||
reasoning: z.boolean(),
|
reasoning: z.boolean(),
|
||||||
temperature: z.boolean(),
|
temperature: z.boolean(),
|
||||||
|
tool_call: z.boolean(),
|
||||||
cost: z.object({
|
cost: z.object({
|
||||||
input: z.number(),
|
input: z.number(),
|
||||||
output: z.number(),
|
output: z.number(),
|
||||||
inputCached: z.number(),
|
cache_read: z.number().optional(),
|
||||||
outputCached: z.number(),
|
cache_write: z.number().optional(),
|
||||||
}),
|
}),
|
||||||
limit: z.object({
|
limit: z.object({
|
||||||
context: z.number(),
|
context: z.number(),
|
||||||
output: z.number(),
|
output: z.number(),
|
||||||
}),
|
}),
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
|
options: z.record(z.any()),
|
||||||
})
|
})
|
||||||
.openapi({
|
.openapi({
|
||||||
ref: "Model.Info",
|
ref: "Model.Info",
|
||||||
@@ -32,9 +34,11 @@ export namespace ModelsDev {
|
|||||||
|
|
||||||
export const Provider = z
|
export const Provider = z
|
||||||
.object({
|
.object({
|
||||||
|
api: z.string().optional(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
env: z.array(z.string()),
|
env: z.array(z.string()),
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
|
npm: z.string().optional(),
|
||||||
models: z.record(Model),
|
models: z.record(Model),
|
||||||
})
|
})
|
||||||
.openapi({
|
.openapi({
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import type { Tool } from "../tool/tool"
|
|||||||
import { WriteTool } from "../tool/write"
|
import { WriteTool } from "../tool/write"
|
||||||
import { TodoReadTool, TodoWriteTool } from "../tool/todo"
|
import { TodoReadTool, TodoWriteTool } from "../tool/todo"
|
||||||
import { AuthAnthropic } from "../auth/anthropic"
|
import { AuthAnthropic } from "../auth/anthropic"
|
||||||
|
import { AuthGithubCopilot } from "../auth/github-copilot"
|
||||||
import { ModelsDev } from "./models"
|
import { ModelsDev } from "./models"
|
||||||
import { NamedError } from "../util/error"
|
import { NamedError } from "../util/error"
|
||||||
import { Auth } from "../auth"
|
import { Auth } from "../auth"
|
||||||
@@ -27,9 +28,13 @@ import { TaskTool } from "../tool/task"
|
|||||||
export namespace Provider {
|
export namespace Provider {
|
||||||
const log = Log.create({ service: "provider" })
|
const log = Log.create({ service: "provider" })
|
||||||
|
|
||||||
type CustomLoader = (
|
type CustomLoader = (provider: ModelsDev.Provider) => Promise<
|
||||||
provider: ModelsDev.Provider,
|
| {
|
||||||
) => Promise<Record<string, any> | false>
|
getModel?: (sdk: any, modelID: string) => Promise<any>
|
||||||
|
options: Record<string, any>
|
||||||
|
}
|
||||||
|
| false
|
||||||
|
>
|
||||||
|
|
||||||
type Source = "env" | "config" | "custom" | "api"
|
type Source = "env" | "config" | "custom" | "api"
|
||||||
|
|
||||||
@@ -40,27 +45,94 @@ export namespace Provider {
|
|||||||
for (const model of Object.values(provider.models)) {
|
for (const model of Object.values(provider.models)) {
|
||||||
model.cost = {
|
model.cost = {
|
||||||
input: 0,
|
input: 0,
|
||||||
inputCached: 0,
|
|
||||||
output: 0,
|
output: 0,
|
||||||
outputCached: 0,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
apiKey: "",
|
options: {
|
||||||
headers: {
|
apiKey: "",
|
||||||
authorization: `Bearer ${access}`,
|
async fetch(input: any, init: any) {
|
||||||
"anthropic-beta": "oauth-2025-04-20",
|
const access = await AuthAnthropic.access()
|
||||||
|
const headers = {
|
||||||
|
...init.headers,
|
||||||
|
authorization: `Bearer ${access}`,
|
||||||
|
"anthropic-beta": "oauth-2025-04-20",
|
||||||
|
}
|
||||||
|
delete headers["x-api-key"]
|
||||||
|
return fetch(input, {
|
||||||
|
...init,
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"github-copilot": async (provider) => {
|
||||||
|
const info = await AuthGithubCopilot.access()
|
||||||
|
if (!info) return false
|
||||||
|
|
||||||
|
if (provider && provider.models) {
|
||||||
|
for (const model of Object.values(provider.models)) {
|
||||||
|
model.cost = {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
options: {
|
||||||
|
apiKey: "",
|
||||||
|
async fetch(input: any, init: any) {
|
||||||
|
const token = await AuthGithubCopilot.access()
|
||||||
|
if (!token) throw new Error("GitHub Copilot authentication expired")
|
||||||
|
const headers = {
|
||||||
|
...init.headers,
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"User-Agent": "GitHubCopilotChat/0.26.7",
|
||||||
|
"Editor-Version": "vscode/1.99.3",
|
||||||
|
"Editor-Plugin-Version": "copilot-chat/0.26.7",
|
||||||
|
"Copilot-Integration-Id": "vscode-chat",
|
||||||
|
"Openai-Intent": "conversation-edits",
|
||||||
|
}
|
||||||
|
delete headers["x-api-key"]
|
||||||
|
return fetch(input, {
|
||||||
|
...init,
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openai: async () => {
|
||||||
|
return {
|
||||||
|
async getModel(sdk: any, modelID: string) {
|
||||||
|
return sdk.responses(modelID)
|
||||||
|
},
|
||||||
|
options: {},
|
||||||
|
}
|
||||||
|
},
|
||||||
"amazon-bedrock": async () => {
|
"amazon-bedrock": async () => {
|
||||||
if (!process.env["AWS_PROFILE"]) return false
|
if (!process.env["AWS_PROFILE"] && !process.env["AWS_ACCESS_KEY_ID"])
|
||||||
|
return false
|
||||||
|
|
||||||
|
const region = process.env["AWS_REGION"] ?? "us-east-1"
|
||||||
|
|
||||||
const { fromNodeProviderChain } = await import(
|
const { fromNodeProviderChain } = await import(
|
||||||
await BunProc.install("@aws-sdk/credential-providers")
|
await BunProc.install("@aws-sdk/credential-providers")
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
region: process.env["AWS_REGION"] ?? "us-east-1",
|
options: {
|
||||||
credentialProvider: fromNodeProviderChain(),
|
region,
|
||||||
|
credentialProvider: fromNodeProviderChain(),
|
||||||
|
},
|
||||||
|
async getModel(sdk: any, modelID: string) {
|
||||||
|
if (modelID.includes("claude")) {
|
||||||
|
const prefix = region.split("-")[0]
|
||||||
|
modelID = `${prefix}.${modelID}`
|
||||||
|
}
|
||||||
|
return sdk.languageModel(modelID)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -73,6 +145,7 @@ export namespace Provider {
|
|||||||
[providerID: string]: {
|
[providerID: string]: {
|
||||||
source: Source
|
source: Source
|
||||||
info: ModelsDev.Provider
|
info: ModelsDev.Provider
|
||||||
|
getModel?: (sdk: any, modelID: string) => Promise<any>
|
||||||
options: Record<string, any>
|
options: Record<string, any>
|
||||||
}
|
}
|
||||||
} = {}
|
} = {}
|
||||||
@@ -82,32 +155,38 @@ export namespace Provider {
|
|||||||
>()
|
>()
|
||||||
const sdk = new Map<string, SDK>()
|
const sdk = new Map<string, SDK>()
|
||||||
|
|
||||||
log.info("loading")
|
log.info("init")
|
||||||
|
|
||||||
function mergeProvider(
|
function mergeProvider(
|
||||||
id: string,
|
id: string,
|
||||||
options: Record<string, any>,
|
options: Record<string, any>,
|
||||||
source: Source,
|
source: Source,
|
||||||
|
getModel?: (sdk: any, modelID: string) => Promise<any>,
|
||||||
) {
|
) {
|
||||||
const provider = providers[id]
|
const provider = providers[id]
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
|
const info = database[id]
|
||||||
|
if (!info) return
|
||||||
|
if (info.api) options["baseURL"] = info.api
|
||||||
providers[id] = {
|
providers[id] = {
|
||||||
source,
|
source,
|
||||||
info: database[id],
|
info,
|
||||||
options,
|
options,
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
provider.options = mergeDeep(provider.options, options)
|
provider.options = mergeDeep(provider.options, options)
|
||||||
provider.source = source
|
provider.source = source
|
||||||
|
provider.getModel = getModel ?? provider.getModel
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [providerID, provider] of Object.entries(
|
const configProviders = Object.entries(config.provider ?? {})
|
||||||
config.provider ?? {},
|
|
||||||
)) {
|
for (const [providerID, provider] of configProviders) {
|
||||||
const existing = database[providerID]
|
const existing = database[providerID]
|
||||||
const parsed: ModelsDev.Provider = {
|
const parsed: ModelsDev.Provider = {
|
||||||
id: providerID,
|
id: providerID,
|
||||||
|
npm: provider.npm ?? existing?.npm,
|
||||||
name: provider.name ?? existing?.name ?? providerID,
|
name: provider.name ?? existing?.name ?? providerID,
|
||||||
env: provider.env ?? existing?.env ?? [],
|
env: provider.env ?? existing?.env ?? [],
|
||||||
models: existing?.models ?? {},
|
models: existing?.models ?? {},
|
||||||
@@ -121,13 +200,19 @@ export namespace Provider {
|
|||||||
attachment: model.attachment ?? existing?.attachment ?? false,
|
attachment: model.attachment ?? existing?.attachment ?? false,
|
||||||
reasoning: model.reasoning ?? existing?.reasoning ?? false,
|
reasoning: model.reasoning ?? existing?.reasoning ?? false,
|
||||||
temperature: model.temperature ?? existing?.temperature ?? false,
|
temperature: model.temperature ?? existing?.temperature ?? false,
|
||||||
cost: model.cost ??
|
tool_call: model.tool_call ?? existing?.tool_call ?? true,
|
||||||
existing?.cost ?? {
|
cost: {
|
||||||
input: 0,
|
...existing?.cost,
|
||||||
output: 0,
|
...model.cost,
|
||||||
inputCached: 0,
|
input: 0,
|
||||||
outputCached: 0,
|
output: 0,
|
||||||
},
|
cache_read: 0,
|
||||||
|
cache_write: 0,
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
...existing?.options,
|
||||||
|
...model.options,
|
||||||
|
},
|
||||||
limit: model.limit ??
|
limit: model.limit ??
|
||||||
existing?.limit ?? {
|
existing?.limit ?? {
|
||||||
context: 0,
|
context: 0,
|
||||||
@@ -139,8 +224,12 @@ export namespace Provider {
|
|||||||
database[providerID] = parsed
|
database[providerID] = parsed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const disabled = await Config.get().then(
|
||||||
|
(cfg) => new Set(cfg.disabled_providers ?? []),
|
||||||
|
)
|
||||||
// load env
|
// load env
|
||||||
for (const [providerID, provider] of Object.entries(database)) {
|
for (const [providerID, provider] of Object.entries(database)) {
|
||||||
|
if (disabled.has(providerID)) continue
|
||||||
if (provider.env.some((item) => process.env[item])) {
|
if (provider.env.some((item) => process.env[item])) {
|
||||||
mergeProvider(providerID, {}, "env")
|
mergeProvider(providerID, {}, "env")
|
||||||
}
|
}
|
||||||
@@ -148,6 +237,7 @@ export namespace Provider {
|
|||||||
|
|
||||||
// load apikeys
|
// load apikeys
|
||||||
for (const [providerID, provider] of Object.entries(await Auth.all())) {
|
for (const [providerID, provider] of Object.entries(await Auth.all())) {
|
||||||
|
if (disabled.has(providerID)) continue
|
||||||
if (provider.type === "api") {
|
if (provider.type === "api") {
|
||||||
mergeProvider(providerID, { apiKey: provider.key }, "api")
|
mergeProvider(providerID, { apiKey: provider.key }, "api")
|
||||||
}
|
}
|
||||||
@@ -155,19 +245,24 @@ export namespace Provider {
|
|||||||
|
|
||||||
// load custom
|
// load custom
|
||||||
for (const [providerID, fn] of Object.entries(CUSTOM_LOADERS)) {
|
for (const [providerID, fn] of Object.entries(CUSTOM_LOADERS)) {
|
||||||
|
if (disabled.has(providerID)) continue
|
||||||
const result = await fn(database[providerID])
|
const result = await fn(database[providerID])
|
||||||
if (result) mergeProvider(providerID, result, "custom")
|
if (result) {
|
||||||
|
mergeProvider(providerID, result.options, "custom", result.getModel)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// load config
|
// load config
|
||||||
for (const [providerID, provider] of Object.entries(
|
for (const [providerID, provider] of configProviders) {
|
||||||
config.provider ?? {},
|
|
||||||
)) {
|
|
||||||
mergeProvider(providerID, provider.options ?? {}, "config")
|
mergeProvider(providerID, provider.options ?? {}, "config")
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const providerID of Object.keys(providers)) {
|
for (const [providerID, provider] of Object.entries(providers)) {
|
||||||
log.info("loaded", { providerID })
|
if (Object.keys(provider.info.models).length === 0) {
|
||||||
|
delete providers[providerID]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.info("found", { providerID })
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -181,20 +276,22 @@ export namespace Provider {
|
|||||||
return state().then((state) => state.providers)
|
return state().then((state) => state.providers)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getSDK(providerID: string) {
|
async function getSDK(provider: ModelsDev.Provider) {
|
||||||
return (async () => {
|
return (async () => {
|
||||||
|
using _ = log.time("getSDK", {
|
||||||
|
providerID: provider.id,
|
||||||
|
})
|
||||||
const s = await state()
|
const s = await state()
|
||||||
const existing = s.sdk.get(providerID)
|
const existing = s.sdk.get(provider.id)
|
||||||
if (existing) return existing
|
if (existing) return existing
|
||||||
const mod = await import(
|
const pkg = provider.npm ?? provider.id
|
||||||
await BunProc.install(`@ai-sdk/${providerID}`, "alpha")
|
const mod = await import(await BunProc.install(pkg, "latest"))
|
||||||
)
|
|
||||||
const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
|
const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
|
||||||
const loaded = fn(s.providers[providerID]?.options)
|
const loaded = fn(s.providers[provider.id]?.options)
|
||||||
s.sdk.set(providerID, loaded)
|
s.sdk.set(provider.id, loaded)
|
||||||
return loaded as SDK
|
return loaded as SDK
|
||||||
})().catch((e) => {
|
})().catch((e) => {
|
||||||
throw new InitError({ providerID: providerID }, { cause: e })
|
throw new InitError({ providerID: provider.id }, { cause: e })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +300,7 @@ export namespace Provider {
|
|||||||
const s = await state()
|
const s = await state()
|
||||||
if (s.models.has(key)) return s.models.get(key)!
|
if (s.models.has(key)) return s.models.get(key)!
|
||||||
|
|
||||||
log.info("loading", {
|
log.info("getModel", {
|
||||||
providerID,
|
providerID,
|
||||||
modelID,
|
modelID,
|
||||||
})
|
})
|
||||||
@@ -212,13 +309,12 @@ export namespace Provider {
|
|||||||
if (!provider) throw new ModelNotFoundError({ providerID, modelID })
|
if (!provider) throw new ModelNotFoundError({ providerID, modelID })
|
||||||
const info = provider.info.models[modelID]
|
const info = provider.info.models[modelID]
|
||||||
if (!info) throw new ModelNotFoundError({ providerID, modelID })
|
if (!info) throw new ModelNotFoundError({ providerID, modelID })
|
||||||
|
const sdk = await getSDK(provider.info)
|
||||||
const sdk = await getSDK(providerID)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const language =
|
const language = provider.getModel
|
||||||
// @ts-expect-error
|
? await provider.getModel(sdk, modelID)
|
||||||
"responses" in sdk ? sdk.responses(modelID) : sdk.languageModel(modelID)
|
: sdk.languageModel(modelID)
|
||||||
log.info("found", { providerID, modelID })
|
log.info("found", { providerID, modelID })
|
||||||
s.models.set(key, {
|
s.models.set(key, {
|
||||||
info,
|
info,
|
||||||
@@ -255,7 +351,15 @@ export namespace Provider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function defaultModel() {
|
export async function defaultModel() {
|
||||||
const [provider] = await list().then((val) => Object.values(val))
|
const cfg = await Config.get()
|
||||||
|
if (cfg.model) return parseModel(cfg.model)
|
||||||
|
const provider = await list()
|
||||||
|
.then((val) => Object.values(val))
|
||||||
|
.then((x) =>
|
||||||
|
x.find(
|
||||||
|
(p) => !cfg.provider || Object.keys(cfg.provider).includes(p.info.id),
|
||||||
|
),
|
||||||
|
)
|
||||||
if (!provider) throw new Error("no providers found")
|
if (!provider) throw new Error("no providers found")
|
||||||
const [model] = sort(Object.values(provider.info.models))
|
const [model] = sort(Object.values(provider.info.models))
|
||||||
if (!model) throw new Error("no models found")
|
if (!model) throw new Error("no models found")
|
||||||
@@ -265,6 +369,14 @@ export namespace Provider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseModel(model: string) {
|
||||||
|
const [providerID, ...rest] = model.split("/")
|
||||||
|
return {
|
||||||
|
providerID: providerID,
|
||||||
|
modelID: rest.join("/"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const TOOLS = [
|
const TOOLS = [
|
||||||
BashTool,
|
BashTool,
|
||||||
EditTool,
|
EditTool,
|
||||||
@@ -283,20 +395,63 @@ export namespace Provider {
|
|||||||
TaskTool,
|
TaskTool,
|
||||||
TodoReadTool,
|
TodoReadTool,
|
||||||
]
|
]
|
||||||
|
|
||||||
const TOOL_MAPPING: Record<string, Tool.Info[]> = {
|
const TOOL_MAPPING: Record<string, Tool.Info[]> = {
|
||||||
anthropic: TOOLS.filter((t) => t.id !== "opencode.patch"),
|
anthropic: TOOLS.filter((t) => t.id !== "patch"),
|
||||||
openai: TOOLS,
|
openai: TOOLS.map((t) => ({
|
||||||
|
...t,
|
||||||
|
parameters: optionalToNullable(t.parameters),
|
||||||
|
})),
|
||||||
|
azure: TOOLS.map((t) => ({
|
||||||
|
...t,
|
||||||
|
parameters: optionalToNullable(t.parameters),
|
||||||
|
})),
|
||||||
google: TOOLS,
|
google: TOOLS,
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function tools(providerID: string) {
|
export async function tools(providerID: string) {
|
||||||
|
/*
|
||||||
const cfg = await Config.get()
|
const cfg = await Config.get()
|
||||||
if (cfg.tool?.provider?.[providerID])
|
if (cfg.tool?.provider?.[providerID])
|
||||||
return cfg.tool.provider[providerID].map(
|
return cfg.tool.provider[providerID].map(
|
||||||
(id) => TOOLS.find((t) => t.id === id)!,
|
(id) => TOOLS.find((t) => t.id === id)!,
|
||||||
)
|
)
|
||||||
|
*/
|
||||||
return TOOL_MAPPING[providerID] ?? TOOLS
|
return TOOL_MAPPING[providerID] ?? TOOLS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function optionalToNullable(schema: z.ZodTypeAny): z.ZodTypeAny {
|
||||||
|
if (schema instanceof z.ZodObject) {
|
||||||
|
const shape = schema.shape
|
||||||
|
const newShape: Record<string, z.ZodTypeAny> = {}
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(shape)) {
|
||||||
|
const zodValue = value as z.ZodTypeAny
|
||||||
|
if (zodValue instanceof z.ZodOptional) {
|
||||||
|
newShape[key] = zodValue.unwrap().nullable()
|
||||||
|
} else {
|
||||||
|
newShape[key] = optionalToNullable(zodValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return z.object(newShape)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schema instanceof z.ZodArray) {
|
||||||
|
return z.array(optionalToNullable(schema.element))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schema instanceof z.ZodUnion) {
|
||||||
|
return z.union(
|
||||||
|
schema.options.map((option: z.ZodTypeAny) =>
|
||||||
|
optionalToNullable(option),
|
||||||
|
) as [z.ZodTypeAny, z.ZodTypeAny, ...z.ZodTypeAny[]],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return schema
|
||||||
|
}
|
||||||
|
|
||||||
export const ModelNotFoundError = NamedError.create(
|
export const ModelNotFoundError = NamedError.create(
|
||||||
"ProviderModelNotFoundError",
|
"ProviderModelNotFoundError",
|
||||||
z.object({
|
z.object({
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import type { LanguageModelV1Prompt } from "ai"
|
||||||
|
import { unique } from "remeda"
|
||||||
|
|
||||||
|
export namespace ProviderTransform {
|
||||||
|
export function message(
|
||||||
|
msgs: LanguageModelV1Prompt,
|
||||||
|
providerID: string,
|
||||||
|
modelID: string,
|
||||||
|
) {
|
||||||
|
if (providerID === "anthropic" || modelID.includes("anthropic")) {
|
||||||
|
const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
|
||||||
|
const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
|
||||||
|
|
||||||
|
for (const msg of unique([...system, ...final])) {
|
||||||
|
msg.providerMetadata = {
|
||||||
|
...msg.providerMetadata,
|
||||||
|
anthropic: {
|
||||||
|
cacheControl: { type: "ephemeral" },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return msgs
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,8 +12,10 @@ import { App } from "../app/app"
|
|||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
import { mapValues } from "remeda"
|
import { mapValues } from "remeda"
|
||||||
import { NamedError } from "../util/error"
|
import { NamedError } from "../util/error"
|
||||||
import { Fzf } from "../external/fzf"
|
|
||||||
import { ModelsDev } from "../provider/models"
|
import { ModelsDev } from "../provider/models"
|
||||||
|
import { Ripgrep } from "../external/ripgrep"
|
||||||
|
import { Installation } from "../installation"
|
||||||
|
import { Config } from "../config/config"
|
||||||
|
|
||||||
const ERRORS = {
|
const ERRORS = {
|
||||||
400: {
|
400: {
|
||||||
@@ -56,12 +58,16 @@ export namespace Server {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.use((c, next) => {
|
.use(async (c, next) => {
|
||||||
log.info("request", {
|
log.info("request", {
|
||||||
method: c.req.method,
|
method: c.req.method,
|
||||||
path: c.req.path,
|
path: c.req.path,
|
||||||
})
|
})
|
||||||
return next()
|
const start = Date.now()
|
||||||
|
await next()
|
||||||
|
log.info("response", {
|
||||||
|
duration: Date.now() - start,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.get(
|
.get(
|
||||||
"/openapi",
|
"/openapi",
|
||||||
@@ -135,6 +141,25 @@ export namespace Server {
|
|||||||
return c.json(App.info())
|
return c.json(App.info())
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
.post(
|
||||||
|
"/config_get",
|
||||||
|
describeRoute({
|
||||||
|
description: "Get config info",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Get config info",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(Config.Info),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
async (c) => {
|
||||||
|
return c.json(await Config.get())
|
||||||
|
},
|
||||||
|
)
|
||||||
.post(
|
.post(
|
||||||
"/app_initialize",
|
"/app_initialize",
|
||||||
describeRoute({
|
describeRoute({
|
||||||
@@ -265,6 +290,34 @@ export namespace Server {
|
|||||||
return c.json(session)
|
return c.json(session)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
.post(
|
||||||
|
"/session_unshare",
|
||||||
|
describeRoute({
|
||||||
|
description: "Unshare the session",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Successfully unshared session",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(Session.Info),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
zValidator(
|
||||||
|
"json",
|
||||||
|
z.object({
|
||||||
|
sessionID: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
async (c) => {
|
||||||
|
const body = c.req.valid("json")
|
||||||
|
await Session.unshare(body.sessionID)
|
||||||
|
const session = await Session.get(body.sessionID)
|
||||||
|
return c.json(session)
|
||||||
|
},
|
||||||
|
)
|
||||||
.post(
|
.post(
|
||||||
"/session_messages",
|
"/session_messages",
|
||||||
describeRoute({
|
describeRoute({
|
||||||
@@ -453,10 +506,33 @@ export namespace Server {
|
|||||||
async (c) => {
|
async (c) => {
|
||||||
const body = c.req.valid("json")
|
const body = c.req.valid("json")
|
||||||
const app = App.info()
|
const app = App.info()
|
||||||
const result = await Fzf.search(app.path.cwd, body.query)
|
const result = await Ripgrep.files({
|
||||||
|
cwd: app.path.cwd,
|
||||||
|
query: body.query,
|
||||||
|
limit: 10,
|
||||||
|
})
|
||||||
return c.json(result)
|
return c.json(result)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
.post(
|
||||||
|
"installation_info",
|
||||||
|
describeRoute({
|
||||||
|
description: "Get installation info",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Get installation info",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(Installation.Info),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
async (c) => {
|
||||||
|
return c.json(Installation.info())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
import { App } from "../app/app"
|
|
||||||
import { Filesystem } from "../util/filesystem"
|
|
||||||
|
|
||||||
export namespace SessionContext {
|
|
||||||
const FILES = [
|
|
||||||
"AGENTS.md",
|
|
||||||
"CLAUDE.md",
|
|
||||||
"CONTEXT.md", // deprecated
|
|
||||||
]
|
|
||||||
export async function find() {
|
|
||||||
const { cwd, root } = App.info().path
|
|
||||||
const found = []
|
|
||||||
for (const item of FILES) {
|
|
||||||
const matches = await Filesystem.findUp(item, cwd, root)
|
|
||||||
found.push(...matches.map((x) => Bun.file(x).text()))
|
|
||||||
}
|
|
||||||
return Promise.all(found).then((parts) => parts.join("\n\n"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,15 +4,17 @@ import { Identifier } from "../id/id"
|
|||||||
import { Storage } from "../storage/storage"
|
import { Storage } from "../storage/storage"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import {
|
import {
|
||||||
convertToModelMessages,
|
|
||||||
generateText,
|
generateText,
|
||||||
LoadAPIKeyError,
|
LoadAPIKeyError,
|
||||||
stepCountIs,
|
convertToCoreMessages,
|
||||||
streamText,
|
streamText,
|
||||||
tool,
|
tool,
|
||||||
type Tool as AITool,
|
type Tool as AITool,
|
||||||
type LanguageModelUsage,
|
type LanguageModelUsage,
|
||||||
|
type CoreMessage,
|
||||||
type UIMessage,
|
type UIMessage,
|
||||||
|
type ProviderMetadata,
|
||||||
|
wrapLanguageModel,
|
||||||
} from "ai"
|
} from "ai"
|
||||||
import { z, ZodSchema } from "zod"
|
import { z, ZodSchema } from "zod"
|
||||||
import { Decimal } from "decimal.js"
|
import { Decimal } from "decimal.js"
|
||||||
@@ -27,6 +29,11 @@ import { MCP } from "../mcp"
|
|||||||
import { NamedError } from "../util/error"
|
import { NamedError } from "../util/error"
|
||||||
import type { Tool } from "../tool/tool"
|
import type { Tool } from "../tool/tool"
|
||||||
import { SystemPrompt } from "./system"
|
import { SystemPrompt } from "./system"
|
||||||
|
import { Flag } from "../flag/flag"
|
||||||
|
import type { ModelsDev } from "../provider/models"
|
||||||
|
import { Installation } from "../installation"
|
||||||
|
import { Config } from "../config/config"
|
||||||
|
import { ProviderTransform } from "../provider/transform"
|
||||||
|
|
||||||
export namespace Session {
|
export namespace Session {
|
||||||
const log = Log.create({ service: "session" })
|
const log = Log.create({ service: "session" })
|
||||||
@@ -37,11 +44,11 @@ export namespace Session {
|
|||||||
parentID: Identifier.schema("session").optional(),
|
parentID: Identifier.schema("session").optional(),
|
||||||
share: z
|
share: z
|
||||||
.object({
|
.object({
|
||||||
secret: z.string(),
|
|
||||||
url: z.string(),
|
url: z.string(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
|
version: z.string(),
|
||||||
time: z.object({
|
time: z.object({
|
||||||
created: z.number(),
|
created: z.number(),
|
||||||
updated: z.number(),
|
updated: z.number(),
|
||||||
@@ -52,6 +59,12 @@ export namespace Session {
|
|||||||
})
|
})
|
||||||
export type Info = z.output<typeof Info>
|
export type Info = z.output<typeof Info>
|
||||||
|
|
||||||
|
export const ShareInfo = z.object({
|
||||||
|
secret: z.string(),
|
||||||
|
url: z.string(),
|
||||||
|
})
|
||||||
|
export type ShareInfo = z.output<typeof ShareInfo>
|
||||||
|
|
||||||
export const Event = {
|
export const Event = {
|
||||||
Updated: Bus.event(
|
Updated: Bus.event(
|
||||||
"session.updated",
|
"session.updated",
|
||||||
@@ -67,19 +80,30 @@ export namespace Session {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = App.state("session", () => {
|
const state = App.state(
|
||||||
const sessions = new Map<string, Info>()
|
"session",
|
||||||
const messages = new Map<string, Message.Info[]>()
|
() => {
|
||||||
|
const sessions = new Map<string, Info>()
|
||||||
|
const messages = new Map<string, Message.Info[]>()
|
||||||
|
const pending = new Map<string, AbortController>()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sessions,
|
sessions,
|
||||||
messages,
|
messages,
|
||||||
}
|
pending,
|
||||||
})
|
}
|
||||||
|
},
|
||||||
|
async (state) => {
|
||||||
|
for (const [_, controller] of state.pending) {
|
||||||
|
controller.abort()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
export async function create(parentID?: string) {
|
export async function create(parentID?: string) {
|
||||||
const result: Info = {
|
const result: Info = {
|
||||||
id: Identifier.descending("session"),
|
id: Identifier.descending("session"),
|
||||||
|
version: Installation.VERSION,
|
||||||
parentID,
|
parentID,
|
||||||
title:
|
title:
|
||||||
(parentID ? "Child session - " : "New Session - ") +
|
(parentID ? "Child session - " : "New Session - ") +
|
||||||
@@ -92,6 +116,13 @@ export namespace Session {
|
|||||||
log.info("created", result)
|
log.info("created", result)
|
||||||
state().sessions.set(result.id, result)
|
state().sessions.set(result.id, result)
|
||||||
await Storage.writeJSON("session/info/" + result.id, result)
|
await Storage.writeJSON("session/info/" + result.id, result)
|
||||||
|
const cfg = await Config.get()
|
||||||
|
if (!result.parentID && (Flag.OPENCODE_AUTO_SHARE || cfg.autoshare))
|
||||||
|
share(result.id).then((share) => {
|
||||||
|
update(result.id, (draft) => {
|
||||||
|
draft.share = share
|
||||||
|
})
|
||||||
|
})
|
||||||
Bus.publish(Event.Updated, {
|
Bus.publish(Event.Updated, {
|
||||||
info: result,
|
info: result,
|
||||||
})
|
})
|
||||||
@@ -108,19 +139,35 @@ export namespace Session {
|
|||||||
return read as Info
|
return read as Info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getShare(id: string) {
|
||||||
|
return Storage.readJSON<ShareInfo>("session/share/" + id)
|
||||||
|
}
|
||||||
|
|
||||||
export async function share(id: string) {
|
export async function share(id: string) {
|
||||||
const session = await get(id)
|
const session = await get(id)
|
||||||
if (session.share) return session.share
|
if (session.share) return session.share
|
||||||
const share = await Share.create(id)
|
const share = await Share.create(id)
|
||||||
await update(id, (draft) => {
|
await update(id, (draft) => {
|
||||||
draft.share = share
|
draft.share = {
|
||||||
|
url: share.url,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
await Storage.writeJSON<ShareInfo>("session/share/" + id, share)
|
||||||
|
await Share.sync("session/info/" + id, session)
|
||||||
for (const msg of await messages(id)) {
|
for (const msg of await messages(id)) {
|
||||||
await Share.sync("session/message/" + id + "/" + msg.id, msg)
|
await Share.sync("session/message/" + id + "/" + msg.id, msg)
|
||||||
}
|
}
|
||||||
return share
|
return share
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function unshare(id: string) {
|
||||||
|
await Storage.remove("session/share/" + id)
|
||||||
|
await update(id, (draft) => {
|
||||||
|
draft.share = undefined
|
||||||
|
})
|
||||||
|
await Share.remove(id)
|
||||||
|
}
|
||||||
|
|
||||||
export async function update(id: string, editor: (session: Info) => void) {
|
export async function update(id: string, editor: (session: Info) => void) {
|
||||||
const { sessions } = state()
|
const { sessions } = state()
|
||||||
const session = await get(id)
|
const session = await get(id)
|
||||||
@@ -160,10 +207,10 @@ export namespace Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function abort(sessionID: string) {
|
export function abort(sessionID: string) {
|
||||||
const controller = pending.get(sessionID)
|
const controller = state().pending.get(sessionID)
|
||||||
if (!controller) return false
|
if (!controller) return false
|
||||||
controller.abort()
|
controller.abort()
|
||||||
pending.delete(sessionID)
|
state().pending.delete(sessionID)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,8 +242,11 @@ export namespace Session {
|
|||||||
if (previous?.metadata.assistant) {
|
if (previous?.metadata.assistant) {
|
||||||
const tokens =
|
const tokens =
|
||||||
previous.metadata.assistant.tokens.input +
|
previous.metadata.assistant.tokens.input +
|
||||||
|
previous.metadata.assistant.tokens.cache.read +
|
||||||
|
previous.metadata.assistant.tokens.cache.write +
|
||||||
previous.metadata.assistant.tokens.output
|
previous.metadata.assistant.tokens.output
|
||||||
if (
|
if (
|
||||||
|
model.info.limit.context &&
|
||||||
tokens >
|
tokens >
|
||||||
(model.info.limit.context - (model.info.limit.output ?? 0)) * 0.9
|
(model.info.limit.context - (model.info.limit.output ?? 0)) * 0.9
|
||||||
) {
|
) {
|
||||||
@@ -220,34 +270,32 @@ export namespace Session {
|
|||||||
const session = await get(input.sessionID)
|
const session = await get(input.sessionID)
|
||||||
if (msgs.length === 0 && !session.parentID) {
|
if (msgs.length === 0 && !session.parentID) {
|
||||||
generateText({
|
generateText({
|
||||||
maxOutputTokens: 20,
|
maxTokens: input.providerID === "google" ? 1024 : 20,
|
||||||
messages: convertToModelMessages([
|
providerOptions: model.info.options,
|
||||||
|
messages: [
|
||||||
...SystemPrompt.title(input.providerID).map(
|
...SystemPrompt.title(input.providerID).map(
|
||||||
(x): UIMessage => ({
|
(x): CoreMessage => ({
|
||||||
id: Identifier.ascending("message"),
|
|
||||||
role: "system",
|
role: "system",
|
||||||
parts: [
|
content: x,
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: x,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
{
|
...convertToCoreMessages([
|
||||||
role: "user",
|
{
|
||||||
parts: input.parts,
|
role: "user",
|
||||||
},
|
content: "",
|
||||||
]),
|
parts: toParts(input.parts),
|
||||||
temperature: 0,
|
},
|
||||||
|
]),
|
||||||
|
],
|
||||||
model: model.language,
|
model: model.language,
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
return Session.update(input.sessionID, (draft) => {
|
if (result.text)
|
||||||
draft.title = result.text
|
return Session.update(input.sessionID, (draft) => {
|
||||||
})
|
draft.title = result.text
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => { })
|
||||||
}
|
}
|
||||||
const msg: Message.Info = {
|
const msg: Message.Info = {
|
||||||
role: "user",
|
role: "user",
|
||||||
@@ -265,7 +313,7 @@ export namespace Session {
|
|||||||
msgs.push(msg)
|
msgs.push(msg)
|
||||||
|
|
||||||
const system = input.system ?? SystemPrompt.provider(input.providerID)
|
const system = input.system ?? SystemPrompt.provider(input.providerID)
|
||||||
system.push(...(await SystemPrompt.environment(input.sessionID)))
|
system.push(...(await SystemPrompt.environment()))
|
||||||
system.push(...(await SystemPrompt.custom()))
|
system.push(...(await SystemPrompt.custom()))
|
||||||
|
|
||||||
const next: Message.Info = {
|
const next: Message.Info = {
|
||||||
@@ -284,6 +332,7 @@ export namespace Session {
|
|||||||
input: 0,
|
input: 0,
|
||||||
output: 0,
|
output: 0,
|
||||||
reasoning: 0,
|
reasoning: 0,
|
||||||
|
cache: { read: 0, write: 0 },
|
||||||
},
|
},
|
||||||
modelID: input.modelID,
|
modelID: input.modelID,
|
||||||
providerID: input.providerID,
|
providerID: input.providerID,
|
||||||
@@ -310,6 +359,16 @@ export namespace Session {
|
|||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
abort: abort.signal,
|
abort: abort.signal,
|
||||||
messageID: next.id,
|
messageID: next.id,
|
||||||
|
metadata: async (val) => {
|
||||||
|
next.metadata.tool[opts.toolCallId] = {
|
||||||
|
...val,
|
||||||
|
time: {
|
||||||
|
start: 0,
|
||||||
|
end: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
await updateMessage(next)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
next.metadata!.tool![opts.toolCallId] = {
|
next.metadata!.tool![opts.toolCallId] = {
|
||||||
...result.metadata,
|
...result.metadata,
|
||||||
@@ -374,13 +433,29 @@ export namespace Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let text: Message.TextPart | undefined
|
let text: Message.TextPart | undefined
|
||||||
|
await Bun.write(
|
||||||
|
"/tmp/message.json",
|
||||||
|
JSON.stringify(
|
||||||
|
[
|
||||||
|
...system.map(
|
||||||
|
(x): CoreMessage => ({
|
||||||
|
role: "system",
|
||||||
|
content: x,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
...convertToCoreMessages(
|
||||||
|
msgs.map(toUIMessage).filter((x) => x.parts.length > 0),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
)
|
||||||
const result = streamText({
|
const result = streamText({
|
||||||
onStepFinish: async (step) => {
|
onStepFinish: async (step) => {
|
||||||
log.info("step finish", {
|
log.info("step finish", { finishReason: step.finishReason })
|
||||||
finishReason: step.finishReason,
|
|
||||||
})
|
|
||||||
const assistant = next.metadata!.assistant!
|
const assistant = next.metadata!.assistant!
|
||||||
const usage = getUsage(step.usage, model.info)
|
const usage = getUsage(model.info, step.usage, step.providerMetadata)
|
||||||
assistant.cost += usage.cost
|
assistant.cost += usage.cost
|
||||||
assistant.tokens = usage.tokens
|
assistant.tokens = usage.tokens
|
||||||
await updateMessage(next)
|
await updateMessage(next)
|
||||||
@@ -393,24 +468,111 @@ export namespace Session {
|
|||||||
}
|
}
|
||||||
text = undefined
|
text = undefined
|
||||||
},
|
},
|
||||||
async onChunk(input) {
|
async onFinish(input) {
|
||||||
const value = input.chunk
|
log.info("message finish", {
|
||||||
|
reason: input.finishReason,
|
||||||
|
})
|
||||||
|
const assistant = next.metadata!.assistant!
|
||||||
|
const usage = getUsage(model.info, input.usage, input.providerMetadata)
|
||||||
|
assistant.cost = usage.cost
|
||||||
|
await updateMessage(next)
|
||||||
|
},
|
||||||
|
onError(err) {
|
||||||
|
log.error("callback error", err)
|
||||||
|
switch (true) {
|
||||||
|
case LoadAPIKeyError.isInstance(err.error):
|
||||||
|
next.metadata.error = new Provider.AuthError(
|
||||||
|
{
|
||||||
|
providerID: input.providerID,
|
||||||
|
message: err.error.message,
|
||||||
|
},
|
||||||
|
{ cause: err.error },
|
||||||
|
).toObject()
|
||||||
|
break
|
||||||
|
case err.error instanceof Error:
|
||||||
|
next.metadata.error = new NamedError.Unknown(
|
||||||
|
{ message: err.error.toString() },
|
||||||
|
{ cause: err.error },
|
||||||
|
).toObject()
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
next.metadata.error = new NamedError.Unknown(
|
||||||
|
{ message: JSON.stringify(err.error) },
|
||||||
|
{ cause: err.error },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Bus.publish(Event.Error, {
|
||||||
|
error: next.metadata.error,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// async prepareStep(step) {
|
||||||
|
// next.parts.push({
|
||||||
|
// type: "step-start",
|
||||||
|
// })
|
||||||
|
// await updateMessage(next)
|
||||||
|
// return step
|
||||||
|
// },
|
||||||
|
toolCallStreaming: true,
|
||||||
|
abortSignal: abort.signal,
|
||||||
|
maxSteps: 1000,
|
||||||
|
providerOptions: model.info.options,
|
||||||
|
messages: [
|
||||||
|
...system.map(
|
||||||
|
(x): CoreMessage => ({
|
||||||
|
role: "system",
|
||||||
|
content: x,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
...convertToCoreMessages(
|
||||||
|
msgs.map(toUIMessage).filter((x) => x.parts.length > 0),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
temperature: model.info.temperature ? 0 : undefined,
|
||||||
|
tools: model.info.tool_call === false ? undefined : tools,
|
||||||
|
model: wrapLanguageModel({
|
||||||
|
model: model.language,
|
||||||
|
middleware: [
|
||||||
|
{
|
||||||
|
async transformParams(args) {
|
||||||
|
if (args.type === "stream") {
|
||||||
|
args.params.prompt = ProviderTransform.message(
|
||||||
|
args.params.prompt,
|
||||||
|
input.providerID,
|
||||||
|
input.modelID,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return args.params
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
for await (const value of result.fullStream) {
|
||||||
l.info("part", {
|
l.info("part", {
|
||||||
type: value.type,
|
type: value.type,
|
||||||
})
|
})
|
||||||
switch (value.type) {
|
switch (value.type) {
|
||||||
case "text":
|
case "step-start":
|
||||||
|
next.parts.push({
|
||||||
|
type: "step-start",
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case "text-delta":
|
||||||
if (!text) {
|
if (!text) {
|
||||||
text = value
|
text = {
|
||||||
next.parts.push(value)
|
type: "text",
|
||||||
|
text: value.textDelta,
|
||||||
|
}
|
||||||
|
next.parts.push(text)
|
||||||
break
|
break
|
||||||
} else text.text += value.text
|
} else text.text += value.textDelta
|
||||||
break
|
break
|
||||||
|
|
||||||
case "tool-call": {
|
case "tool-call": {
|
||||||
const [match] = next.parts.flatMap((p) =>
|
const [match] = next.parts.flatMap((p) =>
|
||||||
p.type === "tool-invocation" &&
|
p.type === "tool-invocation" &&
|
||||||
p.toolInvocation.toolCallId === value.toolCallId
|
p.toolInvocation.toolCallId === value.toolCallId
|
||||||
? [p]
|
? [p]
|
||||||
: [],
|
: [],
|
||||||
)
|
)
|
||||||
@@ -443,20 +605,27 @@ export namespace Session {
|
|||||||
break
|
break
|
||||||
|
|
||||||
case "tool-call-delta":
|
case "tool-call-delta":
|
||||||
break
|
continue
|
||||||
|
|
||||||
|
// for some reason ai sdk claims to not send this part but it does
|
||||||
|
// @ts-expect-error
|
||||||
case "tool-result":
|
case "tool-result":
|
||||||
const match = next.parts.find(
|
const match = next.parts.find(
|
||||||
(p) =>
|
(p) =>
|
||||||
p.type === "tool-invocation" &&
|
p.type === "tool-invocation" &&
|
||||||
|
// @ts-expect-error
|
||||||
p.toolInvocation.toolCallId === value.toolCallId,
|
p.toolInvocation.toolCallId === value.toolCallId,
|
||||||
)
|
)
|
||||||
if (match && match.type === "tool-invocation") {
|
if (match && match.type === "tool-invocation") {
|
||||||
match.toolInvocation = {
|
match.toolInvocation = {
|
||||||
|
// @ts-expect-error
|
||||||
args: value.args,
|
args: value.args,
|
||||||
|
// @ts-expect-error
|
||||||
toolCallId: value.toolCallId,
|
toolCallId: value.toolCallId,
|
||||||
|
// @ts-expect-error
|
||||||
toolName: value.toolName,
|
toolName: value.toolName,
|
||||||
state: "result",
|
state: "result",
|
||||||
|
// @ts-expect-error
|
||||||
result: value.result as string,
|
result: value.result as string,
|
||||||
}
|
}
|
||||||
Bus.publish(Message.Event.PartUpdated, {
|
Bus.publish(Message.Event.PartUpdated, {
|
||||||
@@ -471,82 +640,40 @@ export namespace Session {
|
|||||||
l.info("unhandled", {
|
l.info("unhandled", {
|
||||||
type: value.type,
|
type: value.type,
|
||||||
})
|
})
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
await updateMessage(next)
|
await updateMessage(next)
|
||||||
},
|
}
|
||||||
async onFinish(input) {
|
} catch (e: any) {
|
||||||
const assistant = next.metadata!.assistant!
|
log.error("stream error", {
|
||||||
const usage = getUsage(input.totalUsage, model.info)
|
error: e,
|
||||||
assistant.cost = usage.cost
|
})
|
||||||
await updateMessage(next)
|
switch (true) {
|
||||||
},
|
case LoadAPIKeyError.isInstance(e):
|
||||||
onError(err) {
|
next.metadata.error = new Provider.AuthError(
|
||||||
log.error("error", err)
|
{
|
||||||
switch (true) {
|
providerID: input.providerID,
|
||||||
case LoadAPIKeyError.isInstance(err.error):
|
message: e.message,
|
||||||
next.metadata.error = new Provider.AuthError(
|
},
|
||||||
{
|
{ cause: e },
|
||||||
providerID: input.providerID,
|
).toObject()
|
||||||
message: err.error.message,
|
break
|
||||||
},
|
case e instanceof Error:
|
||||||
{ cause: err.error },
|
next.metadata.error = new NamedError.Unknown(
|
||||||
).toObject()
|
{ message: e.toString() },
|
||||||
break
|
{ cause: e },
|
||||||
case err.error instanceof Error:
|
).toObject()
|
||||||
next.metadata.error = new NamedError.Unknown(
|
break
|
||||||
{ message: err.error.toString() },
|
default:
|
||||||
{ cause: err.error },
|
next.metadata.error = new NamedError.Unknown(
|
||||||
).toObject()
|
{ message: JSON.stringify(e) },
|
||||||
break
|
{ cause: e },
|
||||||
default:
|
)
|
||||||
next.metadata.error = new NamedError.Unknown(
|
}
|
||||||
{ message: JSON.stringify(err.error) },
|
Bus.publish(Event.Error, {
|
||||||
{ cause: err.error },
|
error: next.metadata.error,
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
Bus.publish(Event.Error, {
|
|
||||||
error: next.metadata.error,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
async prepareStep(step) {
|
|
||||||
next.parts.push({
|
|
||||||
type: "step-start",
|
|
||||||
})
|
|
||||||
await updateMessage(next)
|
|
||||||
return step
|
|
||||||
},
|
|
||||||
toolCallStreaming: true,
|
|
||||||
abortSignal: abort.signal,
|
|
||||||
stopWhen: stepCountIs(1000),
|
|
||||||
messages: convertToModelMessages([
|
|
||||||
...system.map(
|
|
||||||
(x): UIMessage => ({
|
|
||||||
id: Identifier.ascending("message"),
|
|
||||||
role: "system",
|
|
||||||
parts: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: x,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
...msgs,
|
|
||||||
]),
|
|
||||||
temperature: model.info.id === "codex-mini-latest" ? undefined : 0,
|
|
||||||
tools: {
|
|
||||||
...(await MCP.tools()),
|
|
||||||
...tools,
|
|
||||||
},
|
|
||||||
model: model.language,
|
|
||||||
})
|
|
||||||
await result.consumeStream({
|
|
||||||
onError: (err) => {
|
|
||||||
log.error("stream error", {
|
|
||||||
err,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
next.metadata!.time.completed = Date.now()
|
next.metadata!.time.completed = Date.now()
|
||||||
for (const part of next.parts) {
|
for (const part of next.parts) {
|
||||||
if (
|
if (
|
||||||
@@ -600,6 +727,7 @@ export namespace Session {
|
|||||||
input: 0,
|
input: 0,
|
||||||
output: 0,
|
output: 0,
|
||||||
reasoning: 0,
|
reasoning: 0,
|
||||||
|
cache: { read: 0, write: 0 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
time: {
|
time: {
|
||||||
@@ -608,70 +736,115 @@ export namespace Session {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
await updateMessage(next)
|
await updateMessage(next)
|
||||||
const result = await generateText({
|
|
||||||
|
let text: Message.TextPart | undefined
|
||||||
|
const result = streamText({
|
||||||
abortSignal: abort.signal,
|
abortSignal: abort.signal,
|
||||||
model: model.language,
|
model: model.language,
|
||||||
messages: convertToModelMessages([
|
messages: [
|
||||||
...system.map(
|
...system.map(
|
||||||
(x): UIMessage => ({
|
(x): CoreMessage => ({
|
||||||
id: Identifier.ascending("message"),
|
|
||||||
role: "system",
|
role: "system",
|
||||||
parts: [
|
content: x,
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: x,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
...filtered,
|
...convertToCoreMessages(filtered.map(toUIMessage)),
|
||||||
{
|
{
|
||||||
role: "user",
|
role: "user",
|
||||||
parts: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: "Provide a detailed but concise summary of our conversation above. Focus on information that would be helpful for continuing the conversation, including what we did, what we're doing, which files we're working on, and what we're going to do next.",
|
text: "Provide a detailed but concise summary of our conversation above. Focus on information that would be helpful for continuing the conversation, including what we did, what we're doing, which files we're working on, and what we're going to do next.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]),
|
],
|
||||||
|
onStepFinish: async (step) => {
|
||||||
|
const assistant = next.metadata!.assistant!
|
||||||
|
const usage = getUsage(model.info, step.usage, step.providerMetadata)
|
||||||
|
assistant.cost += usage.cost
|
||||||
|
assistant.tokens = usage.tokens
|
||||||
|
await updateMessage(next)
|
||||||
|
if (text) {
|
||||||
|
Bus.publish(Message.Event.PartUpdated, {
|
||||||
|
part: text,
|
||||||
|
messageID: next.id,
|
||||||
|
sessionID: next.metadata.sessionID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
text = undefined
|
||||||
|
},
|
||||||
|
async onFinish(input) {
|
||||||
|
const assistant = next.metadata!.assistant!
|
||||||
|
const usage = getUsage(model.info, input.usage, input.providerMetadata)
|
||||||
|
assistant.cost = usage.cost
|
||||||
|
assistant.tokens = usage.tokens
|
||||||
|
next.metadata!.time.completed = Date.now()
|
||||||
|
await updateMessage(next)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
next.parts.push({
|
|
||||||
type: "text",
|
for await (const value of result.fullStream) {
|
||||||
text: result.text,
|
switch (value.type) {
|
||||||
})
|
case "text-delta":
|
||||||
const assistant = next.metadata!.assistant!
|
if (!text) {
|
||||||
const usage = getUsage(result.usage, model.info)
|
text = {
|
||||||
assistant.cost = usage.cost
|
type: "text",
|
||||||
assistant.tokens = usage.tokens
|
text: value.textDelta,
|
||||||
await updateMessage(next)
|
}
|
||||||
|
next.parts.push(text)
|
||||||
|
} else text.text += value.textDelta
|
||||||
|
|
||||||
|
await updateMessage(next)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const pending = new Map<string, AbortController>()
|
|
||||||
function lock(sessionID: string) {
|
function lock(sessionID: string) {
|
||||||
log.info("locking", { sessionID })
|
log.info("locking", { sessionID })
|
||||||
if (pending.has(sessionID)) throw new BusyError(sessionID)
|
if (state().pending.has(sessionID)) throw new BusyError(sessionID)
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
pending.set(sessionID, controller)
|
state().pending.set(sessionID, controller)
|
||||||
return {
|
return {
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
[Symbol.dispose]() {
|
[Symbol.dispose]() {
|
||||||
log.info("unlocking", { sessionID })
|
log.info("unlocking", { sessionID })
|
||||||
pending.delete(sessionID)
|
state().pending.delete(sessionID)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUsage(usage: LanguageModelUsage, model: Provider.Model) {
|
function getUsage(
|
||||||
|
model: ModelsDev.Model,
|
||||||
|
usage: LanguageModelUsage,
|
||||||
|
metadata?: ProviderMetadata,
|
||||||
|
) {
|
||||||
const tokens = {
|
const tokens = {
|
||||||
input: usage.inputTokens ?? 0,
|
input: usage.promptTokens ?? 0,
|
||||||
output: usage.outputTokens ?? 0,
|
output: usage.completionTokens ?? 0,
|
||||||
reasoning: usage.reasoningTokens ?? 0,
|
reasoning: 0,
|
||||||
|
cache: {
|
||||||
|
write: (metadata?.["anthropic"]?.["cacheCreationInputTokens"] ??
|
||||||
|
0) as number,
|
||||||
|
read: (metadata?.["anthropic"]?.["cacheReadInputTokens"] ??
|
||||||
|
0) as number,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
cost: new Decimal(0)
|
cost: new Decimal(0)
|
||||||
.add(new Decimal(tokens.input).mul(model.cost.input).div(1_000_000))
|
.add(new Decimal(tokens.input).mul(model.cost.input).div(1_000_000))
|
||||||
.add(new Decimal(tokens.output).mul(model.cost.output).div(1_000_000))
|
.add(new Decimal(tokens.output).mul(model.cost.output).div(1_000_000))
|
||||||
|
.add(
|
||||||
|
new Decimal(tokens.cache.read)
|
||||||
|
.mul(model.cost.cache_read ?? 0)
|
||||||
|
.div(1_000_000),
|
||||||
|
)
|
||||||
|
.add(
|
||||||
|
new Decimal(tokens.cache.write)
|
||||||
|
.mul(model.cost.cache_write ?? 0)
|
||||||
|
.div(1_000_000),
|
||||||
|
)
|
||||||
.toNumber(),
|
.toNumber(),
|
||||||
tokens,
|
tokens,
|
||||||
}
|
}
|
||||||
@@ -703,3 +876,57 @@ export namespace Session {
|
|||||||
await App.initialize()
|
await App.initialize()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toUIMessage(msg: Message.Info): UIMessage {
|
||||||
|
if (msg.role === "assistant") {
|
||||||
|
return {
|
||||||
|
id: msg.id,
|
||||||
|
role: "assistant",
|
||||||
|
content: "",
|
||||||
|
parts: toParts(msg.parts),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.role === "user") {
|
||||||
|
return {
|
||||||
|
id: msg.id,
|
||||||
|
role: "user",
|
||||||
|
content: "",
|
||||||
|
parts: toParts(msg.parts),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
function toParts(parts: Message.Part[]): UIMessage["parts"] {
|
||||||
|
const result: UIMessage["parts"] = []
|
||||||
|
for (const part of parts) {
|
||||||
|
switch (part.type) {
|
||||||
|
case "text":
|
||||||
|
result.push({ type: "text", text: part.text })
|
||||||
|
break
|
||||||
|
case "file":
|
||||||
|
result.push({
|
||||||
|
type: "file",
|
||||||
|
data: part.url,
|
||||||
|
mimeType: part.mediaType,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case "tool-invocation":
|
||||||
|
result.push({
|
||||||
|
type: "tool-invocation",
|
||||||
|
toolInvocation: part.toolInvocation,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case "step-start":
|
||||||
|
result.push({
|
||||||
|
type: "step-start",
|
||||||
|
})
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|||||||
@@ -135,49 +135,55 @@ export namespace Message {
|
|||||||
id: z.string(),
|
id: z.string(),
|
||||||
role: z.enum(["user", "assistant"]),
|
role: z.enum(["user", "assistant"]),
|
||||||
parts: z.array(Part),
|
parts: z.array(Part),
|
||||||
metadata: z.object({
|
metadata: z
|
||||||
time: z.object({
|
.object({
|
||||||
created: z.number(),
|
time: z.object({
|
||||||
completed: z.number().optional(),
|
created: z.number(),
|
||||||
}),
|
completed: z.number().optional(),
|
||||||
error: z
|
}),
|
||||||
.discriminatedUnion("name", [
|
error: z
|
||||||
Provider.AuthError.Schema,
|
.discriminatedUnion("name", [
|
||||||
NamedError.Unknown.Schema,
|
Provider.AuthError.Schema,
|
||||||
])
|
NamedError.Unknown.Schema,
|
||||||
.optional(),
|
])
|
||||||
sessionID: z.string(),
|
.optional(),
|
||||||
tool: z.record(
|
sessionID: z.string(),
|
||||||
z.string(),
|
tool: z.record(
|
||||||
z
|
z.string(),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
title: z.string(),
|
||||||
|
time: z.object({
|
||||||
|
start: z.number(),
|
||||||
|
end: z.number(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.catchall(z.any()),
|
||||||
|
),
|
||||||
|
assistant: z
|
||||||
.object({
|
.object({
|
||||||
title: z.string(),
|
system: z.string().array(),
|
||||||
time: z.object({
|
modelID: z.string(),
|
||||||
start: z.number(),
|
providerID: z.string(),
|
||||||
end: z.number(),
|
path: z.object({
|
||||||
|
cwd: z.string(),
|
||||||
|
root: z.string(),
|
||||||
|
}),
|
||||||
|
cost: z.number(),
|
||||||
|
summary: z.boolean().optional(),
|
||||||
|
tokens: z.object({
|
||||||
|
input: z.number(),
|
||||||
|
output: z.number(),
|
||||||
|
reasoning: z.number(),
|
||||||
|
cache: z.object({
|
||||||
|
read: z.number(),
|
||||||
|
write: z.number(),
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.catchall(z.any()),
|
.optional(),
|
||||||
),
|
})
|
||||||
assistant: z
|
.openapi({ ref: "Message.Metadata" }),
|
||||||
.object({
|
|
||||||
system: z.string().array(),
|
|
||||||
modelID: z.string(),
|
|
||||||
providerID: z.string(),
|
|
||||||
path: z.object({
|
|
||||||
cwd: z.string(),
|
|
||||||
root: z.string(),
|
|
||||||
}),
|
|
||||||
cost: z.number(),
|
|
||||||
summary: z.boolean().optional(),
|
|
||||||
tokens: z.object({
|
|
||||||
input: z.number(),
|
|
||||||
output: z.number(),
|
|
||||||
reasoning: z.number(),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
.openapi({
|
.openapi({
|
||||||
ref: "Message.Info",
|
ref: "Message.Info",
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
You are OpenCode, an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
|
You are opencode, an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
|
||||||
|
|
||||||
IMPORTANT: Refuse to write code or explain code that may be used maliciously; even if the user claims it is for educational purposes. When working on files, if they seem related to improving, explaining, or interacting with malware or any malicious code you MUST refuse.
|
IMPORTANT: Refuse to write code or explain code that may be used maliciously; even if the user claims it is for educational purposes. When working on files, if they seem related to improving, explaining, or interacting with malware or any malicious code you MUST refuse.
|
||||||
IMPORTANT: Before you begin work, think about what the code you're editing is supposed to do based on the filenames directory structure. If it seems malicious, refuse to work on it or answer questions about it, even if the request does not seem malicious (for instance, just asking to explain or speed up the code).
|
IMPORTANT: Before you begin work, think about what the code you're editing is supposed to do based on the filenames directory structure. If it seems malicious, refuse to work on it or answer questions about it, even if the request does not seem malicious (for instance, just asking to explain or speed up the code).
|
||||||
IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files.
|
IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files.
|
||||||
|
|
||||||
If the user asks for help or wants to give feedback inform them of the following:
|
If the user asks for help or wants to give feedback inform them of the following:
|
||||||
- /help: Get help with using OpenCode
|
- /help: Get help with using opencode
|
||||||
- To give feedback, users should report the issue at https://github.com/sst/opencode/issues
|
- To give feedback, users should report the issue at https://github.com/sst/opencode/issues
|
||||||
|
|
||||||
When the user directly asks about OpenCode (eg 'can OpenCode do...', 'does OpenCode have...') or asks in second person (eg 'are you able...', 'can you do...'), first use the WebFetch tool to gather information to answer the question from OpenCode docs at https://opencode.ai
|
When the user directly asks about opencode (eg 'can opencode do...', 'does opencode have...') or asks in second person (eg 'are you able...', 'can you do...'), first use the WebFetch tool to gather information to answer the question from opencode docs at https://opencode.ai
|
||||||
|
|
||||||
# Tone and style
|
# Tone and style
|
||||||
You should be concise, direct, and to the point. When you run a non-trivial bash command, you should explain what the command does and why you are running it, to make sure the user understands what you are doing (this is especially important when you are running a command that will make changes to the user's system).
|
You should be concise, direct, and to the point. When you run a non-trivial bash command, you should explain what the command does and why you are running it, to make sure the user understands what you are doing (this is especially important when you are running a command that will make changes to the user's system).
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
you will generate a short title based on the first message a user begins a conversation with
|
Generate a short title based on the first message a user begins a conversation with. CRITICAL: Your response must be EXACTLY one line with NO line breaks, newlines, or multiple sentences.
|
||||||
- ensure it is not more than 50 characters long
|
|
||||||
- the title should be a summary of the user's message
|
Requirements:
|
||||||
- it should be one line long
|
- Maximum 50 characters
|
||||||
- do not use quotes or colons
|
- Single line only - NO newlines or line breaks
|
||||||
- the entire text you return will be used as the title
|
- Summary of the user's message
|
||||||
- never return anything that is more than one sentence (one line) long
|
- No quotes, colons, or special formatting
|
||||||
|
- Do not include explanatory text like "summary:" or similar
|
||||||
|
- Your entire response becomes the title
|
||||||
|
|
||||||
|
IMPORTANT: Return only the title text on a single line. Do not add any explanations, formatting, or additional text.
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { App } from "../app/app"
|
import { App } from "../app/app"
|
||||||
import { ListTool } from "../tool/ls"
|
import { Ripgrep } from "../external/ripgrep"
|
||||||
|
import { Global } from "../global"
|
||||||
import { Filesystem } from "../util/filesystem"
|
import { Filesystem } from "../util/filesystem"
|
||||||
|
import path from "path"
|
||||||
|
import os from "os"
|
||||||
|
|
||||||
import PROMPT_ANTHROPIC from "./prompt/anthropic.txt"
|
import PROMPT_ANTHROPIC from "./prompt/anthropic.txt"
|
||||||
import PROMPT_ANTHROPIC_SPOOF from "./prompt/anthropic_spoof.txt"
|
import PROMPT_ANTHROPIC_SPOOF from "./prompt/anthropic_spoof.txt"
|
||||||
@@ -22,8 +25,57 @@ export namespace SystemPrompt {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function environment(sessionID: string) {
|
export async function environment() {
|
||||||
const app = App.info()
|
const app = App.info()
|
||||||
|
|
||||||
|
;async () => {
|
||||||
|
const files = await Ripgrep.files({
|
||||||
|
cwd: app.path.cwd,
|
||||||
|
})
|
||||||
|
type Node = {
|
||||||
|
children: Record<string, Node>
|
||||||
|
}
|
||||||
|
const root: Node = {
|
||||||
|
children: {},
|
||||||
|
}
|
||||||
|
for (const file of files) {
|
||||||
|
const parts = file.split("/")
|
||||||
|
let node = root
|
||||||
|
for (const part of parts) {
|
||||||
|
const existing = node.children[part]
|
||||||
|
if (existing) {
|
||||||
|
node = existing
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
node.children[part] = {
|
||||||
|
children: {},
|
||||||
|
}
|
||||||
|
node = node.children[part]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(path: string[], node: Node): string {
|
||||||
|
// if (path.length === 3) return "\t".repeat(path.length) + "..."
|
||||||
|
const lines: string[] = []
|
||||||
|
const entries = Object.entries(node.children).sort(([a], [b]) =>
|
||||||
|
a.localeCompare(b),
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const [name, child] of entries) {
|
||||||
|
const currentPath = [...path, name]
|
||||||
|
const indent = "\t".repeat(path.length)
|
||||||
|
const hasChildren = Object.keys(child.children).length > 0
|
||||||
|
lines.push(`${indent}${name}` + (hasChildren ? "/" : ""))
|
||||||
|
|
||||||
|
if (hasChildren) lines.push(render(currentPath, child))
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join("\n")
|
||||||
|
}
|
||||||
|
const result = render([], root)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
[
|
[
|
||||||
`Here is some useful information about the environment you are running in:`,
|
`Here is some useful information about the environment you are running in:`,
|
||||||
@@ -33,9 +85,9 @@ export namespace SystemPrompt {
|
|||||||
` Platform: ${process.platform}`,
|
` Platform: ${process.platform}`,
|
||||||
` Today's date: ${new Date().toDateString()}`,
|
` Today's date: ${new Date().toDateString()}`,
|
||||||
`</env>`,
|
`</env>`,
|
||||||
`<project>`,
|
// `<project>`,
|
||||||
` ${app.git ? await ListTool.execute({ path: app.path.cwd, ignore: [] }, { sessionID: sessionID, messageID: "", abort: AbortSignal.any([]) }).then((x) => x.output) : ""}`,
|
// ` ${app.git ? await tree() : ""}`,
|
||||||
`</project>`,
|
// `</project>`,
|
||||||
].join("\n"),
|
].join("\n"),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -52,7 +104,17 @@ export namespace SystemPrompt {
|
|||||||
const matches = await Filesystem.findUp(item, cwd, root)
|
const matches = await Filesystem.findUp(item, cwd, root)
|
||||||
found.push(...matches.map((x) => Bun.file(x).text()))
|
found.push(...matches.map((x) => Bun.file(x).text()))
|
||||||
}
|
}
|
||||||
return Promise.all(found)
|
found.push(
|
||||||
|
Bun.file(path.join(Global.Path.config, "AGENTS.md"))
|
||||||
|
.text()
|
||||||
|
.catch(() => ""),
|
||||||
|
)
|
||||||
|
found.push(
|
||||||
|
Bun.file(path.join(os.homedir(), ".claude", "CLAUDE.md"))
|
||||||
|
.text()
|
||||||
|
.catch(() => ""),
|
||||||
|
)
|
||||||
|
return Promise.all(found).then((result) => result.filter(Boolean))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function summarize(providerID: string) {
|
export function summarize(providerID: string) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { App } from "../app/app"
|
import { App } from "../app/app"
|
||||||
import { Bus } from "../bus"
|
import { Bus } from "../bus"
|
||||||
|
import { Installation } from "../installation"
|
||||||
import { Session } from "../session"
|
import { Session } from "../session"
|
||||||
import { Storage } from "../storage/storage"
|
import { Storage } from "../storage/storage"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
@@ -19,10 +20,11 @@ export namespace Share {
|
|||||||
export async function sync(key: string, content: any) {
|
export async function sync(key: string, content: any) {
|
||||||
const [root, ...splits] = key.split("/")
|
const [root, ...splits] = key.split("/")
|
||||||
if (root !== "session") return
|
if (root !== "session") return
|
||||||
const [, sessionID] = splits
|
const [sub, sessionID] = splits
|
||||||
const session = await Session.get(sessionID)
|
if (sub === "share") return
|
||||||
if (!session.share) return
|
const share = await Session.getShare(sessionID).catch(() => {})
|
||||||
const { secret } = session.share
|
if (!share) return
|
||||||
|
const { secret } = share
|
||||||
pending.set(key, content)
|
pending.set(key, content)
|
||||||
queue = queue
|
queue = queue
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
@@ -55,7 +57,10 @@ export namespace Share {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const URL =
|
export const URL =
|
||||||
process.env["OPENCODE_API"] ?? "https://api.dev.opencode.ai"
|
process.env["OPENCODE_API"] ??
|
||||||
|
(Installation.isSnapshot() || Installation.isDev()
|
||||||
|
? "https://api.dev.opencode.ai"
|
||||||
|
: "https://api.opencode.ai")
|
||||||
|
|
||||||
export async function create(sessionID: string) {
|
export async function create(sessionID: string) {
|
||||||
return fetch(`${URL}/share_create`, {
|
return fetch(`${URL}/share_create`, {
|
||||||
@@ -65,4 +70,11 @@ export namespace Share {
|
|||||||
.then((x) => x.json())
|
.then((x) => x.json())
|
||||||
.then((x) => x as { url: string; secret: string })
|
.then((x) => x as { url: string; secret: string })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function remove(id: string) {
|
||||||
|
return fetch(`${URL}/share_delete`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ id }),
|
||||||
|
}).then((x) => x.json())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,10 @@ export namespace Storage {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const locks = new Map<string, Promise<void>>()
|
export async function remove(key: string) {
|
||||||
|
const target = path.join(state().dir, key + ".json")
|
||||||
|
await fs.unlink(target).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
export async function readJSON<T>(key: string) {
|
export async function readJSON<T>(key: string) {
|
||||||
return Bun.file(path.join(state().dir, key + ".json")).json() as Promise<T>
|
return Bun.file(path.join(state().dir, key + ".json")).json() as Promise<T>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const DEFAULT_TIMEOUT = 1 * 60 * 1000
|
|||||||
const MAX_TIMEOUT = 10 * 60 * 1000
|
const MAX_TIMEOUT = 10 * 60 * 1000
|
||||||
|
|
||||||
export const BashTool = Tool.define({
|
export const BashTool = Tool.define({
|
||||||
id: "opencode.bash",
|
id: "bash",
|
||||||
description: DESCRIPTION,
|
description: DESCRIPTION,
|
||||||
parameters: z.object({
|
parameters: z.object({
|
||||||
command: z.string().describe("The command to execute"),
|
command: z.string().describe("The command to execute"),
|
||||||
@@ -35,7 +35,7 @@ export const BashTool = Tool.define({
|
|||||||
.min(0)
|
.min(0)
|
||||||
.max(MAX_TIMEOUT)
|
.max(MAX_TIMEOUT)
|
||||||
.describe("Optional timeout in milliseconds")
|
.describe("Optional timeout in milliseconds")
|
||||||
.nullable(),
|
.optional(),
|
||||||
description: z
|
description: z
|
||||||
.string()
|
.string()
|
||||||
.describe(
|
.describe(
|
||||||
@@ -63,10 +63,18 @@ export const BashTool = Tool.define({
|
|||||||
metadata: {
|
metadata: {
|
||||||
stderr,
|
stderr,
|
||||||
stdout,
|
stdout,
|
||||||
|
exit: process.exitCode,
|
||||||
description: params.description,
|
description: params.description,
|
||||||
title: params.command,
|
title: params.command,
|
||||||
},
|
},
|
||||||
output: stdout.replaceAll(/\x1b\[[0-9;]*m/g, ""),
|
output: [
|
||||||
|
`<stdout>`,
|
||||||
|
stdout ?? "",
|
||||||
|
`</stdout>`,
|
||||||
|
`<stderr>`,
|
||||||
|
stderr ?? "",
|
||||||
|
`</stderr>`,
|
||||||
|
].join("\n"),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ Usage notes:
|
|||||||
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
|
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
|
||||||
- If the output exceeds 30000 characters, output will be truncated before being returned to you.
|
- If the output exceeds 30000 characters, output will be truncated before being returned to you.
|
||||||
- VERY IMPORTANT: You MUST avoid using search commands like `find` and `grep`. Instead use Grep, Glob, or Task to search. You MUST avoid read tools like `cat`, `head`, `tail`, and `ls`, and use Read and LS to read files.
|
- VERY IMPORTANT: You MUST avoid using search commands like `find` and `grep`. Instead use Grep, Glob, or Task to search. You MUST avoid read tools like `cat`, `head`, `tail`, and `ls`, and use Read and LS to read files.
|
||||||
- If you _still_ need to run `grep`, STOP. ALWAYS USE ripgrep at `rg` (or /usr/bin/rg) first, which all OpenCode users have pre-installed.
|
- If you _still_ need to run `grep`, STOP. ALWAYS USE ripgrep at `rg` (or /usr/bin/rg) first, which all opencode users have pre-installed.
|
||||||
- When issuing multiple commands, use the ';' or '&&' operator to separate them. DO NOT use newlines (newlines are ok in quoted strings).
|
- When issuing multiple commands, use the ';' or '&&' operator to separate them. DO NOT use newlines (newlines are ok in quoted strings).
|
||||||
- Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of `cd`. You may use `cd` if the User explicitly requests it.
|
- Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of `cd`. You may use `cd` if the User explicitly requests it.
|
||||||
<good-example>
|
<good-example>
|
||||||
@@ -60,9 +60,9 @@ When the user asks you to create a new git commit, follow these steps carefully:
|
|||||||
3. You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance. ALWAYS run the following commands in parallel:
|
3. You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance. ALWAYS run the following commands in parallel:
|
||||||
- Add relevant untracked files to the staging area.
|
- Add relevant untracked files to the staging area.
|
||||||
- Create the commit with a message ending with:
|
- Create the commit with a message ending with:
|
||||||
🤖 Generated with [OpenCode](https://opencode.ai)
|
🤖 Generated with [opencode](https://opencode.ai)
|
||||||
|
|
||||||
Co-Authored-By: OpenCode <noreply@opencode.ai>
|
Co-Authored-By: opencode <noreply@opencode.ai>
|
||||||
- Run git status to make sure the commit succeeded.
|
- Run git status to make sure the commit succeeded.
|
||||||
|
|
||||||
4. If the commit fails due to pre-commit hook changes, retry the commit ONCE to include these automated changes. If it fails again, it usually means a pre-commit hook is preventing the commit. If the commit succeeds but you notice that files were modified by the pre-commit hook, you MUST amend your commit to include them.
|
4. If the commit fails due to pre-commit hook changes, retry the commit ONCE to include these automated changes. If it fails again, it usually means a pre-commit hook is preventing the commit. If the commit succeeds but you notice that files were modified by the pre-commit hook, you MUST amend your commit to include them.
|
||||||
@@ -81,9 +81,9 @@ Important notes:
|
|||||||
git commit -m "$(cat <<'EOF'
|
git commit -m "$(cat <<'EOF'
|
||||||
Commit message here.
|
Commit message here.
|
||||||
|
|
||||||
🤖 Generated with [OpenCode](https://opencode.ai)
|
🤖 Generated with [opencode](https://opencode.ai)
|
||||||
|
|
||||||
Co-Authored-By: OpenCode <noreply@opencode.ai>
|
Co-Authored-By: opencode <noreply@opencode.ai>
|
||||||
EOF
|
EOF
|
||||||
)"
|
)"
|
||||||
</example>
|
</example>
|
||||||
@@ -128,7 +128,7 @@ gh pr create --title "the pr title" --body "$(cat <<'EOF'
|
|||||||
## Test plan
|
## Test plan
|
||||||
[Checklist of TODOs for testing the pull request...]
|
[Checklist of TODOs for testing the pull request...]
|
||||||
|
|
||||||
🤖 Generated with [OpenCode](https://opencode.ai)
|
🤖 Generated with [opencode](https://opencode.ai)
|
||||||
EOF
|
EOF
|
||||||
)"
|
)"
|
||||||
</example>
|
</example>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import DESCRIPTION from "./edit.txt"
|
|||||||
import { App } from "../app/app"
|
import { App } from "../app/app"
|
||||||
|
|
||||||
export const EditTool = Tool.define({
|
export const EditTool = Tool.define({
|
||||||
id: "opencode.edit",
|
id: "edit",
|
||||||
description: DESCRIPTION,
|
description: DESCRIPTION,
|
||||||
parameters: z.object({
|
parameters: z.object({
|
||||||
filePath: z.string().describe("The absolute path to the file to modify"),
|
filePath: z.string().describe("The absolute path to the file to modify"),
|
||||||
@@ -21,8 +21,8 @@ export const EditTool = Tool.define({
|
|||||||
),
|
),
|
||||||
replaceAll: z
|
replaceAll: z
|
||||||
.boolean()
|
.boolean()
|
||||||
.nullable()
|
.optional()
|
||||||
.describe("Replace all occurences of old_string (default false)"),
|
.describe("Replace all occurrences of old_string (default false)"),
|
||||||
}),
|
}),
|
||||||
async execute(params, ctx) {
|
async execute(params, ctx) {
|
||||||
if (!params.filePath) {
|
if (!params.filePath) {
|
||||||
@@ -35,7 +35,7 @@ export const EditTool = Tool.define({
|
|||||||
: path.join(app.path.cwd, params.filePath)
|
: path.join(app.path.cwd, params.filePath)
|
||||||
|
|
||||||
await Permission.ask({
|
await Permission.ask({
|
||||||
id: "opencode.edit",
|
id: "edit",
|
||||||
sessionID: ctx.sessionID,
|
sessionID: ctx.sessionID,
|
||||||
title: "Edit this file: " + filepath,
|
title: "Edit this file: " + filepath,
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -87,7 +87,9 @@ export const EditTool = Tool.define({
|
|||||||
await file.write(contentNew)
|
await file.write(contentNew)
|
||||||
})()
|
})()
|
||||||
|
|
||||||
const diff = createTwoFilesPatch(filepath, filepath, contentOld, contentNew)
|
const diff = trimDiff(
|
||||||
|
createTwoFilesPatch(filepath, filepath, contentOld, contentNew),
|
||||||
|
)
|
||||||
|
|
||||||
FileTimes.read(ctx.sessionID, filepath)
|
FileTimes.read(ctx.sessionID, filepath)
|
||||||
|
|
||||||
@@ -113,3 +115,39 @@ export const EditTool = Tool.define({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function trimDiff(diff: string): string {
|
||||||
|
const lines = diff.split("\n")
|
||||||
|
const contentLines = lines.filter(
|
||||||
|
(line) =>
|
||||||
|
(line.startsWith("+") || line.startsWith("-") || line.startsWith(" ")) &&
|
||||||
|
!line.startsWith("---") &&
|
||||||
|
!line.startsWith("+++"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (contentLines.length === 0) return diff
|
||||||
|
|
||||||
|
let min = Infinity
|
||||||
|
for (const line of contentLines) {
|
||||||
|
const content = line.slice(1)
|
||||||
|
if (content.trim().length > 0) {
|
||||||
|
const match = content.match(/^(\s*)/)
|
||||||
|
if (match) min = Math.min(min, match[1].length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (min === Infinity || min === 0) return diff
|
||||||
|
const trimmedLines = lines.map((line) => {
|
||||||
|
if (
|
||||||
|
(line.startsWith("+") || line.startsWith("-") || line.startsWith(" ")) &&
|
||||||
|
!line.startsWith("---") &&
|
||||||
|
!line.startsWith("+++")
|
||||||
|
) {
|
||||||
|
const prefix = line[0]
|
||||||
|
const content = line.slice(1)
|
||||||
|
return prefix + content.slice(min)
|
||||||
|
}
|
||||||
|
return line
|
||||||
|
})
|
||||||
|
|
||||||
|
return trimmedLines.join("\n")
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import { App } from "../app/app"
|
|||||||
import DESCRIPTION from "./glob.txt"
|
import DESCRIPTION from "./glob.txt"
|
||||||
|
|
||||||
export const GlobTool = Tool.define({
|
export const GlobTool = Tool.define({
|
||||||
id: "opencode.glob",
|
id: "glob",
|
||||||
description: DESCRIPTION,
|
description: DESCRIPTION,
|
||||||
parameters: z.object({
|
parameters: z.object({
|
||||||
pattern: z.string().describe("The glob pattern to match files against"),
|
pattern: z.string().describe("The glob pattern to match files against"),
|
||||||
path: z
|
path: z
|
||||||
.string()
|
.string()
|
||||||
.nullable()
|
.optional()
|
||||||
.describe(
|
.describe(
|
||||||
`The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter "undefined" or "null" - simply omit it for the default behavior. Must be a valid directory path if provided.`,
|
`The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter "undefined" or "null" - simply omit it for the default behavior. Must be a valid directory path if provided.`,
|
||||||
),
|
),
|
||||||
@@ -27,7 +27,7 @@ export const GlobTool = Tool.define({
|
|||||||
const glob = new Bun.Glob(params.pattern)
|
const glob = new Bun.Glob(params.pattern)
|
||||||
const files = []
|
const files = []
|
||||||
let truncated = false
|
let truncated = false
|
||||||
for await (const file of glob.scan({ cwd: search })) {
|
for await (const file of glob.scan({ cwd: search, dot: true })) {
|
||||||
if (files.length >= limit) {
|
if (files.length >= limit) {
|
||||||
truncated = true
|
truncated = true
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Ripgrep } from "../external/ripgrep"
|
|||||||
import DESCRIPTION from "./grep.txt"
|
import DESCRIPTION from "./grep.txt"
|
||||||
|
|
||||||
export const GrepTool = Tool.define({
|
export const GrepTool = Tool.define({
|
||||||
id: "opencode.grep",
|
id: "grep",
|
||||||
description: DESCRIPTION,
|
description: DESCRIPTION,
|
||||||
parameters: z.object({
|
parameters: z.object({
|
||||||
pattern: z
|
pattern: z
|
||||||
@@ -14,13 +14,13 @@ export const GrepTool = Tool.define({
|
|||||||
.describe("The regex pattern to search for in file contents"),
|
.describe("The regex pattern to search for in file contents"),
|
||||||
path: z
|
path: z
|
||||||
.string()
|
.string()
|
||||||
.nullable()
|
.optional()
|
||||||
.describe(
|
.describe(
|
||||||
"The directory to search in. Defaults to the current working directory.",
|
"The directory to search in. Defaults to the current working directory.",
|
||||||
),
|
),
|
||||||
include: z
|
include: z
|
||||||
.string()
|
.string()
|
||||||
.nullable()
|
.optional()
|
||||||
.describe(
|
.describe(
|
||||||
'File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")',
|
'File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")',
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { App } from "../app/app"
|
|||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import DESCRIPTION from "./ls.txt"
|
import DESCRIPTION from "./ls.txt"
|
||||||
|
|
||||||
const IGNORE_PATTERNS = [
|
export const IGNORE_PATTERNS = [
|
||||||
"node_modules/",
|
"node_modules/",
|
||||||
"__pycache__/",
|
"__pycache__/",
|
||||||
".git/",
|
".git/",
|
||||||
@@ -18,8 +18,10 @@ const IGNORE_PATTERNS = [
|
|||||||
".vscode/",
|
".vscode/",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const LIMIT = 100
|
||||||
|
|
||||||
export const ListTool = Tool.define({
|
export const ListTool = Tool.define({
|
||||||
id: "opencode.list",
|
id: "list",
|
||||||
description: DESCRIPTION,
|
description: DESCRIPTION,
|
||||||
parameters: z.object({
|
parameters: z.object({
|
||||||
path: z
|
path: z
|
||||||
@@ -27,11 +29,11 @@ export const ListTool = Tool.define({
|
|||||||
.describe(
|
.describe(
|
||||||
"The absolute path to the directory to list (must be absolute, not relative)",
|
"The absolute path to the directory to list (must be absolute, not relative)",
|
||||||
)
|
)
|
||||||
.nullable(),
|
.optional(),
|
||||||
ignore: z
|
ignore: z
|
||||||
.array(z.string())
|
.array(z.string())
|
||||||
.describe("List of glob patterns to ignore")
|
.describe("List of glob patterns to ignore")
|
||||||
.nullable(),
|
.optional(),
|
||||||
}),
|
}),
|
||||||
async execute(params) {
|
async execute(params) {
|
||||||
const app = App.info()
|
const app = App.info()
|
||||||
@@ -40,13 +42,12 @@ export const ListTool = Tool.define({
|
|||||||
const glob = new Bun.Glob("**/*")
|
const glob = new Bun.Glob("**/*")
|
||||||
const files = []
|
const files = []
|
||||||
|
|
||||||
for await (const file of glob.scan({ cwd: searchPath })) {
|
for await (const file of glob.scan({ cwd: searchPath, dot: true })) {
|
||||||
if (file.startsWith(".") || IGNORE_PATTERNS.some((p) => file.includes(p)))
|
if (IGNORE_PATTERNS.some((p) => file.includes(p))) continue
|
||||||
continue
|
|
||||||
if (params.ignore?.some((pattern) => new Bun.Glob(pattern).match(file)))
|
if (params.ignore?.some((pattern) => new Bun.Glob(pattern).match(file)))
|
||||||
continue
|
continue
|
||||||
files.push(file)
|
files.push(file)
|
||||||
if (files.length >= 1000) break
|
if (files.length >= LIMIT) break
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build directory structure
|
// Build directory structure
|
||||||
@@ -100,7 +101,7 @@ export const ListTool = Tool.define({
|
|||||||
return {
|
return {
|
||||||
metadata: {
|
metadata: {
|
||||||
count: files.length,
|
count: files.length,
|
||||||
truncated: files.length >= 1000,
|
truncated: files.length >= LIMIT,
|
||||||
title: path.relative(app.path.root, searchPath),
|
title: path.relative(app.path.root, searchPath),
|
||||||
},
|
},
|
||||||
output,
|
output,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { App } from "../app/app"
|
|||||||
import DESCRIPTION from "./lsp-diagnostics.txt"
|
import DESCRIPTION from "./lsp-diagnostics.txt"
|
||||||
|
|
||||||
export const LspDiagnosticTool = Tool.define({
|
export const LspDiagnosticTool = Tool.define({
|
||||||
id: "opencode.lsp_diagnostics",
|
id: "lsp_diagnostics",
|
||||||
description: DESCRIPTION,
|
description: DESCRIPTION,
|
||||||
parameters: z.object({
|
parameters: z.object({
|
||||||
path: z.string().describe("The path to the file to get diagnostics."),
|
path: z.string().describe("The path to the file to get diagnostics."),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { App } from "../app/app"
|
|||||||
import DESCRIPTION from "./lsp-hover.txt"
|
import DESCRIPTION from "./lsp-hover.txt"
|
||||||
|
|
||||||
export const LspHoverTool = Tool.define({
|
export const LspHoverTool = Tool.define({
|
||||||
id: "opencode.lsp_hover",
|
id: "lsp_hover",
|
||||||
description: DESCRIPTION,
|
description: DESCRIPTION,
|
||||||
parameters: z.object({
|
parameters: z.object({
|
||||||
file: z.string().describe("The path to the file to get diagnostics."),
|
file: z.string().describe("The path to the file to get diagnostics."),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import path from "path"
|
|||||||
import { App } from "../app/app"
|
import { App } from "../app/app"
|
||||||
|
|
||||||
export const MultiEditTool = Tool.define({
|
export const MultiEditTool = Tool.define({
|
||||||
id: "opencode.multiedit",
|
id: "multiedit",
|
||||||
description: DESCRIPTION,
|
description: DESCRIPTION,
|
||||||
parameters: z.object({
|
parameters: z.object({
|
||||||
filePath: z.string().describe("The absolute path to the file to modify"),
|
filePath: z.string().describe("The absolute path to the file to modify"),
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ To make multiple file edits, provide the following:
|
|||||||
2. edits: An array of edit operations to perform, where each edit contains:
|
2. edits: An array of edit operations to perform, where each edit contains:
|
||||||
- old_string: The text to replace (must match the file contents exactly, including all whitespace and indentation)
|
- old_string: The text to replace (must match the file contents exactly, including all whitespace and indentation)
|
||||||
- new_string: The edited text to replace the old_string
|
- new_string: The edited text to replace the old_string
|
||||||
- replace_all: Replace all occurences of old_string. This parameter is optional and defaults to false.
|
- replace_all: Replace all occurrences of old_string. This parameter is optional and defaults to false.
|
||||||
|
|
||||||
IMPORTANT:
|
IMPORTANT:
|
||||||
- All edits are applied in sequence, in the order they are provided
|
- All edits are applied in sequence, in the order they are provided
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import * as fs from "fs/promises"
|
|||||||
import { Tool } from "./tool"
|
import { Tool } from "./tool"
|
||||||
import { FileTimes } from "./util/file-times"
|
import { FileTimes } from "./util/file-times"
|
||||||
import DESCRIPTION from "./patch.txt"
|
import DESCRIPTION from "./patch.txt"
|
||||||
import { App } from "../app/app"
|
|
||||||
|
|
||||||
const PatchParams = z.object({
|
const PatchParams = z.object({
|
||||||
patchText: z
|
patchText: z
|
||||||
@@ -233,7 +232,7 @@ async function applyCommit(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const PatchTool = Tool.define({
|
export const PatchTool = Tool.define({
|
||||||
id: "opencode.patch",
|
id: "patch",
|
||||||
description: DESCRIPTION,
|
description: DESCRIPTION,
|
||||||
parameters: PatchParams,
|
parameters: PatchParams,
|
||||||
execute: async (params, ctx) => {
|
execute: async (params, ctx) => {
|
||||||
|
|||||||
@@ -12,18 +12,18 @@ const DEFAULT_READ_LIMIT = 2000
|
|||||||
const MAX_LINE_LENGTH = 2000
|
const MAX_LINE_LENGTH = 2000
|
||||||
|
|
||||||
export const ReadTool = Tool.define({
|
export const ReadTool = Tool.define({
|
||||||
id: "opencode.read",
|
id: "read",
|
||||||
description: DESCRIPTION,
|
description: DESCRIPTION,
|
||||||
parameters: z.object({
|
parameters: z.object({
|
||||||
filePath: z.string().describe("The path to the file to read"),
|
filePath: z.string().describe("The path to the file to read"),
|
||||||
offset: z
|
offset: z
|
||||||
.number()
|
.number()
|
||||||
.describe("The line number to start reading from (0-based)")
|
.describe("The line number to start reading from (0-based)")
|
||||||
.nullable(),
|
.optional(),
|
||||||
limit: z
|
limit: z
|
||||||
.number()
|
.number()
|
||||||
.describe("The number of lines to read (defaults to 2000)")
|
.describe("The number of lines to read (defaults to 2000)")
|
||||||
.nullable(),
|
.optional(),
|
||||||
}),
|
}),
|
||||||
async execute(params, ctx) {
|
async execute(params, ctx) {
|
||||||
let filePath = params.filePath
|
let filePath = params.filePath
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import { Tool } from "./tool"
|
|||||||
import DESCRIPTION from "./task.txt"
|
import DESCRIPTION from "./task.txt"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { Session } from "../session"
|
import { Session } from "../session"
|
||||||
|
import { Bus } from "../bus"
|
||||||
|
import { Message } from "../session/message"
|
||||||
|
|
||||||
export const TaskTool = Tool.define({
|
export const TaskTool = Tool.define({
|
||||||
id: "opencode.task",
|
id: "task",
|
||||||
description: DESCRIPTION,
|
description: DESCRIPTION,
|
||||||
parameters: z.object({
|
parameters: z.object({
|
||||||
description: z
|
description: z
|
||||||
@@ -17,6 +19,31 @@ export const TaskTool = Tool.define({
|
|||||||
const msg = await Session.getMessage(ctx.sessionID, ctx.messageID)
|
const msg = await Session.getMessage(ctx.sessionID, ctx.messageID)
|
||||||
const metadata = msg.metadata.assistant!
|
const metadata = msg.metadata.assistant!
|
||||||
|
|
||||||
|
function summary(input: Message.Info) {
|
||||||
|
const result = []
|
||||||
|
|
||||||
|
for (const part of input.parts) {
|
||||||
|
if (part.type === "tool-invocation") {
|
||||||
|
result.push({
|
||||||
|
toolInvocation: part.toolInvocation,
|
||||||
|
metadata: input.metadata.tool[part.toolInvocation.toolCallId],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsub = Bus.subscribe(Message.Event.Updated, async (evt) => {
|
||||||
|
if (evt.properties.info.metadata.sessionID !== session.id) return
|
||||||
|
ctx.metadata({
|
||||||
|
title: params.description,
|
||||||
|
summary: summary(evt.properties.info),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx.abort.addEventListener("abort", () => {
|
||||||
|
Session.abort(session.id)
|
||||||
|
})
|
||||||
const result = await Session.chat({
|
const result = await Session.chat({
|
||||||
sessionID: session.id,
|
sessionID: session.id,
|
||||||
modelID: metadata.modelID,
|
modelID: metadata.modelID,
|
||||||
@@ -28,10 +55,11 @@ export const TaskTool = Tool.define({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
unsub()
|
||||||
return {
|
return {
|
||||||
metadata: {
|
metadata: {
|
||||||
title: params.description,
|
title: params.description,
|
||||||
|
summary: summary(result),
|
||||||
},
|
},
|
||||||
output: result.parts.findLast((x) => x.type === "text")!.text,
|
output: result.parts.findLast((x) => x.type === "text")!.text,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const state = App.state("todo-tool", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const TodoWriteTool = Tool.define({
|
export const TodoWriteTool = Tool.define({
|
||||||
id: "opencode.todowrite",
|
id: "todowrite",
|
||||||
description: DESCRIPTION_WRITE,
|
description: DESCRIPTION_WRITE,
|
||||||
parameters: z.object({
|
parameters: z.object({
|
||||||
todos: z.array(TodoInfo).describe("The updated todo list"),
|
todos: z.array(TodoInfo).describe("The updated todo list"),
|
||||||
@@ -42,7 +42,7 @@ export const TodoWriteTool = Tool.define({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const TodoReadTool = Tool.define({
|
export const TodoReadTool = Tool.define({
|
||||||
id: "opencode.todoread",
|
id: "todoread",
|
||||||
description: "Use this tool to read your todo list",
|
description: "Use this tool to read your todo list",
|
||||||
parameters: z.object({}),
|
parameters: z.object({}),
|
||||||
async execute(_params, opts) {
|
async execute(_params, opts) {
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ export namespace Tool {
|
|||||||
title: string
|
title: string
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
}
|
}
|
||||||
export type Context = {
|
export type Context<M extends Metadata = Metadata> = {
|
||||||
sessionID: string
|
sessionID: string
|
||||||
messageID: string
|
messageID: string
|
||||||
abort: AbortSignal
|
abort: AbortSignal
|
||||||
|
metadata(meta: M): void
|
||||||
}
|
}
|
||||||
export interface Info<
|
export interface Info<
|
||||||
Parameters extends StandardSchemaV1 = StandardSchemaV1,
|
Parameters extends StandardSchemaV1 = StandardSchemaV1,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const DEFAULT_TIMEOUT = 30 * 1000 // 30 seconds
|
|||||||
const MAX_TIMEOUT = 120 * 1000 // 2 minutes
|
const MAX_TIMEOUT = 120 * 1000 // 2 minutes
|
||||||
|
|
||||||
export const WebFetchTool = Tool.define({
|
export const WebFetchTool = Tool.define({
|
||||||
id: "opencode.webfetch",
|
id: "webfetch",
|
||||||
description: DESCRIPTION,
|
description: DESCRIPTION,
|
||||||
parameters: z.object({
|
parameters: z.object({
|
||||||
url: z.string().describe("The URL to fetch content from"),
|
url: z.string().describe("The URL to fetch content from"),
|
||||||
@@ -22,7 +22,7 @@ export const WebFetchTool = Tool.define({
|
|||||||
.min(0)
|
.min(0)
|
||||||
.max(MAX_TIMEOUT / 1000)
|
.max(MAX_TIMEOUT / 1000)
|
||||||
.describe("Optional timeout in seconds (max 120)")
|
.describe("Optional timeout in seconds (max 120)")
|
||||||
.nullable(),
|
.optional(),
|
||||||
}),
|
}),
|
||||||
async execute(params, ctx) {
|
async execute(params, ctx) {
|
||||||
// Validate URL
|
// Validate URL
|
||||||
@@ -76,7 +76,7 @@ export const WebFetchTool = Tool.define({
|
|||||||
switch (params.format) {
|
switch (params.format) {
|
||||||
case "text":
|
case "text":
|
||||||
if (contentType.includes("text/html")) {
|
if (contentType.includes("text/html")) {
|
||||||
const text = extractTextFromHTML(content)
|
const text = await extractTextFromHTML(content)
|
||||||
return {
|
return {
|
||||||
output: text,
|
output: text,
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -127,10 +127,45 @@ export const WebFetchTool = Tool.define({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
function extractTextFromHTML(html: string): string {
|
async function extractTextFromHTML(html: string) {
|
||||||
const doc = new DOMParser().parseFromString(html, "text/html")
|
let text = ""
|
||||||
const text = doc.body.textContent || doc.body.innerText || ""
|
let skipContent = false
|
||||||
return text.replace(/\s+/g, " ").trim()
|
|
||||||
|
const rewriter = new HTMLRewriter()
|
||||||
|
.on("script, style, noscript, iframe, object, embed", {
|
||||||
|
element() {
|
||||||
|
skipContent = true
|
||||||
|
},
|
||||||
|
text() {
|
||||||
|
// Skip text content inside these elements
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.on("*", {
|
||||||
|
element(element) {
|
||||||
|
// Reset skip flag when entering other elements
|
||||||
|
if (
|
||||||
|
![
|
||||||
|
"script",
|
||||||
|
"style",
|
||||||
|
"noscript",
|
||||||
|
"iframe",
|
||||||
|
"object",
|
||||||
|
"embed",
|
||||||
|
].includes(element.tagName)
|
||||||
|
) {
|
||||||
|
skipContent = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
text(input) {
|
||||||
|
if (!skipContent) {
|
||||||
|
text += input.text
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.transform(new Response(html))
|
||||||
|
|
||||||
|
await rewriter.text()
|
||||||
|
return text.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertHTMLToMarkdown(html: string): string {
|
function convertHTMLToMarkdown(html: string): string {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
- Allows OpenCode to search the web and use the results to inform responses
|
- Allows opencode to search the web and use the results to inform responses
|
||||||
- Provides up-to-date information for current events and recent data
|
- Provides up-to-date information for current events and recent data
|
||||||
- Returns search result information formatted as search result blocks
|
- Returns search result information formatted as search result blocks
|
||||||
- Use this tool for accessing information beyond Claude's knowledge cutoff
|
- Use this tool for accessing information beyond Claude's knowledge cutoff
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import DESCRIPTION from "./write.txt"
|
|||||||
import { App } from "../app/app"
|
import { App } from "../app/app"
|
||||||
|
|
||||||
export const WriteTool = Tool.define({
|
export const WriteTool = Tool.define({
|
||||||
id: "opencode.write",
|
id: "write",
|
||||||
description: DESCRIPTION,
|
description: DESCRIPTION,
|
||||||
parameters: z.object({
|
parameters: z.object({
|
||||||
filePath: z
|
filePath: z
|
||||||
@@ -29,7 +29,7 @@ export const WriteTool = Tool.define({
|
|||||||
if (exists) await FileTimes.assert(ctx.sessionID, filepath)
|
if (exists) await FileTimes.assert(ctx.sessionID, filepath)
|
||||||
|
|
||||||
await Permission.ask({
|
await Permission.ask({
|
||||||
id: "opencode.write",
|
id: "write",
|
||||||
sessionID: ctx.sessionID,
|
sessionID: ctx.sessionID,
|
||||||
title: exists
|
title: exists
|
||||||
? "Overwrite this file: " + filepath
|
? "Overwrite this file: " + filepath
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { z, type ZodSchema } from "zod"
|
import { z, type ZodSchema } from "zod"
|
||||||
import { Log } from "./log"
|
// import { Log } from "./log"
|
||||||
|
|
||||||
const log = Log.create()
|
// const log = Log.create()
|
||||||
|
|
||||||
export abstract class NamedError extends Error {
|
export abstract class NamedError extends Error {
|
||||||
abstract schema(): ZodSchema
|
abstract schema(): ZodSchema
|
||||||
@@ -11,15 +11,16 @@ export abstract class NamedError extends Error {
|
|||||||
name: Name,
|
name: Name,
|
||||||
data: Data,
|
data: Data,
|
||||||
) {
|
) {
|
||||||
|
const schema = z
|
||||||
|
.object({
|
||||||
|
name: z.literal(name),
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
.openapi({
|
||||||
|
ref: name,
|
||||||
|
})
|
||||||
const result = class extends NamedError {
|
const result = class extends NamedError {
|
||||||
public static readonly Schema = z
|
public static readonly Schema = schema
|
||||||
.object({
|
|
||||||
name: z.literal(name),
|
|
||||||
data: data,
|
|
||||||
})
|
|
||||||
.openapi({
|
|
||||||
ref: name,
|
|
||||||
})
|
|
||||||
|
|
||||||
public readonly name = name as Name
|
public readonly name = name as Name
|
||||||
|
|
||||||
@@ -29,7 +30,6 @@ export abstract class NamedError extends Error {
|
|||||||
) {
|
) {
|
||||||
super(name, options)
|
super(name, options)
|
||||||
this.name = name
|
this.name = name
|
||||||
log.error(name, this.data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static isInstance(input: any): input is InstanceType<typeof result> {
|
static isInstance(input: any): input is InstanceType<typeof result> {
|
||||||
@@ -37,7 +37,7 @@ export abstract class NamedError extends Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
schema() {
|
schema() {
|
||||||
return result.Schema
|
return schema
|
||||||
}
|
}
|
||||||
|
|
||||||
toObject() {
|
toObject() {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import path from "path"
|
|||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
export namespace Log {
|
export namespace Log {
|
||||||
export const Default = create()
|
export const Default = create({ service: "default" })
|
||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
print: boolean
|
print: boolean
|
||||||
@@ -45,6 +45,7 @@ export namespace Log {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let last = Date.now()
|
||||||
export function create(tags?: Record<string, any>) {
|
export function create(tags?: Record<string, any>) {
|
||||||
tags = tags || {}
|
tags = tags || {}
|
||||||
|
|
||||||
@@ -56,20 +57,24 @@ export namespace Log {
|
|||||||
.filter(([_, value]) => value !== undefined && value !== null)
|
.filter(([_, value]) => value !== undefined && value !== null)
|
||||||
.map(([key, value]) => `${key}=${value}`)
|
.map(([key, value]) => `${key}=${value}`)
|
||||||
.join(" ")
|
.join(" ")
|
||||||
|
const next = new Date()
|
||||||
|
const diff = next.getTime() - last
|
||||||
|
last = next.getTime()
|
||||||
return (
|
return (
|
||||||
[new Date().toISOString(), prefix, message].filter(Boolean).join(" ") +
|
[next.toISOString().split(".")[0], "+" + diff + "ms", prefix, message]
|
||||||
"\n"
|
.filter(Boolean)
|
||||||
|
.join(" ") + "\n"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const result = {
|
const result = {
|
||||||
info(message?: any, extra?: Record<string, any>) {
|
info(message?: any, extra?: Record<string, any>) {
|
||||||
process.stderr.write(build(message, extra))
|
process.stderr.write("INFO " + build(message, extra))
|
||||||
},
|
},
|
||||||
error(message?: any, extra?: Record<string, any>) {
|
error(message?: any, extra?: Record<string, any>) {
|
||||||
process.stderr.write(build(message, extra))
|
process.stderr.write("ERROR " + build(message, extra))
|
||||||
},
|
},
|
||||||
warn(message?: any, extra?: Record<string, any>) {
|
warn(message?: any, extra?: Record<string, any>) {
|
||||||
process.stderr.write(build(message, extra))
|
process.stderr.write("WARN " + build(message, extra))
|
||||||
},
|
},
|
||||||
tag(key: string, value: string) {
|
tag(key: string, value: string) {
|
||||||
if (tags) tags[key] = value
|
if (tags) tags[key] = value
|
||||||
@@ -78,6 +83,23 @@ export namespace Log {
|
|||||||
clone() {
|
clone() {
|
||||||
return Log.create({ ...tags })
|
return Log.create({ ...tags })
|
||||||
},
|
},
|
||||||
|
time(message: string, extra?: Record<string, any>) {
|
||||||
|
const now = Date.now()
|
||||||
|
result.info(message, { status: "started", ...extra })
|
||||||
|
function stop() {
|
||||||
|
result.info(message, {
|
||||||
|
status: "completed",
|
||||||
|
duration: Date.now() - now,
|
||||||
|
...extra,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
stop,
|
||||||
|
[Symbol.dispose]() {
|
||||||
|
stop()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -3,25 +3,37 @@ import { App } from "../../src/app/app"
|
|||||||
import { GlobTool } from "../../src/tool/glob"
|
import { GlobTool } from "../../src/tool/glob"
|
||||||
import { ListTool } from "../../src/tool/ls"
|
import { ListTool } from "../../src/tool/ls"
|
||||||
|
|
||||||
|
const ctx = {
|
||||||
|
sessionID: "test",
|
||||||
|
messageID: "",
|
||||||
|
abort: AbortSignal.any([]),
|
||||||
|
metadata: () => {},
|
||||||
|
}
|
||||||
describe("tool.glob", () => {
|
describe("tool.glob", () => {
|
||||||
test("truncate", async () => {
|
test("truncate", async () => {
|
||||||
await App.provide({ cwd: process.cwd(), version: "test" }, async () => {
|
await App.provide({ cwd: process.cwd() }, async () => {
|
||||||
let result = await GlobTool.execute(
|
let result = await GlobTool.execute(
|
||||||
{ pattern: "./node_modules/**/*" },
|
{
|
||||||
{ sessionID: "test" },
|
pattern: "../../node_modules/**/*",
|
||||||
|
path: undefined,
|
||||||
|
},
|
||||||
|
ctx,
|
||||||
)
|
)
|
||||||
expect(result.metadata.truncated).toBe(true)
|
expect(result.metadata.truncated).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
test("basic", async () => {
|
test("basic", async () => {
|
||||||
await App.provide({ cwd: process.cwd(), version: "test" }, async () => {
|
await App.provide({ cwd: process.cwd() }, async () => {
|
||||||
let result = await GlobTool.execute(
|
let result = await GlobTool.execute(
|
||||||
{ pattern: "*.json" },
|
{
|
||||||
{ sessionID: "test" },
|
pattern: "*.json",
|
||||||
|
path: undefined,
|
||||||
|
},
|
||||||
|
ctx,
|
||||||
)
|
)
|
||||||
expect(result.metadata).toMatchObject({
|
expect(result.metadata).toMatchObject({
|
||||||
truncated: false,
|
truncated: false,
|
||||||
count: 2,
|
count: 3,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -29,15 +41,12 @@ describe("tool.glob", () => {
|
|||||||
|
|
||||||
describe("tool.ls", () => {
|
describe("tool.ls", () => {
|
||||||
test("basic", async () => {
|
test("basic", async () => {
|
||||||
const result = await App.provide(
|
const result = await App.provide({ cwd: process.cwd() }, async () => {
|
||||||
{ cwd: process.cwd(), version: "test" },
|
return await ListTool.execute(
|
||||||
async () => {
|
{ path: "./example", ignore: [".git"] },
|
||||||
return await ListTool.execute(
|
ctx,
|
||||||
{ path: "./example" },
|
)
|
||||||
{ sessionID: "test" },
|
})
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
expect(result.output).toMatchSnapshot()
|
expect(result.output).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,219 +0,0 @@
|
|||||||
time=2025-05-30T22:01:45.386-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-05-30T22:01:45.391-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-05-30T22:01:50.683-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-05-30T22:01:50.683-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-05-30T22:01:50.683-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-05-30T22:01:50.683-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-05-30T22:01:50.683-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-05-30T22:01:50.683-04:00 level=INFO msg="TUI exited" result="{width:98 height:57 currentPage:chat previousPage: pages:map[chat:0xc00013b450] loadedPages:map[chat:true] status:{app:0xc0002e05b0 queue:[] width:98 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc0002e05b0 showPermissions:false permissions:0xc000159408 showHelp:false help:0xc0006822d0 showQuit:true quit:0xc00024b479 showSessionDialog:false sessionDialog:0xc0001f0240 showCommandDialog:false commandDialog:0xc0003cbba0 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6a7a0} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6a620}] showModelDialog:false modelDialog:0xc0001f45a0 showInitDialog:false initDialog:{width:98 height:57 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d4c88 showThemeDialog:false themeDialog:0xc0001f0480 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc0001f04c0}"
|
|
||||||
time=2025-05-30T22:13:24.046-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-05-30T22:13:24.051-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-05-30T22:13:25.991-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-05-30T22:13:25.991-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-05-30T22:13:25.991-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-05-30T22:13:25.991-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-05-30T22:13:25.991-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-05-30T22:13:25.991-04:00 level=INFO msg="TUI exited" result="{width:199 height:57 currentPage:chat previousPage: pages:map[chat:0xc00025f950] loadedPages:map[chat:true] status:{app:0xc0000ca230 queue:[] width:199 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc0000ca230 showPermissions:false permissions:0xc00029f908 showHelp:false help:0xc00045d9b0 showQuit:true quit:0xc0005a0be9 showSessionDialog:false sessionDialog:0xc00012e3c0 showCommandDialog:false commandDialog:0xc0004379e0 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6a7a0} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6a620}] showModelDialog:false modelDialog:0xc0002f2e60 showInitDialog:false initDialog:{width:199 height:57 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc00013ac88 showThemeDialog:false themeDialog:0xc00012e600 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc00012e640}"
|
|
||||||
time=2025-05-31T16:00:29.137-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-05-31T16:00:29.141-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-05-31T16:00:36.530-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-05-31T16:00:36.531-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-05-31T16:00:36.531-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-05-31T16:00:36.531-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-05-31T16:00:36.531-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-05-31T16:00:36.531-04:00 level=INFO msg="TUI exited" result="{width:106 height:54 currentPage:chat previousPage: pages:map[chat:0xc000157450] loadedPages:map[chat:true] status:{app:0xc00020c5b0 queue:[] width:106 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc00020c5b0 showPermissions:false permissions:0xc000175408 showHelp:false help:0xc00070c270 showQuit:true quit:0xc000299979 showSessionDialog:false sessionDialog:0xc0001f02c0 showCommandDialog:false commandDialog:0xc0003cbba0 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6a7a0} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6a620}] showModelDialog:false modelDialog:0xc00021a5a0 showInitDialog:false initDialog:{width:106 height:54 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d4c88 showThemeDialog:false themeDialog:0xc0001f0500 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc0001f0540}"
|
|
||||||
time=2025-05-31T16:06:20.089-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-05-31T16:06:20.094-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-05-31T16:06:20.095-04:00 level=ERROR msg="Failed to subscribe to events" error="Get \"http://localhost:16713/event\": dial tcp [::1]:16713: connect: connection refused"
|
|
||||||
time=2025-05-31T17:54:04.009-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-05-31T17:54:04.014-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-05-31T17:54:06.337-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-05-31T17:54:06.337-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-05-31T17:54:06.337-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-05-31T17:54:06.337-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-05-31T17:54:06.337-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-05-31T17:54:06.337-04:00 level=INFO msg="TUI exited" result="{width:106 height:25 currentPage:chat previousPage: pages:map[chat:0xc0002332c0] loadedPages:map[chat:true] status:{app:0xc0002b1810 queue:[] width:106 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc0002b1810 showPermissions:false permissions:0xc000267408 showHelp:false help:0xc00048dbc0 showQuit:true quit:0xc0004a2719 showSessionDialog:false sessionDialog:0xc000319ec0 showCommandDialog:false commandDialog:0xc000387980 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6a7a0} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6a620}] showModelDialog:false modelDialog:0xc0000c6960 showInitDialog:false initDialog:{width:106 height:25 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d6c88 showThemeDialog:false themeDialog:0xc0000ac480 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc0000ac4c0}"
|
|
||||||
time=2025-05-31T17:54:17.103-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-05-31T17:54:17.108-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-05-31T17:54:18.391-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-05-31T17:54:18.392-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-05-31T17:54:18.392-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-05-31T17:54:18.392-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-05-31T17:54:18.392-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-05-31T17:54:18.392-04:00 level=INFO msg="TUI exited" result="{width:106 height:25 currentPage:chat previousPage: pages:map[chat:0xc00042a960] loadedPages:map[chat:true] status:{app:0xc000163ce0 queue:[] width:106 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc000163ce0 showPermissions:false permissions:0xc0001df408 showHelp:false help:0xc0005198f0 showQuit:true quit:0xc0003a5ef9 showSessionDialog:false sessionDialog:0xc000323840 showCommandDialog:false commandDialog:0xc00043b0e0 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6a7a0} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6a620}] showModelDialog:false modelDialog:0xc0004028c0 showInitDialog:false initDialog:{width:106 height:25 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d6c88 showThemeDialog:false themeDialog:0xc000323a80 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc000323ac0}"
|
|
||||||
time=2025-05-31T17:59:54.360-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-05-31T17:59:54.364-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-05-31T17:59:55.814-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-05-31T17:59:55.815-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-05-31T17:59:55.815-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-05-31T17:59:55.815-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-05-31T17:59:55.815-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-05-31T17:59:55.815-04:00 level=INFO msg="TUI exited" result="{width:106 height:25 currentPage:chat previousPage: pages:map[chat:0xc0002787d0] loadedPages:map[chat:true] status:{app:0xc0003fed90 queue:[] width:106 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc0003fed90 showPermissions:false permissions:0xc0002b1908 showHelp:false help:0xc000126150 showQuit:true quit:0xc00011d439 showSessionDialog:false sessionDialog:0xc00025e380 showCommandDialog:false commandDialog:0xc00047fc00 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6a7a0} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6a620}] showModelDialog:false modelDialog:0xc0002f6d20 showInitDialog:false initDialog:{width:106 height:25 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0001b2c88 showThemeDialog:false themeDialog:0xc00025e5c0 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc00025e600}"
|
|
||||||
time=2025-05-31T17:59:56.746-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-05-31T17:59:56.750-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-05-31T18:00:10.757-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-05-31T18:00:10.757-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-05-31T18:00:10.757-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-05-31T18:00:10.757-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-05-31T18:00:10.757-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-05-31T18:00:10.757-04:00 level=INFO msg="TUI exited" result="{width:211 height:54 currentPage:chat previousPage: pages:map[chat:0xc00053b090] loadedPages:map[chat:true] status:{app:0xc000300cb0 queue:[] width:211 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc000300cb0 showPermissions:false permissions:0xc0002c5408 showHelp:false help:0xc000682f90 showQuit:true quit:0xc0006134d9 showSessionDialog:false sessionDialog:0xc00031f980 showCommandDialog:false commandDialog:0xc0003d9520 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6a7a0} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6a620}] showModelDialog:false modelDialog:0xc000395220 showInitDialog:false initDialog:{width:211 height:54 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc00013ac88 showThemeDialog:false themeDialog:0xc00031fbc0 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc00031fc00}"
|
|
||||||
time=2025-05-31T18:35:42.289-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-05-31T18:35:42.294-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-05-31T18:36:54.773-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-05-31T18:36:54.773-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-05-31T18:36:54.773-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-05-31T18:36:54.773-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-05-31T18:36:54.773-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-05-31T18:36:54.773-04:00 level=INFO msg="TUI exited" result="{width:106 height:25 currentPage:chat previousPage: pages:map[chat:0xc00012f0e0] loadedPages:map[chat:true] status:{app:0xc0002aa070 queue:[] width:106 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc0002aa070 showPermissions:false permissions:0xc000267408 showHelp:false help:0xc00041b8f0 showQuit:true quit:0xc000345ee9 showSessionDialog:false sessionDialog:0xc00032ba40 showCommandDialog:false commandDialog:0xc00043b300 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6a7a0} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6a620}] showModelDialog:false modelDialog:0xc000426f00 showInitDialog:false initDialog:{width:106 height:25 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d6c88 showThemeDialog:false themeDialog:0xc00032bc80 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc00032bcc0}"
|
|
||||||
time=2025-05-31T18:36:56.011-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-05-31T18:36:56.015-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-05-31T18:37:44.063-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-05-31T18:37:44.064-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-05-31T18:37:44.064-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-05-31T18:37:44.064-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-05-31T18:37:44.064-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-05-31T18:37:44.064-04:00 level=INFO msg="TUI exited" result="{width:211 height:54 currentPage:chat previousPage: pages:map[chat:0xc000420280] loadedPages:map[chat:true] status:{app:0xc0002d8000 queue:[] width:211 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc0002d8000 showPermissions:false permissions:0xc000271408 showHelp:false help:0xc00048da70 showQuit:true quit:0xc000390809 showSessionDialog:false sessionDialog:0xc000323b80 showCommandDialog:false commandDialog:0xc0003e5920 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6a7a0} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6a620}] showModelDialog:false modelDialog:0xc00025f9a0 showInitDialog:false initDialog:{width:211 height:54 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d6c88 showThemeDialog:false themeDialog:0xc000323dc0 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc000323e00}"
|
|
||||||
time=2025-05-31T20:32:32.443-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-05-31T20:32:32.448-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-05-31T20:33:09.783-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-05-31T20:33:09.783-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-05-31T20:33:09.783-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-05-31T20:33:09.783-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-05-31T20:33:09.783-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-05-31T20:33:09.783-04:00 level=INFO msg="TUI exited" result="{width:199 height:56 currentPage:chat previousPage: pages:map[chat:0xc00032c960] loadedPages:map[chat:true] status:{app:0xc000279420 queue:[] width:199 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc000279420 showPermissions:false permissions:0xc0001fb408 showHelp:false help:0xc000154150 showQuit:true quit:0xc000528849 showSessionDialog:false sessionDialog:0xc000309e40 showCommandDialog:false commandDialog:0xc0003a3800 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6a800} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6a680}] showModelDialog:false modelDialog:0xc0002e7cc0 showInitDialog:false initDialog:{width:199 height:56 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d4c88 showThemeDialog:false themeDialog:0xc0000ac400 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc0000ac440}"
|
|
||||||
time=2025-06-01T14:37:36.423-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-06-01T14:37:36.427-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-06-01T14:38:19.951-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-06-01T14:38:19.951-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-06-01T14:38:19.951-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-06-01T14:38:19.951-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-06-01T14:38:19.951-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-06-01T14:38:19.951-04:00 level=INFO msg="TUI exited" result="{width:199 height:56 currentPage:chat previousPage: pages:map[chat:0xc00035b9f0] loadedPages:map[chat:true] status:{app:0xc000226d90 queue:[] width:199 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc000226d90 showPermissions:false permissions:0xc00027f908 showHelp:false help:0xc0005139e0 showQuit:true quit:0xc000510d49 showSessionDialog:false sessionDialog:0xc0001e84c0 showCommandDialog:false commandDialog:0xc00051a160 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6a800} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6a680}] showModelDialog:false modelDialog:0xc0002675e0 showInitDialog:false initDialog:{width:199 height:56 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc00013ac88 showThemeDialog:false themeDialog:0xc0001e8700 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc0001e8740}"
|
|
||||||
time=2025-06-01T14:38:50.886-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-06-01T14:38:50.891-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-06-01T14:38:53.495-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-06-01T14:38:53.495-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-06-01T14:38:53.495-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-06-01T14:38:53.495-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-06-01T14:38:53.495-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-06-01T14:38:53.495-04:00 level=INFO msg="TUI exited" result="{width:199 height:56 currentPage:chat previousPage: pages:map[chat:0xc0005ac8c0] loadedPages:map[chat:true] status:{app:0xc0002796c0 queue:[] width:199 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc0002796c0 showPermissions:false permissions:0xc00028b408 showHelp:false help:0xc000490d80 showQuit:true quit:0xc000582589 showSessionDialog:false sessionDialog:0xc0003359c0 showCommandDialog:false commandDialog:0xc00042d480 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6a800} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6a680}] showModelDialog:false modelDialog:0xc000389360 showInitDialog:false initDialog:{width:199 height:56 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d4c88 showThemeDialog:false themeDialog:0xc000335c00 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc000335c80}"
|
|
||||||
time=2025-06-01T14:39:49.852-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-06-01T14:39:49.856-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-06-01T14:39:57.071-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-06-01T14:39:57.071-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-06-01T14:39:57.071-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-06-01T14:39:57.071-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-06-01T14:39:57.071-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-06-01T14:39:57.071-04:00 level=INFO msg="TUI exited" result="{width:199 height:56 currentPage:chat previousPage: pages:map[chat:0xc000616f00] loadedPages:map[chat:true] status:{app:0xc000333490 queue:[] width:199 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc000333490 showPermissions:false permissions:0xc0004faa08 showHelp:false help:0xc000471140 showQuit:true quit:0xc000459299 showSessionDialog:false sessionDialog:0xc000352500 showCommandDialog:false commandDialog:0xc00041ed80 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6a800} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6a680}] showModelDialog:false modelDialog:0xc000515a40 showInitDialog:false initDialog:{width:199 height:56 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc00013ac88 showThemeDialog:false themeDialog:0xc000352740 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc000352780}"
|
|
||||||
time=2025-06-01T14:40:21.954-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-06-01T14:40:21.958-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-06-01T14:41:29.195-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-06-01T14:41:29.195-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-06-01T14:41:29.195-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-06-01T14:41:29.195-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-06-01T14:41:29.195-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-06-01T14:41:29.195-04:00 level=INFO msg="TUI exited" result="{width:199 height:56 currentPage:chat previousPage: pages:map[chat:0xc0002cc280] loadedPages:map[chat:true] status:{app:0xc0002e64d0 queue:[] width:199 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc0002e64d0 showPermissions:false permissions:0xc00026f408 showHelp:false help:0xc00051c1b0 showQuit:true quit:0xc00051a819 showSessionDialog:false sessionDialog:0xc00030fec0 showCommandDialog:false commandDialog:0xc00042d760 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6a800} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6a680}] showModelDialog:false modelDialog:0xc0002ce1e0 showInitDialog:false initDialog:{width:199 height:56 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d4c88 showThemeDialog:false themeDialog:0xc0000ac480 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc0000ac4c0}"
|
|
||||||
time=2025-06-01T14:58:27.272-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-06-01T14:58:27.276-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-06-01T14:58:59.711-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-06-01T14:58:59.711-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-06-01T14:58:59.711-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-06-01T14:58:59.711-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-06-01T14:58:59.711-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-06-01T14:58:59.711-04:00 level=INFO msg="TUI exited" result="{width:199 height:56 currentPage:chat previousPage: pages:map[chat:0xc000316280] loadedPages:map[chat:true] status:{app:0xc0002b5810 queue:[] width:199 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc0002b5810 showPermissions:false permissions:0xc000269408 showHelp:false help:0xc000490e10 showQuit:true quit:0xc00047a929 showSessionDialog:false sessionDialog:0xc0000adb40 showCommandDialog:false commandDialog:0xc0003e59c0 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6a800} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6a680}] showModelDialog:false modelDialog:0xc00024fd60 showInitDialog:false initDialog:{width:199 height:56 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d4c88 showThemeDialog:false themeDialog:0xc0000add80 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc0000addc0}"
|
|
||||||
time=2025-06-01T15:02:54.453-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-06-01T15:02:54.458-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-06-01T15:02:56.136-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-06-01T15:02:56.136-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-06-01T15:02:56.136-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-06-01T15:02:56.136-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-06-01T15:02:56.136-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-06-01T15:02:56.136-04:00 level=INFO msg="TUI exited" result="{width:199 height:56 currentPage:chat previousPage: pages:map[chat:0xc000392ff0] loadedPages:map[chat:true] status:{app:0xc0001ecc40 queue:[] width:199 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc0001ecc40 showPermissions:false permissions:0xc000205408 showHelp:false help:0xc00051c0c0 showQuit:true quit:0xc0003b3f49 showSessionDialog:false sessionDialog:0xc000319980 showCommandDialog:false commandDialog:0xc00042d220 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6b340} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6b1c0}] showModelDialog:false modelDialog:0xc0005c52c0 showInitDialog:false initDialog:{width:199 height:56 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d4c88 showThemeDialog:false themeDialog:0xc000319bc0 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc000319c00}"
|
|
||||||
time=2025-06-01T15:02:57.053-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-06-01T15:02:57.057-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-06-01T15:02:58.135-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-06-01T15:02:58.135-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-06-01T15:02:58.135-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-06-01T15:02:58.135-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-06-01T15:02:58.135-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-06-01T15:02:58.135-04:00 level=INFO msg="TUI exited" result="{width:199 height:56 currentPage:chat previousPage: pages:map[chat:0xc0004411d0] loadedPages:map[chat:true] status:{app:0xc00023ee70 queue:[] width:199 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc00023ee70 showPermissions:false permissions:0xc000177408 showHelp:false help:0xc000520030 showQuit:true quit:0xc000314929 showSessionDialog:false sessionDialog:0xc000319d00 showCommandDialog:false commandDialog:0xc0003e5860 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6b340} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6b1c0}] showModelDialog:false modelDialog:0xc0002c9a40 showInitDialog:false initDialog:{width:199 height:56 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d4c88 showThemeDialog:false themeDialog:0xc000319f40 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc0000ac000}"
|
|
||||||
time=2025-06-01T15:15:13.582-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-06-01T15:15:13.587-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-06-01T15:15:19.009-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-06-01T15:15:19.010-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-06-01T15:15:19.010-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-06-01T15:15:19.010-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-06-01T15:15:19.010-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-06-01T15:15:19.010-04:00 level=INFO msg="TUI exited" result="{width:199 height:56 currentPage:chat previousPage: pages:map[chat:0xc0001490e0] loadedPages:map[chat:true] status:{app:0xc0001efb90 queue:[] width:199 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc0001efb90 showPermissions:false permissions:0xc000167408 showHelp:false help:0xc00052c1b0 showQuit:true quit:0xc000254629 showSessionDialog:false sessionDialog:0xc00030fe80 showCommandDialog:false commandDialog:0xc0003a3420 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6b340} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6b1c0}] showModelDialog:false modelDialog:0xc0000c6640 showInitDialog:false initDialog:{width:199 height:56 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d4c88 showThemeDialog:false themeDialog:0xc0000ac440 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc0000ac480}"
|
|
||||||
time=2025-06-01T15:15:20.678-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-06-01T15:15:20.683-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-06-01T15:15:23.252-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-06-01T15:15:23.253-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-06-01T15:15:23.253-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-06-01T15:15:23.253-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-06-01T15:15:23.253-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-06-01T15:15:23.253-04:00 level=INFO msg="TUI exited" result="{width:199 height:56 currentPage:chat previousPage: pages:map[chat:0xc0002c47d0] loadedPages:map[chat:true] status:{app:0xc0003363f0 queue:[] width:199 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc0003363f0 showPermissions:false permissions:0xc0002f3408 showHelp:false help:0xc0007055f0 showQuit:true quit:0xc00041c9b9 showSessionDialog:false sessionDialog:0xc00033bd00 showCommandDialog:false commandDialog:0xc000437700 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6b340} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6b1c0}] showModelDialog:false modelDialog:0xc0003d9c20 showInitDialog:false initDialog:{width:199 height:56 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc00013ac88 showThemeDialog:false themeDialog:0xc00033bf40 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc00034a140}"
|
|
||||||
time=2025-06-02T11:40:21.643-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-06-02T11:40:21.648-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-06-02T11:40:24.684-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-06-02T11:40:24.684-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-06-02T11:40:24.684-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-06-02T11:40:24.684-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-06-02T11:40:24.684-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-06-02T11:40:24.684-04:00 level=INFO msg="TUI exited" result="{width:347 height:89 currentPage:chat previousPage: pages:map[chat:0xc00020d180] loadedPages:map[chat:true] status:{app:0xc0002a8230 queue:[] width:347 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc0002a8230 showPermissions:false permissions:0xc000239408 showHelp:false help:0xc00051c1e0 showQuit:true quit:0xc000598909 showSessionDialog:false sessionDialog:0xc000309f00 showCommandDialog:false commandDialog:0xc0003a3660 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6b340} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6b1c0}] showModelDialog:false modelDialog:0xc0000c4a00 showInitDialog:false initDialog:{width:347 height:89 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d4c88 showThemeDialog:false themeDialog:0xc0000aa4c0 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc0000aa500}"
|
|
||||||
time=2025-06-02T11:40:55.224-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-06-02T11:40:55.228-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-06-02T11:41:03.512-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-06-02T11:41:03.512-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-06-02T11:41:03.512-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-06-02T11:41:03.512-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-06-02T11:41:03.512-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-06-02T11:41:03.512-04:00 level=INFO msg="TUI exited" result="{width:347 height:89 currentPage:chat previousPage: pages:map[chat:0xc0001467d0] loadedPages:map[chat:true] status:{app:0xc0004feee0 queue:[] width:347 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc0004feee0 showPermissions:false permissions:0xc000167408 showHelp:false help:0xc00059cd50 showQuit:true quit:0xc00038aaa9 showSessionDialog:false sessionDialog:0xc00030ff00 showCommandDialog:false commandDialog:0xc0003e5aa0 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6b340} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6b1c0}] showModelDialog:false modelDialog:0xc00029be00 showInitDialog:false initDialog:{width:347 height:89 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d4c88 showThemeDialog:false themeDialog:0xc0000aa4c0 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc0000aa500}"
|
|
||||||
time=2025-06-02T11:41:05.131-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-06-02T11:41:05.136-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-06-02T11:41:06.417-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-06-02T11:41:06.417-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-06-02T11:41:06.417-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-06-02T11:41:06.417-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-06-02T11:41:06.417-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-06-02T11:41:06.417-04:00 level=INFO msg="TUI exited" result="{width:347 height:89 currentPage:chat previousPage: pages:map[chat:0xc0002527d0] loadedPages:map[chat:true] status:{app:0xc0002e0d90 queue:[] width:347 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc0002e0d90 showPermissions:false permissions:0xc00027b408 showHelp:false help:0xc0004900c0 showQuit:true quit:0xc00047ae69 showSessionDialog:false sessionDialog:0xc000319f40 showCommandDialog:false commandDialog:0xc00042d880 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6b340} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6b1c0}] showModelDialog:false modelDialog:0xc000357e00 showInitDialog:false initDialog:{width:347 height:89 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d4c88 showThemeDialog:false themeDialog:0xc0000aa500 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc0000aa540}"
|
|
||||||
time=2025-06-02T19:36:04.879-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-06-02T19:36:04.883-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-06-02T19:36:07.068-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-06-02T19:36:07.068-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-06-02T19:36:07.068-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-06-02T19:36:07.068-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-06-02T19:36:07.068-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-06-02T19:36:07.068-04:00 level=INFO msg="TUI exited" result="{width:145 height:36 currentPage:chat previousPage: pages:map[chat:0xc000544b40] loadedPages:map[chat:true] status:{app:0xc000249b90 queue:[] width:145 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc000249b90 showPermissions:false permissions:0xc000207408 showHelp:false help:0xc00011a1e0 showQuit:true quit:0xc0003890b9 showSessionDialog:false sessionDialog:0xc000319f40 showCommandDialog:false commandDialog:0xc0003e5520 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6b340} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6b1c0}] showModelDialog:false modelDialog:0xc000547220 showInitDialog:false initDialog:{width:145 height:36 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d4c88 showThemeDialog:false themeDialog:0xc0000ac500 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc0000ac540}"
|
|
||||||
time=2025-06-02T19:44:20.524-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-06-02T19:44:20.529-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-06-02T19:45:43.720-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-06-02T19:45:43.720-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-06-02T19:45:43.720-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-06-02T19:45:43.720-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-06-02T19:45:43.720-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-06-02T19:45:43.720-04:00 level=INFO msg="TUI exited" result="{width:145 height:36 currentPage:chat previousPage: pages:map[chat:0xc0001f87d0] loadedPages:map[chat:true] status:{app:0xc000270cb0 queue:[] width:145 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc000270cb0 showPermissions:false permissions:0xc00022f408 showHelp:false help:0xc000490e70 showQuit:true quit:0xc000388ab9 showSessionDialog:false sessionDialog:0xc000319f00 showCommandDialog:false commandDialog:0xc0003e55e0 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6b340} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6b1c0}] showModelDialog:false modelDialog:0xc00030bd60 showInitDialog:false initDialog:{width:145 height:36 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d4c88 showThemeDialog:false themeDialog:0xc0000ac4c0 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc0000ac500}"
|
|
||||||
time=2025-06-02T19:45:47.456-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-06-02T19:45:47.462-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-06-02T19:46:50.039-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-06-02T19:46:50.039-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-06-02T19:46:50.039-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-06-02T19:46:50.039-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-06-02T19:46:50.039-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-06-02T19:46:50.039-04:00 level=INFO msg="TUI exited" result="{width:145 height:36 currentPage:chat previousPage: pages:map[chat:0xc00035b9f0] loadedPages:map[chat:true] status:{app:0xc0000ec230 queue:[] width:145 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc0000ec230 showPermissions:false permissions:0xc0005e5408 showHelp:false help:0xc0005ad950 showQuit:true quit:0xc0005a0c09 showSessionDialog:false sessionDialog:0xc00012e440 showCommandDialog:false commandDialog:0xc0003c2160 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6b340} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6b1c0}] showModelDialog:false modelDialog:0xc0002c74a0 showInitDialog:false initDialog:{width:145 height:36 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc00013ac88 showThemeDialog:false themeDialog:0xc00012e680 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc00012e6c0}"
|
|
||||||
time=2025-06-02T19:47:11.433-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-06-02T19:47:11.438-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-06-02T19:48:43.841-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-06-02T19:48:43.841-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-06-02T19:48:43.842-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-06-02T19:48:43.842-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-06-02T19:48:43.842-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-06-02T19:48:43.842-04:00 level=INFO msg="TUI exited" result="{width:145 height:36 currentPage:chat previousPage: pages:map[chat:0xc0001f9040] loadedPages:map[chat:true] status:{app:0xc000270070 queue:[] width:145 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc000270070 showPermissions:false permissions:0xc00022f408 showHelp:false help:0xc000122090 showQuit:true quit:0xc000447c19 showSessionDialog:false sessionDialog:0xc000323b40 showCommandDialog:false commandDialog:0xc0003cb540 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6b340} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6b1c0}] showModelDialog:false modelDialog:0xc00044d5e0 showInitDialog:false initDialog:{width:145 height:36 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d4c88 showThemeDialog:false themeDialog:0xc000323d80 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc000323dc0}"
|
|
||||||
time=2025-06-02T19:48:57.679-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-06-02T19:48:57.685-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-06-02T19:50:56.190-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-06-02T19:50:56.190-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-06-02T19:50:56.190-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-06-02T19:50:56.190-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-06-02T19:50:56.190-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-06-02T19:50:56.190-04:00 level=INFO msg="TUI exited" result="{width:145 height:36 currentPage:chat previousPage: pages:map[chat:0xc000564be0] loadedPages:map[chat:true] status:{app:0xc000250d20 queue:[] width:145 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc000250d20 showPermissions:false permissions:0xc0004d6a08 showHelp:false help:0xc00061d5c0 showQuit:true quit:0xc0005578a9 showSessionDialog:false sessionDialog:0xc00032a640 showCommandDialog:false commandDialog:0xc0003e51e0 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6b340} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6b1c0}] showModelDialog:false modelDialog:0xc000620aa0 showInitDialog:false initDialog:{width:145 height:36 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d4c88 showThemeDialog:false themeDialog:0xc00032a880 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc00032a8c0}"
|
|
||||||
@@ -2,17 +2,14 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea/v2"
|
tea "github.com/charmbracelet/bubbletea/v2"
|
||||||
zone "github.com/lrstanley/bubblezone"
|
|
||||||
"github.com/sst/opencode/internal/app"
|
"github.com/sst/opencode/internal/app"
|
||||||
"github.com/sst/opencode/internal/pubsub"
|
|
||||||
"github.com/sst/opencode/internal/tui"
|
"github.com/sst/opencode/internal/tui"
|
||||||
"github.com/sst/opencode/pkg/client"
|
"github.com/sst/opencode/pkg/client"
|
||||||
)
|
)
|
||||||
@@ -20,18 +17,22 @@ import (
|
|||||||
var Version = "dev"
|
var Version = "dev"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
version := Version
|
||||||
|
if version != "dev" && !strings.HasPrefix(Version, "v") {
|
||||||
|
version = "v" + Version
|
||||||
|
}
|
||||||
|
|
||||||
url := os.Getenv("OPENCODE_SERVER")
|
url := os.Getenv("OPENCODE_SERVER")
|
||||||
httpClient, err := client.NewClientWithResponses(url)
|
|
||||||
|
appInfoStr := os.Getenv("OPENCODE_APP_INFO")
|
||||||
|
var appInfo client.AppInfo
|
||||||
|
err := json.Unmarshal([]byte(appInfoStr), &appInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to create client", "error", err)
|
slog.Error("Failed to unmarshal app info", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
paths, err := httpClient.PostPathGetWithResponse(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
logfile := filepath.Join(paths.JSON200.Data, "log", "tui.log")
|
|
||||||
|
|
||||||
|
logfile := filepath.Join(appInfo.Path.Data, "log", "tui.log")
|
||||||
if _, err := os.Stat(filepath.Dir(logfile)); os.IsNotExist(err) {
|
if _, err := os.Stat(filepath.Dir(logfile)); os.IsNotExist(err) {
|
||||||
err := os.MkdirAll(filepath.Dir(logfile), 0755)
|
err := os.MkdirAll(filepath.Dir(logfile), 0755)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -48,26 +49,26 @@ func main() {
|
|||||||
logger := slog.New(slog.NewTextHandler(file, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
logger := slog.New(slog.NewTextHandler(file, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||||
slog.SetDefault(logger)
|
slog.SetDefault(logger)
|
||||||
|
|
||||||
|
httpClient, err := client.NewClientWithResponses(url)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to create client", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
// Create main context for the application
|
// Create main context for the application
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
version := Version
|
app_, err := app.New(ctx, version, appInfo, httpClient)
|
||||||
if version != "dev" && !strings.HasPrefix(Version, "v") {
|
|
||||||
version = "v" + Version
|
|
||||||
}
|
|
||||||
app_, err := app.New(ctx, version, httpClient)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up the TUI
|
|
||||||
zone.NewGlobal()
|
|
||||||
program := tea.NewProgram(
|
program := tea.NewProgram(
|
||||||
tui.NewModel(app_),
|
tui.NewModel(app_),
|
||||||
// tea.WithMouseCellMotion(),
|
|
||||||
tea.WithKeyboardEnhancements(),
|
|
||||||
tea.WithAltScreen(),
|
tea.WithAltScreen(),
|
||||||
|
tea.WithKeyboardEnhancements(),
|
||||||
|
tea.WithMouseCellMotion(),
|
||||||
)
|
)
|
||||||
|
|
||||||
eventClient, err := client.NewClient(url)
|
eventClient, err := client.NewClient(url)
|
||||||
@@ -88,133 +89,11 @@ func main() {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Setup the subscriptions, this will send services events to the TUI
|
|
||||||
ch, cancelSubs := setupSubscriptions(app_, ctx)
|
|
||||||
|
|
||||||
// Create a context for the TUI message handler
|
|
||||||
tuiCtx, tuiCancel := context.WithCancel(ctx)
|
|
||||||
var tuiWg sync.WaitGroup
|
|
||||||
tuiWg.Add(1)
|
|
||||||
|
|
||||||
// Set up message handling for the TUI
|
|
||||||
go func() {
|
|
||||||
defer tuiWg.Done()
|
|
||||||
// defer logging.RecoverPanic("TUI-message-handler", func() {
|
|
||||||
// attemptTUIRecovery(program)
|
|
||||||
// })
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-tuiCtx.Done():
|
|
||||||
slog.Info("TUI message handler shutting down")
|
|
||||||
return
|
|
||||||
case msg, ok := <-ch:
|
|
||||||
if !ok {
|
|
||||||
slog.Info("TUI message channel closed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
program.Send(msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Cleanup function for when the program exits
|
|
||||||
cleanup := func() {
|
|
||||||
// Cancel subscriptions first
|
|
||||||
cancelSubs()
|
|
||||||
|
|
||||||
// Then cancel TUI message handler
|
|
||||||
tuiCancel()
|
|
||||||
|
|
||||||
// Wait for TUI message handler to finish
|
|
||||||
tuiWg.Wait()
|
|
||||||
|
|
||||||
slog.Info("All goroutines cleaned up")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the TUI
|
// Run the TUI
|
||||||
result, err := program.Run()
|
result, err := program.Run()
|
||||||
cleanup()
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("TUI error", "error", err)
|
slog.Error("TUI error", "error", err)
|
||||||
// return fmt.Errorf("TUI error: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Info("TUI exited", "result", result)
|
slog.Info("TUI exited", "result", result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupSubscriber[T any](
|
|
||||||
ctx context.Context,
|
|
||||||
wg *sync.WaitGroup,
|
|
||||||
name string,
|
|
||||||
subscriber func(context.Context) <-chan pubsub.Event[T],
|
|
||||||
outputCh chan<- tea.Msg,
|
|
||||||
) {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
// defer logging.RecoverPanic(fmt.Sprintf("subscription-%s", name), nil)
|
|
||||||
|
|
||||||
subCh := subscriber(ctx)
|
|
||||||
if subCh == nil {
|
|
||||||
slog.Warn("subscription channel is nil", "name", name)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case event, ok := <-subCh:
|
|
||||||
if !ok {
|
|
||||||
slog.Info("subscription channel closed", "name", name)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var msg tea.Msg = event
|
|
||||||
|
|
||||||
select {
|
|
||||||
case outputCh <- msg:
|
|
||||||
case <-time.After(2 * time.Second):
|
|
||||||
slog.Warn("message dropped due to slow consumer", "name", name)
|
|
||||||
case <-ctx.Done():
|
|
||||||
slog.Info("subscription cancelled", "name", name)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case <-ctx.Done():
|
|
||||||
slog.Info("subscription cancelled", "name", name)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupSubscriptions(app *app.App, parentCtx context.Context) (chan tea.Msg, func()) {
|
|
||||||
ch := make(chan tea.Msg, 100)
|
|
||||||
|
|
||||||
wg := sync.WaitGroup{}
|
|
||||||
ctx, cancel := context.WithCancel(parentCtx) // Inherit from parent context
|
|
||||||
|
|
||||||
setupSubscriber(ctx, &wg, "status", app.Status.Subscribe, ch)
|
|
||||||
|
|
||||||
cleanupFunc := func() {
|
|
||||||
slog.Info("Cancelling all subscriptions")
|
|
||||||
cancel() // Signal all goroutines to stop
|
|
||||||
|
|
||||||
waitCh := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
// defer logging.RecoverPanic("subscription-cleanup", nil)
|
|
||||||
wg.Wait()
|
|
||||||
close(waitCh)
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-waitCh:
|
|
||||||
slog.Info("All subscription goroutines completed successfully")
|
|
||||||
close(ch) // Only close after all writers are confirmed done
|
|
||||||
case <-time.After(5 * time.Second):
|
|
||||||
slog.Warn("Timed out waiting for some subscription goroutines to complete")
|
|
||||||
close(ch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ch, cleanupFunc
|
|
||||||
}
|
|
||||||
|
|||||||
+1
-6
@@ -5,14 +5,12 @@ go 1.24.0
|
|||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v1.5.0
|
github.com/BurntSushi/toml v1.5.0
|
||||||
github.com/alecthomas/chroma/v2 v2.18.0
|
github.com/alecthomas/chroma/v2 v2.18.0
|
||||||
github.com/bmatcuk/doublestar/v4 v4.8.1
|
|
||||||
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1
|
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1
|
||||||
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3
|
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3
|
||||||
github.com/charmbracelet/glamour v0.10.0
|
github.com/charmbracelet/glamour v0.10.0
|
||||||
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta1
|
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1
|
||||||
github.com/charmbracelet/x/ansi v0.8.0
|
github.com/charmbracelet/x/ansi v0.8.0
|
||||||
github.com/lithammer/fuzzysearch v1.1.8
|
github.com/lithammer/fuzzysearch v1.1.8
|
||||||
github.com/lrstanley/bubblezone v0.0.0-20250315020633-c249a3fe1231
|
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6
|
||||||
github.com/muesli/reflow v0.3.0
|
github.com/muesli/reflow v0.3.0
|
||||||
github.com/muesli/termenv v0.16.0
|
github.com/muesli/termenv v0.16.0
|
||||||
@@ -27,7 +25,6 @@ require (
|
|||||||
dario.cat/mergo v1.0.2 // indirect
|
dario.cat/mergo v1.0.2 // indirect
|
||||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
||||||
github.com/atombender/go-jsonschema v0.20.0 // indirect
|
github.com/atombender/go-jsonschema v0.20.0 // indirect
|
||||||
github.com/charmbracelet/bubbletea v1.3.4 // indirect
|
|
||||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect
|
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect
|
||||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
|
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
|
||||||
github.com/charmbracelet/x/input v0.3.5-0.20250424101541-abb4d9a9b197 // indirect
|
github.com/charmbracelet/x/input v0.3.5-0.20250424101541-abb4d9a9b197 // indirect
|
||||||
@@ -66,13 +63,11 @@ require (
|
|||||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||||
github.com/disintegration/imaging v1.6.2
|
github.com/disintegration/imaging v1.6.2
|
||||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
|
||||||
github.com/google/go-cmp v0.7.0 // indirect
|
github.com/google/go-cmp v0.7.0 // indirect
|
||||||
github.com/gorilla/css v1.0.1 // indirect
|
github.com/gorilla/css v1.0.1 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0
|
github.com/lucasb-eyer/go-colorful v1.2.0
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
|
|||||||
+2
-13
@@ -24,12 +24,8 @@ github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/
|
|||||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||||
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
|
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
|
||||||
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
|
|
||||||
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
|
||||||
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1 h1:swACzss0FjnyPz1enfX56GKkLiuKg5FlyVmOLIlU2kE=
|
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1 h1:swACzss0FjnyPz1enfX56GKkLiuKg5FlyVmOLIlU2kE=
|
||||||
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
|
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
|
||||||
github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI=
|
|
||||||
github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
|
|
||||||
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3 h1:5A2e3myxXMpCES+kjEWgGsaf9VgZXjZbLi5iMTH7j40=
|
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3 h1:5A2e3myxXMpCES+kjEWgGsaf9VgZXjZbLi5iMTH7j40=
|
||||||
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3/go.mod h1:ZFDg5oPjyRYrPAa3iFrtP1DO8xy+LUQxd9JFHEcuwJY=
|
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3/go.mod h1:ZFDg5oPjyRYrPAa3iFrtP1DO8xy+LUQxd9JFHEcuwJY=
|
||||||
github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
|
github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
|
||||||
@@ -38,8 +34,8 @@ github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V
|
|||||||
github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk=
|
github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk=
|
||||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
|
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
|
||||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
|
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
|
||||||
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta1 h1:SOylT6+BQzPHEjn15TIzawBPVD0QmhKXbcb3jY0ZIKU=
|
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1 h1:D9AJJuYTN5pvz6mpIGO1ijLKpfTYSHOtKGgwoTQ4Gog=
|
||||||
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta1/go.mod h1:tRlx/Hu0lo/j9viunCN2H+Ze6JrmdjQlXUQvvArgaOc=
|
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1/go.mod h1:tRlx/Hu0lo/j9viunCN2H+Ze6JrmdjQlXUQvvArgaOc=
|
||||||
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
|
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
|
||||||
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
|
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.14-0.20250501183327-ad3bc78c6a81 h1:iGrflaL5jQW6crML+pZx/ulWAVZQR3CQoRGvFsr2Tyg=
|
github.com/charmbracelet/x/cellbuf v0.0.14-0.20250501183327-ad3bc78c6a81 h1:iGrflaL5jQW6crML+pZx/ulWAVZQR3CQoRGvFsr2Tyg=
|
||||||
@@ -69,8 +65,6 @@ github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cn
|
|||||||
github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58=
|
github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58=
|
||||||
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 h1:PRxIJD8XjimM5aTknUK9w6DHLDox2r2M3DI4i2pnd3w=
|
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 h1:PRxIJD8XjimM5aTknUK9w6DHLDox2r2M3DI4i2pnd3w=
|
||||||
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5qlB+mlV1okblJqcSMtR4c52UKxDiX9GRBS8+Q=
|
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5qlB+mlV1okblJqcSMtR4c52UKxDiX9GRBS8+Q=
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||||
@@ -126,16 +120,12 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
||||||
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
||||||
github.com/lrstanley/bubblezone v0.0.0-20250315020633-c249a3fe1231 h1:9rjt7AfnrXKNSZhp36A3/4QAZAwGGCGD/p8Bse26zms=
|
|
||||||
github.com/lrstanley/bubblezone v0.0.0-20250315020633-c249a3fe1231/go.mod h1:S5etECMx+sZnW0Gm100Ma9J1PgVCTgNyFaqGu2b08b4=
|
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
|
||||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
|
||||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
@@ -267,7 +257,6 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Vendored
BIN
Binary file not shown.
+216
-109
@@ -5,122 +5,189 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea/v2"
|
tea "github.com/charmbracelet/bubbletea/v2"
|
||||||
"github.com/sst/opencode/internal/commands"
|
"github.com/sst/opencode/internal/commands"
|
||||||
|
"github.com/sst/opencode/internal/components/toast"
|
||||||
"github.com/sst/opencode/internal/config"
|
"github.com/sst/opencode/internal/config"
|
||||||
"github.com/sst/opencode/internal/state"
|
|
||||||
"github.com/sst/opencode/internal/status"
|
|
||||||
"github.com/sst/opencode/internal/theme"
|
"github.com/sst/opencode/internal/theme"
|
||||||
"github.com/sst/opencode/internal/util"
|
"github.com/sst/opencode/internal/util"
|
||||||
"github.com/sst/opencode/pkg/client"
|
"github.com/sst/opencode/pkg/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var RootPath string
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
ConfigPath string
|
Info client.AppInfo
|
||||||
Config *config.Config
|
Version string
|
||||||
Client *client.ClientWithResponses
|
StatePath string
|
||||||
Provider *client.ProviderInfo
|
Config *client.ConfigInfo
|
||||||
Model *client.ModelInfo
|
Client *client.ClientWithResponses
|
||||||
Session *client.SessionInfo
|
State *config.State
|
||||||
Messages []client.MessageInfo
|
Provider *client.ProviderInfo
|
||||||
Status status.Service
|
Model *client.ModelInfo
|
||||||
Commands commands.Registry
|
Session *client.SessionInfo
|
||||||
|
Messages []client.MessageInfo
|
||||||
|
Commands commands.CommandRegistry
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppInfo struct {
|
type SessionSelectedMsg = *client.SessionInfo
|
||||||
client.AppInfo
|
type ModelSelectedMsg struct {
|
||||||
Version string
|
Provider client.ProviderInfo
|
||||||
|
Model client.ModelInfo
|
||||||
|
}
|
||||||
|
type SessionClearedMsg struct{}
|
||||||
|
type CompactSessionMsg struct{}
|
||||||
|
type SendMsg struct {
|
||||||
|
Text string
|
||||||
|
Attachments []Attachment
|
||||||
|
}
|
||||||
|
type CompletionDialogTriggeredMsg struct {
|
||||||
|
InitialValue string
|
||||||
|
}
|
||||||
|
type OptimisticMessageAddedMsg struct {
|
||||||
|
Message client.MessageInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
var Info AppInfo
|
func New(
|
||||||
|
ctx context.Context,
|
||||||
|
version string,
|
||||||
|
appInfo client.AppInfo,
|
||||||
|
httpClient *client.ClientWithResponses,
|
||||||
|
) (*App, error) {
|
||||||
|
RootPath = appInfo.Path.Root
|
||||||
|
|
||||||
func New(ctx context.Context, version string, httpClient *client.ClientWithResponses) (*App, error) {
|
configResponse, err := httpClient.PostConfigGetWithResponse(ctx)
|
||||||
err := status.InitService()
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to initialize status service", "error", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
appInfoResponse, _ := httpClient.PostAppInfoWithResponse(ctx)
|
|
||||||
appInfo := appInfoResponse.JSON200
|
|
||||||
Info = AppInfo{Version: version}
|
|
||||||
Info.Git = appInfo.Git
|
|
||||||
Info.Path = appInfo.Path
|
|
||||||
Info.Time = appInfo.Time
|
|
||||||
Info.User = appInfo.User
|
|
||||||
|
|
||||||
providersResponse, err := httpClient.PostProviderListWithResponse(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
providers := []client.ProviderInfo{}
|
if configResponse.StatusCode() != 200 || configResponse.JSON200 == nil {
|
||||||
var defaultProvider *client.ProviderInfo
|
return nil, fmt.Errorf("failed to get config: %d", configResponse.StatusCode())
|
||||||
var defaultModel *client.ModelInfo
|
}
|
||||||
|
configInfo := configResponse.JSON200
|
||||||
var anthropic *client.ProviderInfo
|
if configInfo.Keybinds == nil {
|
||||||
for _, provider := range providersResponse.JSON200.Providers {
|
leader := "ctrl+x"
|
||||||
if provider.Id == "anthropic" {
|
keybinds := client.ConfigKeybinds{
|
||||||
anthropic = &provider
|
Leader: &leader,
|
||||||
}
|
}
|
||||||
|
configInfo.Keybinds = &keybinds
|
||||||
}
|
}
|
||||||
|
|
||||||
// default to anthropic if available
|
appStatePath := filepath.Join(appInfo.Path.State, "tui")
|
||||||
if anthropic != nil {
|
appState, err := config.LoadState(appStatePath)
|
||||||
defaultProvider = anthropic
|
|
||||||
defaultModel = getDefaultModel(providersResponse, *anthropic)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, provider := range providersResponse.JSON200.Providers {
|
|
||||||
if defaultProvider == nil || defaultModel == nil {
|
|
||||||
defaultProvider = &provider
|
|
||||||
defaultModel = getDefaultModel(providersResponse, provider)
|
|
||||||
}
|
|
||||||
providers = append(providers, provider)
|
|
||||||
}
|
|
||||||
if len(providers) == 0 {
|
|
||||||
return nil, fmt.Errorf("no providers found")
|
|
||||||
}
|
|
||||||
|
|
||||||
appConfigPath := filepath.Join(Info.Path.Config, "config")
|
|
||||||
appConfig, err := config.LoadConfig(appConfigPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Info("No TUI config found, using default values", "error", err)
|
appState = config.NewState()
|
||||||
appConfig = config.NewConfig("opencode", defaultProvider.Id, defaultModel.Id)
|
config.SaveState(appStatePath, appState)
|
||||||
config.SaveConfig(appConfigPath, appConfig)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentProvider *client.ProviderInfo
|
if configInfo.Theme != nil {
|
||||||
var currentModel *client.ModelInfo
|
appState.Theme = *configInfo.Theme
|
||||||
for _, provider := range providers {
|
}
|
||||||
if provider.Id == appConfig.Provider {
|
if configInfo.Model != nil {
|
||||||
currentProvider = &provider
|
splits := strings.Split(*configInfo.Model, "/")
|
||||||
|
appState.Provider = splits[0]
|
||||||
|
appState.Model = strings.Join(splits[1:], "/")
|
||||||
|
}
|
||||||
|
|
||||||
for _, model := range provider.Models {
|
// Load themes from all directories
|
||||||
if model.Id == appConfig.Model {
|
if err := theme.LoadThemesFromDirectories(
|
||||||
currentModel = &model
|
appInfo.Path.Config,
|
||||||
|
appInfo.Path.Root,
|
||||||
|
appInfo.Path.Cwd,
|
||||||
|
); err != nil {
|
||||||
|
slog.Warn("Failed to load themes from directories", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if appState.Theme != "" {
|
||||||
|
theme.SetTheme(appState.Theme)
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug("Loaded config", "config", configInfo)
|
||||||
|
|
||||||
|
app := &App{
|
||||||
|
Info: appInfo,
|
||||||
|
Version: version,
|
||||||
|
StatePath: appStatePath,
|
||||||
|
Config: configInfo,
|
||||||
|
State: appState,
|
||||||
|
Client: httpClient,
|
||||||
|
Session: &client.SessionInfo{},
|
||||||
|
Messages: []client.MessageInfo{},
|
||||||
|
Commands: commands.LoadFromConfig(configInfo),
|
||||||
|
}
|
||||||
|
|
||||||
|
return app, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) InitializeProvider() tea.Cmd {
|
||||||
|
return func() tea.Msg {
|
||||||
|
providersResponse, err := a.Client.PostProviderListWithResponse(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to list providers", "error", err)
|
||||||
|
// TODO: notify user
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if providersResponse != nil && providersResponse.StatusCode() != 200 {
|
||||||
|
slog.Error("failed to retrieve providers", "status", providersResponse.StatusCode(), "message", string(providersResponse.Body))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
providers := []client.ProviderInfo{}
|
||||||
|
var defaultProvider *client.ProviderInfo
|
||||||
|
var defaultModel *client.ModelInfo
|
||||||
|
|
||||||
|
var anthropic *client.ProviderInfo
|
||||||
|
for _, provider := range providersResponse.JSON200.Providers {
|
||||||
|
if provider.Id == "anthropic" {
|
||||||
|
anthropic = &provider
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// default to anthropic if available
|
||||||
|
if anthropic != nil {
|
||||||
|
defaultProvider = anthropic
|
||||||
|
defaultModel = getDefaultModel(providersResponse, *anthropic)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, provider := range providersResponse.JSON200.Providers {
|
||||||
|
if defaultProvider == nil || defaultModel == nil {
|
||||||
|
defaultProvider = &provider
|
||||||
|
defaultModel = getDefaultModel(providersResponse, provider)
|
||||||
|
}
|
||||||
|
providers = append(providers, provider)
|
||||||
|
}
|
||||||
|
if len(providers) == 0 {
|
||||||
|
slog.Error("No providers configured")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentProvider *client.ProviderInfo
|
||||||
|
var currentModel *client.ModelInfo
|
||||||
|
for _, provider := range providers {
|
||||||
|
if provider.Id == a.State.Provider {
|
||||||
|
currentProvider = &provider
|
||||||
|
|
||||||
|
for _, model := range provider.Models {
|
||||||
|
if model.Id == a.State.Model {
|
||||||
|
currentModel = &model
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if currentProvider == nil || currentModel == nil {
|
||||||
|
currentProvider = defaultProvider
|
||||||
|
currentModel = defaultModel
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: handle no provider or model setup, yet
|
||||||
|
return ModelSelectedMsg{
|
||||||
|
Provider: *currentProvider,
|
||||||
|
Model: *currentModel,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
app := &App{
|
|
||||||
ConfigPath: appConfigPath,
|
|
||||||
Config: appConfig,
|
|
||||||
Client: httpClient,
|
|
||||||
Provider: currentProvider,
|
|
||||||
Model: currentModel,
|
|
||||||
Session: &client.SessionInfo{},
|
|
||||||
Messages: []client.MessageInfo{},
|
|
||||||
Status: status.GetService(),
|
|
||||||
Commands: commands.NewCommandRegistry(),
|
|
||||||
}
|
|
||||||
|
|
||||||
theme.SetTheme(appConfig.Theme)
|
|
||||||
|
|
||||||
return app, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDefaultModel(response *client.PostProviderListResponse, provider client.ProviderInfo) *client.ModelInfo {
|
func getDefaultModel(response *client.PostProviderListResponse, provider client.ProviderInfo) *client.ModelInfo {
|
||||||
@@ -151,8 +218,11 @@ func (a *App) IsBusy() bool {
|
|||||||
return lastMessage.Metadata.Time.Completed == nil
|
return lastMessage.Metadata.Time.Completed == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) SaveConfig() {
|
func (a *App) SaveState() {
|
||||||
config.SaveConfig(a.ConfigPath, a.Config)
|
err := config.SaveState(a.StatePath, a.State)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to save state", "error", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) InitializeProject(ctx context.Context) tea.Cmd {
|
func (a *App) InitializeProject(ctx context.Context) tea.Cmd {
|
||||||
@@ -160,31 +230,49 @@ func (a *App) InitializeProject(ctx context.Context) tea.Cmd {
|
|||||||
|
|
||||||
session, err := a.CreateSession(ctx)
|
session, err := a.CreateSession(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
status.Error(err.Error())
|
// status.Error(err.Error())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
a.Session = session
|
a.Session = session
|
||||||
cmds = append(cmds, util.CmdHandler(state.SessionSelectedMsg(session)))
|
cmds = append(cmds, util.CmdHandler(SessionSelectedMsg(session)))
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
// TODO: Handle no provider or model setup, yet
|
|
||||||
response, err := a.Client.PostSessionInitialize(ctx, client.PostSessionInitializeJSONRequestBody{
|
response, err := a.Client.PostSessionInitialize(ctx, client.PostSessionInitializeJSONRequestBody{
|
||||||
SessionID: a.Session.Id,
|
SessionID: a.Session.Id,
|
||||||
ProviderID: a.Provider.Id,
|
ProviderID: a.Provider.Id,
|
||||||
ModelID: a.Model.Id,
|
ModelID: a.Model.Id,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
status.Error(err.Error())
|
slog.Error("Failed to initialize project", "error", err)
|
||||||
|
// status.Error(err.Error())
|
||||||
}
|
}
|
||||||
if response != nil && response.StatusCode != 200 {
|
if response != nil && response.StatusCode != 200 {
|
||||||
status.Error(fmt.Sprintf("failed to initialize project: %d", response.StatusCode))
|
slog.Error("Failed to initialize project", "error", response.StatusCode)
|
||||||
|
// status.Error(fmt.Sprintf("failed to initialize project: %d", response.StatusCode))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return tea.Batch(cmds...)
|
return tea.Batch(cmds...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) CompactSession(ctx context.Context) tea.Cmd {
|
||||||
|
go func() {
|
||||||
|
response, err := a.Client.PostSessionSummarizeWithResponse(ctx, client.PostSessionSummarizeJSONRequestBody{
|
||||||
|
SessionID: a.Session.Id,
|
||||||
|
ProviderID: a.Provider.Id,
|
||||||
|
ModelID: a.Model.Id,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to compact session", "error", err)
|
||||||
|
}
|
||||||
|
if response != nil && response.StatusCode() != 200 {
|
||||||
|
slog.Error("Failed to compact session", "error", response.StatusCode)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) MarkProjectInitialized(ctx context.Context) error {
|
func (a *App) MarkProjectInitialized(ctx context.Context) error {
|
||||||
response, err := a.Client.PostAppInitialize(ctx)
|
response, err := a.Client.PostAppInitialize(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -214,17 +302,10 @@ func (a *App) SendChatMessage(ctx context.Context, text string, attachments []At
|
|||||||
if a.Session.Id == "" {
|
if a.Session.Id == "" {
|
||||||
session, err := a.CreateSession(ctx)
|
session, err := a.CreateSession(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
status.Error(err.Error())
|
return toast.NewErrorToast(err.Error())
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
a.Session = session
|
a.Session = session
|
||||||
cmds = append(cmds, util.CmdHandler(state.SessionSelectedMsg(session)))
|
cmds = append(cmds, util.CmdHandler(SessionSelectedMsg(session)))
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Handle attachments when API supports them
|
|
||||||
if len(attachments) > 0 {
|
|
||||||
// For now, ignore attachments
|
|
||||||
// return "", fmt.Errorf("attachments not supported yet")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
part := client.MessagePart{}
|
part := client.MessagePart{}
|
||||||
@@ -234,7 +315,26 @@ func (a *App) SendChatMessage(ctx context.Context, text string, attachments []At
|
|||||||
})
|
})
|
||||||
parts := []client.MessagePart{part}
|
parts := []client.MessagePart{part}
|
||||||
|
|
||||||
go func() {
|
optimisticMessage := client.MessageInfo{
|
||||||
|
Id: fmt.Sprintf("optimistic-%d", time.Now().UnixNano()),
|
||||||
|
Role: client.User,
|
||||||
|
Parts: parts,
|
||||||
|
Metadata: client.MessageMetadata{
|
||||||
|
SessionID: a.Session.Id,
|
||||||
|
Time: struct {
|
||||||
|
Completed *float32 `json:"completed,omitempty"`
|
||||||
|
Created float32 `json:"created"`
|
||||||
|
}{
|
||||||
|
Created: float32(time.Now().Unix()),
|
||||||
|
},
|
||||||
|
Tool: make(map[string]client.MessageMetadata_Tool_AdditionalProperties),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Messages = append(a.Messages, optimisticMessage)
|
||||||
|
cmds = append(cmds, util.CmdHandler(OptimisticMessageAddedMsg{Message: optimisticMessage}))
|
||||||
|
|
||||||
|
cmds = append(cmds, func() tea.Msg {
|
||||||
response, err := a.Client.PostSessionChat(ctx, client.PostSessionChatJSONRequestBody{
|
response, err := a.Client.PostSessionChat(ctx, client.PostSessionChatJSONRequestBody{
|
||||||
SessionID: a.Session.Id,
|
SessionID: a.Session.Id,
|
||||||
Parts: parts,
|
Parts: parts,
|
||||||
@@ -242,14 +342,17 @@ func (a *App) SendChatMessage(ctx context.Context, text string, attachments []At
|
|||||||
ModelID: a.Model.Id,
|
ModelID: a.Model.Id,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to send message", "error", err)
|
errormsg := fmt.Sprintf("failed to send message: %v", err)
|
||||||
status.Error(err.Error())
|
slog.Error(errormsg)
|
||||||
|
return toast.NewErrorToast(errormsg)()
|
||||||
}
|
}
|
||||||
if response != nil && response.StatusCode != 200 {
|
if response != nil && response.StatusCode != 200 {
|
||||||
slog.Error("Failed to send message", "error", fmt.Sprintf("failed to send message: %d", response.StatusCode))
|
errormsg := fmt.Sprintf("failed to send message: %d", response.StatusCode)
|
||||||
status.Error(fmt.Sprintf("failed to send message: %d", response.StatusCode))
|
slog.Error(errormsg)
|
||||||
|
return toast.NewErrorToast(errormsg)()
|
||||||
}
|
}
|
||||||
}()
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
// The actual response will come through SSE
|
// The actual response will come through SSE
|
||||||
// For now, just return success
|
// For now, just return success
|
||||||
@@ -262,12 +365,12 @@ func (a *App) Cancel(ctx context.Context, sessionID string) error {
|
|||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to cancel session", "error", err)
|
slog.Error("Failed to cancel session", "error", err)
|
||||||
status.Error(err.Error())
|
// status.Error(err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if response != nil && response.StatusCode != 200 {
|
if response != nil && response.StatusCode != 200 {
|
||||||
slog.Error("Failed to cancel session", "error", fmt.Sprintf("failed to cancel session: %d", response.StatusCode))
|
slog.Error("Failed to cancel session", "error", fmt.Sprintf("failed to cancel session: %d", response.StatusCode))
|
||||||
status.Error(fmt.Sprintf("failed to cancel session: %d", response.StatusCode))
|
// status.Error(fmt.Sprintf("failed to cancel session: %d", response.StatusCode))
|
||||||
return fmt.Errorf("failed to cancel session: %d", response.StatusCode)
|
return fmt.Errorf("failed to cancel session: %d", response.StatusCode)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -323,3 +426,7 @@ func (a *App) ListProviders(ctx context.Context) ([]client.ProviderInfo, error)
|
|||||||
providers := *resp.JSON200
|
providers := *resp.JSON200
|
||||||
return providers.Providers, nil
|
return providers.Providers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// func (a *App) loadCustomKeybinds() {
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
|||||||
@@ -1,71 +1,281 @@
|
|||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/charmbracelet/bubbles/v2/key"
|
"encoding/json"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea/v2"
|
||||||
|
"github.com/sst/opencode/pkg/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Command represents a user-triggerable action.
|
type ExecuteCommandMsg Command
|
||||||
|
type ExecuteCommandsMsg []Command
|
||||||
|
type CommandExecutedMsg Command
|
||||||
|
|
||||||
|
type Keybinding struct {
|
||||||
|
RequiresLeader bool
|
||||||
|
Key string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k Keybinding) Matches(msg tea.KeyPressMsg, leader bool) bool {
|
||||||
|
key := k.Key
|
||||||
|
key = strings.TrimSpace(key)
|
||||||
|
return key == msg.String() && (k.RequiresLeader == leader)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommandName string
|
||||||
type Command struct {
|
type Command struct {
|
||||||
// Name is the identifier used for slash commands (e.g., "new").
|
Name CommandName
|
||||||
Name string
|
|
||||||
// Description is a short explanation of what the command does.
|
|
||||||
Description string
|
Description string
|
||||||
// KeyBinding is the keyboard shortcut to trigger this command.
|
Keybindings []Keybinding
|
||||||
KeyBinding key.Binding
|
Trigger string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Registry holds all the available commands.
|
func (c Command) Keys() []string {
|
||||||
type Registry map[string]Command
|
var keys []string
|
||||||
|
for _, k := range c.Keybindings {
|
||||||
// ExecuteCommandMsg is a message sent when a command should be executed.
|
keys = append(keys, k.Key)
|
||||||
type ExecuteCommandMsg struct {
|
}
|
||||||
Name string
|
return keys
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCommandRegistry() Registry {
|
type CommandRegistry map[CommandName]Command
|
||||||
return Registry{
|
|
||||||
"help": {
|
func (r CommandRegistry) Sorted() []Command {
|
||||||
Name: "help",
|
var commands []Command
|
||||||
|
for _, command := range r {
|
||||||
|
commands = append(commands, command)
|
||||||
|
}
|
||||||
|
slices.SortFunc(commands, func(a, b Command) int {
|
||||||
|
if a.Name == AppExitCommand {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if b.Name == AppExitCommand {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return strings.Compare(string(a.Name), string(b.Name))
|
||||||
|
})
|
||||||
|
return commands
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r CommandRegistry) Matches(msg tea.KeyPressMsg, leader bool) []Command {
|
||||||
|
var matched []Command
|
||||||
|
for _, command := range r.Sorted() {
|
||||||
|
if command.Matches(msg, leader) {
|
||||||
|
matched = append(matched, command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matched
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
AppHelpCommand CommandName = "app_help"
|
||||||
|
EditorOpenCommand CommandName = "editor_open"
|
||||||
|
SessionNewCommand CommandName = "session_new"
|
||||||
|
SessionListCommand CommandName = "session_list"
|
||||||
|
SessionShareCommand CommandName = "session_share"
|
||||||
|
SessionInterruptCommand CommandName = "session_interrupt"
|
||||||
|
SessionCompactCommand CommandName = "session_compact"
|
||||||
|
ToolDetailsCommand CommandName = "tool_details"
|
||||||
|
ModelListCommand CommandName = "model_list"
|
||||||
|
ThemeListCommand CommandName = "theme_list"
|
||||||
|
ProjectInitCommand CommandName = "project_init"
|
||||||
|
InputClearCommand CommandName = "input_clear"
|
||||||
|
InputPasteCommand CommandName = "input_paste"
|
||||||
|
InputSubmitCommand CommandName = "input_submit"
|
||||||
|
InputNewlineCommand CommandName = "input_newline"
|
||||||
|
HistoryPreviousCommand CommandName = "history_previous"
|
||||||
|
HistoryNextCommand CommandName = "history_next"
|
||||||
|
MessagesPageUpCommand CommandName = "messages_page_up"
|
||||||
|
MessagesPageDownCommand CommandName = "messages_page_down"
|
||||||
|
MessagesHalfPageUpCommand CommandName = "messages_half_page_up"
|
||||||
|
MessagesHalfPageDownCommand CommandName = "messages_half_page_down"
|
||||||
|
MessagesPreviousCommand CommandName = "messages_previous"
|
||||||
|
MessagesNextCommand CommandName = "messages_next"
|
||||||
|
MessagesFirstCommand CommandName = "messages_first"
|
||||||
|
MessagesLastCommand CommandName = "messages_last"
|
||||||
|
AppExitCommand CommandName = "app_exit"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (k Command) Matches(msg tea.KeyPressMsg, leader bool) bool {
|
||||||
|
for _, binding := range k.Keybindings {
|
||||||
|
if binding.Matches(msg, leader) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseBindings(bindings ...string) []Keybinding {
|
||||||
|
var parsedBindings []Keybinding
|
||||||
|
for _, binding := range bindings {
|
||||||
|
for p := range strings.SplitSeq(binding, ",") {
|
||||||
|
requireLeader := strings.HasPrefix(p, "<leader>")
|
||||||
|
keybinding := strings.ReplaceAll(p, "<leader>", "")
|
||||||
|
keybinding = strings.TrimSpace(keybinding)
|
||||||
|
parsedBindings = append(parsedBindings, Keybinding{
|
||||||
|
RequiresLeader: requireLeader,
|
||||||
|
Key: keybinding,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parsedBindings
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadFromConfig(config *client.ConfigInfo) CommandRegistry {
|
||||||
|
defaults := []Command{
|
||||||
|
{
|
||||||
|
Name: AppHelpCommand,
|
||||||
Description: "show help",
|
Description: "show help",
|
||||||
KeyBinding: key.NewBinding(
|
Keybindings: parseBindings("<leader>h"),
|
||||||
key.WithKeys("f1", "super+/", "super+h"),
|
Trigger: "help",
|
||||||
),
|
|
||||||
},
|
},
|
||||||
"new": {
|
{
|
||||||
Name: "new",
|
Name: EditorOpenCommand,
|
||||||
|
Description: "open editor",
|
||||||
|
Keybindings: parseBindings("<leader>e"),
|
||||||
|
Trigger: "editor",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: SessionNewCommand,
|
||||||
Description: "new session",
|
Description: "new session",
|
||||||
KeyBinding: key.NewBinding(
|
Keybindings: parseBindings("<leader>n"),
|
||||||
key.WithKeys("f2", "super+n"),
|
Trigger: "new",
|
||||||
),
|
|
||||||
},
|
},
|
||||||
"sessions": {
|
{
|
||||||
Name: "sessions",
|
Name: SessionListCommand,
|
||||||
Description: "switch session",
|
Description: "list sessions",
|
||||||
KeyBinding: key.NewBinding(
|
Keybindings: parseBindings("<leader>l"),
|
||||||
key.WithKeys("f3", "super+s"),
|
Trigger: "sessions",
|
||||||
),
|
|
||||||
},
|
},
|
||||||
"model": {
|
{
|
||||||
Name: "model",
|
Name: SessionShareCommand,
|
||||||
Description: "switch model",
|
Description: "share session",
|
||||||
KeyBinding: key.NewBinding(
|
Keybindings: parseBindings("<leader>s"),
|
||||||
key.WithKeys("f4", "super+m"),
|
Trigger: "share",
|
||||||
),
|
|
||||||
},
|
},
|
||||||
"theme": {
|
{
|
||||||
Name: "theme",
|
Name: SessionInterruptCommand,
|
||||||
Description: "switch theme",
|
Description: "interrupt session",
|
||||||
KeyBinding: key.NewBinding(
|
Keybindings: parseBindings("esc"),
|
||||||
key.WithKeys("f5", "super+t"),
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
"quit": {
|
{
|
||||||
Name: "quit",
|
Name: SessionCompactCommand,
|
||||||
Description: "quit",
|
Description: "compact the session",
|
||||||
KeyBinding: key.NewBinding(
|
Keybindings: parseBindings("<leader>c"),
|
||||||
key.WithKeys("f10", "ctrl+c", "super+q"),
|
Trigger: "compact",
|
||||||
),
|
},
|
||||||
|
{
|
||||||
|
Name: ToolDetailsCommand,
|
||||||
|
Description: "toggle tool details",
|
||||||
|
Keybindings: parseBindings("<leader>d"),
|
||||||
|
Trigger: "details",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: ModelListCommand,
|
||||||
|
Description: "list models",
|
||||||
|
Keybindings: parseBindings("<leader>m"),
|
||||||
|
Trigger: "models",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: ThemeListCommand,
|
||||||
|
Description: "list themes",
|
||||||
|
Keybindings: parseBindings("<leader>t"),
|
||||||
|
Trigger: "themes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: ProjectInitCommand,
|
||||||
|
Description: "create/update AGENTS.md",
|
||||||
|
Keybindings: parseBindings("<leader>i"),
|
||||||
|
Trigger: "init",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: InputClearCommand,
|
||||||
|
Description: "clear input",
|
||||||
|
Keybindings: parseBindings("ctrl+c"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: InputPasteCommand,
|
||||||
|
Description: "paste content",
|
||||||
|
Keybindings: parseBindings("ctrl+v"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: InputSubmitCommand,
|
||||||
|
Description: "submit message",
|
||||||
|
Keybindings: parseBindings("enter"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: InputNewlineCommand,
|
||||||
|
Description: "insert newline",
|
||||||
|
Keybindings: parseBindings("shift+enter", "ctrl+j"),
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// Name: HistoryPreviousCommand,
|
||||||
|
// Description: "previous prompt",
|
||||||
|
// Keybindings: parseBindings("up"),
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// Name: HistoryNextCommand,
|
||||||
|
// Description: "next prompt",
|
||||||
|
// Keybindings: parseBindings("down"),
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
Name: MessagesPageUpCommand,
|
||||||
|
Description: "page up",
|
||||||
|
Keybindings: parseBindings("pgup"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: MessagesPageDownCommand,
|
||||||
|
Description: "page down",
|
||||||
|
Keybindings: parseBindings("pgdown"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: MessagesHalfPageUpCommand,
|
||||||
|
Description: "half page up",
|
||||||
|
Keybindings: parseBindings("ctrl+alt+u"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: MessagesHalfPageDownCommand,
|
||||||
|
Description: "half page down",
|
||||||
|
Keybindings: parseBindings("ctrl+alt+d"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: MessagesPreviousCommand,
|
||||||
|
Description: "previous message",
|
||||||
|
Keybindings: parseBindings("ctrl+alt+k"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: MessagesNextCommand,
|
||||||
|
Description: "next message",
|
||||||
|
Keybindings: parseBindings("ctrl+alt+j"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: MessagesFirstCommand,
|
||||||
|
Description: "first message",
|
||||||
|
Keybindings: parseBindings("ctrl+g"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: MessagesLastCommand,
|
||||||
|
Description: "last message",
|
||||||
|
Keybindings: parseBindings("ctrl+alt+g"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: AppExitCommand,
|
||||||
|
Description: "exit the app",
|
||||||
|
Keybindings: parseBindings("ctrl+c", "<leader>q"),
|
||||||
|
Trigger: "exit",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
registry := make(CommandRegistry)
|
||||||
|
keybinds := map[string]string{}
|
||||||
|
marshalled, _ := json.Marshal(*config.Keybinds)
|
||||||
|
json.Unmarshal(marshalled, &keybinds)
|
||||||
|
for _, command := range defaults {
|
||||||
|
if keybind, ok := keybinds[string(command.Name)]; ok {
|
||||||
|
command.Keybindings = parseBindings(keybind)
|
||||||
|
}
|
||||||
|
registry[command.Name] = command
|
||||||
|
}
|
||||||
|
return registry
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,14 @@ package completions
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/lipgloss/v2"
|
||||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||||
"github.com/sst/opencode/internal/app"
|
"github.com/sst/opencode/internal/app"
|
||||||
|
"github.com/sst/opencode/internal/commands"
|
||||||
"github.com/sst/opencode/internal/components/dialog"
|
"github.com/sst/opencode/internal/components/dialog"
|
||||||
|
"github.com/sst/opencode/internal/theme"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CommandCompletionProvider struct {
|
type CommandCompletionProvider struct {
|
||||||
@@ -27,15 +31,42 @@ func (c *CommandCompletionProvider) GetEntry() dialog.CompletionItemI {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *CommandCompletionProvider) GetEmptyMessage() string {
|
||||||
|
return "no matching commands"
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCommandCompletionItem(cmd commands.Command, space int, t theme.Theme) dialog.CompletionItemI {
|
||||||
|
spacer := strings.Repeat(" ", space)
|
||||||
|
title := " /" + cmd.Trigger + lipgloss.NewStyle().Foreground(t.TextMuted()).Render(spacer+cmd.Description)
|
||||||
|
value := string(cmd.Name)
|
||||||
|
return dialog.NewCompletionItem(dialog.CompletionItem{
|
||||||
|
Title: title,
|
||||||
|
Value: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (c *CommandCompletionProvider) GetChildEntries(query string) ([]dialog.CompletionItemI, error) {
|
func (c *CommandCompletionProvider) GetChildEntries(query string) ([]dialog.CompletionItemI, error) {
|
||||||
|
t := theme.CurrentTheme()
|
||||||
|
commands := c.app.Commands
|
||||||
|
|
||||||
|
space := 1
|
||||||
|
for _, cmd := range c.app.Commands {
|
||||||
|
if lipgloss.Width(cmd.Trigger) > space {
|
||||||
|
space = lipgloss.Width(cmd.Trigger)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
space += 2
|
||||||
|
|
||||||
|
sorted := commands.Sorted()
|
||||||
if query == "" {
|
if query == "" {
|
||||||
// If no query, return all commands
|
// If no query, return all commands
|
||||||
items := []dialog.CompletionItemI{}
|
items := []dialog.CompletionItemI{}
|
||||||
for _, cmd := range c.app.Commands {
|
for _, cmd := range sorted {
|
||||||
items = append(items, dialog.NewCompletionItem(dialog.CompletionItem{
|
if cmd.Trigger == "" {
|
||||||
Title: " /" + cmd.Name,
|
continue
|
||||||
Value: "/" + cmd.Name,
|
}
|
||||||
}))
|
space := space - lipgloss.Width(cmd.Trigger)
|
||||||
|
items = append(items, getCommandCompletionItem(cmd, space, t))
|
||||||
}
|
}
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
@@ -44,12 +75,13 @@ func (c *CommandCompletionProvider) GetChildEntries(query string) ([]dialog.Comp
|
|||||||
var commandNames []string
|
var commandNames []string
|
||||||
commandMap := make(map[string]dialog.CompletionItemI)
|
commandMap := make(map[string]dialog.CompletionItemI)
|
||||||
|
|
||||||
for _, cmd := range c.app.Commands {
|
for _, cmd := range sorted {
|
||||||
commandNames = append(commandNames, cmd.Name)
|
if cmd.Trigger == "" {
|
||||||
commandMap[cmd.Name] = dialog.NewCompletionItem(dialog.CompletionItem{
|
continue
|
||||||
Title: " /" + cmd.Name,
|
}
|
||||||
Value: "/" + cmd.Name,
|
space := space - lipgloss.Width(cmd.Trigger)
|
||||||
})
|
commandNames = append(commandNames, cmd.Trigger)
|
||||||
|
commandMap[cmd.Trigger] = getCommandCompletionItem(cmd, space, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find fuzzy matches
|
// Find fuzzy matches
|
||||||
@@ -65,7 +97,5 @@ func (c *CommandCompletionProvider) GetChildEntries(query string) ([]dialog.Comp
|
|||||||
items = append(items, item)
|
items = append(items, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ func (cg *filesAndFoldersContextGroup) GetEntry() dialog.CompletionItemI {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cg *filesAndFoldersContextGroup) GetEmptyMessage() string {
|
||||||
|
return "no matching files"
|
||||||
|
}
|
||||||
|
|
||||||
func (cg *filesAndFoldersContextGroup) getFiles(query string) ([]string, error) {
|
func (cg *filesAndFoldersContextGroup) getFiles(query string) ([]string, error) {
|
||||||
response, err := cg.app.Client.PostFileSearchWithResponse(context.Background(), client.PostFileSearchJSONRequestBody{
|
response, err := cg.app.Client.PostFileSearchWithResponse(context.Background(), client.PostFileSearchJSONRequestBody{
|
||||||
Query: query,
|
Query: query,
|
||||||
|
|||||||
@@ -20,10 +20,13 @@ func NewCompletionManager(app *app.App) *CompletionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *CompletionManager) DefaultProvider() dialog.CompletionProvider {
|
||||||
|
return m.providers["commands"]
|
||||||
|
}
|
||||||
|
|
||||||
func (m *CompletionManager) GetProvider(input string) dialog.CompletionProvider {
|
func (m *CompletionManager) GetProvider(input string) dialog.CompletionProvider {
|
||||||
if strings.HasPrefix(input, "/") {
|
if strings.HasPrefix(input, "/") {
|
||||||
return m.providers["commands"]
|
return m.providers["commands"]
|
||||||
}
|
}
|
||||||
return m.providers["files"]
|
return m.providers["files"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
package chat
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/sst/opencode/internal/app"
|
|
||||||
"github.com/sst/opencode/internal/styles"
|
|
||||||
"github.com/sst/opencode/internal/theme"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SendMsg struct {
|
|
||||||
Text string
|
|
||||||
Attachments []app.Attachment
|
|
||||||
}
|
|
||||||
|
|
||||||
func repo(width int) string {
|
|
||||||
repo := "github.com/sst/opencode"
|
|
||||||
t := theme.CurrentTheme()
|
|
||||||
|
|
||||||
return styles.BaseStyle().
|
|
||||||
Foreground(t.TextMuted()).
|
|
||||||
Width(width).
|
|
||||||
Render(repo)
|
|
||||||
}
|
|
||||||
@@ -3,248 +3,85 @@ package chat
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/v2/key"
|
|
||||||
"github.com/charmbracelet/bubbles/v2/spinner"
|
"github.com/charmbracelet/bubbles/v2/spinner"
|
||||||
"github.com/charmbracelet/bubbles/v2/textarea"
|
|
||||||
tea "github.com/charmbracelet/bubbletea/v2"
|
tea "github.com/charmbracelet/bubbletea/v2"
|
||||||
"github.com/charmbracelet/lipgloss/v2"
|
"github.com/charmbracelet/lipgloss/v2"
|
||||||
"github.com/sst/opencode/internal/app"
|
"github.com/sst/opencode/internal/app"
|
||||||
"github.com/sst/opencode/internal/commands"
|
"github.com/sst/opencode/internal/commands"
|
||||||
"github.com/sst/opencode/internal/components/dialog"
|
"github.com/sst/opencode/internal/components/dialog"
|
||||||
|
"github.com/sst/opencode/internal/components/textarea"
|
||||||
"github.com/sst/opencode/internal/image"
|
"github.com/sst/opencode/internal/image"
|
||||||
"github.com/sst/opencode/internal/layout"
|
"github.com/sst/opencode/internal/layout"
|
||||||
"github.com/sst/opencode/internal/status"
|
|
||||||
"github.com/sst/opencode/internal/styles"
|
"github.com/sst/opencode/internal/styles"
|
||||||
"github.com/sst/opencode/internal/theme"
|
"github.com/sst/opencode/internal/theme"
|
||||||
"github.com/sst/opencode/internal/util"
|
"github.com/sst/opencode/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type EditorComponent interface {
|
||||||
|
tea.Model
|
||||||
|
tea.ViewModel
|
||||||
|
layout.Sizeable
|
||||||
|
Content() string
|
||||||
|
Lines() int
|
||||||
|
Value() string
|
||||||
|
Submit() (tea.Model, tea.Cmd)
|
||||||
|
Clear() (tea.Model, tea.Cmd)
|
||||||
|
Paste() (tea.Model, tea.Cmd)
|
||||||
|
Newline() (tea.Model, tea.Cmd)
|
||||||
|
Previous() (tea.Model, tea.Cmd)
|
||||||
|
Next() (tea.Model, tea.Cmd)
|
||||||
|
}
|
||||||
|
|
||||||
type editorComponent struct {
|
type editorComponent struct {
|
||||||
width int
|
|
||||||
height int
|
|
||||||
app *app.App
|
app *app.App
|
||||||
|
width, height int
|
||||||
textarea textarea.Model
|
textarea textarea.Model
|
||||||
attachments []app.Attachment
|
attachments []app.Attachment
|
||||||
deleteMode bool
|
|
||||||
history []string
|
history []string
|
||||||
historyIndex int
|
historyIndex int
|
||||||
currentMessage string
|
currentMessage string
|
||||||
spinner spinner.Model
|
spinner spinner.Model
|
||||||
}
|
}
|
||||||
|
|
||||||
type EditorKeyMaps struct {
|
|
||||||
Send key.Binding
|
|
||||||
OpenEditor key.Binding
|
|
||||||
Paste key.Binding
|
|
||||||
HistoryUp key.Binding
|
|
||||||
HistoryDown key.Binding
|
|
||||||
}
|
|
||||||
|
|
||||||
type DeleteAttachmentKeyMaps struct {
|
|
||||||
AttachmentDeleteMode key.Binding
|
|
||||||
Escape key.Binding
|
|
||||||
DeleteAllAttachments key.Binding
|
|
||||||
}
|
|
||||||
|
|
||||||
var editorMaps = EditorKeyMaps{
|
|
||||||
Send: key.NewBinding(
|
|
||||||
key.WithKeys("enter"),
|
|
||||||
key.WithHelp("enter", "send message"),
|
|
||||||
),
|
|
||||||
OpenEditor: key.NewBinding(
|
|
||||||
key.WithKeys("f12"),
|
|
||||||
key.WithHelp("f12", "open editor"),
|
|
||||||
),
|
|
||||||
Paste: key.NewBinding(
|
|
||||||
key.WithKeys("ctrl+v"),
|
|
||||||
key.WithHelp("ctrl+v", "paste content"),
|
|
||||||
),
|
|
||||||
HistoryUp: key.NewBinding(
|
|
||||||
key.WithKeys("up"),
|
|
||||||
key.WithHelp("up", "previous message"),
|
|
||||||
),
|
|
||||||
HistoryDown: key.NewBinding(
|
|
||||||
key.WithKeys("down"),
|
|
||||||
key.WithHelp("down", "next message"),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
var DeleteKeyMaps = DeleteAttachmentKeyMaps{
|
|
||||||
AttachmentDeleteMode: key.NewBinding(
|
|
||||||
key.WithKeys("ctrl+r"),
|
|
||||||
key.WithHelp("ctrl+r+{i}", "delete attachment at index i"),
|
|
||||||
),
|
|
||||||
Escape: key.NewBinding(
|
|
||||||
key.WithKeys("esc"),
|
|
||||||
key.WithHelp("esc", "cancel delete mode"),
|
|
||||||
),
|
|
||||||
DeleteAllAttachments: key.NewBinding(
|
|
||||||
key.WithKeys("r"),
|
|
||||||
key.WithHelp("ctrl+r+r", "delete all attachments"),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
maxAttachments = 5
|
|
||||||
)
|
|
||||||
|
|
||||||
func (m *editorComponent) Init() tea.Cmd {
|
func (m *editorComponent) Init() tea.Cmd {
|
||||||
return tea.Batch(textarea.Blink, m.spinner.Tick)
|
return tea.Batch(textarea.Blink, m.spinner.Tick, tea.EnableReportFocus)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
var cmds []tea.Cmd
|
var cmds []tea.Cmd
|
||||||
var cmd tea.Cmd
|
var cmd tea.Cmd
|
||||||
|
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case dialog.ThemeChangedMsg:
|
case spinner.TickMsg:
|
||||||
|
m.spinner, cmd = m.spinner.Update(msg)
|
||||||
|
return m, cmd
|
||||||
|
case tea.KeyPressMsg:
|
||||||
|
// Maximize editor responsiveness for printable characters
|
||||||
|
if msg.Text != "" {
|
||||||
|
m.textarea, cmd = m.textarea.Update(msg)
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
return m, tea.Batch(cmds...)
|
||||||
|
}
|
||||||
|
case dialog.ThemeSelectedMsg:
|
||||||
m.textarea = createTextArea(&m.textarea)
|
m.textarea = createTextArea(&m.textarea)
|
||||||
|
m.spinner = createSpinner()
|
||||||
|
return m, tea.Batch(m.spinner.Tick, textarea.Blink)
|
||||||
case dialog.CompletionSelectedMsg:
|
case dialog.CompletionSelectedMsg:
|
||||||
if msg.IsCommand {
|
if msg.IsCommand {
|
||||||
// Execute the command directly
|
|
||||||
commandName := strings.TrimPrefix(msg.CompletionValue, "/")
|
commandName := strings.TrimPrefix(msg.CompletionValue, "/")
|
||||||
m.textarea.Reset()
|
updated, cmd := m.Clear()
|
||||||
return m, util.CmdHandler(commands.ExecuteCommandMsg{Name: commandName})
|
m = updated.(*editorComponent)
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
cmds = append(cmds, util.CmdHandler(commands.ExecuteCommandMsg(m.app.Commands[commands.CommandName(commandName)])))
|
||||||
|
return m, tea.Batch(cmds...)
|
||||||
} else {
|
} else {
|
||||||
// For files, replace the text in the editor
|
|
||||||
existingValue := m.textarea.Value()
|
existingValue := m.textarea.Value()
|
||||||
modifiedValue := strings.Replace(existingValue, msg.SearchString, msg.CompletionValue, 1)
|
modifiedValue := strings.Replace(existingValue, msg.SearchString, msg.CompletionValue, 1)
|
||||||
m.textarea.SetValue(modifiedValue)
|
m.textarea.SetValue(modifiedValue + " ")
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
case tea.KeyMsg:
|
|
||||||
switch msg.String() {
|
|
||||||
case "ctrl+c":
|
|
||||||
if m.textarea.Value() != "" {
|
|
||||||
m.textarea.Reset()
|
|
||||||
return m, func() tea.Msg {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "shift+enter":
|
|
||||||
value := m.textarea.Value()
|
|
||||||
m.textarea.SetValue(value + "\n")
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if key.Matches(msg, DeleteKeyMaps.AttachmentDeleteMode) {
|
|
||||||
m.deleteMode = true
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
if key.Matches(msg, DeleteKeyMaps.DeleteAllAttachments) && m.deleteMode {
|
|
||||||
m.deleteMode = false
|
|
||||||
m.attachments = nil
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
// if m.deleteMode && len(msg.Runes) > 0 && unicode.IsDigit(msg.Runes[0]) {
|
|
||||||
// num := int(msg.Runes[0] - '0')
|
|
||||||
// m.deleteMode = false
|
|
||||||
// if num < 10 && len(m.attachments) > num {
|
|
||||||
// if num == 0 {
|
|
||||||
// m.attachments = m.attachments[num+1:]
|
|
||||||
// } else {
|
|
||||||
// m.attachments = slices.Delete(m.attachments, num, num+1)
|
|
||||||
// }
|
|
||||||
// return m, nil
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
if key.Matches(msg, messageKeys.PageUp) || key.Matches(msg, messageKeys.PageDown) ||
|
|
||||||
key.Matches(msg, messageKeys.HalfPageUp) || key.Matches(msg, messageKeys.HalfPageDown) {
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
if key.Matches(msg, editorMaps.OpenEditor) {
|
|
||||||
if m.app.IsBusy() {
|
|
||||||
status.Warn("Agent is working, please wait...")
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
value := m.textarea.Value()
|
|
||||||
m.textarea.Reset()
|
|
||||||
return m, m.openEditor(value)
|
|
||||||
}
|
|
||||||
if key.Matches(msg, DeleteKeyMaps.Escape) {
|
|
||||||
m.deleteMode = false
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if key.Matches(msg, editorMaps.Paste) {
|
|
||||||
imageBytes, text, err := image.GetImageFromClipboard()
|
|
||||||
if err != nil {
|
|
||||||
slog.Error(err.Error())
|
|
||||||
return m, cmd
|
|
||||||
}
|
|
||||||
if len(imageBytes) != 0 {
|
|
||||||
attachmentName := fmt.Sprintf("clipboard-image-%d", len(m.attachments))
|
|
||||||
attachment := app.Attachment{FilePath: attachmentName, FileName: attachmentName, Content: imageBytes, MimeType: "image/png"}
|
|
||||||
m.attachments = append(m.attachments, attachment)
|
|
||||||
} else {
|
|
||||||
m.textarea.SetValue(m.textarea.Value() + text)
|
|
||||||
}
|
|
||||||
return m, cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle history navigation with up/down arrow keys
|
|
||||||
// Only handle history navigation if the filepicker is not open and completion dialog is not open
|
|
||||||
if m.textarea.Focused() && key.Matches(msg, editorMaps.HistoryUp) {
|
|
||||||
// TODO: fix this
|
|
||||||
// && !m.app.IsFilepickerOpen() && !m.app.IsCompletionDialogOpen() {
|
|
||||||
// Get the current line number
|
|
||||||
currentLine := m.textarea.Line()
|
|
||||||
|
|
||||||
// Only navigate history if we're at the first line
|
|
||||||
if currentLine == 0 && len(m.history) > 0 {
|
|
||||||
// Save current message if we're just starting to navigate
|
|
||||||
if m.historyIndex == len(m.history) {
|
|
||||||
m.currentMessage = m.textarea.Value()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Go to previous message in history
|
|
||||||
if m.historyIndex > 0 {
|
|
||||||
m.historyIndex--
|
|
||||||
m.textarea.SetValue(m.history[m.historyIndex])
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.textarea.Focused() && key.Matches(msg, editorMaps.HistoryDown) {
|
|
||||||
// TODO: fix this
|
|
||||||
// && !m.app.IsFilepickerOpen() && !m.app.IsCompletionDialogOpen() {
|
|
||||||
// Get the current line number and total lines
|
|
||||||
currentLine := m.textarea.Line()
|
|
||||||
value := m.textarea.Value()
|
|
||||||
lines := strings.Split(value, "\n")
|
|
||||||
totalLines := len(lines)
|
|
||||||
|
|
||||||
// Only navigate history if we're at the last line
|
|
||||||
if currentLine == totalLines-1 {
|
|
||||||
if m.historyIndex < len(m.history)-1 {
|
|
||||||
// Go to next message in history
|
|
||||||
m.historyIndex++
|
|
||||||
m.textarea.SetValue(m.history[m.historyIndex])
|
|
||||||
} else if m.historyIndex == len(m.history)-1 {
|
|
||||||
// Return to the current message being composed
|
|
||||||
m.historyIndex = len(m.history)
|
|
||||||
m.textarea.SetValue(m.currentMessage)
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle Enter key
|
|
||||||
if m.textarea.Focused() && key.Matches(msg, editorMaps.Send) {
|
|
||||||
value := m.textarea.Value()
|
|
||||||
if len(value) > 0 && value[len(value)-1] == '\\' {
|
|
||||||
// If the last character is a backslash, remove it and add a newline
|
|
||||||
m.textarea.SetValue(value[:len(value)-1] + "\n")
|
|
||||||
return m, nil
|
|
||||||
} else {
|
|
||||||
// Otherwise, send the message
|
|
||||||
return m, m.send()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m.spinner, cmd = m.spinner.Update(msg)
|
m.spinner, cmd = m.spinner.Update(msg)
|
||||||
@@ -256,10 +93,10 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, tea.Batch(cmds...)
|
return m, tea.Batch(cmds...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *editorComponent) View() string {
|
func (m *editorComponent) Content() string {
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
base := styles.BaseStyle().Render
|
base := styles.BaseStyle().Background(t.Background()).Render
|
||||||
muted := styles.Muted().Render
|
muted := styles.Muted().Background(t.Background()).Render
|
||||||
promptStyle := lipgloss.NewStyle().
|
promptStyle := lipgloss.NewStyle().
|
||||||
Padding(0, 0, 0, 1).
|
Padding(0, 0, 0, 1).
|
||||||
Bold(true).
|
Bold(true).
|
||||||
@@ -276,94 +113,71 @@ func (m *editorComponent) View() string {
|
|||||||
PaddingTop(1).
|
PaddingTop(1).
|
||||||
PaddingBottom(1).
|
PaddingBottom(1).
|
||||||
Background(t.BackgroundElement()).
|
Background(t.BackgroundElement()).
|
||||||
Border(lipgloss.ThickBorder(), false, true).
|
|
||||||
BorderForeground(t.BackgroundSubtle()).
|
|
||||||
BorderBackground(t.Background()).
|
|
||||||
Render(textarea)
|
Render(textarea)
|
||||||
|
|
||||||
hint := base("enter") + muted(" send ") + base("shift") + muted("+") + base("enter") + muted(" newline")
|
hint := base("enter") + muted(" send ")
|
||||||
if m.app.IsBusy() {
|
if m.app.IsBusy() {
|
||||||
hint = muted("working") + m.spinner.View() + muted(" ") + base("esc") + muted(" interrupt")
|
hint = muted("working") + m.spinner.View() + muted(" ") + base("esc") + muted(" interrupt")
|
||||||
}
|
}
|
||||||
|
|
||||||
model := ""
|
model := ""
|
||||||
if m.app.Model != nil {
|
if m.app.Model != nil {
|
||||||
model = base(m.app.Model.Name) + muted(" • /model")
|
model = muted(m.app.Provider.Name) + base(" "+m.app.Model.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
space := m.width - 2 - lipgloss.Width(model) - lipgloss.Width(hint)
|
space := m.width - 2 - lipgloss.Width(model) - lipgloss.Width(hint)
|
||||||
spacer := lipgloss.NewStyle().Width(space).Render("")
|
spacer := lipgloss.NewStyle().Background(t.Background()).Width(space).Render("")
|
||||||
|
|
||||||
info := lipgloss.JoinHorizontal(lipgloss.Left, hint, spacer, model)
|
info := hint + spacer + model
|
||||||
info = styles.Padded().Render(info)
|
info = styles.Padded().Background(t.Background()).Render(info)
|
||||||
|
|
||||||
content := lipgloss.JoinVertical(
|
|
||||||
lipgloss.Top,
|
|
||||||
// m.attachmentsContent(),
|
|
||||||
"",
|
|
||||||
textarea,
|
|
||||||
info,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
content := strings.Join([]string{"", textarea, info}, "\n")
|
||||||
return content
|
return content
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *editorComponent) View() string {
|
||||||
|
if m.Lines() > 1 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return m.Content()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *editorComponent) GetSize() (width, height int) {
|
||||||
|
return m.width, m.height
|
||||||
|
}
|
||||||
|
|
||||||
func (m *editorComponent) SetSize(width, height int) tea.Cmd {
|
func (m *editorComponent) SetSize(width, height int) tea.Cmd {
|
||||||
m.width = width
|
m.width = width
|
||||||
m.height = height
|
m.height = height
|
||||||
m.textarea.SetWidth(width - 5) // account for the prompt and padding right
|
m.textarea.SetWidth(width - 5) // account for the prompt and padding right
|
||||||
m.textarea.SetHeight(height - 4) // account for info underneath
|
// m.textarea.SetHeight(height - 4)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *editorComponent) GetSize() (int, int) {
|
func (m *editorComponent) Lines() int {
|
||||||
return m.width, m.height
|
return m.textarea.LineCount()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *editorComponent) openEditor(value string) tea.Cmd {
|
func (m *editorComponent) Value() string {
|
||||||
editor := os.Getenv("EDITOR")
|
return m.textarea.Value()
|
||||||
if editor == "" {
|
|
||||||
editor = "nvim"
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpfile, err := os.CreateTemp("", "msg_*.md")
|
|
||||||
tmpfile.WriteString(value)
|
|
||||||
if err != nil {
|
|
||||||
status.Error(err.Error())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
tmpfile.Close()
|
|
||||||
c := exec.Command(editor, tmpfile.Name()) //nolint:gosec
|
|
||||||
c.Stdin = os.Stdin
|
|
||||||
c.Stdout = os.Stdout
|
|
||||||
c.Stderr = os.Stderr
|
|
||||||
return tea.ExecProcess(c, func(err error) tea.Msg {
|
|
||||||
if err != nil {
|
|
||||||
status.Error(err.Error())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
content, err := os.ReadFile(tmpfile.Name())
|
|
||||||
if err != nil {
|
|
||||||
status.Error(err.Error())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if len(content) == 0 {
|
|
||||||
status.Warn("Message is empty")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
os.Remove(tmpfile.Name())
|
|
||||||
attachments := m.attachments
|
|
||||||
m.attachments = nil
|
|
||||||
return SendMsg{
|
|
||||||
Text: string(content),
|
|
||||||
Attachments: attachments,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *editorComponent) send() tea.Cmd {
|
func (m *editorComponent) Submit() (tea.Model, tea.Cmd) {
|
||||||
value := strings.TrimSpace(m.textarea.Value())
|
value := strings.TrimSpace(m.Value())
|
||||||
m.textarea.Reset()
|
if value == "" {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
if len(value) > 0 && value[len(value)-1] == '\\' {
|
||||||
|
// If the last character is a backslash, remove it and add a newline
|
||||||
|
m.textarea.SetValue(value[:len(value)-1] + "\n")
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmds []tea.Cmd
|
||||||
|
updated, cmd := m.Clear()
|
||||||
|
m = updated.(*editorComponent)
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
|
||||||
attachments := m.attachments
|
attachments := m.attachments
|
||||||
|
|
||||||
// Save to history if not empty and not a duplicate of the last entry
|
// Save to history if not empty and not a duplicate of the last entry
|
||||||
@@ -376,52 +190,77 @@ func (m *editorComponent) send() tea.Cmd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
m.attachments = nil
|
m.attachments = nil
|
||||||
if value == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for slash command
|
cmds = append(cmds, util.CmdHandler(app.SendMsg{Text: value, Attachments: attachments}))
|
||||||
// if strings.HasPrefix(value, "/") {
|
return m, tea.Batch(cmds...)
|
||||||
// commandName := strings.TrimPrefix(value, "/")
|
|
||||||
// if _, ok := m.app.Commands[commandName]; ok {
|
|
||||||
// return util.CmdHandler(commands.ExecuteCommandMsg{Name: commandName})
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
slog.Info("Send message", "value", value)
|
|
||||||
|
|
||||||
return tea.Batch(
|
|
||||||
util.CmdHandler(SendMsg{
|
|
||||||
Text: value,
|
|
||||||
Attachments: attachments,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *editorComponent) attachmentsContent() string {
|
func (m *editorComponent) Clear() (tea.Model, tea.Cmd) {
|
||||||
if len(m.attachments) == 0 {
|
m.textarea.Reset()
|
||||||
return ""
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
t := theme.CurrentTheme()
|
func (m *editorComponent) Paste() (tea.Model, tea.Cmd) {
|
||||||
var styledAttachments []string
|
imageBytes, text, err := image.GetImageFromClipboard()
|
||||||
attachmentStyles := styles.BaseStyle().
|
if err != nil {
|
||||||
MarginLeft(1).
|
slog.Error(err.Error())
|
||||||
Background(t.TextMuted()).
|
return m, nil
|
||||||
Foreground(t.Text())
|
|
||||||
for i, attachment := range m.attachments {
|
|
||||||
var filename string
|
|
||||||
if len(attachment.FileName) > 10 {
|
|
||||||
filename = fmt.Sprintf(" %s %s...", styles.DocumentIcon, attachment.FileName[0:7])
|
|
||||||
} else {
|
|
||||||
filename = fmt.Sprintf(" %s %s", styles.DocumentIcon, attachment.FileName)
|
|
||||||
}
|
|
||||||
if m.deleteMode {
|
|
||||||
filename = fmt.Sprintf("%d%s", i, filename)
|
|
||||||
}
|
|
||||||
styledAttachments = append(styledAttachments, attachmentStyles.Render(filename))
|
|
||||||
}
|
}
|
||||||
content := lipgloss.JoinHorizontal(lipgloss.Left, styledAttachments...)
|
if len(imageBytes) != 0 {
|
||||||
return content
|
attachmentName := fmt.Sprintf("clipboard-image-%d", len(m.attachments))
|
||||||
|
attachment := app.Attachment{FilePath: attachmentName, FileName: attachmentName, Content: imageBytes, MimeType: "image/png"}
|
||||||
|
m.attachments = append(m.attachments, attachment)
|
||||||
|
} else {
|
||||||
|
m.textarea.SetValue(m.textarea.Value() + text)
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *editorComponent) Newline() (tea.Model, tea.Cmd) {
|
||||||
|
m.textarea.Newline()
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *editorComponent) Previous() (tea.Model, tea.Cmd) {
|
||||||
|
currentLine := m.textarea.Line()
|
||||||
|
|
||||||
|
// Only navigate history if we're at the first line
|
||||||
|
if currentLine == 0 && len(m.history) > 0 {
|
||||||
|
// Save current message if we're just starting to navigate
|
||||||
|
if m.historyIndex == len(m.history) {
|
||||||
|
m.currentMessage = m.textarea.Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go to previous message in history
|
||||||
|
if m.historyIndex > 0 {
|
||||||
|
m.historyIndex--
|
||||||
|
m.textarea.SetValue(m.history[m.historyIndex])
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *editorComponent) Next() (tea.Model, tea.Cmd) {
|
||||||
|
currentLine := m.textarea.Line()
|
||||||
|
value := m.textarea.Value()
|
||||||
|
lines := strings.Split(value, "\n")
|
||||||
|
totalLines := len(lines)
|
||||||
|
|
||||||
|
// Only navigate history if we're at the last line
|
||||||
|
if currentLine == totalLines-1 {
|
||||||
|
if m.historyIndex < len(m.history)-1 {
|
||||||
|
// Go to next message in history
|
||||||
|
m.historyIndex++
|
||||||
|
m.textarea.SetValue(m.history[m.historyIndex])
|
||||||
|
} else if m.historyIndex == len(m.history)-1 {
|
||||||
|
// Return to the current message being composed
|
||||||
|
m.historyIndex = len(m.history)
|
||||||
|
m.textarea.SetValue(m.currentMessage)
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createTextArea(existing *textarea.Model) textarea.Model {
|
func createTextArea(existing *textarea.Model) textarea.Model {
|
||||||
@@ -456,12 +295,19 @@ func createTextArea(existing *textarea.Model) textarea.Model {
|
|||||||
return ta
|
return ta
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *editorComponent) GetValue() string {
|
func createSpinner() spinner.Model {
|
||||||
return m.textarea.Value()
|
return spinner.New(
|
||||||
|
spinner.WithSpinner(spinner.Ellipsis),
|
||||||
|
spinner.WithStyle(
|
||||||
|
styles.
|
||||||
|
Muted().
|
||||||
|
Background(theme.CurrentTheme().Background()).
|
||||||
|
Width(3)),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewEditorComponent(app *app.App) layout.ModelWithView {
|
func NewEditorComponent(app *app.App) EditorComponent {
|
||||||
s := spinner.New(spinner.WithSpinner(spinner.Ellipsis), spinner.WithStyle(styles.Muted().Width(3)))
|
s := createSpinner()
|
||||||
ta := createTextArea(nil)
|
ta := createTextArea(nil)
|
||||||
|
|
||||||
return &editorComponent{
|
return &editorComponent{
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package chat
|
package chat
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -24,7 +24,7 @@ import (
|
|||||||
|
|
||||||
func toMarkdown(content string, width int, backgroundColor compat.AdaptiveColor) string {
|
func toMarkdown(content string, width int, backgroundColor compat.AdaptiveColor) string {
|
||||||
r := styles.GetMarkdownRenderer(width, backgroundColor)
|
r := styles.GetMarkdownRenderer(width, backgroundColor)
|
||||||
content = strings.ReplaceAll(content, app.Info.Path.Root+"/", "")
|
content = strings.ReplaceAll(content, app.RootPath+"/", "")
|
||||||
rendered, _ := r.Render(content)
|
rendered, _ := r.Render(content)
|
||||||
lines := strings.Split(rendered, "\n")
|
lines := strings.Split(rendered, "\n")
|
||||||
|
|
||||||
@@ -44,7 +44,6 @@ func toMarkdown(content string, width int, backgroundColor compat.AdaptiveColor)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
content = strings.Join(lines, "\n")
|
content = strings.Join(lines, "\n")
|
||||||
return strings.TrimSuffix(content, "\n")
|
return strings.TrimSuffix(content, "\n")
|
||||||
}
|
}
|
||||||
@@ -131,13 +130,13 @@ func renderContentBlock(content string, options ...renderingOption) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
style := styles.BaseStyle().
|
style := styles.BaseStyle().
|
||||||
MarginTop(renderer.marginTop).
|
// MarginTop(renderer.marginTop).
|
||||||
MarginBottom(renderer.marginBottom).
|
// MarginBottom(renderer.marginBottom).
|
||||||
PaddingTop(renderer.paddingTop).
|
PaddingTop(renderer.paddingTop).
|
||||||
PaddingBottom(renderer.paddingBottom).
|
PaddingBottom(renderer.paddingBottom).
|
||||||
PaddingLeft(renderer.paddingLeft).
|
PaddingLeft(renderer.paddingLeft).
|
||||||
PaddingRight(renderer.paddingRight).
|
PaddingRight(renderer.paddingRight).
|
||||||
Background(t.BackgroundSubtle()).
|
Background(t.BackgroundPanel()).
|
||||||
Foreground(t.TextMuted()).
|
Foreground(t.TextMuted()).
|
||||||
BorderStyle(lipgloss.ThickBorder())
|
BorderStyle(lipgloss.ThickBorder())
|
||||||
|
|
||||||
@@ -146,7 +145,7 @@ func renderContentBlock(content string, options ...renderingOption) string {
|
|||||||
align = *renderer.align
|
align = *renderer.align
|
||||||
}
|
}
|
||||||
|
|
||||||
borderColor := t.BackgroundSubtle()
|
borderColor := t.BackgroundPanel()
|
||||||
if renderer.borderColor != nil {
|
if renderer.borderColor != nil {
|
||||||
borderColor = *renderer.borderColor
|
borderColor = *renderer.borderColor
|
||||||
}
|
}
|
||||||
@@ -159,7 +158,7 @@ func renderContentBlock(content string, options ...renderingOption) string {
|
|||||||
AlignHorizontal(align).
|
AlignHorizontal(align).
|
||||||
BorderLeftForeground(borderColor).
|
BorderLeftForeground(borderColor).
|
||||||
BorderLeftBackground(t.Background()).
|
BorderLeftBackground(t.Background()).
|
||||||
BorderRightForeground(t.BackgroundSubtle()).
|
BorderRightForeground(t.BackgroundPanel()).
|
||||||
BorderRightBackground(t.Background())
|
BorderRightBackground(t.Background())
|
||||||
case lipgloss.Right:
|
case lipgloss.Right:
|
||||||
style = style.
|
style = style.
|
||||||
@@ -168,7 +167,7 @@ func renderContentBlock(content string, options ...renderingOption) string {
|
|||||||
AlignHorizontal(align).
|
AlignHorizontal(align).
|
||||||
BorderRightForeground(borderColor).
|
BorderRightForeground(borderColor).
|
||||||
BorderRightBackground(t.Background()).
|
BorderRightBackground(t.Background()).
|
||||||
BorderLeftForeground(t.BackgroundSubtle()).
|
BorderLeftForeground(t.BackgroundPanel()).
|
||||||
BorderLeftBackground(t.Background())
|
BorderLeftBackground(t.Background())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,26 +179,42 @@ func renderContentBlock(content string, options ...renderingOption) string {
|
|||||||
layout.Current.Container.Width,
|
layout.Current.Container.Width,
|
||||||
align,
|
align,
|
||||||
content,
|
content,
|
||||||
|
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||||
)
|
)
|
||||||
content = lipgloss.PlaceHorizontal(
|
content = lipgloss.PlaceHorizontal(
|
||||||
layout.Current.Viewport.Width,
|
layout.Current.Viewport.Width,
|
||||||
lipgloss.Center,
|
lipgloss.Center,
|
||||||
content,
|
content,
|
||||||
|
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||||
)
|
)
|
||||||
|
if renderer.marginTop > 0 {
|
||||||
|
for range renderer.marginTop {
|
||||||
|
content = "\n" + content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if renderer.marginBottom > 0 {
|
||||||
|
for range renderer.marginBottom {
|
||||||
|
content = content + "\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return content
|
return content
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func calculatePadding() int {
|
||||||
|
if layout.Current.Viewport.Width < 80 {
|
||||||
|
return 5
|
||||||
|
} else if layout.Current.Viewport.Width < 120 {
|
||||||
|
return 15
|
||||||
|
} else {
|
||||||
|
return 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func renderText(message client.MessageInfo, text string, author string) string {
|
func renderText(message client.MessageInfo, text string, author string) string {
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
width := layout.Current.Container.Width
|
width := layout.Current.Container.Width
|
||||||
padding := 0
|
padding := calculatePadding()
|
||||||
if layout.Current.Viewport.Width < 80 {
|
|
||||||
padding = 5
|
|
||||||
} else if layout.Current.Viewport.Width < 120 {
|
|
||||||
padding = 15
|
|
||||||
} else {
|
|
||||||
padding = 20
|
|
||||||
}
|
|
||||||
|
|
||||||
timestamp := time.UnixMilli(int64(message.Metadata.Time.Created)).Local().Format("02 Jan 2006 03:04 PM")
|
timestamp := time.UnixMilli(int64(message.Metadata.Time.Created)).Local().Format("02 Jan 2006 03:04 PM")
|
||||||
if time.Now().Format("02 Jan 2006") == timestamp[:11] {
|
if time.Now().Format("02 Jan 2006") == timestamp[:11] {
|
||||||
@@ -208,18 +223,17 @@ func renderText(message client.MessageInfo, text string, author string) string {
|
|||||||
}
|
}
|
||||||
info := fmt.Sprintf("%s (%s)", author, timestamp)
|
info := fmt.Sprintf("%s (%s)", author, timestamp)
|
||||||
|
|
||||||
align := lipgloss.Left
|
|
||||||
switch message.Role {
|
|
||||||
case client.User:
|
|
||||||
align = lipgloss.Right
|
|
||||||
case client.Assistant:
|
|
||||||
align = lipgloss.Left
|
|
||||||
}
|
|
||||||
|
|
||||||
textWidth := max(lipgloss.Width(text), lipgloss.Width(info))
|
textWidth := max(lipgloss.Width(text), lipgloss.Width(info))
|
||||||
markdownWidth := min(textWidth, width-padding-4) // -4 for the border and padding
|
markdownWidth := min(textWidth, width-padding-4) // -4 for the border and padding
|
||||||
content := toMarkdown(text, markdownWidth, t.BackgroundSubtle())
|
if message.Role == client.Assistant {
|
||||||
content = lipgloss.JoinVertical(align, content, info)
|
markdownWidth = width - padding - 4 - 2
|
||||||
|
}
|
||||||
|
if message.Role == client.User {
|
||||||
|
text = strings.ReplaceAll(text, "<", "\\<")
|
||||||
|
text = strings.ReplaceAll(text, ">", "\\>")
|
||||||
|
}
|
||||||
|
content := toMarkdown(text, markdownWidth, t.BackgroundPanel())
|
||||||
|
content = strings.Join([]string{content, info}, "\n")
|
||||||
|
|
||||||
switch message.Role {
|
switch message.Role {
|
||||||
case client.User:
|
case client.User:
|
||||||
@@ -239,10 +253,12 @@ func renderText(message client.MessageInfo, text string, author string) string {
|
|||||||
func renderToolInvocation(
|
func renderToolInvocation(
|
||||||
toolCall client.MessageToolInvocationToolCall,
|
toolCall client.MessageToolInvocationToolCall,
|
||||||
result *string,
|
result *string,
|
||||||
metadata client.MessageInfo_Metadata_Tool_AdditionalProperties,
|
metadata client.MessageMetadata_Tool_AdditionalProperties,
|
||||||
showResult bool,
|
showDetails bool,
|
||||||
|
isLast bool,
|
||||||
|
contentOnly bool,
|
||||||
) string {
|
) string {
|
||||||
ignoredTools := []string{"opencode_todoread"}
|
ignoredTools := []string{"todoread"}
|
||||||
if slices.Contains(ignoredTools, toolCall.ToolName) {
|
if slices.Contains(ignoredTools, toolCall.ToolName) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -251,7 +267,7 @@ func renderToolInvocation(
|
|||||||
innerWidth := outerWidth - 6
|
innerWidth := outerWidth - 6
|
||||||
paddingTop := 0
|
paddingTop := 0
|
||||||
paddingBottom := 0
|
paddingBottom := 0
|
||||||
if showResult {
|
if showDetails {
|
||||||
paddingTop = 1
|
paddingTop = 1
|
||||||
if result == nil || *result == "" {
|
if result == nil || *result == "" {
|
||||||
paddingBottom = 1
|
paddingBottom = 1
|
||||||
@@ -261,41 +277,56 @@ func renderToolInvocation(
|
|||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
style := styles.Muted().
|
style := styles.Muted().
|
||||||
Width(outerWidth).
|
Width(outerWidth).
|
||||||
Background(t.BackgroundSubtle()).
|
Background(t.BackgroundPanel()).
|
||||||
PaddingTop(paddingTop).
|
PaddingTop(paddingTop).
|
||||||
PaddingBottom(paddingBottom).
|
PaddingBottom(paddingBottom).
|
||||||
PaddingLeft(2).
|
PaddingLeft(2).
|
||||||
PaddingRight(2).
|
PaddingRight(2).
|
||||||
BorderLeft(true).
|
BorderLeft(true).
|
||||||
BorderRight(true).
|
BorderRight(true).
|
||||||
BorderForeground(t.BackgroundSubtle()).
|
BorderBackground(t.Background()).
|
||||||
|
BorderForeground(t.BackgroundPanel()).
|
||||||
BorderStyle(lipgloss.ThickBorder())
|
BorderStyle(lipgloss.ThickBorder())
|
||||||
|
|
||||||
if toolCall.State == "partial-call" {
|
if toolCall.State == "partial-call" {
|
||||||
|
title := renderToolAction(toolCall.ToolName)
|
||||||
|
if !showDetails {
|
||||||
|
title = "∟ " + title
|
||||||
|
padding := calculatePadding()
|
||||||
|
style := lipgloss.NewStyle().Width(outerWidth - padding - 4).Background(t.BackgroundPanel())
|
||||||
|
return renderContentBlock(style.Render(title),
|
||||||
|
WithAlign(lipgloss.Left),
|
||||||
|
WithBorderColor(t.Accent()),
|
||||||
|
WithPaddingTop(0),
|
||||||
|
WithPaddingBottom(1),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
style = style.Foreground(t.TextMuted())
|
style = style.Foreground(t.TextMuted())
|
||||||
return style.Render(renderToolAction(toolCall.ToolName))
|
return style.Render(title)
|
||||||
}
|
}
|
||||||
|
|
||||||
toolArgs := ""
|
toolArgs := ""
|
||||||
toolArgsMap := make(map[string]any)
|
toolArgsMap := make(map[string]any)
|
||||||
if toolCall.Args != nil {
|
if toolCall.Args != nil {
|
||||||
value := *toolCall.Args
|
value := *toolCall.Args
|
||||||
m, ok := value.(map[string]any)
|
if m, ok := value.(map[string]any); ok {
|
||||||
if ok {
|
|
||||||
toolArgsMap = m
|
toolArgsMap = m
|
||||||
firstKey := ""
|
|
||||||
|
keys := make([]string, 0, len(toolArgsMap))
|
||||||
for key := range toolArgsMap {
|
for key := range toolArgsMap {
|
||||||
firstKey = key
|
keys = append(keys, key)
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
slices.Sort(keys)
|
||||||
|
firstKey := ""
|
||||||
|
if len(keys) > 0 {
|
||||||
|
firstKey = keys[0]
|
||||||
|
}
|
||||||
|
|
||||||
toolArgs = renderArgs(&toolArgsMap, firstKey)
|
toolArgs = renderArgs(&toolArgsMap, firstKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(toolArgsMap) == 0 {
|
|
||||||
slog.Debug("no args")
|
|
||||||
}
|
|
||||||
|
|
||||||
body := ""
|
body := ""
|
||||||
error := ""
|
error := ""
|
||||||
finished := result != nil && *result != ""
|
finished := result != nil && *result != ""
|
||||||
@@ -304,7 +335,7 @@ func renderToolInvocation(
|
|||||||
if m, ok := metadata.Get("message"); ok {
|
if m, ok := metadata.Get("message"); ok {
|
||||||
style = style.BorderLeftForeground(t.Error())
|
style = style.BorderLeftForeground(t.Error())
|
||||||
error = styles.BaseStyle().
|
error = styles.BaseStyle().
|
||||||
Background(t.BackgroundSubtle()).
|
Background(t.BackgroundPanel()).
|
||||||
Foreground(t.Error()).
|
Foreground(t.Error()).
|
||||||
Render(m.(string))
|
Render(m.(string))
|
||||||
error = renderContentBlock(
|
error = renderContentBlock(
|
||||||
@@ -316,30 +347,19 @@ func renderToolInvocation(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
elapsed := ""
|
|
||||||
start := metadata.Time.Start
|
|
||||||
end := metadata.Time.End
|
|
||||||
durationMs := end - start
|
|
||||||
duration := time.Duration(durationMs * float32(time.Millisecond))
|
|
||||||
roundedDuration := time.Duration(duration.Round(time.Millisecond))
|
|
||||||
if durationMs > 1000 {
|
|
||||||
roundedDuration = time.Duration(duration.Round(time.Second))
|
|
||||||
}
|
|
||||||
elapsed = styles.Muted().Render(roundedDuration.String())
|
|
||||||
|
|
||||||
title := ""
|
title := ""
|
||||||
switch toolCall.ToolName {
|
switch toolCall.ToolName {
|
||||||
case "opencode_read":
|
case "read":
|
||||||
toolArgs = renderArgs(&toolArgsMap, "filePath")
|
toolArgs = renderArgs(&toolArgsMap, "filePath")
|
||||||
title = fmt.Sprintf("Read: %s %s", toolArgs, elapsed)
|
title = fmt.Sprintf("READ %s", toolArgs)
|
||||||
if preview, ok := metadata.Get("preview"); ok && toolArgsMap["filePath"] != nil {
|
if preview, ok := metadata.Get("preview"); ok && toolArgsMap["filePath"] != nil {
|
||||||
filename := toolArgsMap["filePath"].(string)
|
filename := toolArgsMap["filePath"].(string)
|
||||||
body = preview.(string)
|
body = preview.(string)
|
||||||
body = renderFile(filename, body, WithTruncate(6))
|
body = renderFile(filename, body, WithTruncate(6))
|
||||||
}
|
}
|
||||||
case "opencode_edit":
|
case "edit":
|
||||||
if filename, ok := toolArgsMap["filePath"].(string); ok {
|
if filename, ok := toolArgsMap["filePath"].(string); ok {
|
||||||
title = fmt.Sprintf("Edit: %s %s", relative(filename), elapsed)
|
title = fmt.Sprintf("EDIT %s", relative(filename))
|
||||||
if d, ok := metadata.Get("diff"); ok {
|
if d, ok := metadata.Get("diff"); ok {
|
||||||
patch := d.(string)
|
patch := d.(string)
|
||||||
var formattedDiff string
|
var formattedDiff string
|
||||||
@@ -356,12 +376,13 @@ func renderToolInvocation(
|
|||||||
formattedDiff = strings.TrimSpace(formattedDiff)
|
formattedDiff = strings.TrimSpace(formattedDiff)
|
||||||
formattedDiff = lipgloss.NewStyle().
|
formattedDiff = lipgloss.NewStyle().
|
||||||
BorderStyle(lipgloss.ThickBorder()).
|
BorderStyle(lipgloss.ThickBorder()).
|
||||||
BorderForeground(t.BackgroundSubtle()).
|
BorderBackground(t.Background()).
|
||||||
|
BorderForeground(t.BackgroundPanel()).
|
||||||
BorderLeft(true).
|
BorderLeft(true).
|
||||||
BorderRight(true).
|
BorderRight(true).
|
||||||
Render(formattedDiff)
|
Render(formattedDiff)
|
||||||
|
|
||||||
if showResult {
|
if showDetails {
|
||||||
style = style.Width(lipgloss.Width(formattedDiff))
|
style = style.Width(lipgloss.Width(formattedDiff))
|
||||||
title += "\n"
|
title += "\n"
|
||||||
}
|
}
|
||||||
@@ -373,40 +394,43 @@ func renderToolInvocation(
|
|||||||
lipgloss.Center,
|
lipgloss.Center,
|
||||||
lipgloss.Top,
|
lipgloss.Top,
|
||||||
body,
|
body,
|
||||||
|
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "opencode_write":
|
case "write":
|
||||||
if filename, ok := toolArgsMap["filePath"].(string); ok {
|
if filename, ok := toolArgsMap["filePath"].(string); ok {
|
||||||
title = fmt.Sprintf("Write: %s %s", relative(filename), elapsed)
|
title = fmt.Sprintf("WRITE %s", relative(filename))
|
||||||
if content, ok := toolArgsMap["content"].(string); ok {
|
if content, ok := toolArgsMap["content"].(string); ok {
|
||||||
body = renderFile(filename, content)
|
body = renderFile(filename, content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "opencode_bash":
|
case "bash":
|
||||||
if description, ok := toolArgsMap["description"].(string); ok {
|
if description, ok := toolArgsMap["description"].(string); ok {
|
||||||
title = fmt.Sprintf("Shell: %s %s", description, elapsed)
|
title = fmt.Sprintf("SHELL %s", description)
|
||||||
}
|
}
|
||||||
if stdout, ok := metadata.Get("stdout"); ok {
|
if stdout, ok := metadata.Get("stdout"); ok {
|
||||||
command := toolArgsMap["command"].(string)
|
command := toolArgsMap["command"].(string)
|
||||||
stdout := stdout.(string)
|
stdout := stdout.(string)
|
||||||
body = fmt.Sprintf("```console\n> %s\n%s```", command, stdout)
|
body = fmt.Sprintf("```console\n> %s\n%s```", command, stdout)
|
||||||
body = toMarkdown(body, innerWidth, t.BackgroundSubtle())
|
body = toMarkdown(body, innerWidth, t.BackgroundPanel())
|
||||||
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
|
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
|
||||||
}
|
}
|
||||||
case "opencode_webfetch":
|
case "webfetch":
|
||||||
toolArgs = renderArgs(&toolArgsMap, "url")
|
toolArgs = renderArgs(&toolArgsMap, "url")
|
||||||
title = fmt.Sprintf("Fetching: %s %s", toolArgs, elapsed)
|
title = fmt.Sprintf("FETCH %s", toolArgs)
|
||||||
if format, ok := toolArgsMap["format"].(string); ok {
|
if format, ok := toolArgsMap["format"].(string); ok {
|
||||||
body = *result
|
if result != nil {
|
||||||
body = truncateHeight(body, 10)
|
body = *result
|
||||||
if format == "html" || format == "markdown" {
|
body = truncateHeight(body, 10)
|
||||||
body = toMarkdown(body, innerWidth, t.BackgroundSubtle())
|
if format == "html" || format == "markdown" {
|
||||||
|
body = toMarkdown(body, innerWidth, t.BackgroundPanel())
|
||||||
|
}
|
||||||
|
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
|
||||||
}
|
}
|
||||||
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
|
|
||||||
}
|
}
|
||||||
case "opencode_todowrite":
|
case "todowrite":
|
||||||
title = fmt.Sprintf("Planning %s", elapsed)
|
title = fmt.Sprintf("PLAN")
|
||||||
|
|
||||||
if to, ok := metadata.Get("todos"); ok && finished {
|
if to, ok := metadata.Get("todos"); ok && finished {
|
||||||
todos := to.([]any)
|
todos := to.([]any)
|
||||||
@@ -422,17 +446,79 @@ func renderToolInvocation(
|
|||||||
body += fmt.Sprintf("- [ ] %s\n", content)
|
body += fmt.Sprintf("- [ ] %s\n", content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
body = toMarkdown(body, innerWidth, t.BackgroundSubtle())
|
body = toMarkdown(body, innerWidth, t.BackgroundPanel())
|
||||||
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
|
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
|
||||||
}
|
}
|
||||||
|
case "task":
|
||||||
|
if description, ok := toolArgsMap["description"].(string); ok {
|
||||||
|
title = fmt.Sprintf("TASK %s", description)
|
||||||
|
if summary, ok := metadata.Get("summary"); ok {
|
||||||
|
toolcalls := summary.([]any)
|
||||||
|
// toolcalls :=
|
||||||
|
|
||||||
|
steps := []string{}
|
||||||
|
for _, toolcall := range toolcalls {
|
||||||
|
call := toolcall.(map[string]any)
|
||||||
|
if toolInvocation, ok := call["toolInvocation"].(map[string]any); ok {
|
||||||
|
data, _ := json.Marshal(toolInvocation)
|
||||||
|
var toolCall client.MessageToolInvocationToolCall
|
||||||
|
_ = json.Unmarshal(data, &toolCall)
|
||||||
|
|
||||||
|
if metadata, ok := call["metadata"].(map[string]any); ok {
|
||||||
|
data, _ = json.Marshal(metadata)
|
||||||
|
var toolMetadata client.MessageMetadata_Tool_AdditionalProperties
|
||||||
|
_ = json.Unmarshal(data, &toolMetadata)
|
||||||
|
|
||||||
|
step := renderToolInvocation(
|
||||||
|
toolCall,
|
||||||
|
nil,
|
||||||
|
toolMetadata,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
steps = append(steps, step)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body = strings.Join(steps, "\n")
|
||||||
|
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
toolName := renderToolName(toolCall.ToolName)
|
toolName := renderToolName(toolCall.ToolName)
|
||||||
title = fmt.Sprintf("%s: %s %s", toolName, toolArgs, elapsed)
|
title = fmt.Sprintf("%s %s", toolName, toolArgs)
|
||||||
|
if result == nil {
|
||||||
|
empty := ""
|
||||||
|
result = &empty
|
||||||
|
}
|
||||||
body = *result
|
body = *result
|
||||||
body = truncateHeight(body, 10)
|
body = truncateHeight(body, 10)
|
||||||
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
|
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if contentOnly {
|
||||||
|
title = "∟ " + title
|
||||||
|
return title
|
||||||
|
}
|
||||||
|
|
||||||
|
if !showDetails {
|
||||||
|
title = "∟ " + title
|
||||||
|
padding := calculatePadding()
|
||||||
|
style := lipgloss.NewStyle().Width(outerWidth - padding - 4).Background(t.BackgroundPanel())
|
||||||
|
paddingBottom := 0
|
||||||
|
if isLast {
|
||||||
|
paddingBottom = 1
|
||||||
|
}
|
||||||
|
return renderContentBlock(style.Render(title),
|
||||||
|
WithAlign(lipgloss.Left),
|
||||||
|
WithBorderColor(t.Accent()),
|
||||||
|
WithPaddingTop(0),
|
||||||
|
WithPaddingBottom(paddingBottom),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if body == "" && error == "" {
|
if body == "" && error == "" {
|
||||||
body = *result
|
body = *result
|
||||||
body = truncateHeight(body, 10)
|
body = truncateHeight(body, 10)
|
||||||
@@ -440,11 +526,16 @@ func renderToolInvocation(
|
|||||||
}
|
}
|
||||||
|
|
||||||
content := style.Render(title)
|
content := style.Render(title)
|
||||||
content = lipgloss.PlaceHorizontal(layout.Current.Viewport.Width, lipgloss.Center, content)
|
content = lipgloss.PlaceHorizontal(
|
||||||
if showResult && body != "" && error == "" {
|
layout.Current.Viewport.Width,
|
||||||
|
lipgloss.Center,
|
||||||
|
content,
|
||||||
|
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||||
|
)
|
||||||
|
if showDetails && body != "" && error == "" {
|
||||||
content += "\n" + body
|
content += "\n" + body
|
||||||
}
|
}
|
||||||
if showResult && error != "" {
|
if showDetails && error != "" {
|
||||||
content += "\n" + error
|
content += "\n" + error
|
||||||
}
|
}
|
||||||
return content
|
return content
|
||||||
@@ -452,22 +543,18 @@ func renderToolInvocation(
|
|||||||
|
|
||||||
func renderToolName(name string) string {
|
func renderToolName(name string) string {
|
||||||
switch name {
|
switch name {
|
||||||
// case agent.AgentToolName:
|
case "list":
|
||||||
// return "Task"
|
return "LIST"
|
||||||
case "opencode_ls":
|
case "webfetch":
|
||||||
return "List"
|
return "FETCH"
|
||||||
case "opencode_webfetch":
|
case "todowrite":
|
||||||
return "Fetch"
|
return "PLAN"
|
||||||
case "opencode_todoread":
|
|
||||||
return "Planning"
|
|
||||||
case "opencode_todowrite":
|
|
||||||
return "Planning"
|
|
||||||
default:
|
default:
|
||||||
normalizedName := name
|
normalizedName := name
|
||||||
if strings.HasPrefix(name, "opencode_") {
|
if strings.HasPrefix(name, "opencode_") {
|
||||||
normalizedName = strings.TrimPrefix(name, "opencode_")
|
normalizedName = strings.TrimPrefix(name, "opencode_")
|
||||||
}
|
}
|
||||||
return cases.Title(language.Und).String(normalizedName)
|
return cases.Upper(language.Und).String(normalizedName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -508,34 +595,36 @@ func renderFile(filename string, content string, options ...fileRenderingOption)
|
|||||||
content = truncateHeight(content, renderer.height)
|
content = truncateHeight(content, renderer.height)
|
||||||
}
|
}
|
||||||
content = fmt.Sprintf("```%s\n%s\n```", extension(renderer.filename), content)
|
content = fmt.Sprintf("```%s\n%s\n```", extension(renderer.filename), content)
|
||||||
content = toMarkdown(content, width, t.BackgroundSubtle())
|
content = toMarkdown(content, width, t.BackgroundPanel())
|
||||||
|
|
||||||
return renderContentBlock(content, WithFullWidth(), WithMarginBottom(1))
|
return renderContentBlock(content, WithFullWidth(), WithMarginBottom(1))
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderToolAction(name string) string {
|
func renderToolAction(name string) string {
|
||||||
switch name {
|
switch name {
|
||||||
// case agent.AgentToolName:
|
case "task":
|
||||||
// return "Preparing prompt..."
|
return "Searching..."
|
||||||
case "opencode_bash":
|
case "bash":
|
||||||
return "Building command..."
|
return "Building command..."
|
||||||
case "opencode_edit":
|
case "edit":
|
||||||
return "Preparing edit..."
|
return "Preparing edit..."
|
||||||
case "opencode_fetch":
|
case "webfetch":
|
||||||
return "Writing fetch..."
|
return "Fetching from the web..."
|
||||||
case "opencode_glob":
|
case "glob":
|
||||||
return "Finding files..."
|
return "Finding files..."
|
||||||
case "opencode_grep":
|
case "grep":
|
||||||
return "Searching content..."
|
return "Searching content..."
|
||||||
case "opencode_ls":
|
case "list":
|
||||||
return "Listing directory..."
|
return "Listing directory..."
|
||||||
case "opencode_read":
|
case "read":
|
||||||
return "Reading file..."
|
return "Reading file..."
|
||||||
case "opencode_write":
|
case "write":
|
||||||
return "Preparing write..."
|
return "Preparing write..."
|
||||||
case "opencode_patch":
|
case "todowrite", "todoread":
|
||||||
|
return "Planning..."
|
||||||
|
case "patch":
|
||||||
return "Preparing patch..."
|
return "Preparing patch..."
|
||||||
case "opencode_batch":
|
case "batch":
|
||||||
return "Running batch operations..."
|
return "Running batch operations..."
|
||||||
}
|
}
|
||||||
return "Working..."
|
return "Working..."
|
||||||
@@ -545,9 +634,17 @@ func renderArgs(args *map[string]any, titleKey string) string {
|
|||||||
if args == nil || len(*args) == 0 {
|
if args == nil || len(*args) == 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
keys := make([]string, 0, len(*args))
|
||||||
|
for key := range *args {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
slices.Sort(keys)
|
||||||
|
|
||||||
title := ""
|
title := ""
|
||||||
parts := []string{}
|
parts := []string{}
|
||||||
for key, value := range *args {
|
for _, key := range keys {
|
||||||
|
value := (*args)[key]
|
||||||
if value == nil {
|
if value == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -575,7 +672,7 @@ func truncateHeight(content string, height int) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func relative(path string) string {
|
func relative(path string) string {
|
||||||
return strings.TrimPrefix(path, app.Info.Path.Root+"/")
|
return strings.TrimPrefix(path, app.RootPath+"/")
|
||||||
}
|
}
|
||||||
|
|
||||||
func extension(path string) string {
|
func extension(path string) string {
|
||||||
|
|||||||
@@ -1,116 +1,108 @@
|
|||||||
package chat
|
package chat
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/v2/key"
|
|
||||||
"github.com/charmbracelet/bubbles/v2/spinner"
|
"github.com/charmbracelet/bubbles/v2/spinner"
|
||||||
"github.com/charmbracelet/bubbles/v2/viewport"
|
"github.com/charmbracelet/bubbles/v2/viewport"
|
||||||
tea "github.com/charmbracelet/bubbletea/v2"
|
tea "github.com/charmbracelet/bubbletea/v2"
|
||||||
"github.com/charmbracelet/lipgloss/v2"
|
"github.com/charmbracelet/lipgloss/v2"
|
||||||
"github.com/sst/opencode/internal/app"
|
"github.com/sst/opencode/internal/app"
|
||||||
|
"github.com/sst/opencode/internal/components/commands"
|
||||||
"github.com/sst/opencode/internal/components/dialog"
|
"github.com/sst/opencode/internal/components/dialog"
|
||||||
"github.com/sst/opencode/internal/layout"
|
"github.com/sst/opencode/internal/layout"
|
||||||
"github.com/sst/opencode/internal/state"
|
|
||||||
"github.com/sst/opencode/internal/styles"
|
"github.com/sst/opencode/internal/styles"
|
||||||
"github.com/sst/opencode/internal/theme"
|
"github.com/sst/opencode/internal/theme"
|
||||||
"github.com/sst/opencode/pkg/client"
|
"github.com/sst/opencode/pkg/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type MessagesComponent interface {
|
||||||
|
tea.Model
|
||||||
|
tea.ViewModel
|
||||||
|
PageUp() (tea.Model, tea.Cmd)
|
||||||
|
PageDown() (tea.Model, tea.Cmd)
|
||||||
|
HalfPageUp() (tea.Model, tea.Cmd)
|
||||||
|
HalfPageDown() (tea.Model, tea.Cmd)
|
||||||
|
First() (tea.Model, tea.Cmd)
|
||||||
|
Last() (tea.Model, tea.Cmd)
|
||||||
|
// Previous() (tea.Model, tea.Cmd)
|
||||||
|
// Next() (tea.Model, tea.Cmd)
|
||||||
|
ToolDetailsVisible() bool
|
||||||
|
}
|
||||||
|
|
||||||
type messagesComponent struct {
|
type messagesComponent struct {
|
||||||
app *app.App
|
|
||||||
width, height int
|
width, height int
|
||||||
|
app *app.App
|
||||||
viewport viewport.Model
|
viewport viewport.Model
|
||||||
spinner spinner.Model
|
spinner spinner.Model
|
||||||
rendering bool
|
|
||||||
attachments viewport.Model
|
attachments viewport.Model
|
||||||
showToolResults bool
|
commands commands.CommandsComponent
|
||||||
cache *MessageCache
|
cache *MessageCache
|
||||||
|
rendering bool
|
||||||
|
showToolDetails bool
|
||||||
tail bool
|
tail bool
|
||||||
}
|
}
|
||||||
type renderFinishedMsg struct{}
|
type renderFinishedMsg struct{}
|
||||||
type ToggleToolMessagesMsg struct{}
|
type ToggleToolDetailsMsg struct{}
|
||||||
|
|
||||||
type MessageKeys struct {
|
|
||||||
PageDown key.Binding
|
|
||||||
PageUp key.Binding
|
|
||||||
HalfPageUp key.Binding
|
|
||||||
HalfPageDown key.Binding
|
|
||||||
}
|
|
||||||
|
|
||||||
var messageKeys = MessageKeys{
|
|
||||||
PageDown: key.NewBinding(
|
|
||||||
key.WithKeys("pgdown"),
|
|
||||||
key.WithHelp("f/pgdn", "page down"),
|
|
||||||
),
|
|
||||||
PageUp: key.NewBinding(
|
|
||||||
key.WithKeys("pgup"),
|
|
||||||
key.WithHelp("b/pgup", "page up"),
|
|
||||||
),
|
|
||||||
HalfPageUp: key.NewBinding(
|
|
||||||
key.WithKeys("ctrl+u"),
|
|
||||||
key.WithHelp("ctrl+u", "½ page up"),
|
|
||||||
),
|
|
||||||
HalfPageDown: key.NewBinding(
|
|
||||||
key.WithKeys("ctrl+d", "ctrl+d"),
|
|
||||||
key.WithHelp("ctrl+d", "½ page down"),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *messagesComponent) Init() tea.Cmd {
|
func (m *messagesComponent) Init() tea.Cmd {
|
||||||
return tea.Batch(m.viewport.Init(), m.spinner.Tick)
|
return tea.Batch(m.viewport.Init(), m.spinner.Tick, m.commands.Init())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
var cmds []tea.Cmd
|
var cmds []tea.Cmd
|
||||||
switch msg := msg.(type) {
|
switch msg.(type) {
|
||||||
case SendMsg:
|
case app.SendMsg:
|
||||||
m.viewport.GotoBottom()
|
m.viewport.GotoBottom()
|
||||||
m.tail = true
|
m.tail = true
|
||||||
return m, nil
|
return m, nil
|
||||||
case dialog.ThemeChangedMsg:
|
case app.OptimisticMessageAddedMsg:
|
||||||
m.cache.Clear()
|
|
||||||
m.renderView()
|
m.renderView()
|
||||||
return m, nil
|
if m.tail {
|
||||||
case ToggleToolMessagesMsg:
|
m.viewport.GotoBottom()
|
||||||
m.showToolResults = !m.showToolResults
|
|
||||||
m.renderView()
|
|
||||||
return m, nil
|
|
||||||
case state.SessionSelectedMsg:
|
|
||||||
m.cache.Clear()
|
|
||||||
cmd := m.Reload()
|
|
||||||
m.viewport.GotoBottom()
|
|
||||||
return m, cmd
|
|
||||||
case state.SessionClearedMsg:
|
|
||||||
m.cache.Clear()
|
|
||||||
cmd := m.Reload()
|
|
||||||
return m, cmd
|
|
||||||
case tea.KeyMsg:
|
|
||||||
if key.Matches(msg, messageKeys.PageUp) ||
|
|
||||||
key.Matches(msg, messageKeys.PageDown) ||
|
|
||||||
key.Matches(msg, messageKeys.HalfPageUp) ||
|
|
||||||
key.Matches(msg, messageKeys.HalfPageDown) {
|
|
||||||
u, cmd := m.viewport.Update(msg)
|
|
||||||
m.viewport = u
|
|
||||||
m.tail = m.viewport.AtBottom()
|
|
||||||
cmds = append(cmds, cmd)
|
|
||||||
}
|
}
|
||||||
|
return m, nil
|
||||||
|
case dialog.ThemeSelectedMsg:
|
||||||
|
m.cache.Clear()
|
||||||
|
return m, m.Reload()
|
||||||
|
case ToggleToolDetailsMsg:
|
||||||
|
m.showToolDetails = !m.showToolDetails
|
||||||
|
return m, m.Reload()
|
||||||
|
case app.SessionSelectedMsg:
|
||||||
|
m.cache.Clear()
|
||||||
|
m.tail = true
|
||||||
|
return m, m.Reload()
|
||||||
|
case app.SessionClearedMsg:
|
||||||
|
m.cache.Clear()
|
||||||
|
cmd := m.Reload()
|
||||||
|
return m, cmd
|
||||||
case renderFinishedMsg:
|
case renderFinishedMsg:
|
||||||
m.rendering = false
|
m.rendering = false
|
||||||
if m.tail {
|
if m.tail {
|
||||||
m.viewport.GotoBottom()
|
m.viewport.GotoBottom()
|
||||||
}
|
}
|
||||||
case state.StateUpdatedMsg:
|
case client.EventSessionUpdated, client.EventMessageUpdated:
|
||||||
m.renderView()
|
m.renderView()
|
||||||
if m.tail {
|
if m.tail {
|
||||||
m.viewport.GotoBottom()
|
m.viewport.GotoBottom()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewport, cmd := m.viewport.Update(msg)
|
||||||
|
m.viewport = viewport
|
||||||
|
m.tail = m.viewport.AtBottom()
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
|
||||||
spinner, cmd := m.spinner.Update(msg)
|
spinner, cmd := m.spinner.Update(msg)
|
||||||
m.spinner = spinner
|
m.spinner = spinner
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
|
|
||||||
|
updated, cmd := m.commands.Update(msg)
|
||||||
|
m.commands = updated.(commands.CommandsComponent)
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
|
||||||
return m, tea.Batch(cmds...)
|
return m, tea.Batch(cmds...)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,16 +127,27 @@ func (m *messagesComponent) renderView() {
|
|||||||
for _, message := range m.app.Messages {
|
for _, message := range m.app.Messages {
|
||||||
var content string
|
var content string
|
||||||
var cached bool
|
var cached bool
|
||||||
|
lastToolIndex := 0
|
||||||
|
lastToolIndices := []int{}
|
||||||
|
for i, p := range message.Parts {
|
||||||
|
part, _ := p.ValueByDiscriminator()
|
||||||
|
switch part.(type) {
|
||||||
|
case client.MessagePartText:
|
||||||
|
lastToolIndices = append(lastToolIndices, lastToolIndex)
|
||||||
|
case client.MessagePartToolInvocation:
|
||||||
|
lastToolIndex = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
author := ""
|
author := ""
|
||||||
switch message.Role {
|
switch message.Role {
|
||||||
case client.User:
|
case client.User:
|
||||||
author = app.Info.User
|
author = m.app.Info.User
|
||||||
case client.Assistant:
|
case client.Assistant:
|
||||||
author = message.Metadata.Assistant.ModelID
|
author = message.Metadata.Assistant.ModelID
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, p := range message.Parts {
|
for i, p := range message.Parts {
|
||||||
part, err := p.ValueByDiscriminator()
|
part, err := p.ValueByDiscriminator()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue //TODO: handle error?
|
continue //TODO: handle error?
|
||||||
@@ -171,9 +174,10 @@ func (m *messagesComponent) renderView() {
|
|||||||
previousBlockType = assistantTextBlock
|
previousBlockType = assistantTextBlock
|
||||||
}
|
}
|
||||||
case client.MessagePartToolInvocation:
|
case client.MessagePartToolInvocation:
|
||||||
|
isLastToolInvocation := slices.Contains(lastToolIndices, i)
|
||||||
toolInvocationPart := part.(client.MessagePartToolInvocation)
|
toolInvocationPart := part.(client.MessagePartToolInvocation)
|
||||||
toolCall, _ := toolInvocationPart.ToolInvocation.AsMessageToolInvocationToolCall()
|
toolCall, _ := toolInvocationPart.ToolInvocation.AsMessageToolInvocationToolCall()
|
||||||
metadata := client.MessageInfo_Metadata_Tool_AdditionalProperties{}
|
metadata := client.MessageMetadata_Tool_AdditionalProperties{}
|
||||||
if _, ok := message.Metadata.Tool[toolCall.ToolCallId]; ok {
|
if _, ok := message.Metadata.Tool[toolCall.ToolCallId]; ok {
|
||||||
metadata = message.Metadata.Tool[toolCall.ToolCallId]
|
metadata = message.Metadata.Tool[toolCall.ToolCallId]
|
||||||
}
|
}
|
||||||
@@ -186,20 +190,34 @@ func (m *messagesComponent) renderView() {
|
|||||||
if toolCall.State == "result" {
|
if toolCall.State == "result" {
|
||||||
key := m.cache.GenerateKey(message.Id,
|
key := m.cache.GenerateKey(message.Id,
|
||||||
toolCall.ToolCallId,
|
toolCall.ToolCallId,
|
||||||
m.showToolResults,
|
m.showToolDetails,
|
||||||
layout.Current.Viewport.Width,
|
layout.Current.Viewport.Width,
|
||||||
)
|
)
|
||||||
content, cached = m.cache.Get(key)
|
content, cached = m.cache.Get(key)
|
||||||
if !cached {
|
if !cached {
|
||||||
content = renderToolInvocation(toolCall, result, metadata, m.showToolResults)
|
content = renderToolInvocation(
|
||||||
|
toolCall,
|
||||||
|
result,
|
||||||
|
metadata,
|
||||||
|
m.showToolDetails,
|
||||||
|
isLastToolInvocation,
|
||||||
|
false,
|
||||||
|
)
|
||||||
m.cache.Set(key, content)
|
m.cache.Set(key, content)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// if the tool call isn't finished, never cache
|
// if the tool call isn't finished, don't cache
|
||||||
content = renderToolInvocation(toolCall, result, metadata, m.showToolResults)
|
content = renderToolInvocation(
|
||||||
|
toolCall,
|
||||||
|
result,
|
||||||
|
metadata,
|
||||||
|
m.showToolDetails,
|
||||||
|
isLastToolInvocation,
|
||||||
|
false,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if previousBlockType != toolInvocationBlock {
|
if previousBlockType != toolInvocationBlock && m.showToolDetails {
|
||||||
blocks = append(blocks, "")
|
blocks = append(blocks, "")
|
||||||
}
|
}
|
||||||
blocks = append(blocks, content)
|
blocks = append(blocks, content)
|
||||||
@@ -242,8 +260,8 @@ func (m *messagesComponent) header() string {
|
|||||||
|
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
width := layout.Current.Container.Width
|
width := layout.Current.Container.Width
|
||||||
base := styles.BaseStyle().Render
|
base := styles.BaseStyle().Background(t.Background()).Render
|
||||||
muted := styles.Muted().Render
|
muted := styles.Muted().Background(t.Background()).Render
|
||||||
headerLines := []string{}
|
headerLines := []string{}
|
||||||
headerLines = append(headerLines, toMarkdown("# "+m.app.Session.Title, width-6, t.Background()))
|
headerLines = append(headerLines, toMarkdown("# "+m.app.Session.Title, width-6, t.Background()))
|
||||||
if m.app.Session.Share != nil && m.app.Session.Share.Url != "" {
|
if m.app.Session.Share != nil && m.app.Session.Share.Url != "" {
|
||||||
@@ -257,11 +275,11 @@ func (m *messagesComponent) header() string {
|
|||||||
Width(width).
|
Width(width).
|
||||||
PaddingLeft(2).
|
PaddingLeft(2).
|
||||||
PaddingRight(2).
|
PaddingRight(2).
|
||||||
// Background(t.BackgroundElement()).
|
Background(t.Background()).
|
||||||
BorderLeft(true).
|
BorderLeft(true).
|
||||||
BorderRight(true).
|
BorderRight(true).
|
||||||
BorderBackground(t.Background()).
|
BorderBackground(t.Background()).
|
||||||
BorderForeground(t.BackgroundSubtle()).
|
BorderForeground(t.BackgroundElement()).
|
||||||
BorderStyle(lipgloss.ThickBorder()).
|
BorderStyle(lipgloss.ThickBorder()).
|
||||||
Render(header)
|
Render(header)
|
||||||
|
|
||||||
@@ -273,25 +291,33 @@ func (m *messagesComponent) View() string {
|
|||||||
return m.home()
|
return m.home()
|
||||||
}
|
}
|
||||||
if m.rendering {
|
if m.rendering {
|
||||||
return m.viewport.View()
|
return lipgloss.Place(
|
||||||
|
m.width,
|
||||||
|
m.height,
|
||||||
|
lipgloss.Center,
|
||||||
|
lipgloss.Center,
|
||||||
|
"Loading session...",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
t := theme.CurrentTheme()
|
||||||
return lipgloss.JoinVertical(
|
return lipgloss.JoinVertical(
|
||||||
lipgloss.Left,
|
lipgloss.Left,
|
||||||
lipgloss.PlaceHorizontal(m.width, lipgloss.Center, m.header()),
|
lipgloss.PlaceHorizontal(
|
||||||
|
m.width,
|
||||||
|
lipgloss.Center,
|
||||||
|
m.header(),
|
||||||
|
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||||
|
),
|
||||||
m.viewport.View(),
|
m.viewport.View(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *messagesComponent) home() string {
|
func (m *messagesComponent) home() string {
|
||||||
// t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
baseStyle := styles.BaseStyle()
|
baseStyle := styles.BaseStyle().Background(t.Background())
|
||||||
base := baseStyle.Render
|
base := baseStyle.Render
|
||||||
muted := styles.Muted().Render
|
muted := styles.Muted().Background(t.Background()).Render
|
||||||
|
|
||||||
// mark := `
|
|
||||||
// ███▀▀█
|
|
||||||
// ███ █
|
|
||||||
// ▀▀▀▀▀▀ `
|
|
||||||
open := `
|
open := `
|
||||||
█▀▀█ █▀▀█ █▀▀ █▀▀▄
|
█▀▀█ █▀▀█ █▀▀ █▀▀▄
|
||||||
█░░█ █░░█ █▀▀ █░░█
|
█░░█ █░░█ █▀▀ █░░█
|
||||||
@@ -303,56 +329,51 @@ func (m *messagesComponent) home() string {
|
|||||||
|
|
||||||
logo := lipgloss.JoinHorizontal(
|
logo := lipgloss.JoinHorizontal(
|
||||||
lipgloss.Top,
|
lipgloss.Top,
|
||||||
// styles.BaseStyle().Foreground(t.Primary()).Render(mark),
|
muted(open),
|
||||||
styles.Muted().Render(open),
|
base(code),
|
||||||
styles.BaseStyle().Render(code),
|
|
||||||
)
|
)
|
||||||
// cwd := app.Info.Path.Cwd
|
// cwd := app.Info.Path.Cwd
|
||||||
// config := app.Info.Path.Config
|
// config := app.Info.Path.Config
|
||||||
|
|
||||||
commands := [][]string{
|
versionStyle := lipgloss.NewStyle().
|
||||||
{"/help", "show help"},
|
Background(t.Background()).
|
||||||
{"/sessions", "list sessions"},
|
Foreground(t.TextMuted()).
|
||||||
{"/new", "start a new session"},
|
Width(lipgloss.Width(logo)).
|
||||||
{"/model", "switch model"},
|
Align(lipgloss.Right)
|
||||||
{"/share", "share the current session"},
|
version := versionStyle.Render(m.app.Version)
|
||||||
{"/exit", "exit the app"},
|
|
||||||
}
|
|
||||||
|
|
||||||
commandLines := []string{}
|
logoAndVersion := strings.Join([]string{logo, version}, "\n")
|
||||||
for _, command := range commands {
|
logoAndVersion = lipgloss.PlaceHorizontal(
|
||||||
commandLines = append(commandLines, (base(command[0]) + " " + muted(command[1])))
|
m.width,
|
||||||
}
|
lipgloss.Center,
|
||||||
|
logoAndVersion,
|
||||||
logoAndVersion := lipgloss.JoinVertical(
|
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||||
lipgloss.Right,
|
)
|
||||||
logo,
|
m.commands.SetBackgroundColor(t.Background())
|
||||||
muted(app.Info.Version),
|
commands := lipgloss.PlaceHorizontal(
|
||||||
|
m.width,
|
||||||
|
lipgloss.Center,
|
||||||
|
m.commands.View(),
|
||||||
|
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||||
)
|
)
|
||||||
|
|
||||||
lines := []string{}
|
lines := []string{}
|
||||||
lines = append(lines, "")
|
|
||||||
lines = append(lines, "")
|
|
||||||
lines = append(lines, logoAndVersion)
|
lines = append(lines, logoAndVersion)
|
||||||
lines = append(lines, "")
|
lines = append(lines, "")
|
||||||
|
lines = append(lines, "")
|
||||||
// lines = append(lines, base("cwd ")+muted(cwd))
|
// lines = append(lines, base("cwd ")+muted(cwd))
|
||||||
// lines = append(lines, base("config ")+muted(config))
|
// lines = append(lines, base("config ")+muted(config))
|
||||||
// lines = append(lines, "")
|
// lines = append(lines, "")
|
||||||
lines = append(lines, commandLines...)
|
lines = append(lines, commands)
|
||||||
lines = append(lines, "")
|
|
||||||
if m.rendering {
|
|
||||||
lines = append(lines, styles.Muted().Render("Loading session..."))
|
|
||||||
} else {
|
|
||||||
lines = append(lines, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center,
|
return lipgloss.Place(
|
||||||
baseStyle.Width(lipgloss.Width(logoAndVersion)).Render(
|
m.width,
|
||||||
lipgloss.JoinVertical(
|
m.height,
|
||||||
lipgloss.Top,
|
lipgloss.Center,
|
||||||
lines...,
|
lipgloss.Center,
|
||||||
),
|
baseStyle.Render(strings.Join(lines, "\n")),
|
||||||
))
|
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *messagesComponent) SetSize(width, height int) tea.Cmd {
|
func (m *messagesComponent) SetSize(width, height int) tea.Cmd {
|
||||||
@@ -369,6 +390,7 @@ func (m *messagesComponent) SetSize(width, height int) tea.Cmd {
|
|||||||
m.viewport.SetHeight(height - lipgloss.Height(m.header()))
|
m.viewport.SetHeight(height - lipgloss.Height(m.header()))
|
||||||
m.attachments.SetWidth(width + 40)
|
m.attachments.SetWidth(width + 40)
|
||||||
m.attachments.SetHeight(3)
|
m.attachments.SetHeight(3)
|
||||||
|
m.commands.SetSize(width, height)
|
||||||
m.renderView()
|
m.renderView()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -385,26 +407,67 @@ func (m *messagesComponent) Reload() tea.Cmd {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMessagesComponent(app *app.App) layout.ModelWithView {
|
func (m *messagesComponent) PageUp() (tea.Model, tea.Cmd) {
|
||||||
|
m.viewport.ViewUp()
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *messagesComponent) PageDown() (tea.Model, tea.Cmd) {
|
||||||
|
m.viewport.ViewDown()
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *messagesComponent) HalfPageUp() (tea.Model, tea.Cmd) {
|
||||||
|
m.viewport.HalfViewUp()
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *messagesComponent) HalfPageDown() (tea.Model, tea.Cmd) {
|
||||||
|
m.viewport.HalfViewDown()
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *messagesComponent) First() (tea.Model, tea.Cmd) {
|
||||||
|
m.viewport.GotoTop()
|
||||||
|
m.tail = false
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *messagesComponent) Last() (tea.Model, tea.Cmd) {
|
||||||
|
m.viewport.GotoBottom()
|
||||||
|
m.tail = true
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *messagesComponent) ToolDetailsVisible() bool {
|
||||||
|
return m.showToolDetails
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMessagesComponent(app *app.App) MessagesComponent {
|
||||||
customSpinner := spinner.Spinner{
|
customSpinner := spinner.Spinner{
|
||||||
Frames: []string{" ", "┃", "┃"},
|
Frames: []string{" ", "┃", "┃"},
|
||||||
FPS: time.Second / 3,
|
FPS: time.Second / 3,
|
||||||
}
|
}
|
||||||
s := spinner.New(spinner.WithSpinner(customSpinner))
|
s := spinner.New(spinner.WithSpinner(customSpinner))
|
||||||
|
|
||||||
vp := viewport.New() //(0, 0)
|
vp := viewport.New()
|
||||||
attachments := viewport.New() //(0, 0)
|
attachments := viewport.New()
|
||||||
vp.KeyMap.PageUp = messageKeys.PageUp
|
vp.KeyMap = viewport.KeyMap{}
|
||||||
vp.KeyMap.PageDown = messageKeys.PageDown
|
|
||||||
vp.KeyMap.HalfPageUp = messageKeys.HalfPageUp
|
t := theme.CurrentTheme()
|
||||||
vp.KeyMap.HalfPageDown = messageKeys.HalfPageDown
|
commandsView := commands.New(
|
||||||
|
app,
|
||||||
|
commands.WithBackground(t.Background()),
|
||||||
|
commands.WithLimit(6),
|
||||||
|
)
|
||||||
|
|
||||||
return &messagesComponent{
|
return &messagesComponent{
|
||||||
app: app,
|
app: app,
|
||||||
viewport: vp,
|
viewport: vp,
|
||||||
spinner: s,
|
spinner: s,
|
||||||
attachments: attachments,
|
attachments: attachments,
|
||||||
showToolResults: true,
|
commands: commandsView,
|
||||||
|
showToolDetails: true,
|
||||||
cache: NewMessageCache(),
|
cache: NewMessageCache(),
|
||||||
tail: true,
|
tail: true,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,234 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea/v2"
|
||||||
|
"github.com/charmbracelet/lipgloss/v2"
|
||||||
|
"github.com/charmbracelet/lipgloss/v2/compat"
|
||||||
|
"github.com/sst/opencode/internal/app"
|
||||||
|
"github.com/sst/opencode/internal/commands"
|
||||||
|
"github.com/sst/opencode/internal/layout"
|
||||||
|
"github.com/sst/opencode/internal/styles"
|
||||||
|
"github.com/sst/opencode/internal/theme"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CommandsComponent interface {
|
||||||
|
tea.Model
|
||||||
|
tea.ViewModel
|
||||||
|
layout.Sizeable
|
||||||
|
SetBackgroundColor(color compat.AdaptiveColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
type commandsComponent struct {
|
||||||
|
app *app.App
|
||||||
|
width, height int
|
||||||
|
showKeybinds bool
|
||||||
|
showAll bool
|
||||||
|
background *compat.AdaptiveColor
|
||||||
|
limit *int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *commandsComponent) SetSize(width, height int) tea.Cmd {
|
||||||
|
c.width = width
|
||||||
|
c.height = height
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *commandsComponent) GetSize() (int, int) {
|
||||||
|
return c.width, c.height
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *commandsComponent) SetBackgroundColor(color compat.AdaptiveColor) {
|
||||||
|
c.background = &color
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *commandsComponent) Init() tea.Cmd {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *commandsComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
c.width = msg.Width
|
||||||
|
c.height = msg.Height
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *commandsComponent) View() string {
|
||||||
|
t := theme.CurrentTheme()
|
||||||
|
|
||||||
|
triggerStyle := lipgloss.NewStyle().
|
||||||
|
Foreground(t.Primary()).
|
||||||
|
Bold(true)
|
||||||
|
|
||||||
|
descriptionStyle := lipgloss.NewStyle().
|
||||||
|
Foreground(t.Text())
|
||||||
|
|
||||||
|
keybindStyle := lipgloss.NewStyle().
|
||||||
|
Foreground(t.TextMuted())
|
||||||
|
|
||||||
|
if c.background != nil {
|
||||||
|
triggerStyle = triggerStyle.Background(*c.background)
|
||||||
|
descriptionStyle = descriptionStyle.Background(*c.background)
|
||||||
|
keybindStyle = keybindStyle.Background(*c.background)
|
||||||
|
}
|
||||||
|
|
||||||
|
var commandsToShow []commands.Command
|
||||||
|
var triggeredCommands []commands.Command
|
||||||
|
var untriggeredCommands []commands.Command
|
||||||
|
|
||||||
|
for _, cmd := range c.app.Commands.Sorted() {
|
||||||
|
if c.showAll || cmd.Trigger != "" {
|
||||||
|
if cmd.Trigger != "" {
|
||||||
|
triggeredCommands = append(triggeredCommands, cmd)
|
||||||
|
} else if c.showAll {
|
||||||
|
untriggeredCommands = append(untriggeredCommands, cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine triggered commands first, then untriggered
|
||||||
|
commandsToShow = append(commandsToShow, triggeredCommands...)
|
||||||
|
commandsToShow = append(commandsToShow, untriggeredCommands...)
|
||||||
|
|
||||||
|
if c.limit != nil && len(commandsToShow) > *c.limit {
|
||||||
|
commandsToShow = commandsToShow[:*c.limit]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(commandsToShow) == 0 {
|
||||||
|
if c.showAll {
|
||||||
|
return styles.Muted().Render("No commands available")
|
||||||
|
}
|
||||||
|
return styles.Muted().Render("No commands with triggers available")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate column widths
|
||||||
|
maxTriggerWidth := 0
|
||||||
|
maxDescriptionWidth := 0
|
||||||
|
maxKeybindWidth := 0
|
||||||
|
|
||||||
|
// Prepare command data
|
||||||
|
type commandRow struct {
|
||||||
|
trigger string
|
||||||
|
description string
|
||||||
|
keybinds string
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := make([]commandRow, 0, len(commandsToShow))
|
||||||
|
|
||||||
|
for _, cmd := range commandsToShow {
|
||||||
|
trigger := ""
|
||||||
|
if cmd.Trigger != "" {
|
||||||
|
trigger = "/" + cmd.Trigger
|
||||||
|
} else {
|
||||||
|
trigger = string(cmd.Name)
|
||||||
|
}
|
||||||
|
description := cmd.Description
|
||||||
|
|
||||||
|
// Format keybindings
|
||||||
|
var keybindStrs []string
|
||||||
|
if c.showKeybinds {
|
||||||
|
for _, kb := range cmd.Keybindings {
|
||||||
|
if kb.RequiresLeader {
|
||||||
|
keybindStrs = append(keybindStrs, *c.app.Config.Keybinds.Leader+" "+kb.Key)
|
||||||
|
} else {
|
||||||
|
keybindStrs = append(keybindStrs, kb.Key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keybinds := strings.Join(keybindStrs, ", ")
|
||||||
|
|
||||||
|
rows = append(rows, commandRow{
|
||||||
|
trigger: trigger,
|
||||||
|
description: description,
|
||||||
|
keybinds: keybinds,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update max widths
|
||||||
|
if len(trigger) > maxTriggerWidth {
|
||||||
|
maxTriggerWidth = len(trigger)
|
||||||
|
}
|
||||||
|
if len(description) > maxDescriptionWidth {
|
||||||
|
maxDescriptionWidth = len(description)
|
||||||
|
}
|
||||||
|
if len(keybinds) > maxKeybindWidth {
|
||||||
|
maxKeybindWidth = len(keybinds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add padding between columns
|
||||||
|
columnPadding := 3
|
||||||
|
|
||||||
|
// Build the output
|
||||||
|
var output strings.Builder
|
||||||
|
|
||||||
|
maxWidth := 0
|
||||||
|
for _, row := range rows {
|
||||||
|
// Pad each column to align properly
|
||||||
|
trigger := fmt.Sprintf("%-*s", maxTriggerWidth, row.trigger)
|
||||||
|
description := fmt.Sprintf("%-*s", maxDescriptionWidth, row.description)
|
||||||
|
|
||||||
|
// Apply styles and combine
|
||||||
|
line := triggerStyle.Render(trigger) +
|
||||||
|
triggerStyle.Render(strings.Repeat(" ", columnPadding)) +
|
||||||
|
descriptionStyle.Render(description)
|
||||||
|
|
||||||
|
if c.showKeybinds && row.keybinds != "" {
|
||||||
|
line += keybindStyle.Render(strings.Repeat(" ", columnPadding)) +
|
||||||
|
keybindStyle.Render(row.keybinds)
|
||||||
|
}
|
||||||
|
|
||||||
|
output.WriteString(line + "\n")
|
||||||
|
maxWidth = max(maxWidth, lipgloss.Width(line))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove trailing newline
|
||||||
|
result := strings.TrimSuffix(output.String(), "\n")
|
||||||
|
if c.background != nil {
|
||||||
|
result = lipgloss.NewStyle().Background(c.background).Width(maxWidth).Render(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
type Option func(*commandsComponent)
|
||||||
|
|
||||||
|
func WithKeybinds(show bool) Option {
|
||||||
|
return func(c *commandsComponent) {
|
||||||
|
c.showKeybinds = show
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithBackground(background compat.AdaptiveColor) Option {
|
||||||
|
return func(c *commandsComponent) {
|
||||||
|
c.background = &background
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithLimit(limit int) Option {
|
||||||
|
return func(c *commandsComponent) {
|
||||||
|
c.limit = &limit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithShowAll(showAll bool) Option {
|
||||||
|
return func(c *commandsComponent) {
|
||||||
|
c.showAll = showAll
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(app *app.App, opts ...Option) CommandsComponent {
|
||||||
|
c := &commandsComponent{
|
||||||
|
app: app,
|
||||||
|
background: nil,
|
||||||
|
showKeybinds: true,
|
||||||
|
showAll: false,
|
||||||
|
}
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(c)
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
@@ -1,264 +0,0 @@
|
|||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea/v2"
|
|
||||||
"github.com/charmbracelet/lipgloss/v2"
|
|
||||||
"github.com/sst/opencode/internal/app"
|
|
||||||
"github.com/sst/opencode/internal/layout"
|
|
||||||
"github.com/sst/opencode/internal/pubsub"
|
|
||||||
"github.com/sst/opencode/internal/status"
|
|
||||||
"github.com/sst/opencode/internal/styles"
|
|
||||||
"github.com/sst/opencode/internal/theme"
|
|
||||||
)
|
|
||||||
|
|
||||||
type StatusComponent interface {
|
|
||||||
layout.ModelWithView
|
|
||||||
}
|
|
||||||
|
|
||||||
type statusComponent struct {
|
|
||||||
app *app.App
|
|
||||||
queue []status.StatusMessage
|
|
||||||
width int
|
|
||||||
messageTTL time.Duration
|
|
||||||
activeUntil time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// clearMessageCmd is a command that clears status messages after a timeout
|
|
||||||
func (m statusComponent) clearMessageCmd() tea.Cmd {
|
|
||||||
return tea.Tick(time.Second, func(t time.Time) tea.Msg {
|
|
||||||
return statusCleanupMsg{time: t}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// statusCleanupMsg is a message that triggers cleanup of expired status messages
|
|
||||||
type statusCleanupMsg struct {
|
|
||||||
time time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m statusComponent) Init() tea.Cmd {
|
|
||||||
return m.clearMessageCmd()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m statusComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
||||||
switch msg := msg.(type) {
|
|
||||||
case tea.WindowSizeMsg:
|
|
||||||
m.width = msg.Width
|
|
||||||
return m, nil
|
|
||||||
case pubsub.Event[status.StatusMessage]:
|
|
||||||
if msg.Type == status.EventStatusPublished {
|
|
||||||
// If this is a critical message, move it to the front of the queue
|
|
||||||
if msg.Payload.Critical {
|
|
||||||
// Insert at the front of the queue
|
|
||||||
m.queue = append([]status.StatusMessage{msg.Payload}, m.queue...)
|
|
||||||
|
|
||||||
// Reset active time to show critical message immediately
|
|
||||||
m.activeUntil = time.Time{}
|
|
||||||
} else {
|
|
||||||
// Otherwise, just add it to the queue
|
|
||||||
m.queue = append(m.queue, msg.Payload)
|
|
||||||
|
|
||||||
// If this is the first message and nothing is active, activate it immediately
|
|
||||||
if len(m.queue) == 1 && m.activeUntil.IsZero() {
|
|
||||||
now := time.Now()
|
|
||||||
duration := m.messageTTL
|
|
||||||
if msg.Payload.Duration > 0 {
|
|
||||||
duration = msg.Payload.Duration
|
|
||||||
}
|
|
||||||
m.activeUntil = now.Add(duration)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case statusCleanupMsg:
|
|
||||||
now := msg.time
|
|
||||||
|
|
||||||
// If the active message has expired, remove it and activate the next one
|
|
||||||
if !m.activeUntil.IsZero() && m.activeUntil.Before(now) {
|
|
||||||
// Current message expired, remove it if we have one
|
|
||||||
if len(m.queue) > 0 {
|
|
||||||
m.queue = m.queue[1:]
|
|
||||||
}
|
|
||||||
m.activeUntil = time.Time{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have messages in queue but none are active, activate the first one
|
|
||||||
if len(m.queue) > 0 && m.activeUntil.IsZero() {
|
|
||||||
// Use custom duration if specified, otherwise use default
|
|
||||||
duration := m.messageTTL
|
|
||||||
if m.queue[0].Duration > 0 {
|
|
||||||
duration = m.queue[0].Duration
|
|
||||||
}
|
|
||||||
m.activeUntil = now.Add(duration)
|
|
||||||
}
|
|
||||||
|
|
||||||
return m, m.clearMessageCmd()
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func logo() string {
|
|
||||||
t := theme.CurrentTheme()
|
|
||||||
base := lipgloss.NewStyle().Background(t.BackgroundElement()).Foreground(t.TextMuted()).Render
|
|
||||||
emphasis := lipgloss.NewStyle().Bold(true).Background(t.BackgroundElement()).Foreground(t.Text()).Render
|
|
||||||
|
|
||||||
open := base("open")
|
|
||||||
code := emphasis("code ")
|
|
||||||
version := base(app.Info.Version)
|
|
||||||
return styles.Padded().
|
|
||||||
Background(t.BackgroundElement()).
|
|
||||||
Render(open + code + version)
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatTokensAndCost(tokens float32, contextWindow float32, cost float32) string {
|
|
||||||
// Format tokens in human-readable format (e.g., 110K, 1.2M)
|
|
||||||
var formattedTokens string
|
|
||||||
switch {
|
|
||||||
case tokens >= 1_000_000:
|
|
||||||
formattedTokens = fmt.Sprintf("%.1fM", float64(tokens)/1_000_000)
|
|
||||||
case tokens >= 1_000:
|
|
||||||
formattedTokens = fmt.Sprintf("%.1fK", float64(tokens)/1_000)
|
|
||||||
default:
|
|
||||||
formattedTokens = fmt.Sprintf("%d", int(tokens))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove .0 suffix if present
|
|
||||||
if strings.HasSuffix(formattedTokens, ".0K") {
|
|
||||||
formattedTokens = strings.Replace(formattedTokens, ".0K", "K", 1)
|
|
||||||
}
|
|
||||||
if strings.HasSuffix(formattedTokens, ".0M") {
|
|
||||||
formattedTokens = strings.Replace(formattedTokens, ".0M", "M", 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format cost with $ symbol and 2 decimal places
|
|
||||||
formattedCost := fmt.Sprintf("$%.2f", cost)
|
|
||||||
percentage := (float64(tokens) / float64(contextWindow)) * 100
|
|
||||||
|
|
||||||
return fmt.Sprintf("Tokens: %s (%d%%), Cost: %s", formattedTokens, int(percentage), formattedCost)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m statusComponent) View() string {
|
|
||||||
if m.app.Session.Id == "" {
|
|
||||||
return styles.BaseStyle().
|
|
||||||
Width(m.width).
|
|
||||||
Height(2).
|
|
||||||
Render("")
|
|
||||||
}
|
|
||||||
|
|
||||||
t := theme.CurrentTheme()
|
|
||||||
logo := logo()
|
|
||||||
|
|
||||||
cwd := styles.Padded().
|
|
||||||
Foreground(t.TextMuted()).
|
|
||||||
Background(t.BackgroundSubtle()).
|
|
||||||
Render(app.Info.Path.Cwd)
|
|
||||||
|
|
||||||
sessionInfo := ""
|
|
||||||
if m.app.Session.Id != "" {
|
|
||||||
tokens := float32(0)
|
|
||||||
cost := float32(0)
|
|
||||||
contextWindow := m.app.Model.Limit.Context
|
|
||||||
|
|
||||||
for _, message := range m.app.Messages {
|
|
||||||
if message.Metadata.Assistant != nil {
|
|
||||||
cost += message.Metadata.Assistant.Cost
|
|
||||||
usage := message.Metadata.Assistant.Tokens
|
|
||||||
if usage.Output > 0 {
|
|
||||||
tokens = (usage.Input + usage.Output + usage.Reasoning)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionInfo = styles.Padded().
|
|
||||||
Background(t.BackgroundElement()).
|
|
||||||
Foreground(t.TextMuted()).
|
|
||||||
Render(formatTokensAndCost(tokens, contextWindow, cost))
|
|
||||||
}
|
|
||||||
|
|
||||||
// diagnostics := styles.Padded().Background(t.BackgroundElement()).Render(m.projectDiagnostics())
|
|
||||||
|
|
||||||
space := max(
|
|
||||||
0,
|
|
||||||
m.width-lipgloss.Width(logo)-lipgloss.Width(cwd)-lipgloss.Width(sessionInfo),
|
|
||||||
)
|
|
||||||
spacer := lipgloss.NewStyle().Background(t.BackgroundSubtle()).Width(space).Render("")
|
|
||||||
|
|
||||||
status := logo + cwd + spacer + sessionInfo
|
|
||||||
|
|
||||||
blank := styles.BaseStyle().Background(t.Background()).Width(m.width).Render("")
|
|
||||||
return blank + "\n" + status
|
|
||||||
|
|
||||||
// Display the first status message if available
|
|
||||||
// var statusMessage string
|
|
||||||
// if len(m.queue) > 0 {
|
|
||||||
// sm := m.queue[0]
|
|
||||||
// infoStyle := styles.Padded().
|
|
||||||
// Foreground(t.Background())
|
|
||||||
//
|
|
||||||
// switch sm.Level {
|
|
||||||
// case "info":
|
|
||||||
// infoStyle = infoStyle.Background(t.Info())
|
|
||||||
// case "warn":
|
|
||||||
// infoStyle = infoStyle.Background(t.Warning())
|
|
||||||
// case "error":
|
|
||||||
// infoStyle = infoStyle.Background(t.Error())
|
|
||||||
// case "debug":
|
|
||||||
// infoStyle = infoStyle.Background(t.TextMuted())
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Truncate message if it's longer than available width
|
|
||||||
// msg := sm.Message
|
|
||||||
// availWidth := statusWidth - 10
|
|
||||||
//
|
|
||||||
// // If we have enough space, show inline
|
|
||||||
// if availWidth >= minInlineWidth {
|
|
||||||
// if len(msg) > availWidth && availWidth > 0 {
|
|
||||||
// msg = msg[:availWidth] + "..."
|
|
||||||
// }
|
|
||||||
// status += infoStyle.Width(statusWidth).Render(msg)
|
|
||||||
// } else {
|
|
||||||
// // Otherwise, prepare a full-width message to show above
|
|
||||||
// if len(msg) > m.width-10 && m.width > 10 {
|
|
||||||
// msg = msg[:m.width-10] + "..."
|
|
||||||
// }
|
|
||||||
// statusMessage = infoStyle.Width(m.width).Render(msg)
|
|
||||||
//
|
|
||||||
// // Add empty space in the status bar
|
|
||||||
// status += styles.Padded().
|
|
||||||
// Foreground(t.Text()).
|
|
||||||
// Background(t.BackgroundSubtle()).
|
|
||||||
// Width(statusWidth).
|
|
||||||
// Render("")
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// status += styles.Padded().
|
|
||||||
// Foreground(t.Text()).
|
|
||||||
// Background(t.BackgroundSubtle()).
|
|
||||||
// Width(statusWidth).
|
|
||||||
// Render("")
|
|
||||||
// }
|
|
||||||
|
|
||||||
// status += diagnostics
|
|
||||||
// status += modelName
|
|
||||||
|
|
||||||
// If we have a separate status message, prepend it
|
|
||||||
// if statusMessage != "" {
|
|
||||||
// return statusMessage + "\n" + status
|
|
||||||
// } else {
|
|
||||||
// blank := styles.BaseStyle().Background(t.Background()).Width(m.width).Render("")
|
|
||||||
// return blank + "\n" + status
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewStatusCmp(app *app.App) StatusComponent {
|
|
||||||
statusComponent := &statusComponent{
|
|
||||||
app: app,
|
|
||||||
queue: []status.StatusMessage{},
|
|
||||||
messageTTL: 4 * time.Second,
|
|
||||||
activeUntil: time.Time{},
|
|
||||||
}
|
|
||||||
|
|
||||||
return statusComponent
|
|
||||||
}
|
|
||||||
@@ -1,20 +1,19 @@
|
|||||||
package dialog
|
package dialog
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/v2/key"
|
"github.com/charmbracelet/bubbles/v2/key"
|
||||||
"github.com/charmbracelet/bubbles/v2/textarea"
|
"github.com/charmbracelet/bubbles/v2/textarea"
|
||||||
tea "github.com/charmbracelet/bubbletea/v2"
|
tea "github.com/charmbracelet/bubbletea/v2"
|
||||||
"github.com/charmbracelet/lipgloss/v2"
|
"github.com/sst/opencode/internal/app"
|
||||||
"github.com/sst/opencode/internal/components/list"
|
"github.com/sst/opencode/internal/components/list"
|
||||||
"github.com/sst/opencode/internal/layout"
|
|
||||||
"github.com/sst/opencode/internal/status"
|
|
||||||
"github.com/sst/opencode/internal/styles"
|
"github.com/sst/opencode/internal/styles"
|
||||||
"github.com/sst/opencode/internal/theme"
|
"github.com/sst/opencode/internal/theme"
|
||||||
"github.com/sst/opencode/internal/util"
|
"github.com/sst/opencode/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CompletionItem struct {
|
type CompletionItem struct {
|
||||||
title string
|
|
||||||
Title string
|
Title string
|
||||||
Value string
|
Value string
|
||||||
}
|
}
|
||||||
@@ -36,8 +35,7 @@ func (ci *CompletionItem) Render(selected bool, width int) string {
|
|||||||
|
|
||||||
if selected {
|
if selected {
|
||||||
itemStyle = itemStyle.
|
itemStyle = itemStyle.
|
||||||
Foreground(t.Primary()).
|
Foreground(t.Primary())
|
||||||
Bold(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
title := itemStyle.Render(
|
title := itemStyle.Render(
|
||||||
@@ -63,6 +61,7 @@ type CompletionProvider interface {
|
|||||||
GetId() string
|
GetId() string
|
||||||
GetEntry() CompletionItemI
|
GetEntry() CompletionItemI
|
||||||
GetChildEntries(query string) ([]CompletionItemI, error)
|
GetChildEntries(query string) ([]CompletionItemI, error)
|
||||||
|
GetEmptyMessage() string
|
||||||
}
|
}
|
||||||
|
|
||||||
type CompletionSelectedMsg struct {
|
type CompletionSelectedMsg struct {
|
||||||
@@ -78,7 +77,8 @@ type CompletionDialogCompleteItemMsg struct {
|
|||||||
type CompletionDialogCloseMsg struct{}
|
type CompletionDialogCloseMsg struct{}
|
||||||
|
|
||||||
type CompletionDialog interface {
|
type CompletionDialog interface {
|
||||||
layout.ModelWithView
|
tea.Model
|
||||||
|
tea.ViewModel
|
||||||
SetWidth(width int)
|
SetWidth(width int)
|
||||||
IsEmpty() bool
|
IsEmpty() bool
|
||||||
SetProvider(provider CompletionProvider)
|
SetProvider(provider CompletionProvider)
|
||||||
@@ -100,10 +100,10 @@ type completionDialogKeyMap struct {
|
|||||||
|
|
||||||
var completionDialogKeys = completionDialogKeyMap{
|
var completionDialogKeys = completionDialogKeyMap{
|
||||||
Complete: key.NewBinding(
|
Complete: key.NewBinding(
|
||||||
key.WithKeys("tab", "enter"),
|
key.WithKeys("tab", "enter", "right"),
|
||||||
),
|
),
|
||||||
Cancel: key.NewBinding(
|
Cancel: key.NewBinding(
|
||||||
key.WithKeys(" ", "esc", "backspace"),
|
key.WithKeys(" ", "esc", "backspace", "ctrl+c"),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,37 +111,13 @@ func (c *completionDialogComponent) Init() tea.Cmd {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *completionDialogComponent) complete(item CompletionItemI) tea.Cmd {
|
|
||||||
value := c.pseudoSearchTextArea.Value()
|
|
||||||
|
|
||||||
if value == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this is a command completion
|
|
||||||
isCommand := c.completionProvider.GetId() == "commands"
|
|
||||||
|
|
||||||
return tea.Batch(
|
|
||||||
util.CmdHandler(CompletionSelectedMsg{
|
|
||||||
SearchString: value,
|
|
||||||
CompletionValue: item.GetValue(),
|
|
||||||
IsCommand: isCommand,
|
|
||||||
}),
|
|
||||||
c.close(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *completionDialogComponent) close() tea.Cmd {
|
|
||||||
c.list.SetItems([]CompletionItemI{})
|
|
||||||
c.pseudoSearchTextArea.Reset()
|
|
||||||
c.pseudoSearchTextArea.Blur()
|
|
||||||
|
|
||||||
return util.CmdHandler(CompletionDialogCloseMsg{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
var cmds []tea.Cmd
|
var cmds []tea.Cmd
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
|
case []CompletionItemI:
|
||||||
|
c.list.SetItems(msg)
|
||||||
|
case app.CompletionDialogTriggeredMsg:
|
||||||
|
c.pseudoSearchTextArea.SetValue(msg.InitialValue)
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
if c.pseudoSearchTextArea.Focused() {
|
if c.pseudoSearchTextArea.Focused() {
|
||||||
if !key.Matches(msg, completionDialogKeys.Complete) {
|
if !key.Matches(msg, completionDialogKeys.Complete) {
|
||||||
@@ -156,18 +132,19 @@ func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if query != c.query {
|
if query != c.query {
|
||||||
items, err := c.completionProvider.GetChildEntries(query)
|
|
||||||
if err != nil {
|
|
||||||
status.Error(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
c.list.SetItems(items)
|
|
||||||
c.query = query
|
c.query = query
|
||||||
|
cmd = func() tea.Msg {
|
||||||
|
items, err := c.completionProvider.GetChildEntries(query)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to get completion items", "error", err)
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
u, cmd := c.list.Update(msg)
|
u, cmd := c.list.Update(msg)
|
||||||
c.list = u.(list.List[CompletionItemI])
|
c.list = u.(list.List[CompletionItemI])
|
||||||
|
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,14 +164,16 @@ func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
|
|
||||||
return c, tea.Batch(cmds...)
|
return c, tea.Batch(cmds...)
|
||||||
} else {
|
} else {
|
||||||
items, err := c.completionProvider.GetChildEntries("")
|
cmd := func() tea.Msg {
|
||||||
if err != nil {
|
items, err := c.completionProvider.GetChildEntries("")
|
||||||
status.Error(err.Error())
|
if err != nil {
|
||||||
|
slog.Error("Failed to get completion items", "error", err)
|
||||||
|
}
|
||||||
|
return items
|
||||||
}
|
}
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
c.list.SetItems(items)
|
cmds = append(cmds, c.pseudoSearchTextArea.Focus())
|
||||||
c.pseudoSearchTextArea.SetValue(msg.String())
|
return c, tea.Batch(cmds...)
|
||||||
return c, c.pseudoSearchTextArea.Focus()
|
|
||||||
}
|
}
|
||||||
case tea.WindowSizeMsg:
|
case tea.WindowSizeMsg:
|
||||||
c.width = msg.Width
|
c.width = msg.Width
|
||||||
@@ -222,12 +201,6 @@ func (c *completionDialogComponent) View() string {
|
|||||||
|
|
||||||
return baseStyle.Padding(0, 0).
|
return baseStyle.Padding(0, 0).
|
||||||
Background(t.BackgroundElement()).
|
Background(t.BackgroundElement()).
|
||||||
Border(lipgloss.ThickBorder()).
|
|
||||||
BorderTop(false).
|
|
||||||
BorderBottom(false).
|
|
||||||
BorderRight(true).
|
|
||||||
BorderLeft(true).
|
|
||||||
BorderForeground(t.BackgroundSubtle()).
|
|
||||||
Width(c.width).
|
Width(c.width).
|
||||||
Render(c.list.View())
|
Render(c.list.View())
|
||||||
}
|
}
|
||||||
@@ -243,29 +216,54 @@ func (c *completionDialogComponent) IsEmpty() bool {
|
|||||||
func (c *completionDialogComponent) SetProvider(provider CompletionProvider) {
|
func (c *completionDialogComponent) SetProvider(provider CompletionProvider) {
|
||||||
if c.completionProvider.GetId() != provider.GetId() {
|
if c.completionProvider.GetId() != provider.GetId() {
|
||||||
c.completionProvider = provider
|
c.completionProvider = provider
|
||||||
items, err := provider.GetChildEntries("")
|
c.list.SetEmptyMessage(" " + provider.GetEmptyMessage())
|
||||||
if err != nil {
|
c.list.SetItems([]CompletionItemI{})
|
||||||
status.Error(err.Error())
|
|
||||||
}
|
|
||||||
c.list.SetItems(items)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *completionDialogComponent) complete(item CompletionItemI) tea.Cmd {
|
||||||
|
value := c.pseudoSearchTextArea.Value()
|
||||||
|
if value == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is a command completion
|
||||||
|
isCommand := c.completionProvider.GetId() == "commands"
|
||||||
|
|
||||||
|
return tea.Batch(
|
||||||
|
util.CmdHandler(CompletionSelectedMsg{
|
||||||
|
SearchString: value,
|
||||||
|
CompletionValue: item.GetValue(),
|
||||||
|
IsCommand: isCommand,
|
||||||
|
}),
|
||||||
|
c.close(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *completionDialogComponent) close() tea.Cmd {
|
||||||
|
c.pseudoSearchTextArea.Reset()
|
||||||
|
c.pseudoSearchTextArea.Blur()
|
||||||
|
return util.CmdHandler(CompletionDialogCloseMsg{})
|
||||||
|
}
|
||||||
|
|
||||||
func NewCompletionDialogComponent(completionProvider CompletionProvider) CompletionDialog {
|
func NewCompletionDialogComponent(completionProvider CompletionProvider) CompletionDialog {
|
||||||
ti := textarea.New()
|
ti := textarea.New()
|
||||||
|
|
||||||
items, err := completionProvider.GetChildEntries("")
|
|
||||||
if err != nil {
|
|
||||||
status.Error(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
li := list.NewListComponent(
|
li := list.NewListComponent(
|
||||||
items,
|
[]CompletionItemI{},
|
||||||
7,
|
7,
|
||||||
"No matches",
|
completionProvider.GetEmptyMessage(),
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
items, err := completionProvider.GetChildEntries("")
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to get completion items", "error", err)
|
||||||
|
}
|
||||||
|
li.SetItems(items)
|
||||||
|
}()
|
||||||
|
|
||||||
return &completionDialogComponent{
|
return &completionDialogComponent{
|
||||||
query: "",
|
query: "",
|
||||||
completionProvider: completionProvider,
|
completionProvider: completionProvider,
|
||||||
|
|||||||
@@ -1,83 +1,62 @@
|
|||||||
package dialog
|
package dialog
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"github.com/charmbracelet/bubbles/v2/viewport"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/v2/key"
|
|
||||||
tea "github.com/charmbracelet/bubbletea/v2"
|
tea "github.com/charmbracelet/bubbletea/v2"
|
||||||
"github.com/charmbracelet/lipgloss/v2"
|
"github.com/sst/opencode/internal/app"
|
||||||
|
commandsComponent "github.com/sst/opencode/internal/components/commands"
|
||||||
"github.com/sst/opencode/internal/components/modal"
|
"github.com/sst/opencode/internal/components/modal"
|
||||||
"github.com/sst/opencode/internal/layout"
|
"github.com/sst/opencode/internal/layout"
|
||||||
"github.com/sst/opencode/internal/theme"
|
"github.com/sst/opencode/internal/theme"
|
||||||
)
|
)
|
||||||
|
|
||||||
type helpDialog struct {
|
type helpDialog struct {
|
||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
modal *modal.Modal
|
modal *modal.Modal
|
||||||
bindings []key.Binding
|
app *app.App
|
||||||
|
commandsComponent commandsComponent.CommandsComponent
|
||||||
|
viewport viewport.Model
|
||||||
}
|
}
|
||||||
|
|
||||||
// func (i bindingItem) Render(selected bool, width int) string {
|
|
||||||
// t := theme.CurrentTheme()
|
|
||||||
// baseStyle := styles.BaseStyle().
|
|
||||||
// Width(width - 2).
|
|
||||||
// Background(t.BackgroundElement())
|
|
||||||
//
|
|
||||||
// if selected {
|
|
||||||
// baseStyle = baseStyle.
|
|
||||||
// Background(t.Primary()).
|
|
||||||
// Foreground(t.BackgroundElement()).
|
|
||||||
// Bold(true)
|
|
||||||
// } else {
|
|
||||||
// baseStyle = baseStyle.
|
|
||||||
// Foreground(t.Text())
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return baseStyle.Padding(0, 1).Render(i.binding.Help().Desc)
|
|
||||||
// }
|
|
||||||
|
|
||||||
func (h *helpDialog) Init() tea.Cmd {
|
func (h *helpDialog) Init() tea.Cmd {
|
||||||
return nil
|
return tea.Batch(
|
||||||
|
h.commandsComponent.Init(),
|
||||||
|
h.viewport.Init(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *helpDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (h *helpDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
var cmds []tea.Cmd
|
||||||
|
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case tea.WindowSizeMsg:
|
case tea.WindowSizeMsg:
|
||||||
h.width = msg.Width
|
h.width = msg.Width
|
||||||
h.height = msg.Height
|
h.height = msg.Height
|
||||||
|
// Set viewport size with some padding for the modal
|
||||||
|
h.viewport = viewport.New(viewport.WithWidth(msg.Width-4), viewport.WithHeight(msg.Height-6))
|
||||||
|
h.commandsComponent.SetSize(msg.Width-4, msg.Height-6)
|
||||||
}
|
}
|
||||||
return h, nil
|
|
||||||
|
// Update commands component first to get the latest content
|
||||||
|
_, cmdCmd := h.commandsComponent.Update(msg)
|
||||||
|
cmds = append(cmds, cmdCmd)
|
||||||
|
|
||||||
|
// Update viewport content
|
||||||
|
h.viewport.SetContent(h.commandsComponent.View())
|
||||||
|
|
||||||
|
// Update viewport
|
||||||
|
var vpCmd tea.Cmd
|
||||||
|
h.viewport, vpCmd = h.viewport.Update(msg)
|
||||||
|
cmds = append(cmds, vpCmd)
|
||||||
|
|
||||||
|
return h, tea.Batch(cmds...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *helpDialog) View() string {
|
func (h *helpDialog) View() string {
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
keyStyle := lipgloss.NewStyle().
|
h.commandsComponent.SetBackgroundColor(t.BackgroundElement())
|
||||||
Background(t.BackgroundElement()).
|
return h.viewport.View()
|
||||||
Foreground(t.Text()).
|
|
||||||
Bold(true)
|
|
||||||
descStyle := lipgloss.NewStyle().
|
|
||||||
Background(t.BackgroundElement()).
|
|
||||||
Foreground(t.TextMuted())
|
|
||||||
|
|
||||||
lines := []string{}
|
|
||||||
for _, b := range h.bindings {
|
|
||||||
content := keyStyle.Render(b.Help().Key)
|
|
||||||
content += descStyle.Render(" " + b.Help().Desc)
|
|
||||||
for i, key := range b.Keys() {
|
|
||||||
if i == 0 {
|
|
||||||
keyString := " (" + strings.ToUpper(key) + ")"
|
|
||||||
// space := max(h.width-lipgloss.Width(content)-lipgloss.Width(keyString), 0)
|
|
||||||
// spacer := strings.Repeat(" ", space)
|
|
||||||
// content += descStyle.Render(spacer)
|
|
||||||
content += descStyle.Render(keyString)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lines = append(lines, content)
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Join(lines, "\n")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *helpDialog) Render(background string) string {
|
func (h *helpDialog) Render(background string) string {
|
||||||
@@ -92,9 +71,16 @@ type HelpDialog interface {
|
|||||||
layout.Modal
|
layout.Modal
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHelpDialog(bindings ...key.Binding) HelpDialog {
|
func NewHelpDialog(app *app.App) HelpDialog {
|
||||||
|
vp := viewport.New(viewport.WithHeight(12))
|
||||||
return &helpDialog{
|
return &helpDialog{
|
||||||
bindings: bindings,
|
app: app,
|
||||||
modal: modal.New(),
|
commandsComponent: commandsComponent.New(app,
|
||||||
|
commandsComponent.WithBackground(theme.CurrentTheme().BackgroundElement()),
|
||||||
|
commandsComponent.WithShowAll(true),
|
||||||
|
commandsComponent.WithKeybinds(true),
|
||||||
|
),
|
||||||
|
modal: modal.New(modal.WithTitle("Help")),
|
||||||
|
viewport: vp,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import (
|
|||||||
tea "github.com/charmbracelet/bubbletea/v2"
|
tea "github.com/charmbracelet/bubbletea/v2"
|
||||||
"github.com/charmbracelet/lipgloss/v2"
|
"github.com/charmbracelet/lipgloss/v2"
|
||||||
"github.com/sst/opencode/internal/app"
|
"github.com/sst/opencode/internal/app"
|
||||||
|
"github.com/sst/opencode/internal/components/list"
|
||||||
"github.com/sst/opencode/internal/components/modal"
|
"github.com/sst/opencode/internal/components/modal"
|
||||||
"github.com/sst/opencode/internal/layout"
|
"github.com/sst/opencode/internal/layout"
|
||||||
"github.com/sst/opencode/internal/state"
|
|
||||||
"github.com/sst/opencode/internal/styles"
|
"github.com/sst/opencode/internal/styles"
|
||||||
"github.com/sst/opencode/internal/theme"
|
"github.com/sst/opencode/internal/theme"
|
||||||
"github.com/sst/opencode/internal/util"
|
"github.com/sst/opencode/internal/util"
|
||||||
@@ -34,20 +34,15 @@ type modelDialog struct {
|
|||||||
app *app.App
|
app *app.App
|
||||||
availableProviders []client.ProviderInfo
|
availableProviders []client.ProviderInfo
|
||||||
provider client.ProviderInfo
|
provider client.ProviderInfo
|
||||||
|
width int
|
||||||
selectedIdx int
|
height int
|
||||||
width int
|
hScrollOffset int
|
||||||
height int
|
hScrollPossible bool
|
||||||
scrollOffset int
|
modal *modal.Modal
|
||||||
hScrollOffset int
|
modelList list.List[list.StringItem]
|
||||||
hScrollPossible bool
|
|
||||||
|
|
||||||
modal *modal.Modal
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type modelKeyMap struct {
|
type modelKeyMap struct {
|
||||||
Up key.Binding
|
|
||||||
Down key.Binding
|
|
||||||
Left key.Binding
|
Left key.Binding
|
||||||
Right key.Binding
|
Right key.Binding
|
||||||
Enter key.Binding
|
Enter key.Binding
|
||||||
@@ -55,14 +50,6 @@ type modelKeyMap struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var modelKeys = modelKeyMap{
|
var modelKeys = modelKeyMap{
|
||||||
Up: key.NewBinding(
|
|
||||||
key.WithKeys("up", "k"),
|
|
||||||
key.WithHelp("↑", "previous model"),
|
|
||||||
),
|
|
||||||
Down: key.NewBinding(
|
|
||||||
key.WithKeys("down", "j"),
|
|
||||||
key.WithHelp("↓", "next model"),
|
|
||||||
),
|
|
||||||
Left: key.NewBinding(
|
Left: key.NewBinding(
|
||||||
key.WithKeys("left", "h"),
|
key.WithKeys("left", "h"),
|
||||||
key.WithHelp("←", "scroll left"),
|
key.WithHelp("←", "scroll left"),
|
||||||
@@ -82,15 +69,7 @@ var modelKeys = modelKeyMap{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *modelDialog) Init() tea.Cmd {
|
func (m *modelDialog) Init() tea.Cmd {
|
||||||
// cfg := config.Get()
|
m.setupModelsForProvider(m.provider.Id)
|
||||||
// modelInfo := GetSelectedModel(cfg)
|
|
||||||
// m.availableProviders = getEnabledProviders(cfg)
|
|
||||||
// m.hScrollPossible = len(m.availableProviders) > 1
|
|
||||||
|
|
||||||
// m.provider = modelInfo.Provider
|
|
||||||
// m.hScrollOffset = findProviderIndex(m.availableProviders, m.provider)
|
|
||||||
|
|
||||||
// m.setupModelsForProvider(m.provider)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,26 +77,32 @@ func (m *modelDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
switch {
|
switch {
|
||||||
case key.Matches(msg, modelKeys.Up):
|
|
||||||
m.moveSelectionUp()
|
|
||||||
case key.Matches(msg, modelKeys.Down):
|
|
||||||
m.moveSelectionDown()
|
|
||||||
case key.Matches(msg, modelKeys.Left):
|
case key.Matches(msg, modelKeys.Left):
|
||||||
if m.hScrollPossible {
|
if m.hScrollPossible {
|
||||||
m.switchProvider(-1)
|
m.switchProvider(-1)
|
||||||
}
|
}
|
||||||
|
return m, nil
|
||||||
case key.Matches(msg, modelKeys.Right):
|
case key.Matches(msg, modelKeys.Right):
|
||||||
if m.hScrollPossible {
|
if m.hScrollPossible {
|
||||||
m.switchProvider(1)
|
m.switchProvider(1)
|
||||||
}
|
}
|
||||||
|
return m, nil
|
||||||
case key.Matches(msg, modelKeys.Enter):
|
case key.Matches(msg, modelKeys.Enter):
|
||||||
|
selectedItem, _ := m.modelList.GetSelectedItem()
|
||||||
models := m.models()
|
models := m.models()
|
||||||
|
var selectedModel client.ModelInfo
|
||||||
|
for _, model := range models {
|
||||||
|
if model.Name == string(selectedItem) {
|
||||||
|
selectedModel = model
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
return m, tea.Sequence(
|
return m, tea.Sequence(
|
||||||
util.CmdHandler(modal.CloseModalMsg{}),
|
util.CmdHandler(modal.CloseModalMsg{}),
|
||||||
util.CmdHandler(
|
util.CmdHandler(
|
||||||
state.ModelSelectedMsg{
|
app.ModelSelectedMsg{
|
||||||
Provider: m.provider,
|
Provider: m.provider,
|
||||||
Model: models[m.selectedIdx],
|
Model: selectedModel,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
case key.Matches(msg, modelKeys.Escape):
|
case key.Matches(msg, modelKeys.Escape):
|
||||||
@@ -128,7 +113,10 @@ func (m *modelDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.height = msg.Height
|
m.height = msg.Height
|
||||||
}
|
}
|
||||||
|
|
||||||
return m, nil
|
// Update the list component
|
||||||
|
updatedList, cmd := m.modelList.Update(msg)
|
||||||
|
m.modelList = updatedList.(list.List[list.StringItem])
|
||||||
|
return m, cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modelDialog) models() []client.ModelInfo {
|
func (m *modelDialog) models() []client.ModelInfo {
|
||||||
@@ -138,40 +126,9 @@ func (m *modelDialog) models() []client.ModelInfo {
|
|||||||
return models
|
return models
|
||||||
}
|
}
|
||||||
|
|
||||||
// moveSelectionUp moves the selection up or wraps to bottom
|
|
||||||
func (m *modelDialog) moveSelectionUp() {
|
|
||||||
if m.selectedIdx > 0 {
|
|
||||||
m.selectedIdx--
|
|
||||||
} else {
|
|
||||||
m.selectedIdx = len(m.provider.Models) - 1
|
|
||||||
m.scrollOffset = max(0, len(m.provider.Models)-numVisibleModels)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep selection visible
|
|
||||||
if m.selectedIdx < m.scrollOffset {
|
|
||||||
m.scrollOffset = m.selectedIdx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// moveSelectionDown moves the selection down or wraps to top
|
|
||||||
func (m *modelDialog) moveSelectionDown() {
|
|
||||||
if m.selectedIdx < len(m.provider.Models)-1 {
|
|
||||||
m.selectedIdx++
|
|
||||||
} else {
|
|
||||||
m.selectedIdx = 0
|
|
||||||
m.scrollOffset = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep selection visible
|
|
||||||
if m.selectedIdx >= m.scrollOffset+numVisibleModels {
|
|
||||||
m.scrollOffset = m.selectedIdx - (numVisibleModels - 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *modelDialog) switchProvider(offset int) {
|
func (m *modelDialog) switchProvider(offset int) {
|
||||||
newOffset := m.hScrollOffset + offset
|
newOffset := m.hScrollOffset + offset
|
||||||
|
|
||||||
// Ensure we stay within bounds
|
|
||||||
if newOffset < 0 {
|
if newOffset < 0 {
|
||||||
newOffset = len(m.availableProviders) - 1
|
newOffset = len(m.availableProviders) - 1
|
||||||
}
|
}
|
||||||
@@ -181,121 +138,51 @@ func (m *modelDialog) switchProvider(offset int) {
|
|||||||
|
|
||||||
m.hScrollOffset = newOffset
|
m.hScrollOffset = newOffset
|
||||||
m.provider = m.availableProviders[m.hScrollOffset]
|
m.provider = m.availableProviders[m.hScrollOffset]
|
||||||
|
m.modal.SetTitle(fmt.Sprintf("Select %s Model", m.provider.Name))
|
||||||
m.setupModelsForProvider(m.provider.Id)
|
m.setupModelsForProvider(m.provider.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modelDialog) View() string {
|
func (m *modelDialog) View() string {
|
||||||
t := theme.CurrentTheme()
|
listView := m.modelList.View()
|
||||||
baseStyle := lipgloss.NewStyle().
|
|
||||||
Background(t.BackgroundElement()).
|
|
||||||
Foreground(t.Text())
|
|
||||||
|
|
||||||
// Capitalize first letter of provider name
|
|
||||||
title := baseStyle.
|
|
||||||
Foreground(t.Primary()).
|
|
||||||
Bold(true).
|
|
||||||
Width(maxDialogWidth).
|
|
||||||
Padding(0, 0, 1).
|
|
||||||
Render(fmt.Sprintf("Select %s Model", m.provider.Name))
|
|
||||||
|
|
||||||
// Render visible models
|
|
||||||
endIdx := min(m.scrollOffset+numVisibleModels, len(m.provider.Models))
|
|
||||||
modelItems := make([]string, 0, endIdx-m.scrollOffset)
|
|
||||||
|
|
||||||
models := m.models()
|
|
||||||
for i := m.scrollOffset; i < endIdx; i++ {
|
|
||||||
itemStyle := baseStyle.Width(maxDialogWidth)
|
|
||||||
if i == m.selectedIdx {
|
|
||||||
itemStyle = itemStyle.
|
|
||||||
Background(t.Primary()).
|
|
||||||
Foreground(t.BackgroundElement()).
|
|
||||||
Bold(true)
|
|
||||||
}
|
|
||||||
modelItems = append(modelItems, itemStyle.Render(models[i].Name))
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollIndicator := m.getScrollIndicators(maxDialogWidth)
|
scrollIndicator := m.getScrollIndicators(maxDialogWidth)
|
||||||
|
return strings.Join([]string{listView, scrollIndicator}, "\n")
|
||||||
content := lipgloss.JoinVertical(
|
|
||||||
lipgloss.Left,
|
|
||||||
title,
|
|
||||||
baseStyle.Width(maxDialogWidth).Render(lipgloss.JoinVertical(lipgloss.Left, modelItems...)),
|
|
||||||
scrollIndicator,
|
|
||||||
)
|
|
||||||
|
|
||||||
return content
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modelDialog) getScrollIndicators(maxWidth int) string {
|
func (m *modelDialog) getScrollIndicators(maxWidth int) string {
|
||||||
var indicator string
|
var indicator string
|
||||||
|
|
||||||
if len(m.provider.Models) > numVisibleModels {
|
|
||||||
if m.scrollOffset > 0 {
|
|
||||||
indicator += "↑ "
|
|
||||||
}
|
|
||||||
if m.scrollOffset+numVisibleModels < len(m.provider.Models) {
|
|
||||||
indicator += "↓ "
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.hScrollPossible {
|
if m.hScrollPossible {
|
||||||
if m.hScrollOffset > 0 {
|
indicator = "← → (switch provider) "
|
||||||
indicator = "← " + indicator
|
|
||||||
}
|
|
||||||
if m.hScrollOffset < len(m.availableProviders)-1 {
|
|
||||||
indicator += "→"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if indicator == "" {
|
if indicator == "" {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
baseStyle := styles.BaseStyle()
|
return styles.BaseStyle().
|
||||||
|
Foreground(t.TextMuted()).
|
||||||
return baseStyle.
|
|
||||||
Foreground(t.Primary()).
|
|
||||||
Width(maxWidth).
|
Width(maxWidth).
|
||||||
Align(lipgloss.Right).
|
Align(lipgloss.Right).
|
||||||
Bold(true).
|
|
||||||
Render(indicator)
|
Render(indicator)
|
||||||
}
|
}
|
||||||
|
|
||||||
// findProviderIndex returns the index of the provider in the list, or -1 if not found
|
func (m *modelDialog) setupModelsForProvider(providerId string) {
|
||||||
// func findProviderIndex(providers []string, provider string) int {
|
models := m.models()
|
||||||
// for i, p := range providers {
|
modelNames := make([]string, len(models))
|
||||||
// if p == provider {
|
for i, model := range models {
|
||||||
// return i
|
modelNames[i] = model.Name
|
||||||
// }
|
}
|
||||||
// }
|
|
||||||
// return -1
|
|
||||||
// }
|
|
||||||
|
|
||||||
func (m *modelDialog) setupModelsForProvider(_ string) {
|
m.modelList = list.NewStringList(modelNames, numVisibleModels, "No models available", true)
|
||||||
m.selectedIdx = 0
|
m.modelList.SetMaxWidth(maxDialogWidth)
|
||||||
m.scrollOffset = 0
|
|
||||||
|
|
||||||
// cfg := config.Get()
|
if m.app.Provider != nil && m.app.Model != nil && m.app.Provider.Id == providerId {
|
||||||
// agentCfg := cfg.Agents[config.AgentPrimary]
|
for i, model := range models {
|
||||||
// selectedModelId := agentCfg.Model
|
if model.Id == m.app.Model.Id {
|
||||||
|
m.modelList.SetSelectedIndex(i)
|
||||||
// m.provider = provider
|
break
|
||||||
// m.models = getModelsForProvider(provider)
|
}
|
||||||
|
}
|
||||||
// Try to select the current model if it belongs to this provider
|
}
|
||||||
// if provider == models.SupportedModels[selectedModelId].Provider {
|
|
||||||
// for i, model := range m.models {
|
|
||||||
// if model.ID == selectedModelId {
|
|
||||||
// m.selectedIdx = i
|
|
||||||
// // Adjust scroll position to keep selected model visible
|
|
||||||
// if m.selectedIdx >= numVisibleModels {
|
|
||||||
// m.scrollOffset = m.selectedIdx - (numVisibleModels - 1)
|
|
||||||
// }
|
|
||||||
// break
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modelDialog) Render(background string) string {
|
func (m *modelDialog) Render(background string) string {
|
||||||
@@ -309,11 +196,30 @@ func (s *modelDialog) Close() tea.Cmd {
|
|||||||
func NewModelDialog(app *app.App) ModelDialog {
|
func NewModelDialog(app *app.App) ModelDialog {
|
||||||
availableProviders, _ := app.ListProviders(context.Background())
|
availableProviders, _ := app.ListProviders(context.Background())
|
||||||
|
|
||||||
return &modelDialog{
|
currentProvider := availableProviders[0]
|
||||||
availableProviders: availableProviders,
|
hScrollOffset := 0
|
||||||
hScrollOffset: 0,
|
if app.Provider != nil {
|
||||||
hScrollPossible: len(availableProviders) > 1,
|
for i, provider := range availableProviders {
|
||||||
provider: availableProviders[0],
|
if provider.Id == app.Provider.Id {
|
||||||
modal: modal.New(),
|
currentProvider = provider
|
||||||
|
hScrollOffset = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dialog := &modelDialog{
|
||||||
|
app: app,
|
||||||
|
availableProviders: availableProviders,
|
||||||
|
hScrollOffset: hScrollOffset,
|
||||||
|
hScrollPossible: len(availableProviders) > 1,
|
||||||
|
provider: currentProvider,
|
||||||
|
modal: modal.New(
|
||||||
|
modal.WithTitle(fmt.Sprintf("Select %s Model", currentProvider.Name)),
|
||||||
|
modal.WithMaxWidth(maxDialogWidth+4),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.setupModelsForProvider(currentProvider.Id)
|
||||||
|
return dialog
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"github.com/charmbracelet/bubbles/v2/viewport"
|
"github.com/charmbracelet/bubbles/v2/viewport"
|
||||||
tea "github.com/charmbracelet/bubbletea/v2"
|
tea "github.com/charmbracelet/bubbletea/v2"
|
||||||
"github.com/charmbracelet/lipgloss/v2"
|
"github.com/charmbracelet/lipgloss/v2"
|
||||||
"github.com/sst/opencode/internal/layout"
|
|
||||||
"github.com/sst/opencode/internal/styles"
|
"github.com/sst/opencode/internal/styles"
|
||||||
"github.com/sst/opencode/internal/theme"
|
"github.com/sst/opencode/internal/theme"
|
||||||
"github.com/sst/opencode/internal/util"
|
"github.com/sst/opencode/internal/util"
|
||||||
@@ -30,7 +29,8 @@ type PermissionResponseMsg struct {
|
|||||||
|
|
||||||
// PermissionDialogComponent interface for permission dialog component
|
// PermissionDialogComponent interface for permission dialog component
|
||||||
type PermissionDialogComponent interface {
|
type PermissionDialogComponent interface {
|
||||||
layout.ModelWithView
|
tea.Model
|
||||||
|
tea.ViewModel
|
||||||
// SetPermissions(permission permission.PermissionRequest) tea.Cmd
|
// SetPermissions(permission permission.PermissionRequest) tea.Cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user