mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-17 12:56:41 +02:00
Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| db24bf87c0 | |||
| f4c0d2d2fd | |||
| d240f4c676 | |||
| 9c90cdbe08 | |||
| fc7af31fe5 | |||
| 2f8d23ec66 | |||
| 77ae3fb9b9 | |||
| 4e7f6c47fd | |||
| 50469ed750 | |||
| aaab785493 | |||
| 9751937894 | |||
| 0fc8dfc77e | |||
| 81b7df61ec | |||
| 8217b96d4a | |||
| 7dd0918d32 | |||
| 4b26b43855 | |||
| 9d7cfda9fe | |||
| a3cf18c905 | |||
| 0b1a8ae699 | |||
| eb70b1e5c8 | |||
| 00a3d818b6 | |||
| 2384c7e734 | |||
| 1bad3d9894 | |||
| 4f715e66dc | |||
| ec001ca02f | |||
| a2d3b9f0c8 | |||
| 9cfb6ff964 | |||
| 6ed661c140 | |||
| 9dc00edfc9 | |||
| e063bf888e | |||
| 6f18475428 | |||
| 3664b09812 | |||
| 7050cc0ac3 | |||
| 4d3d63294d | |||
| 6bc61cbc2d | |||
| 01d351bebe | |||
| dbba4a97aa | |||
| 0dc586faef | |||
| f19c6b05f2 | |||
| bc34f08333 | |||
| b7ee16aabd | |||
| ed1b0d97bf | |||
| 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 |
@@ -17,7 +17,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: oven-sh/setup-bun@v1
|
- uses: oven-sh/setup-bun@v1
|
||||||
with:
|
with:
|
||||||
bun-version: latest
|
bun-version: 1.2.17
|
||||||
|
|
||||||
- run: bun install
|
- run: bun install
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: oven-sh/setup-bun@v2
|
- uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
bun-version: 1.2.16
|
bun-version: 1.2.17
|
||||||
|
|
||||||
- name: Install makepkg
|
- name: Install makepkg
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ For more info on how to configure opencode [**head over to our docs**](https://o
|
|||||||
|
|
||||||
### Contributing
|
### 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.
|
To run opencode locally you need.
|
||||||
|
|
||||||
- Bun
|
- Bun
|
||||||
|
|||||||
@@ -20,6 +20,9 @@
|
|||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
|
"bin": {
|
||||||
|
"opencode": "./bin/opencode",
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/prompts": "0.11.0",
|
"@clack/prompts": "0.11.0",
|
||||||
"@flystorage/file-storage": "1.1.0",
|
"@flystorage/file-storage": "1.1.0",
|
||||||
@@ -458,7 +461,7 @@
|
|||||||
|
|
||||||
"@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=="],
|
||||||
|
|
||||||
@@ -596,7 +599,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=="],
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
|
if defined OPENCODE_BIN_PATH (
|
||||||
|
set "resolved=%OPENCODE_BIN_PATH%"
|
||||||
|
goto :execute
|
||||||
|
)
|
||||||
|
|
||||||
|
rem Get the directory of this script
|
||||||
|
set "script_dir=%~dp0"
|
||||||
|
set "script_dir=%script_dir:~0,-1%"
|
||||||
|
|
||||||
|
rem Detect platform and architecture
|
||||||
|
set "platform=win32"
|
||||||
|
|
||||||
|
rem Detect architecture
|
||||||
|
if "%PROCESSOR_ARCHITECTURE%"=="AMD64" (
|
||||||
|
set "arch=x64"
|
||||||
|
) else if "%PROCESSOR_ARCHITECTURE%"=="ARM64" (
|
||||||
|
set "arch=arm64"
|
||||||
|
) else if "%PROCESSOR_ARCHITECTURE%"=="x86" (
|
||||||
|
set "arch=x86"
|
||||||
|
) else (
|
||||||
|
set "arch=x64"
|
||||||
|
)
|
||||||
|
|
||||||
|
set "name=opencode-!platform!-!arch!"
|
||||||
|
set "binary=opencode.exe"
|
||||||
|
|
||||||
|
rem Search for the binary starting from script location
|
||||||
|
set "resolved="
|
||||||
|
set "current_dir=%script_dir%"
|
||||||
|
|
||||||
|
:search_loop
|
||||||
|
set "candidate=%current_dir%\node_modules\%name%\bin\%binary%"
|
||||||
|
if exist "%candidate%" (
|
||||||
|
set "resolved=%candidate%"
|
||||||
|
goto :execute
|
||||||
|
)
|
||||||
|
|
||||||
|
rem Move up one directory
|
||||||
|
for %%i in ("%current_dir%") do set "parent_dir=%%~dpi"
|
||||||
|
set "parent_dir=%parent_dir:~0,-1%"
|
||||||
|
|
||||||
|
rem Check if we've reached the root
|
||||||
|
if "%current_dir%"=="%parent_dir%" goto :not_found
|
||||||
|
set "current_dir=%parent_dir%"
|
||||||
|
goto :search_loop
|
||||||
|
|
||||||
|
:not_found
|
||||||
|
echo It seems that your package manager failed to install the right version of the OpenCode CLI for your platform. You can try manually installing the "%name%" package >&2
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:execute
|
||||||
|
rem Execute the binary with all arguments
|
||||||
|
"%resolved%" %*
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "0.0.0",
|
"version": "0.0.5", "name": "opencode",
|
||||||
"name": "opencode",
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"dev": "bun run ./src/index.ts"
|
"dev": "bun run ./src/index.ts"
|
||||||
},
|
},
|
||||||
|
"bin": {
|
||||||
|
"opencode": "./bin/opencode"
|
||||||
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
"./*": "./src/*.ts"
|
"./*": "./src/*.ts"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -45,7 +46,7 @@ export namespace App {
|
|||||||
const data = path.join(
|
const data = path.join(
|
||||||
Global.Path.data,
|
Global.Path.data,
|
||||||
"project",
|
"project",
|
||||||
git ? git.split(path.sep).join("-") : "global",
|
git ? directory(git) : "global",
|
||||||
)
|
)
|
||||||
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 {
|
||||||
@@ -132,4 +133,13 @@ export namespace App {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function directory(input: string): string {
|
||||||
|
return input
|
||||||
|
.split(path.sep)
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("-")
|
||||||
|
.replace(/[^A-Za-z0-9_]/g, "-")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Global } from "../global"
|
||||||
|
import { lazy } from "../util/lazy"
|
||||||
|
import path from "path"
|
||||||
|
|
||||||
|
export const AuthCopilot = lazy(async () => {
|
||||||
|
const file = Bun.file(path.join(Global.Path.state, "plugin", "copilot.ts"))
|
||||||
|
const response = fetch(
|
||||||
|
"https://raw.githubusercontent.com/sst/opencode-github-copilot/refs/heads/main/auth.ts",
|
||||||
|
)
|
||||||
|
.then((x) => Bun.write(file, x))
|
||||||
|
.catch(() => {})
|
||||||
|
|
||||||
|
if (!file.exists()) {
|
||||||
|
const worked = await response
|
||||||
|
if (!worked) return
|
||||||
|
}
|
||||||
|
const result = await import(file.name!).catch(() => {})
|
||||||
|
if (!result) return
|
||||||
|
return result.AuthCopilot
|
||||||
|
})
|
||||||
@@ -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(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ export namespace BunProc {
|
|||||||
) {
|
) {
|
||||||
log.info("running", {
|
log.info("running", {
|
||||||
cmd: [which(), ...cmd],
|
cmd: [which(), ...cmd],
|
||||||
options,
|
...options,
|
||||||
})
|
})
|
||||||
const result = Bun.spawn([which(), ...cmd], {
|
const result = Bun.spawn([which(), ...cmd], {
|
||||||
...options,
|
...options,
|
||||||
@@ -26,6 +26,15 @@ export namespace BunProc {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
const code = await result.exited
|
const code = await result.exited
|
||||||
|
// @ts-ignore
|
||||||
|
const stdout = await result.stdout.text()
|
||||||
|
// @ts-ignore
|
||||||
|
const stderr = await result.stderr.text()
|
||||||
|
log.info("done", {
|
||||||
|
code,
|
||||||
|
stdout,
|
||||||
|
stderr,
|
||||||
|
})
|
||||||
if (code !== 0) {
|
if (code !== 0) {
|
||||||
throw new Error(`Command failed with exit code ${result.exitCode}`)
|
throw new Error(`Command failed with exit code ${result.exitCode}`)
|
||||||
}
|
}
|
||||||
@@ -53,7 +62,7 @@ export namespace BunProc {
|
|||||||
if (parsed.dependencies[pkg] === version) return mod
|
if (parsed.dependencies[pkg] === version) return mod
|
||||||
parsed.dependencies[pkg] = version
|
parsed.dependencies[pkg] = version
|
||||||
await Bun.write(pkgjson, JSON.stringify(parsed, null, 2))
|
await Bun.write(pkgjson, JSON.stringify(parsed, null, 2))
|
||||||
await BunProc.run(["install"], {
|
await BunProc.run(["install", "--registry=https://registry.npmjs.org"], {
|
||||||
cwd: Global.Path.cache,
|
cwd: Global.Path.cache,
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
new InstallFailedError(
|
new InstallFailedError(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { AuthAnthropic } from "../../auth/anthropic"
|
import { AuthAnthropic } from "../../auth/anthropic"
|
||||||
|
import { AuthCopilot } from "../../auth/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"
|
||||||
@@ -6,17 +7,20 @@ 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, sortBy, values } from "remeda"
|
import { map, pipe, sortBy, values } from "remeda"
|
||||||
|
import path from "path"
|
||||||
|
import os from "os"
|
||||||
|
import { Global } from "../../global"
|
||||||
|
|
||||||
export const AuthCommand = cmd({
|
export const AuthCommand = cmd({
|
||||||
command: "auth",
|
command: "auth",
|
||||||
describe: "Manage credentials",
|
describe: "manage credentials",
|
||||||
builder: (yargs) =>
|
builder: (yargs) =>
|
||||||
yargs
|
yargs
|
||||||
.command(AuthLoginCommand)
|
.command(AuthLoginCommand)
|
||||||
.command(AuthLogoutCommand)
|
.command(AuthLogoutCommand)
|
||||||
.command(AuthListCommand)
|
.command(AuthListCommand)
|
||||||
.demandCommand(),
|
.demandCommand(),
|
||||||
async handler() { },
|
async handler() {},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const AuthListCommand = cmd({
|
export const AuthListCommand = cmd({
|
||||||
@@ -25,30 +29,61 @@ export const AuthListCommand = cmd({
|
|||||||
describe: "list providers",
|
describe: "list providers",
|
||||||
async handler() {
|
async handler() {
|
||||||
UI.empty()
|
UI.empty()
|
||||||
prompts.intro("Credentials")
|
const authPath = path.join(Global.Path.data, "auth.json")
|
||||||
|
const homedir = os.homedir()
|
||||||
|
const displayPath = authPath.startsWith(homedir)
|
||||||
|
? authPath.replace(homedir, "~")
|
||||||
|
: authPath
|
||||||
|
prompts.intro(`Credentials ${UI.Style.TEXT_DIM}${displayPath}`)
|
||||||
const results = await Auth.all().then((x) => Object.entries(x))
|
const results = await Auth.all().then((x) => Object.entries(x))
|
||||||
const database = await ModelsDev.get()
|
const database = await ModelsDev.get()
|
||||||
|
|
||||||
for (const [providerID, result] of results) {
|
for (const [providerID, result] of results) {
|
||||||
const name = database[providerID]?.name || providerID
|
const name = database[providerID]?.name || providerID
|
||||||
prompts.log.info(`${name} ${UI.Style.TEXT_DIM}(${result.type})`)
|
prompts.log.info(`${name} ${UI.Style.TEXT_DIM}${result.type}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
prompts.outro(`${results.length} credentials`)
|
prompts.outro(`${results.length} credentials`)
|
||||||
|
|
||||||
|
// Environment variables section
|
||||||
|
const activeEnvVars: Array<{ provider: string, envVar: string }> = []
|
||||||
|
|
||||||
|
for (const [providerID, provider] of Object.entries(database)) {
|
||||||
|
for (const envVar of provider.env) {
|
||||||
|
if (process.env[envVar]) {
|
||||||
|
activeEnvVars.push({
|
||||||
|
provider: provider.name || providerID,
|
||||||
|
envVar
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeEnvVars.length > 0) {
|
||||||
|
UI.empty()
|
||||||
|
prompts.intro("Environment")
|
||||||
|
|
||||||
|
for (const { provider, envVar } of activeEnvVars) {
|
||||||
|
prompts.log.info(`${provider} ${UI.Style.TEXT_DIM}${envVar}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
prompts.outro(`${activeEnvVars.length} environment variables`)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
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",
|
||||||
@@ -146,6 +181,44 @@ export const AuthLoginCommand = cmd({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const copilot = await AuthCopilot()
|
||||||
|
if (provider === "github-copilot" && copilot) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||||
|
const deviceInfo = await copilot.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 response = await copilot.poll(deviceInfo.device)
|
||||||
|
if (response.status === "pending") continue
|
||||||
|
if (response.status === "success") {
|
||||||
|
await Auth.set("github-copilot", {
|
||||||
|
type: "oauth",
|
||||||
|
refresh: response.refresh,
|
||||||
|
access: response.access,
|
||||||
|
expires: response.expires,
|
||||||
|
})
|
||||||
|
spinner.stop("Login successful")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (response.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"),
|
||||||
@@ -162,7 +235,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)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { App } from "../../app/app"
|
||||||
|
import { Provider } from "../../provider/provider"
|
||||||
|
import { cmd } from "./cmd"
|
||||||
|
|
||||||
|
export const ModelsCommand = cmd({
|
||||||
|
command: "models",
|
||||||
|
describe: "list all available models",
|
||||||
|
handler: async () => {
|
||||||
|
await App.provide({ cwd: process.cwd() }, async () => {
|
||||||
|
const providers = await Provider.list()
|
||||||
|
|
||||||
|
for (const [providerID, provider] of Object.entries(providers)) {
|
||||||
|
for (const modelID of Object.keys(provider.info.models)) {
|
||||||
|
console.log(`${providerID}/${modelID}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -25,7 +25,7 @@ const TOOL: Record<string, [string, string]> = {
|
|||||||
|
|
||||||
export const RunCommand = cmd({
|
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", {
|
||||||
@@ -36,12 +36,12 @@ export const RunCommand = cmd({
|
|||||||
})
|
})
|
||||||
.option("continue", {
|
.option("continue", {
|
||||||
alias: ["c"],
|
alias: ["c"],
|
||||||
describe: "Continue the last session",
|
describe: "continue the last session",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
})
|
})
|
||||||
.option("session", {
|
.option("session", {
|
||||||
alias: ["s"],
|
alias: ["s"],
|
||||||
describe: "Session ID to continue",
|
describe: "session id to continue",
|
||||||
type: "string",
|
type: "string",
|
||||||
})
|
})
|
||||||
.option("share", {
|
.option("share", {
|
||||||
@@ -51,7 +51,7 @@ export const RunCommand = cmd({
|
|||||||
.option("model", {
|
.option("model", {
|
||||||
type: "string",
|
type: "string",
|
||||||
alias: ["m"],
|
alias: ["m"],
|
||||||
describe: "Model to use in the format of provider/model",
|
describe: "model to use in the format of provider/model",
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
handler: async (args) => {
|
handler: async (args) => {
|
||||||
@@ -134,7 +134,7 @@ export const RunCommand = cmd({
|
|||||||
part.toolInvocation.toolName,
|
part.toolInvocation.toolName,
|
||||||
UI.Style.TEXT_INFO_BOLD,
|
UI.Style.TEXT_INFO_BOLD,
|
||||||
]
|
]
|
||||||
printEvent(color, tool, metadata.title)
|
printEvent(color, tool, metadata?.title || 'Unknown')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (part.type === "text") {
|
if (part.type === "text") {
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { App } from "../../app/app"
|
||||||
|
import { Provider } from "../../provider/provider"
|
||||||
|
import { Server } from "../../server/server"
|
||||||
|
import { Share } from "../../share/share"
|
||||||
|
import { cmd } from "./cmd"
|
||||||
|
|
||||||
|
export const ServeCommand = cmd({
|
||||||
|
command: "serve",
|
||||||
|
builder: (yargs) =>
|
||||||
|
yargs
|
||||||
|
.option("port", {
|
||||||
|
alias: ["p"],
|
||||||
|
type: "number",
|
||||||
|
describe: "port to listen on",
|
||||||
|
default: 4096,
|
||||||
|
})
|
||||||
|
.option("hostname", {
|
||||||
|
alias: ["h"],
|
||||||
|
type: "string",
|
||||||
|
describe: "hostname to listen on",
|
||||||
|
default: "127.0.0.1",
|
||||||
|
}),
|
||||||
|
describe: "starts a headless opencode server",
|
||||||
|
handler: async (args) => {
|
||||||
|
const cwd = process.cwd()
|
||||||
|
await App.provide({ cwd }, async () => {
|
||||||
|
const providers = await Provider.list()
|
||||||
|
if (Object.keys(providers).length === 0) {
|
||||||
|
return "needs_provider"
|
||||||
|
}
|
||||||
|
|
||||||
|
const hostname = args.hostname
|
||||||
|
const port = args.port
|
||||||
|
|
||||||
|
await Share.init()
|
||||||
|
const server = Server.listen({
|
||||||
|
port,
|
||||||
|
hostname,
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`opencode server listening on http://${server.hostname}:${server.port}`,
|
||||||
|
)
|
||||||
|
|
||||||
|
await new Promise(() => {})
|
||||||
|
|
||||||
|
server.stop()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -5,16 +5,16 @@ import { Installation } from "../../installation"
|
|||||||
|
|
||||||
export const UpgradeCommand = {
|
export const UpgradeCommand = {
|
||||||
command: "upgrade [target]",
|
command: "upgrade [target]",
|
||||||
describe: "upgrade opencode to the latest version or a specific version",
|
describe: "upgrade opencode to the latest or a specific version",
|
||||||
builder: (yargs: Argv) => {
|
builder: (yargs: Argv) => {
|
||||||
return yargs
|
return yargs
|
||||||
.positional("target", {
|
.positional("target", {
|
||||||
describe: "specific version to upgrade to (e.g., '0.1.48' or 'v0.1.48')",
|
describe: "version to upgrade to, for ex '0.1.48' or 'v0.1.48'",
|
||||||
type: "string",
|
type: "string",
|
||||||
})
|
})
|
||||||
.option("method", {
|
.option("method", {
|
||||||
alias: "m",
|
alias: "m",
|
||||||
describe: "installation method to use (curl, npm, pnpm, bun, brew)",
|
describe: "installation method to use",
|
||||||
type: "string",
|
type: "string",
|
||||||
choices: ["curl", "npm", "pnpm", "bun", "brew"],
|
choices: ["curl", "npm", "pnpm", "bun", "brew"],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { Config } from "../config/config"
|
import { Config } from "../config/config"
|
||||||
|
import { MCP } from "../mcp"
|
||||||
|
import { UI } from "./ui"
|
||||||
|
|
||||||
export function FormatError(input: unknown) {
|
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))
|
if (Config.JsonError.isInstance(input))
|
||||||
return `Config file at ${input.data.path} is not valid JSON`
|
return `Config file at ${input.data.path} is not valid JSON`
|
||||||
if (Config.InvalidError.isInstance(input))
|
if (Config.InvalidError.isInstance(input))
|
||||||
@@ -10,4 +14,6 @@ export function FormatError(input: unknown) {
|
|||||||
(issue) => "↳ " + issue.message + " " + issue.path.join("."),
|
(issue) => "↳ " + issue.message + " " + issue.path.join("."),
|
||||||
) ?? []),
|
) ?? []),
|
||||||
].join("\n")
|
].join("\n")
|
||||||
|
|
||||||
|
if (UI.CancelledError.isInstance(input)) return ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
import { EOL } from "os"
|
||||||
import { NamedError } from "../util/error"
|
import { NamedError } from "../util/error"
|
||||||
|
|
||||||
export namespace UI {
|
export namespace UI {
|
||||||
@@ -29,7 +30,7 @@ export namespace UI {
|
|||||||
|
|
||||||
export function println(...message: string[]) {
|
export function println(...message: string[]) {
|
||||||
print(...message)
|
print(...message)
|
||||||
Bun.stderr.write("\n")
|
Bun.stderr.write(EOL)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function print(...message: string[]) {
|
export function print(...message: string[]) {
|
||||||
@@ -52,7 +53,7 @@ export namespace UI {
|
|||||||
result.push(row[0])
|
result.push(row[0])
|
||||||
result.push("\x1b[0m")
|
result.push("\x1b[0m")
|
||||||
result.push(row[1])
|
result.push(row[1])
|
||||||
result.push("\n")
|
result.push(EOL)
|
||||||
}
|
}
|
||||||
return result.join("").trimEnd()
|
return result.join("").trimEnd()
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-2
@@ -116,14 +116,17 @@ export namespace Ripgrep {
|
|||||||
export async function files(input: {
|
export async function files(input: {
|
||||||
cwd: string
|
cwd: string
|
||||||
query?: string
|
query?: string
|
||||||
|
glob?: string
|
||||||
limit?: number
|
limit?: number
|
||||||
}) {
|
}) {
|
||||||
const commands = [`${await filepath()} --files --hidden --glob='!.git/*'`]
|
const commands = [
|
||||||
|
`${await filepath()} --files --hidden --glob='!.git/*' ${input.glob ? `--glob='${input.glob}'` : ``}`,
|
||||||
|
]
|
||||||
if (input.query)
|
if (input.query)
|
||||||
commands.push(`${await Fzf.filepath()} --filter=${input.query}`)
|
commands.push(`${await Fzf.filepath()} --filter=${input.query}`)
|
||||||
if (input.limit) commands.push(`head -n ${input.limit}`)
|
if (input.limit) commands.push(`head -n ${input.limit}`)
|
||||||
const joined = commands.join(" | ")
|
const joined = commands.join(" | ")
|
||||||
const result = await $`${{ raw: joined }}`.cwd(input.cwd).text()
|
const result = await $`${{ raw: joined }}`.cwd(input.cwd).nothrow().text()
|
||||||
return result.split("\n").filter(Boolean)
|
return result.split("\n").filter(Boolean)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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 url from "node:url"
|
||||||
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"
|
||||||
@@ -13,6 +14,7 @@ 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 { UpgradeCommand } from "./cli/cmd/upgrade"
|
||||||
|
import { ModelsCommand } from "./cli/cmd/models"
|
||||||
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 { Installation } from "./installation"
|
||||||
@@ -20,12 +22,29 @@ import { Bus } from "./bus"
|
|||||||
import { Config } from "./config/config"
|
import { Config } from "./config/config"
|
||||||
import { NamedError } from "./util/error"
|
import { NamedError } from "./util/error"
|
||||||
import { FormatError } from "./cli/error"
|
import { FormatError } from "./cli/error"
|
||||||
|
import { ServeCommand } from "./cli/cmd/serve"
|
||||||
|
|
||||||
|
const cancel = new AbortController()
|
||||||
|
|
||||||
|
process.on("unhandledRejection", (e) => {
|
||||||
|
Log.Default.error("rejection", {
|
||||||
|
e: e instanceof Error ? e.message : e,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
process.on("uncaughtException", (e) => {
|
||||||
|
Log.Default.error("exception", {
|
||||||
|
e: e instanceof Error ? e.message : e,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const cli = yargs(hideBin(process.argv))
|
const cli = yargs(hideBin(process.argv))
|
||||||
.scriptName("opencode")
|
.scriptName("opencode")
|
||||||
.version(Installation.VERSION)
|
.help("help", "show help")
|
||||||
|
.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 () => {
|
||||||
@@ -38,7 +57,7 @@ const cli = yargs(hideBin(process.argv))
|
|||||||
.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",
|
||||||
@@ -47,7 +66,12 @@ const cli = yargs(hideBin(process.argv))
|
|||||||
handler: async (args) => {
|
handler: async (args) => {
|
||||||
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)
|
try {
|
||||||
|
process.chdir(cwd)
|
||||||
|
} catch (e) {
|
||||||
|
UI.error("Failed to change directory to " + cwd)
|
||||||
|
return
|
||||||
|
}
|
||||||
const result = await App.provide({ cwd }, async (app) => {
|
const result = await App.provide({ cwd }, async (app) => {
|
||||||
const providers = await Provider.list()
|
const providers = await Provider.list()
|
||||||
if (Object.keys(providers).length === 0) {
|
if (Object.keys(providers).length === 0) {
|
||||||
@@ -55,13 +79,22 @@ const cli = yargs(hideBin(process.argv))
|
|||||||
}
|
}
|
||||||
|
|
||||||
await Share.init()
|
await Share.init()
|
||||||
const server = Server.listen()
|
const server = Server.listen({
|
||||||
|
port: 0,
|
||||||
|
hostname: "127.0.0.1",
|
||||||
|
})
|
||||||
|
|
||||||
let cmd = ["go", "run", "./main.go"]
|
let cmd = ["go", "run", "./main.go"]
|
||||||
let cwd = new URL("../../tui/cmd/opencode", import.meta.url).pathname
|
let cwd = url.fileURLToPath(
|
||||||
|
new URL("../../tui/cmd/opencode", import.meta.url),
|
||||||
|
)
|
||||||
if (Bun.embeddedFiles.length > 0) {
|
if (Bun.embeddedFiles.length > 0) {
|
||||||
const blob = Bun.embeddedFiles[0] as File
|
const blob = Bun.embeddedFiles[0] as File
|
||||||
const binary = path.join(Global.Path.cache, "tui", blob.name)
|
let binaryName = blob.name
|
||||||
|
if (process.platform === "win32" && !binaryName.endsWith(".exe")) {
|
||||||
|
binaryName += ".exe"
|
||||||
|
}
|
||||||
|
const binary = path.join(Global.Path.cache, "tui", binaryName)
|
||||||
const file = Bun.file(binary)
|
const file = Bun.file(binary)
|
||||||
if (!(await file.exists())) {
|
if (!(await file.exists())) {
|
||||||
await Bun.write(file, blob, { mode: 0o755 })
|
await Bun.write(file, blob, { mode: 0o755 })
|
||||||
@@ -72,6 +105,7 @@ const cli = yargs(hideBin(process.argv))
|
|||||||
}
|
}
|
||||||
const proc = Bun.spawn({
|
const proc = Bun.spawn({
|
||||||
cmd: [...cmd, ...process.argv.slice(2)],
|
cmd: [...cmd, ...process.argv.slice(2)],
|
||||||
|
signal: cancel.signal,
|
||||||
cwd,
|
cwd,
|
||||||
stdout: "inherit",
|
stdout: "inherit",
|
||||||
stderr: "inherit",
|
stderr: "inherit",
|
||||||
@@ -91,7 +125,8 @@ const cli = yargs(hideBin(process.argv))
|
|||||||
if (Installation.isSnapshot()) return
|
if (Installation.isSnapshot()) return
|
||||||
const config = await Config.global()
|
const config = await Config.global()
|
||||||
if (config.autoupdate === false) return
|
if (config.autoupdate === false) return
|
||||||
const latest = await Installation.latest()
|
const latest = await Installation.latest().catch(() => {})
|
||||||
|
if (!latest) return
|
||||||
if (Installation.VERSION === latest) return
|
if (Installation.VERSION === latest) return
|
||||||
const method = await Installation.method()
|
const method = await Installation.method()
|
||||||
if (method === "unknown") return
|
if (method === "unknown") return
|
||||||
@@ -122,6 +157,8 @@ const cli = yargs(hideBin(process.argv))
|
|||||||
.command(ScrapCommand)
|
.command(ScrapCommand)
|
||||||
.command(AuthCommand)
|
.command(AuthCommand)
|
||||||
.command(UpgradeCommand)
|
.command(UpgradeCommand)
|
||||||
|
.command(ServeCommand)
|
||||||
|
.command(ModelsCommand)
|
||||||
.fail((msg) => {
|
.fail((msg) => {
|
||||||
if (
|
if (
|
||||||
msg.startsWith("Unknown argument") ||
|
msg.startsWith("Unknown argument") ||
|
||||||
@@ -152,8 +189,10 @@ try {
|
|||||||
Log.Default.error("fatal", data)
|
Log.Default.error("fatal", data)
|
||||||
const formatted = FormatError(e)
|
const formatted = FormatError(e)
|
||||||
if (formatted) UI.error(formatted)
|
if (formatted) UI.error(formatted)
|
||||||
if (!formatted)
|
if (formatted === undefined)
|
||||||
UI.error(
|
UI.error(
|
||||||
"Unexpected error, check log file at " + Log.file() + " for more details",
|
"Unexpected error, check log file at " + Log.file() + " for more details",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cancel.abort()
|
||||||
|
|||||||
@@ -115,7 +115,9 @@ export namespace Installation {
|
|||||||
case "bun":
|
case "bun":
|
||||||
return $`bun install -g opencode-ai@${target}`
|
return $`bun install -g opencode-ai@${target}`
|
||||||
case "brew":
|
case "brew":
|
||||||
return $`brew install sst/tap/opencode`
|
return $`brew install sst/tap/opencode`.env({
|
||||||
|
HOMEBREW_NO_AUTO_UPDATE: "1",
|
||||||
|
})
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown method: ${method}`)
|
throw new Error(`Unknown method: ${method}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export async function data() {
|
||||||
|
const json = await fetch("https://models.dev/api.json").then((x) => x.text())
|
||||||
|
return json
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { Global } from "../global"
|
|||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
import { data } from "./models-macro" with { type: "macro" }
|
||||||
|
|
||||||
export namespace ModelsDev {
|
export namespace ModelsDev {
|
||||||
const log = Log.create({ service: "models.dev" })
|
const log = Log.create({ service: "models.dev" })
|
||||||
@@ -54,16 +55,15 @@ export namespace ModelsDev {
|
|||||||
refresh()
|
refresh()
|
||||||
return result as Record<string, Provider>
|
return result as Record<string, Provider>
|
||||||
}
|
}
|
||||||
await refresh()
|
refresh()
|
||||||
return get()
|
const json = await data()
|
||||||
|
return JSON.parse(json) as Record<string, Provider>
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
const file = Bun.file(filepath)
|
const file = Bun.file(filepath)
|
||||||
log.info("refreshing")
|
log.info("refreshing")
|
||||||
const result = await fetch("https://models.dev/api.json")
|
const result = await fetch("https://models.dev/api.json").catch(() => {})
|
||||||
if (!result.ok)
|
if (result && result.ok) await Bun.write(file, result)
|
||||||
throw new Error(`Failed to fetch models.dev: ${result.statusText}`)
|
|
||||||
await Bun.write(file, result)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { AuthCopilot } from "../auth/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,20 +28,21 @@ 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 = (provider: ModelsDev.Provider) => Promise<
|
type CustomLoader = (
|
||||||
| {
|
provider: ModelsDev.Provider,
|
||||||
getModel?: (sdk: any, modelID: string) => Promise<any>
|
api?: string,
|
||||||
options: Record<string, any>
|
) => Promise<{
|
||||||
}
|
autoload: boolean
|
||||||
| false
|
getModel?: (sdk: any, modelID: string) => Promise<any>
|
||||||
>
|
options?: Record<string, any>
|
||||||
|
}>
|
||||||
|
|
||||||
type Source = "env" | "config" | "custom" | "api"
|
type Source = "env" | "config" | "custom" | "api"
|
||||||
|
|
||||||
const CUSTOM_LOADERS: Record<string, CustomLoader> = {
|
const CUSTOM_LOADERS: Record<string, CustomLoader> = {
|
||||||
async anthropic(provider) {
|
async anthropic(provider) {
|
||||||
const access = await AuthAnthropic.access()
|
const access = await AuthAnthropic.access()
|
||||||
if (!access) return false
|
if (!access) return { autoload: false }
|
||||||
for (const model of Object.values(provider.models)) {
|
for (const model of Object.values(provider.models)) {
|
||||||
model.cost = {
|
model.cost = {
|
||||||
input: 0,
|
input: 0,
|
||||||
@@ -48,6 +50,7 @@ export namespace Provider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
autoload: true,
|
||||||
options: {
|
options: {
|
||||||
apiKey: "",
|
apiKey: "",
|
||||||
async fetch(input: any, init: any) {
|
async fetch(input: any, init: any) {
|
||||||
@@ -66,8 +69,56 @@ export namespace Provider {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"github-copilot": async (provider) => {
|
||||||
|
const copilot = await AuthCopilot()
|
||||||
|
if (!copilot) return { autoload: false }
|
||||||
|
let info = await Auth.get("github-copilot")
|
||||||
|
if (!info || info.type !== "oauth") return { autoload: false }
|
||||||
|
|
||||||
|
if (provider && provider.models) {
|
||||||
|
for (const model of Object.values(provider.models)) {
|
||||||
|
model.cost = {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
autoload: true,
|
||||||
|
options: {
|
||||||
|
apiKey: "",
|
||||||
|
async fetch(input: any, init: any) {
|
||||||
|
const info = await Auth.get("github-copilot")
|
||||||
|
if (!info || info.type !== "oauth") return
|
||||||
|
if (!info.access || info.expires < Date.now()) {
|
||||||
|
const tokens = await copilot.access(info.refresh)
|
||||||
|
if (!tokens)
|
||||||
|
throw new Error("GitHub Copilot authentication expired")
|
||||||
|
await Auth.set("github-copilot", {
|
||||||
|
type: "oauth",
|
||||||
|
...tokens,
|
||||||
|
})
|
||||||
|
info.access = tokens.access
|
||||||
|
}
|
||||||
|
const headers = {
|
||||||
|
...init.headers,
|
||||||
|
...copilot.HEADERS,
|
||||||
|
Authorization: `Bearer ${info.access}`,
|
||||||
|
"Openai-Intent": "conversation-edits",
|
||||||
|
}
|
||||||
|
delete headers["x-api-key"]
|
||||||
|
return fetch(input, {
|
||||||
|
...init,
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
openai: async () => {
|
openai: async () => {
|
||||||
return {
|
return {
|
||||||
|
autoload: false,
|
||||||
async getModel(sdk: any, modelID: string) {
|
async getModel(sdk: any, modelID: string) {
|
||||||
return sdk.responses(modelID)
|
return sdk.responses(modelID)
|
||||||
},
|
},
|
||||||
@@ -75,7 +126,8 @@ export namespace Provider {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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 { autoload: false }
|
||||||
|
|
||||||
const region = process.env["AWS_REGION"] ?? "us-east-1"
|
const region = process.env["AWS_REGION"] ?? "us-east-1"
|
||||||
|
|
||||||
@@ -83,6 +135,7 @@ export namespace Provider {
|
|||||||
await BunProc.install("@aws-sdk/credential-providers")
|
await BunProc.install("@aws-sdk/credential-providers")
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
|
autoload: true,
|
||||||
options: {
|
options: {
|
||||||
region,
|
region,
|
||||||
credentialProvider: fromNodeProviderChain(),
|
credentialProvider: fromNodeProviderChain(),
|
||||||
@@ -208,8 +261,14 @@ export namespace Provider {
|
|||||||
for (const [providerID, fn] of Object.entries(CUSTOM_LOADERS)) {
|
for (const [providerID, fn] of Object.entries(CUSTOM_LOADERS)) {
|
||||||
if (disabled.has(providerID)) continue
|
if (disabled.has(providerID)) continue
|
||||||
const result = await fn(database[providerID])
|
const result = await fn(database[providerID])
|
||||||
if (result)
|
if (result && (result.autoload || providers[providerID])) {
|
||||||
mergeProvider(providerID, result.options, "custom", result.getModel)
|
mergeProvider(
|
||||||
|
providerID,
|
||||||
|
result.options ?? {},
|
||||||
|
"custom",
|
||||||
|
result.getModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// load config
|
// load config
|
||||||
|
|||||||
@@ -1,24 +1,25 @@
|
|||||||
import type { CoreMessage } from "ai"
|
import type { LanguageModelV1Prompt } from "ai"
|
||||||
|
import { unique } from "remeda"
|
||||||
|
|
||||||
export namespace ProviderTransform {
|
export namespace ProviderTransform {
|
||||||
export function message(
|
export function message(
|
||||||
msg: CoreMessage,
|
msgs: LanguageModelV1Prompt,
|
||||||
index: number,
|
|
||||||
providerID: string,
|
providerID: string,
|
||||||
modelID: string,
|
modelID: string,
|
||||||
) {
|
) {
|
||||||
if (
|
if (providerID === "anthropic" || modelID.includes("anthropic")) {
|
||||||
(providerID === "anthropic" || modelID.includes("anthropic")) &&
|
const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
|
||||||
index < 4
|
const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
|
||||||
) {
|
|
||||||
msg.providerOptions = {
|
for (const msg of unique([...system, ...final])) {
|
||||||
...msg.providerOptions,
|
msg.providerMetadata = {
|
||||||
anthropic: {
|
...msg.providerMetadata,
|
||||||
cacheControl: { type: "ephemeral" },
|
anthropic: {
|
||||||
},
|
cacheControl: { type: "ephemeral" },
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return msgs
|
||||||
return msg
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -390,6 +390,33 @@ export namespace Server {
|
|||||||
return c.json(Session.abort(body.sessionID))
|
return c.json(Session.abort(body.sessionID))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
.post(
|
||||||
|
"/session_delete",
|
||||||
|
describeRoute({
|
||||||
|
description: "Delete a session and all its data",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Successfully deleted session",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(z.boolean()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
zValidator(
|
||||||
|
"json",
|
||||||
|
z.object({
|
||||||
|
sessionID: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
async (c) => {
|
||||||
|
const body = c.req.valid("json")
|
||||||
|
await Session.remove(body.sessionID)
|
||||||
|
return c.json(true)
|
||||||
|
},
|
||||||
|
)
|
||||||
.post(
|
.post(
|
||||||
"/session_summarize",
|
"/session_summarize",
|
||||||
describeRoute({
|
describeRoute({
|
||||||
@@ -552,10 +579,10 @@ export namespace Server {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listen() {
|
export function listen(opts: { port: number; hostname: string }) {
|
||||||
const server = Bun.serve({
|
const server = Bun.serve({
|
||||||
port: 0,
|
port: opts.port,
|
||||||
hostname: "0.0.0.0",
|
hostname: opts.hostname,
|
||||||
idleTimeout: 0,
|
idleTimeout: 0,
|
||||||
fetch: app().fetch,
|
fetch: app().fetch,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
type CoreMessage,
|
type CoreMessage,
|
||||||
type UIMessage,
|
type UIMessage,
|
||||||
type ProviderMetadata,
|
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"
|
||||||
@@ -71,6 +72,12 @@ export namespace Session {
|
|||||||
info: Info,
|
info: Info,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
Deleted: Bus.event(
|
||||||
|
"session.deleted",
|
||||||
|
z.object({
|
||||||
|
info: Info,
|
||||||
|
}),
|
||||||
|
),
|
||||||
Error: Bus.event(
|
Error: Bus.event(
|
||||||
"session.error",
|
"session.error",
|
||||||
z.object({
|
z.object({
|
||||||
@@ -205,6 +212,17 @@ export namespace Session {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function children(parentID: string) {
|
||||||
|
const result = [] as Session.Info[]
|
||||||
|
for await (const item of Storage.list("session/info")) {
|
||||||
|
const sessionID = path.basename(item, ".json")
|
||||||
|
const session = await get(sessionID)
|
||||||
|
if (session.parentID !== parentID) continue
|
||||||
|
result.push(session)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
export function abort(sessionID: string) {
|
export function abort(sessionID: string) {
|
||||||
const controller = state().pending.get(sessionID)
|
const controller = state().pending.get(sessionID)
|
||||||
if (!controller) return false
|
if (!controller) return false
|
||||||
@@ -213,6 +231,28 @@ export namespace Session {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function remove(sessionID: string, emitEvent = true) {
|
||||||
|
try {
|
||||||
|
abort(sessionID)
|
||||||
|
const session = await get(sessionID)
|
||||||
|
for (const child of await children(sessionID)) {
|
||||||
|
await remove(child.id, false)
|
||||||
|
}
|
||||||
|
await unshare(sessionID).catch(() => {})
|
||||||
|
await Storage.remove(`session/info/${sessionID}`).catch(() => {})
|
||||||
|
await Storage.removeDir(`session/message/${sessionID}/`).catch(() => {})
|
||||||
|
state().sessions.delete(sessionID)
|
||||||
|
state().messages.delete(sessionID)
|
||||||
|
if (emitEvent) {
|
||||||
|
Bus.publish(Event.Deleted, {
|
||||||
|
info: session,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function updateMessage(msg: Message.Info) {
|
async function updateMessage(msg: Message.Info) {
|
||||||
await Storage.writeJSON(
|
await Storage.writeJSON(
|
||||||
"session/message/" + msg.metadata.sessionID + "/" + msg.id,
|
"session/message/" + msg.metadata.sessionID + "/" + msg.id,
|
||||||
@@ -247,7 +287,10 @@ export namespace Session {
|
|||||||
if (
|
if (
|
||||||
model.info.limit.context &&
|
model.info.limit.context &&
|
||||||
tokens >
|
tokens >
|
||||||
(model.info.limit.context - (model.info.limit.output ?? 0)) * 0.9
|
Math.max(
|
||||||
|
(model.info.limit.context - (model.info.limit.output ?? 0)) * 0.9,
|
||||||
|
0,
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
await summarize({
|
await summarize({
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
@@ -285,9 +328,7 @@ export namespace Session {
|
|||||||
parts: toParts(input.parts),
|
parts: toParts(input.parts),
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
].map((msg, i) =>
|
],
|
||||||
ProviderTransform.message(msg, i, input.providerID, input.modelID),
|
|
||||||
),
|
|
||||||
model: model.language,
|
model: model.language,
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
@@ -434,24 +475,6 @@ 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", { finishReason: step.finishReason })
|
log.info("step finish", { finishReason: step.finishReason })
|
||||||
@@ -527,12 +550,26 @@ export namespace Session {
|
|||||||
...convertToCoreMessages(
|
...convertToCoreMessages(
|
||||||
msgs.map(toUIMessage).filter((x) => x.parts.length > 0),
|
msgs.map(toUIMessage).filter((x) => x.parts.length > 0),
|
||||||
),
|
),
|
||||||
].map((msg, i) =>
|
],
|
||||||
ProviderTransform.message(msg, i, input.providerID, input.modelID),
|
|
||||||
),
|
|
||||||
temperature: model.info.temperature ? 0 : undefined,
|
temperature: model.info.temperature ? 0 : undefined,
|
||||||
tools: model.info.tool_call === false ? undefined : tools,
|
tools: model.info.tool_call === false ? undefined : tools,
|
||||||
model: model.language,
|
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 {
|
try {
|
||||||
for await (const value of result.fullStream) {
|
for await (const value of result.fullStream) {
|
||||||
@@ -623,6 +660,21 @@ export namespace Session {
|
|||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case "finish":
|
||||||
|
log.info("message finish", {
|
||||||
|
reason: value.finishReason,
|
||||||
|
})
|
||||||
|
const assistant = next.metadata!.assistant!
|
||||||
|
const usage = getUsage(
|
||||||
|
model.info,
|
||||||
|
value.usage,
|
||||||
|
value.providerMetadata,
|
||||||
|
)
|
||||||
|
assistant.cost = usage.cost
|
||||||
|
await updateMessage(next)
|
||||||
|
if (value.finishReason === "length")
|
||||||
|
throw new Message.OutputLengthError({})
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
l.info("unhandled", {
|
l.info("unhandled", {
|
||||||
type: value.type,
|
type: value.type,
|
||||||
@@ -636,6 +688,9 @@ export namespace Session {
|
|||||||
error: e,
|
error: e,
|
||||||
})
|
})
|
||||||
switch (true) {
|
switch (true) {
|
||||||
|
case Message.OutputLengthError.isInstance(e):
|
||||||
|
next.metadata.error = e
|
||||||
|
break
|
||||||
case LoadAPIKeyError.isInstance(e):
|
case LoadAPIKeyError.isInstance(e):
|
||||||
next.metadata.error = new Provider.AuthError(
|
next.metadata.error = new Provider.AuthError(
|
||||||
{
|
{
|
||||||
@@ -723,7 +778,9 @@ 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: [
|
messages: [
|
||||||
@@ -744,16 +801,46 @@ export namespace Session {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
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(model.info, result.usage, result.providerMetadata)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function lock(sessionID: string) {
|
function lock(sessionID: string) {
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ import { Provider } from "../provider/provider"
|
|||||||
import { NamedError } from "../util/error"
|
import { NamedError } from "../util/error"
|
||||||
|
|
||||||
export namespace Message {
|
export namespace Message {
|
||||||
|
export const OutputLengthError = NamedError.create(
|
||||||
|
"MessageOutputLengthError",
|
||||||
|
z.object({}),
|
||||||
|
)
|
||||||
|
|
||||||
export const ToolCall = z
|
export const ToolCall = z
|
||||||
.object({
|
.object({
|
||||||
state: z.literal("call"),
|
state: z.literal("call"),
|
||||||
@@ -135,53 +140,56 @@ 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(),
|
OutputLengthError.Schema,
|
||||||
sessionID: z.string(),
|
])
|
||||||
tool: z.record(
|
.optional(),
|
||||||
z.string(),
|
sessionID: z.string(),
|
||||||
z
|
tool: z.record(
|
||||||
|
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(),
|
|
||||||
cache: z.object({
|
|
||||||
read: z.number(),
|
|
||||||
write: z.number(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
.openapi({
|
.openapi({
|
||||||
ref: "Message.Info",
|
ref: "Message.Info",
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ export namespace Storage {
|
|||||||
await fs.unlink(target).catch(() => {})
|
await fs.unlink(target).catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function removeDir(key: string) {
|
||||||
|
const target = path.join(state().dir, key)
|
||||||
|
await fs.rm(target, { recursive: true, force: true }).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>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// the approaches in this edit tool are sourced from
|
||||||
|
// https://github.com/cline/cline/blob/main/evals/diff-edits/diff-apply/diff-06-23-25.ts
|
||||||
|
// https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/utils/editCorrector.ts
|
||||||
|
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import { Tool } from "./tool"
|
import { Tool } from "./tool"
|
||||||
@@ -22,13 +26,17 @@ export const EditTool = Tool.define({
|
|||||||
replaceAll: z
|
replaceAll: z
|
||||||
.boolean()
|
.boolean()
|
||||||
.optional()
|
.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) {
|
||||||
throw new Error("filePath is required")
|
throw new Error("filePath is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (params.oldString === params.newString) {
|
||||||
|
throw new Error("oldString and newString must be different")
|
||||||
|
}
|
||||||
|
|
||||||
const app = App.info()
|
const app = App.info()
|
||||||
const filepath = path.isAbsolute(params.filePath)
|
const filepath = path.isAbsolute(params.filePath)
|
||||||
? params.filePath
|
? params.filePath
|
||||||
@@ -55,35 +63,19 @@ export const EditTool = Tool.define({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const file = Bun.file(filepath)
|
const file = Bun.file(filepath)
|
||||||
if (!(await file.exists())) throw new Error(`File ${filepath} not found`)
|
const stats = await file.stat().catch(() => {})
|
||||||
const stats = await file.stat()
|
if (!stats) throw new Error(`File ${filepath} not found`)
|
||||||
if (stats.isDirectory())
|
if (stats.isDirectory())
|
||||||
throw new Error(`Path is a directory, not a file: ${filepath}`)
|
throw new Error(`Path is a directory, not a file: ${filepath}`)
|
||||||
await FileTimes.assert(ctx.sessionID, filepath)
|
await FileTimes.assert(ctx.sessionID, filepath)
|
||||||
contentOld = await file.text()
|
contentOld = await file.text()
|
||||||
const index = contentOld.indexOf(params.oldString)
|
|
||||||
if (index === -1)
|
|
||||||
throw new Error(
|
|
||||||
`oldString not found in file. Make sure it matches exactly, including whitespace and line breaks`,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (params.replaceAll) {
|
|
||||||
contentNew = contentOld.replaceAll(params.oldString, params.newString)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!params.replaceAll) {
|
|
||||||
const lastIndex = contentOld.lastIndexOf(params.oldString)
|
|
||||||
if (index !== lastIndex)
|
|
||||||
throw new Error(
|
|
||||||
`oldString appears multiple times in the file. Please provide more context to ensure a unique match`,
|
|
||||||
)
|
|
||||||
|
|
||||||
contentNew =
|
|
||||||
contentOld.substring(0, index) +
|
|
||||||
params.newString +
|
|
||||||
contentOld.substring(index + params.oldString.length)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
contentNew = replace(
|
||||||
|
contentOld,
|
||||||
|
params.oldString,
|
||||||
|
params.newString,
|
||||||
|
params.replaceAll,
|
||||||
|
)
|
||||||
await file.write(contentNew)
|
await file.write(contentNew)
|
||||||
})()
|
})()
|
||||||
|
|
||||||
@@ -116,6 +108,326 @@ export const EditTool = Tool.define({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export type Replacer = (
|
||||||
|
content: string,
|
||||||
|
find: string,
|
||||||
|
) => Generator<string, void, unknown>
|
||||||
|
|
||||||
|
export const SimpleReplacer: Replacer = function* (_content, find) {
|
||||||
|
yield find
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LineTrimmedReplacer: Replacer = function* (content, find) {
|
||||||
|
const originalLines = content.split("\n")
|
||||||
|
const searchLines = find.split("\n")
|
||||||
|
|
||||||
|
if (searchLines[searchLines.length - 1] === "") {
|
||||||
|
searchLines.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i <= originalLines.length - searchLines.length; i++) {
|
||||||
|
let matches = true
|
||||||
|
|
||||||
|
for (let j = 0; j < searchLines.length; j++) {
|
||||||
|
const originalTrimmed = originalLines[i + j].trim()
|
||||||
|
const searchTrimmed = searchLines[j].trim()
|
||||||
|
|
||||||
|
if (originalTrimmed !== searchTrimmed) {
|
||||||
|
matches = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matches) {
|
||||||
|
let matchStartIndex = 0
|
||||||
|
for (let k = 0; k < i; k++) {
|
||||||
|
matchStartIndex += originalLines[k].length + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
let matchEndIndex = matchStartIndex
|
||||||
|
for (let k = 0; k < searchLines.length; k++) {
|
||||||
|
matchEndIndex += originalLines[i + k].length + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
yield content.substring(matchStartIndex, matchEndIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BlockAnchorReplacer: Replacer = function* (content, find) {
|
||||||
|
const originalLines = content.split("\n")
|
||||||
|
const searchLines = find.split("\n")
|
||||||
|
|
||||||
|
if (searchLines.length < 3) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchLines[searchLines.length - 1] === "") {
|
||||||
|
searchLines.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstLineSearch = searchLines[0].trim()
|
||||||
|
const lastLineSearch = searchLines[searchLines.length - 1].trim()
|
||||||
|
|
||||||
|
// Find blocks where first line matches the search first line
|
||||||
|
for (let i = 0; i < originalLines.length; i++) {
|
||||||
|
if (originalLines[i].trim() !== firstLineSearch) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for the matching last line after this first line
|
||||||
|
for (let j = i + 2; j < originalLines.length; j++) {
|
||||||
|
if (originalLines[j].trim() === lastLineSearch) {
|
||||||
|
// Found a potential block from i to j
|
||||||
|
let matchStartIndex = 0
|
||||||
|
for (let k = 0; k < i; k++) {
|
||||||
|
matchStartIndex += originalLines[k].length + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
let matchEndIndex = matchStartIndex
|
||||||
|
for (let k = 0; k <= j - i; k++) {
|
||||||
|
matchEndIndex += originalLines[i + k].length
|
||||||
|
if (k < j - i) {
|
||||||
|
matchEndIndex += 1 // Add newline character except for the last line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
yield content.substring(matchStartIndex, matchEndIndex)
|
||||||
|
break // Only match the first occurrence of the last line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WhitespaceNormalizedReplacer: Replacer = function* (
|
||||||
|
content,
|
||||||
|
find,
|
||||||
|
) {
|
||||||
|
const normalizeWhitespace = (text: string) => text.replace(/\s+/g, " ").trim()
|
||||||
|
const normalizedFind = normalizeWhitespace(find)
|
||||||
|
|
||||||
|
// Handle single line matches
|
||||||
|
const lines = content.split("\n")
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i]
|
||||||
|
if (normalizeWhitespace(line) === normalizedFind) {
|
||||||
|
yield line
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check for substring matches within lines
|
||||||
|
const normalizedLine = normalizeWhitespace(line)
|
||||||
|
if (normalizedLine.includes(normalizedFind)) {
|
||||||
|
// Find the actual substring in the original line that matches
|
||||||
|
const words = find.trim().split(/\s+/)
|
||||||
|
if (words.length > 0) {
|
||||||
|
const pattern = words
|
||||||
|
.map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
||||||
|
.join("\\s+")
|
||||||
|
try {
|
||||||
|
const regex = new RegExp(pattern)
|
||||||
|
const match = line.match(regex)
|
||||||
|
if (match) {
|
||||||
|
yield match[0]
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Invalid regex pattern, skip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle multi-line matches
|
||||||
|
const findLines = find.split("\n")
|
||||||
|
if (findLines.length > 1) {
|
||||||
|
for (let i = 0; i <= lines.length - findLines.length; i++) {
|
||||||
|
const block = lines.slice(i, i + findLines.length)
|
||||||
|
if (normalizeWhitespace(block.join("\n")) === normalizedFind) {
|
||||||
|
yield block.join("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IndentationFlexibleReplacer: Replacer = function* (content, find) {
|
||||||
|
const removeIndentation = (text: string) => {
|
||||||
|
const lines = text.split("\n")
|
||||||
|
const nonEmptyLines = lines.filter((line) => line.trim().length > 0)
|
||||||
|
if (nonEmptyLines.length === 0) return text
|
||||||
|
|
||||||
|
const minIndent = Math.min(
|
||||||
|
...nonEmptyLines.map((line) => {
|
||||||
|
const match = line.match(/^(\s*)/)
|
||||||
|
return match ? match[1].length : 0
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
return lines
|
||||||
|
.map((line) => (line.trim().length === 0 ? line : line.slice(minIndent)))
|
||||||
|
.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedFind = removeIndentation(find)
|
||||||
|
const contentLines = content.split("\n")
|
||||||
|
const findLines = find.split("\n")
|
||||||
|
|
||||||
|
for (let i = 0; i <= contentLines.length - findLines.length; i++) {
|
||||||
|
const block = contentLines.slice(i, i + findLines.length).join("\n")
|
||||||
|
if (removeIndentation(block) === normalizedFind) {
|
||||||
|
yield block
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EscapeNormalizedReplacer: Replacer = function* (content, find) {
|
||||||
|
const unescapeString = (str: string): string => {
|
||||||
|
return str.replace(/\\(n|t|r|'|"|`|\\|\n|\$)/g, (match, capturedChar) => {
|
||||||
|
switch (capturedChar) {
|
||||||
|
case "n":
|
||||||
|
return "\n"
|
||||||
|
case "t":
|
||||||
|
return "\t"
|
||||||
|
case "r":
|
||||||
|
return "\r"
|
||||||
|
case "'":
|
||||||
|
return "'"
|
||||||
|
case '"':
|
||||||
|
return '"'
|
||||||
|
case "`":
|
||||||
|
return "`"
|
||||||
|
case "\\":
|
||||||
|
return "\\"
|
||||||
|
case "\n":
|
||||||
|
return "\n"
|
||||||
|
case "$":
|
||||||
|
return "$"
|
||||||
|
default:
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const unescapedFind = unescapeString(find)
|
||||||
|
|
||||||
|
// Try direct match with unescaped find string
|
||||||
|
if (content.includes(unescapedFind)) {
|
||||||
|
yield unescapedFind
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also try finding escaped versions in content that match unescaped find
|
||||||
|
const lines = content.split("\n")
|
||||||
|
const findLines = unescapedFind.split("\n")
|
||||||
|
|
||||||
|
for (let i = 0; i <= lines.length - findLines.length; i++) {
|
||||||
|
const block = lines.slice(i, i + findLines.length).join("\n")
|
||||||
|
const unescapedBlock = unescapeString(block)
|
||||||
|
|
||||||
|
if (unescapedBlock === unescapedFind) {
|
||||||
|
yield block
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MultiOccurrenceReplacer: Replacer = function* (content, find) {
|
||||||
|
// This replacer yields all exact matches, allowing the replace function
|
||||||
|
// to handle multiple occurrences based on replaceAll parameter
|
||||||
|
let startIndex = 0
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const index = content.indexOf(find, startIndex)
|
||||||
|
if (index === -1) break
|
||||||
|
|
||||||
|
yield find
|
||||||
|
startIndex = index + find.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TrimmedBoundaryReplacer: Replacer = function* (content, find) {
|
||||||
|
const trimmedFind = find.trim()
|
||||||
|
|
||||||
|
if (trimmedFind === find) {
|
||||||
|
// Already trimmed, no point in trying
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find the trimmed version
|
||||||
|
if (content.includes(trimmedFind)) {
|
||||||
|
yield trimmedFind
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also try finding blocks where trimmed content matches
|
||||||
|
const lines = content.split("\n")
|
||||||
|
const findLines = find.split("\n")
|
||||||
|
|
||||||
|
for (let i = 0; i <= lines.length - findLines.length; i++) {
|
||||||
|
const block = lines.slice(i, i + findLines.length).join("\n")
|
||||||
|
|
||||||
|
if (block.trim() === trimmedFind) {
|
||||||
|
yield block
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ContextAwareReplacer: Replacer = function* (content, find) {
|
||||||
|
const findLines = find.split("\n")
|
||||||
|
if (findLines.length < 3) {
|
||||||
|
// Need at least 3 lines to have meaningful context
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove trailing empty line if present
|
||||||
|
if (findLines[findLines.length - 1] === "") {
|
||||||
|
findLines.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentLines = content.split("\n")
|
||||||
|
|
||||||
|
// Extract first and last lines as context anchors
|
||||||
|
const firstLine = findLines[0].trim()
|
||||||
|
const lastLine = findLines[findLines.length - 1].trim()
|
||||||
|
|
||||||
|
// Find blocks that start and end with the context anchors
|
||||||
|
for (let i = 0; i < contentLines.length; i++) {
|
||||||
|
if (contentLines[i].trim() !== firstLine) continue
|
||||||
|
|
||||||
|
// Look for the matching last line
|
||||||
|
for (let j = i + 2; j < contentLines.length; j++) {
|
||||||
|
if (contentLines[j].trim() === lastLine) {
|
||||||
|
// Found a potential context block
|
||||||
|
const blockLines = contentLines.slice(i, j + 1)
|
||||||
|
const block = blockLines.join("\n")
|
||||||
|
|
||||||
|
// Check if the middle content has reasonable similarity
|
||||||
|
// (simple heuristic: at least 50% of non-empty lines should match when trimmed)
|
||||||
|
if (blockLines.length === findLines.length) {
|
||||||
|
let matchingLines = 0
|
||||||
|
let totalNonEmptyLines = 0
|
||||||
|
|
||||||
|
for (let k = 1; k < blockLines.length - 1; k++) {
|
||||||
|
const blockLine = blockLines[k].trim()
|
||||||
|
const findLine = findLines[k].trim()
|
||||||
|
|
||||||
|
if (blockLine.length > 0 || findLine.length > 0) {
|
||||||
|
totalNonEmptyLines++
|
||||||
|
if (blockLine === findLine) {
|
||||||
|
matchingLines++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
totalNonEmptyLines === 0 ||
|
||||||
|
matchingLines / totalNonEmptyLines >= 0.5
|
||||||
|
) {
|
||||||
|
yield block
|
||||||
|
break // Only match the first occurrence
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function trimDiff(diff: string): string {
|
function trimDiff(diff: string): string {
|
||||||
const lines = diff.split("\n")
|
const lines = diff.split("\n")
|
||||||
const contentLines = lines.filter(
|
const contentLines = lines.filter(
|
||||||
@@ -151,3 +463,42 @@ function trimDiff(diff: string): string {
|
|||||||
|
|
||||||
return trimmedLines.join("\n")
|
return trimmedLines.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function replace(
|
||||||
|
content: string,
|
||||||
|
oldString: string,
|
||||||
|
newString: string,
|
||||||
|
replaceAll = false,
|
||||||
|
): string {
|
||||||
|
if (oldString === newString) {
|
||||||
|
throw new Error("oldString and newString must be different")
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const replacer of [
|
||||||
|
SimpleReplacer,
|
||||||
|
LineTrimmedReplacer,
|
||||||
|
BlockAnchorReplacer,
|
||||||
|
WhitespaceNormalizedReplacer,
|
||||||
|
IndentationFlexibleReplacer,
|
||||||
|
EscapeNormalizedReplacer,
|
||||||
|
TrimmedBoundaryReplacer,
|
||||||
|
ContextAwareReplacer,
|
||||||
|
MultiOccurrenceReplacer,
|
||||||
|
]) {
|
||||||
|
for (const search of replacer(content, oldString)) {
|
||||||
|
const index = content.indexOf(search)
|
||||||
|
if (index === -1) continue
|
||||||
|
if (replaceAll) {
|
||||||
|
return content.replaceAll(search, newString)
|
||||||
|
}
|
||||||
|
const lastIndex = content.lastIndexOf(search)
|
||||||
|
if (index !== lastIndex) continue
|
||||||
|
return (
|
||||||
|
content.substring(0, index) +
|
||||||
|
newString +
|
||||||
|
content.substring(index + search.length)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error("oldString not found in content or was found multiple times")
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import path from "path"
|
|||||||
import { Tool } from "./tool"
|
import { Tool } from "./tool"
|
||||||
import { App } from "../app/app"
|
import { App } from "../app/app"
|
||||||
import DESCRIPTION from "./glob.txt"
|
import DESCRIPTION from "./glob.txt"
|
||||||
|
import { Ripgrep } from "../external/ripgrep"
|
||||||
|
|
||||||
export const GlobTool = Tool.define({
|
export const GlobTool = Tool.define({
|
||||||
id: "glob",
|
id: "glob",
|
||||||
@@ -24,10 +25,12 @@ export const GlobTool = Tool.define({
|
|||||||
: path.resolve(app.path.cwd, search)
|
: path.resolve(app.path.cwd, search)
|
||||||
|
|
||||||
const limit = 100
|
const limit = 100
|
||||||
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, dot: true })) {
|
for (const file of await Ripgrep.files({
|
||||||
|
cwd: search,
|
||||||
|
glob: params.pattern,
|
||||||
|
})) {
|
||||||
if (files.length >= limit) {
|
if (files.length >= limit) {
|
||||||
truncated = true
|
truncated = true
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export namespace Log {
|
|||||||
await fs.mkdir(dir, { recursive: true })
|
await fs.mkdir(dir, { recursive: true })
|
||||||
cleanup(dir)
|
cleanup(dir)
|
||||||
if (options.print) return
|
if (options.print) return
|
||||||
logpath = path.join(dir, new Date().toISOString().split(".")[0] + ".log")
|
logpath = path.join(dir, new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log")
|
||||||
const logfile = Bun.file(logpath)
|
const logfile = Bun.file(logpath)
|
||||||
await fs.truncate(logpath).catch(() => {})
|
await fs.truncate(logpath).catch(() => {})
|
||||||
const writer = logfile.writer()
|
const writer = logfile.writer()
|
||||||
|
|||||||
@@ -0,0 +1,413 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { replace } from "../../src/tool/edit"
|
||||||
|
|
||||||
|
interface TestCase {
|
||||||
|
content: string
|
||||||
|
find: string
|
||||||
|
replace: string
|
||||||
|
all?: boolean
|
||||||
|
fail?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const testCases: TestCase[] = [
|
||||||
|
// SimpleReplacer cases
|
||||||
|
{
|
||||||
|
content: ["function hello() {", ' console.log("world");', "}"].join("\n"),
|
||||||
|
find: 'console.log("world");',
|
||||||
|
replace: 'console.log("universe");',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: [
|
||||||
|
"if (condition) {",
|
||||||
|
" doSomething();",
|
||||||
|
" doSomethingElse();",
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
|
find: [" doSomething();", " doSomethingElse();"].join("\n"),
|
||||||
|
replace: [" doNewThing();", " doAnotherThing();"].join("\n"),
|
||||||
|
},
|
||||||
|
|
||||||
|
// LineTrimmedReplacer cases
|
||||||
|
{
|
||||||
|
content: ["function test() {", ' console.log("hello");', "}"].join("\n"),
|
||||||
|
find: 'console.log("hello");',
|
||||||
|
replace: 'console.log("goodbye");',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: ["const x = 5; ", "const y = 10;"].join("\n"),
|
||||||
|
find: "const x = 5;",
|
||||||
|
replace: "const x = 15;",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: [" if (true) {", " return false;", " }"].join("\n"),
|
||||||
|
find: ["if (true) {", "return false;", "}"].join("\n"),
|
||||||
|
replace: ["if (false) {", "return true;", "}"].join("\n"),
|
||||||
|
},
|
||||||
|
|
||||||
|
// BlockAnchorReplacer cases
|
||||||
|
{
|
||||||
|
content: [
|
||||||
|
"function calculate(a, b) {",
|
||||||
|
" const temp = a + b;",
|
||||||
|
" const result = temp * 2;",
|
||||||
|
" return result;",
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
|
find: [
|
||||||
|
"function calculate(a, b) {",
|
||||||
|
" // different middle content",
|
||||||
|
" return result;",
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
|
replace: ["function calculate(a, b) {", " return a * b * 2;", "}"].join(
|
||||||
|
"\n",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: [
|
||||||
|
"class MyClass {",
|
||||||
|
" constructor() {",
|
||||||
|
" this.value = 0;",
|
||||||
|
" }",
|
||||||
|
" ",
|
||||||
|
" getValue() {",
|
||||||
|
" return this.value;",
|
||||||
|
" }",
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
|
find: ["class MyClass {", " // different implementation", "}"].join("\n"),
|
||||||
|
replace: [
|
||||||
|
"class MyClass {",
|
||||||
|
" constructor() {",
|
||||||
|
" this.value = 42;",
|
||||||
|
" }",
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
|
},
|
||||||
|
|
||||||
|
// WhitespaceNormalizedReplacer cases
|
||||||
|
{
|
||||||
|
content: ["function test() {", '\tconsole.log("hello");', "}"].join("\n"),
|
||||||
|
find: ' console.log("hello");',
|
||||||
|
replace: ' console.log("world");',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "const x = 5;",
|
||||||
|
find: "const x = 5;",
|
||||||
|
replace: "const x = 10;",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "if\t( condition\t) {",
|
||||||
|
find: "if ( condition ) {",
|
||||||
|
replace: "if (newCondition) {",
|
||||||
|
},
|
||||||
|
|
||||||
|
// IndentationFlexibleReplacer cases
|
||||||
|
{
|
||||||
|
content: [
|
||||||
|
" function nested() {",
|
||||||
|
' console.log("deeply nested");',
|
||||||
|
" return true;",
|
||||||
|
" }",
|
||||||
|
].join("\n"),
|
||||||
|
find: [
|
||||||
|
"function nested() {",
|
||||||
|
' console.log("deeply nested");',
|
||||||
|
" return true;",
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
|
replace: [
|
||||||
|
"function nested() {",
|
||||||
|
' console.log("updated");',
|
||||||
|
" return false;",
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: [
|
||||||
|
" if (true) {",
|
||||||
|
' console.log("level 1");',
|
||||||
|
' console.log("level 2");',
|
||||||
|
" }",
|
||||||
|
].join("\n"),
|
||||||
|
find: [
|
||||||
|
"if (true) {",
|
||||||
|
'console.log("level 1");',
|
||||||
|
' console.log("level 2");',
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
|
replace: ["if (true) {", 'console.log("updated");', "}"].join("\n"),
|
||||||
|
},
|
||||||
|
|
||||||
|
// replaceAll option cases
|
||||||
|
{
|
||||||
|
content: [
|
||||||
|
'console.log("test");',
|
||||||
|
'console.log("test");',
|
||||||
|
'console.log("test");',
|
||||||
|
].join("\n"),
|
||||||
|
find: 'console.log("test");',
|
||||||
|
replace: 'console.log("updated");',
|
||||||
|
all: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: ['console.log("test");', 'console.log("test");'].join("\n"),
|
||||||
|
find: 'console.log("test");',
|
||||||
|
replace: 'console.log("updated");',
|
||||||
|
all: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Error cases
|
||||||
|
{
|
||||||
|
content: 'console.log("hello");',
|
||||||
|
find: "nonexistent string",
|
||||||
|
replace: "updated",
|
||||||
|
fail: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: ["test", "test", "different content", "test"].join("\n"),
|
||||||
|
find: "test",
|
||||||
|
replace: "updated",
|
||||||
|
all: false,
|
||||||
|
fail: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Edge cases
|
||||||
|
{
|
||||||
|
content: "",
|
||||||
|
find: "",
|
||||||
|
replace: "new content",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "const regex = /[.*+?^${}()|[\\\\]\\\\\\\\]/g;",
|
||||||
|
find: "/[.*+?^${}()|[\\\\]\\\\\\\\]/g",
|
||||||
|
replace: "/\\\\w+/g",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: 'const message = "Hello 世界! 🌍";',
|
||||||
|
find: "Hello 世界! 🌍",
|
||||||
|
replace: "Hello World! 🌎",
|
||||||
|
},
|
||||||
|
|
||||||
|
// EscapeNormalizedReplacer cases
|
||||||
|
{
|
||||||
|
content: 'console.log("Hello\nWorld");',
|
||||||
|
find: 'console.log("Hello\\nWorld");',
|
||||||
|
replace: 'console.log("Hello\nUniverse");',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "const str = 'It's working';",
|
||||||
|
find: "const str = 'It\\'s working';",
|
||||||
|
replace: "const str = 'It's fixed';",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "const template = `Hello ${name}`;",
|
||||||
|
find: "const template = `Hello \\${name}`;",
|
||||||
|
replace: "const template = `Hi ${name}`;",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "const path = 'C:\\Users\\test';",
|
||||||
|
find: "const path = 'C:\\\\Users\\\\test';",
|
||||||
|
replace: "const path = 'C:\\Users\\admin';",
|
||||||
|
},
|
||||||
|
|
||||||
|
// MultiOccurrenceReplacer cases (with replaceAll)
|
||||||
|
{
|
||||||
|
content: ["debug('start');", "debug('middle');", "debug('end');"].join(
|
||||||
|
"\n",
|
||||||
|
),
|
||||||
|
find: "debug",
|
||||||
|
replace: "log",
|
||||||
|
all: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "const x = 1; const y = 1; const z = 1;",
|
||||||
|
find: "1",
|
||||||
|
replace: "2",
|
||||||
|
all: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// TrimmedBoundaryReplacer cases
|
||||||
|
{
|
||||||
|
content: [" function test() {", " return true;", " }"].join("\n"),
|
||||||
|
find: ["function test() {", " return true;", "}"].join("\n"),
|
||||||
|
replace: ["function test() {", " return false;", "}"].join("\n"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "\n const value = 42; \n",
|
||||||
|
find: "const value = 42;",
|
||||||
|
replace: "const value = 24;",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: ["", " if (condition) {", " doSomething();", " }", ""].join(
|
||||||
|
"\n",
|
||||||
|
),
|
||||||
|
find: ["if (condition) {", " doSomething();", "}"].join("\n"),
|
||||||
|
replace: ["if (condition) {", " doNothing();", "}"].join("\n"),
|
||||||
|
},
|
||||||
|
|
||||||
|
// ContextAwareReplacer cases
|
||||||
|
{
|
||||||
|
content: [
|
||||||
|
"function calculate(a, b) {",
|
||||||
|
" const temp = a + b;",
|
||||||
|
" const result = temp * 2;",
|
||||||
|
" return result;",
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
|
find: [
|
||||||
|
"function calculate(a, b) {",
|
||||||
|
" // some different content here",
|
||||||
|
" // more different content",
|
||||||
|
" return result;",
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
|
replace: ["function calculate(a, b) {", " return (a + b) * 2;", "}"].join(
|
||||||
|
"\n",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: [
|
||||||
|
"class TestClass {",
|
||||||
|
" constructor() {",
|
||||||
|
" this.value = 0;",
|
||||||
|
" }",
|
||||||
|
" ",
|
||||||
|
" method() {",
|
||||||
|
" return this.value;",
|
||||||
|
" }",
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
|
find: [
|
||||||
|
"class TestClass {",
|
||||||
|
" // different implementation",
|
||||||
|
" // with multiple lines",
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
|
replace: ["class TestClass {", " getValue() { return 42; }", "}"].join(
|
||||||
|
"\n",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
|
||||||
|
// Combined edge cases for new replacers
|
||||||
|
{
|
||||||
|
content: '\tconsole.log("test");\t',
|
||||||
|
find: 'console.log("test");',
|
||||||
|
replace: 'console.log("updated");',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: [" ", "function test() {", " return 'value';", "}", " "].join(
|
||||||
|
"\n",
|
||||||
|
),
|
||||||
|
find: ["function test() {", "return 'value';", "}"].join("\n"),
|
||||||
|
replace: ["function test() {", "return 'new value';", "}"].join("\n"),
|
||||||
|
},
|
||||||
|
|
||||||
|
// Test for same oldString and newString (should fail)
|
||||||
|
{
|
||||||
|
content: 'console.log("test");',
|
||||||
|
find: 'console.log("test");',
|
||||||
|
replace: 'console.log("test");',
|
||||||
|
fail: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Additional tests for fixes made
|
||||||
|
|
||||||
|
// WhitespaceNormalizedReplacer - test regex special characters that could cause errors
|
||||||
|
{
|
||||||
|
content: 'const pattern = "test[123]";',
|
||||||
|
find: 'test[123]',
|
||||||
|
replace: 'test[456]',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: 'const regex = "^start.*end$";',
|
||||||
|
find: '^start.*end$',
|
||||||
|
replace: '^begin.*finish$',
|
||||||
|
},
|
||||||
|
|
||||||
|
// EscapeNormalizedReplacer - test single backslash vs double backslash
|
||||||
|
{
|
||||||
|
content: 'const path = "C:\\Users";',
|
||||||
|
find: 'const path = "C:\\Users";',
|
||||||
|
replace: 'const path = "D:\\Users";',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: 'console.log("Line1\\nLine2");',
|
||||||
|
find: 'console.log("Line1\\nLine2");',
|
||||||
|
replace: 'console.log("First\\nSecond");',
|
||||||
|
},
|
||||||
|
|
||||||
|
// BlockAnchorReplacer - test edge case with exact newline boundaries
|
||||||
|
{
|
||||||
|
content: ["function test() {", " return true;", "}"].join("\n"),
|
||||||
|
find: ["function test() {", " // middle", "}"].join("\n"),
|
||||||
|
replace: ["function test() {", " return false;", "}"].join("\n"),
|
||||||
|
},
|
||||||
|
|
||||||
|
// ContextAwareReplacer - test with trailing newline in find string
|
||||||
|
{
|
||||||
|
content: [
|
||||||
|
"class Test {",
|
||||||
|
" method1() {",
|
||||||
|
" return 1;",
|
||||||
|
" }",
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
|
find: [
|
||||||
|
"class Test {",
|
||||||
|
" // different content",
|
||||||
|
"}",
|
||||||
|
"", // trailing empty line
|
||||||
|
].join("\n"),
|
||||||
|
replace: ["class Test {", " method2() { return 2; }", "}"].join("\n"),
|
||||||
|
},
|
||||||
|
|
||||||
|
// Test validation for empty strings with same oldString and newString
|
||||||
|
{
|
||||||
|
content: "",
|
||||||
|
find: "",
|
||||||
|
replace: "",
|
||||||
|
fail: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Test multiple occurrences with replaceAll=false (should fail)
|
||||||
|
{
|
||||||
|
content: ["const a = 1;", "const b = 1;", "const c = 1;"].join("\n"),
|
||||||
|
find: "= 1",
|
||||||
|
replace: "= 2",
|
||||||
|
all: false,
|
||||||
|
fail: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Test whitespace normalization with multiple spaces and tabs mixed
|
||||||
|
{
|
||||||
|
content: "if\t \t( \tcondition\t )\t{",
|
||||||
|
find: "if ( condition ) {",
|
||||||
|
replace: "if (newCondition) {",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Test escape sequences in template literals
|
||||||
|
{
|
||||||
|
content: "const msg = `Hello\\tWorld`;",
|
||||||
|
find: "const msg = `Hello\\tWorld`;",
|
||||||
|
replace: "const msg = `Hi\\tWorld`;",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
describe("EditTool Replacers", () => {
|
||||||
|
test.each(testCases)("case %#", (testCase) => {
|
||||||
|
if (testCase.fail) {
|
||||||
|
expect(() => {
|
||||||
|
replace(testCase.content, testCase.find, testCase.replace, testCase.all)
|
||||||
|
}).toThrow()
|
||||||
|
} else {
|
||||||
|
const result = replace(
|
||||||
|
testCase.content,
|
||||||
|
testCase.find,
|
||||||
|
testCase.replace,
|
||||||
|
testCase.all,
|
||||||
|
)
|
||||||
|
expect(result).toContain(testCase.replace)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -14,7 +14,7 @@ describe("tool.glob", () => {
|
|||||||
await App.provide({ cwd: process.cwd() }, async () => {
|
await App.provide({ cwd: process.cwd() }, async () => {
|
||||||
let result = await GlobTool.execute(
|
let result = await GlobTool.execute(
|
||||||
{
|
{
|
||||||
pattern: "./node_modules/**/*",
|
pattern: "../../node_modules/**/*",
|
||||||
path: undefined,
|
path: undefined,
|
||||||
},
|
},
|
||||||
ctx,
|
ctx,
|
||||||
@@ -33,7 +33,7 @@ describe("tool.glob", () => {
|
|||||||
)
|
)
|
||||||
expect(result.metadata).toMatchObject({
|
expect(result.metadata).toMatchObject({
|
||||||
truncated: false,
|
truncated: false,
|
||||||
count: 2,
|
count: 3,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
opencode-test
|
||||||
@@ -26,7 +26,11 @@ func main() {
|
|||||||
|
|
||||||
appInfoStr := os.Getenv("OPENCODE_APP_INFO")
|
appInfoStr := os.Getenv("OPENCODE_APP_INFO")
|
||||||
var appInfo client.AppInfo
|
var appInfo client.AppInfo
|
||||||
json.Unmarshal([]byte(appInfoStr), &appInfo)
|
err := json.Unmarshal([]byte(appInfoStr), &appInfo)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to unmarshal app info", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
logfile := filepath.Join(appInfo.Path.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) {
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"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/theme"
|
"github.com/sst/opencode/internal/theme"
|
||||||
"github.com/sst/opencode/internal/util"
|
"github.com/sst/opencode/internal/util"
|
||||||
@@ -44,9 +46,12 @@ type SendMsg struct {
|
|||||||
Text string
|
Text string
|
||||||
Attachments []Attachment
|
Attachments []Attachment
|
||||||
}
|
}
|
||||||
type CompletionDialogTriggerdMsg struct {
|
type CompletionDialogTriggeredMsg struct {
|
||||||
InitialValue string
|
InitialValue string
|
||||||
}
|
}
|
||||||
|
type OptimisticMessageAddedMsg struct {
|
||||||
|
Message client.MessageInfo
|
||||||
|
}
|
||||||
|
|
||||||
func New(
|
func New(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
@@ -126,6 +131,10 @@ func (a *App) InitializeProvider() tea.Cmd {
|
|||||||
// TODO: notify user
|
// TODO: notify user
|
||||||
return nil
|
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{}
|
providers := []client.ProviderInfo{}
|
||||||
var defaultProvider *client.ProviderInfo
|
var defaultProvider *client.ProviderInfo
|
||||||
var defaultModel *client.ModelInfo
|
var defaultModel *client.ModelInfo
|
||||||
@@ -248,17 +257,19 @@ func (a *App) InitializeProject(ctx context.Context) tea.Cmd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) CompactSession(ctx context.Context) tea.Cmd {
|
func (a *App) CompactSession(ctx context.Context) tea.Cmd {
|
||||||
response, err := a.Client.PostSessionSummarizeWithResponse(ctx, client.PostSessionSummarizeJSONRequestBody{
|
go func() {
|
||||||
SessionID: a.Session.Id,
|
response, err := a.Client.PostSessionSummarizeWithResponse(ctx, client.PostSessionSummarizeJSONRequestBody{
|
||||||
ProviderID: a.Provider.Id,
|
SessionID: a.Session.Id,
|
||||||
ModelID: a.Model.Id,
|
ProviderID: a.Provider.Id,
|
||||||
})
|
ModelID: a.Model.Id,
|
||||||
if err != nil {
|
})
|
||||||
slog.Error("Failed to compact session", "error", err)
|
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)
|
if response != nil && response.StatusCode() != 200 {
|
||||||
}
|
slog.Error("Failed to compact session", "error", response.StatusCode)
|
||||||
|
}
|
||||||
|
}()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,19 +302,12 @@ 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(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{}
|
||||||
part.FromMessagePartText(client.MessagePartText{
|
part.FromMessagePartText(client.MessagePartText{
|
||||||
Type: "text",
|
Type: "text",
|
||||||
@@ -311,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,
|
||||||
@@ -319,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
|
||||||
@@ -370,6 +396,19 @@ func (a *App) ListSessions(ctx context.Context) ([]client.SessionInfo, error) {
|
|||||||
return sessions, nil
|
return sessions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) DeleteSession(ctx context.Context, sessionID string) error {
|
||||||
|
resp, err := a.Client.PostSessionDeleteWithResponse(ctx, client.PostSessionDeleteJSONRequestBody{
|
||||||
|
SessionID: sessionID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if resp.StatusCode() != 200 {
|
||||||
|
return fmt.Errorf("failed to delete session: %d", resp.StatusCode())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) ListMessages(ctx context.Context, sessionId string) ([]client.MessageInfo, error) {
|
func (a *App) ListMessages(ctx context.Context, sessionId string) ([]client.MessageInfo, error) {
|
||||||
resp, err := a.Client.PostSessionMessagesWithResponse(ctx, client.PostSessionMessagesJSONRequestBody{SessionID: sessionId})
|
resp, err := a.Client.PostSessionMessagesWithResponse(ctx, client.PostSessionMessagesJSONRequestBody{SessionID: sessionId})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -32,17 +32,19 @@ type EditorComponent interface {
|
|||||||
Newline() (tea.Model, tea.Cmd)
|
Newline() (tea.Model, tea.Cmd)
|
||||||
Previous() (tea.Model, tea.Cmd)
|
Previous() (tea.Model, tea.Cmd)
|
||||||
Next() (tea.Model, tea.Cmd)
|
Next() (tea.Model, tea.Cmd)
|
||||||
|
SetInterruptKeyInDebounce(inDebounce bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
type editorComponent struct {
|
type editorComponent struct {
|
||||||
app *app.App
|
app *app.App
|
||||||
width, height int
|
width, height int
|
||||||
textarea textarea.Model
|
textarea textarea.Model
|
||||||
attachments []app.Attachment
|
attachments []app.Attachment
|
||||||
history []string
|
history []string
|
||||||
historyIndex int
|
historyIndex int
|
||||||
currentMessage string
|
currentMessage string
|
||||||
spinner spinner.Model
|
spinner spinner.Model
|
||||||
|
interruptKeyInDebounce bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *editorComponent) Init() tea.Cmd {
|
func (m *editorComponent) Init() tea.Cmd {
|
||||||
@@ -78,8 +80,15 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, tea.Batch(cmds...)
|
return m, tea.Batch(cmds...)
|
||||||
} else {
|
} else {
|
||||||
existingValue := m.textarea.Value()
|
existingValue := m.textarea.Value()
|
||||||
modifiedValue := strings.Replace(existingValue, msg.SearchString, msg.CompletionValue, 1)
|
|
||||||
m.textarea.SetValue(modifiedValue + " ")
|
// Replace the current token (after last space)
|
||||||
|
lastSpaceIndex := strings.LastIndex(existingValue, " ")
|
||||||
|
if lastSpaceIndex == -1 {
|
||||||
|
m.textarea.SetValue(msg.CompletionValue + " ")
|
||||||
|
} else {
|
||||||
|
modifiedValue := existingValue[:lastSpaceIndex+1] + msg.CompletionValue
|
||||||
|
m.textarea.SetValue(modifiedValue + " ")
|
||||||
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -115,9 +124,14 @@ func (m *editorComponent) Content() string {
|
|||||||
Background(t.BackgroundElement()).
|
Background(t.BackgroundElement()).
|
||||||
Render(textarea)
|
Render(textarea)
|
||||||
|
|
||||||
hint := base("enter") + muted(" send ")
|
hint := base(m.getSubmitKeyText()) + muted(" send ")
|
||||||
if m.app.IsBusy() {
|
if m.app.IsBusy() {
|
||||||
hint = muted("working") + m.spinner.View() + muted(" ") + base("esc") + muted(" interrupt")
|
keyText := m.getInterruptKeyText()
|
||||||
|
if m.interruptKeyInDebounce {
|
||||||
|
hint = muted("working") + m.spinner.View() + muted(" ") + base(keyText+" again") + muted(" interrupt")
|
||||||
|
} else {
|
||||||
|
hint = muted("working") + m.spinner.View() + muted(" ") + base(keyText) + muted(" interrupt")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
model := ""
|
model := ""
|
||||||
@@ -263,6 +277,18 @@ func (m *editorComponent) Next() (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *editorComponent) SetInterruptKeyInDebounce(inDebounce bool) {
|
||||||
|
m.interruptKeyInDebounce = inDebounce
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *editorComponent) getInterruptKeyText() string {
|
||||||
|
return m.app.Commands[commands.SessionInterruptCommand].Keys()[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *editorComponent) getSubmitKeyText() string {
|
||||||
|
return m.app.Commands[commands.InputSubmitCommand].Keys()[0]
|
||||||
|
}
|
||||||
|
|
||||||
func createTextArea(existing *textarea.Model) textarea.Model {
|
func createTextArea(existing *textarea.Model) textarea.Model {
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
bgColor := t.BackgroundElement()
|
bgColor := t.BackgroundElement()
|
||||||
@@ -311,11 +337,12 @@ func NewEditorComponent(app *app.App) EditorComponent {
|
|||||||
ta := createTextArea(nil)
|
ta := createTextArea(nil)
|
||||||
|
|
||||||
return &editorComponent{
|
return &editorComponent{
|
||||||
app: app,
|
app: app,
|
||||||
textarea: ta,
|
textarea: ta,
|
||||||
history: []string{},
|
history: []string{},
|
||||||
historyIndex: 0,
|
historyIndex: 0,
|
||||||
currentMessage: "",
|
currentMessage: "",
|
||||||
spinner: s,
|
spinner: s,
|
||||||
|
interruptKeyInDebounce: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
@@ -227,7 +226,11 @@ func renderText(message client.MessageInfo, text string, author string) string {
|
|||||||
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
|
||||||
if message.Role == client.Assistant {
|
if message.Role == client.Assistant {
|
||||||
markdownWidth = width - padding - 4
|
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 := toMarkdown(text, markdownWidth, t.BackgroundPanel())
|
||||||
content = strings.Join([]string{content, info}, "\n")
|
content = strings.Join([]string{content, info}, "\n")
|
||||||
@@ -250,7 +253,7 @@ 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,
|
||||||
showDetails bool,
|
showDetails bool,
|
||||||
isLast bool,
|
isLast bool,
|
||||||
contentOnly bool,
|
contentOnly bool,
|
||||||
@@ -463,7 +466,7 @@ func renderToolInvocation(
|
|||||||
|
|
||||||
if metadata, ok := call["metadata"].(map[string]any); ok {
|
if metadata, ok := call["metadata"].(map[string]any); ok {
|
||||||
data, _ = json.Marshal(metadata)
|
data, _ = json.Marshal(metadata)
|
||||||
var toolMetadata client.MessageInfo_Metadata_Tool_AdditionalProperties
|
var toolMetadata client.MessageMetadata_Tool_AdditionalProperties
|
||||||
_ = json.Unmarshal(data, &toolMetadata)
|
_ = json.Unmarshal(data, &toolMetadata)
|
||||||
|
|
||||||
step := renderToolInvocation(
|
step := renderToolInvocation(
|
||||||
|
|||||||
@@ -58,6 +58,12 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.viewport.GotoBottom()
|
m.viewport.GotoBottom()
|
||||||
m.tail = true
|
m.tail = true
|
||||||
return m, nil
|
return m, nil
|
||||||
|
case app.OptimisticMessageAddedMsg:
|
||||||
|
m.renderView()
|
||||||
|
if m.tail {
|
||||||
|
m.viewport.GotoBottom()
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
case dialog.ThemeSelectedMsg:
|
case dialog.ThemeSelectedMsg:
|
||||||
m.cache.Clear()
|
m.cache.Clear()
|
||||||
return m, m.Reload()
|
return m, m.Reload()
|
||||||
@@ -171,7 +177,7 @@ func (m *messagesComponent) renderView() {
|
|||||||
isLastToolInvocation := slices.Contains(lastToolIndices, i)
|
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]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ type commandsComponent struct {
|
|||||||
app *app.App
|
app *app.App
|
||||||
width, height int
|
width, height int
|
||||||
showKeybinds bool
|
showKeybinds bool
|
||||||
|
showAll bool
|
||||||
background *compat.AdaptiveColor
|
background *compat.AdaptiveColor
|
||||||
limit *int
|
limit *int
|
||||||
}
|
}
|
||||||
@@ -75,17 +76,32 @@ func (c *commandsComponent) View() string {
|
|||||||
keybindStyle = keybindStyle.Background(*c.background)
|
keybindStyle = keybindStyle.Background(*c.background)
|
||||||
}
|
}
|
||||||
|
|
||||||
var commandsWithTriggers []commands.Command
|
var commandsToShow []commands.Command
|
||||||
|
var triggeredCommands []commands.Command
|
||||||
|
var untriggeredCommands []commands.Command
|
||||||
|
|
||||||
for _, cmd := range c.app.Commands.Sorted() {
|
for _, cmd := range c.app.Commands.Sorted() {
|
||||||
if cmd.Trigger != "" {
|
if c.showAll || cmd.Trigger != "" {
|
||||||
commandsWithTriggers = append(commandsWithTriggers, cmd)
|
if cmd.Trigger != "" {
|
||||||
|
triggeredCommands = append(triggeredCommands, cmd)
|
||||||
|
} else if c.showAll {
|
||||||
|
untriggeredCommands = append(untriggeredCommands, cmd)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if c.limit != nil && len(commandsWithTriggers) > *c.limit {
|
|
||||||
commandsWithTriggers = commandsWithTriggers[:*c.limit]
|
// 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(commandsWithTriggers) == 0 {
|
if len(commandsToShow) == 0 {
|
||||||
|
if c.showAll {
|
||||||
|
return styles.Muted().Render("No commands available")
|
||||||
|
}
|
||||||
return styles.Muted().Render("No commands with triggers available")
|
return styles.Muted().Render("No commands with triggers available")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,10 +117,15 @@ func (c *commandsComponent) View() string {
|
|||||||
keybinds string
|
keybinds string
|
||||||
}
|
}
|
||||||
|
|
||||||
rows := make([]commandRow, 0, len(commandsWithTriggers))
|
rows := make([]commandRow, 0, len(commandsToShow))
|
||||||
|
|
||||||
for _, cmd := range commandsWithTriggers {
|
for _, cmd := range commandsToShow {
|
||||||
trigger := "/" + cmd.Trigger
|
trigger := ""
|
||||||
|
if cmd.Trigger != "" {
|
||||||
|
trigger = "/" + cmd.Trigger
|
||||||
|
} else {
|
||||||
|
trigger = string(cmd.Name)
|
||||||
|
}
|
||||||
description := cmd.Description
|
description := cmd.Description
|
||||||
|
|
||||||
// Format keybindings
|
// Format keybindings
|
||||||
@@ -144,6 +165,7 @@ func (c *commandsComponent) View() string {
|
|||||||
// Build the output
|
// Build the output
|
||||||
var output strings.Builder
|
var output strings.Builder
|
||||||
|
|
||||||
|
maxWidth := 0
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
// Pad each column to align properly
|
// Pad each column to align properly
|
||||||
trigger := fmt.Sprintf("%-*s", maxTriggerWidth, row.trigger)
|
trigger := fmt.Sprintf("%-*s", maxTriggerWidth, row.trigger)
|
||||||
@@ -160,10 +182,14 @@ func (c *commandsComponent) View() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
output.WriteString(line + "\n")
|
output.WriteString(line + "\n")
|
||||||
|
maxWidth = max(maxWidth, lipgloss.Width(line))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove trailing newline
|
// Remove trailing newline
|
||||||
result := strings.TrimSuffix(output.String(), "\n")
|
result := strings.TrimSuffix(output.String(), "\n")
|
||||||
|
if c.background != nil {
|
||||||
|
result = lipgloss.NewStyle().Background(c.background).Width(maxWidth).Render(result)
|
||||||
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -188,11 +214,18 @@ func WithLimit(limit int) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithShowAll(showAll bool) Option {
|
||||||
|
return func(c *commandsComponent) {
|
||||||
|
c.showAll = showAll
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func New(app *app.App, opts ...Option) CommandsComponent {
|
func New(app *app.App, opts ...Option) CommandsComponent {
|
||||||
c := &commandsComponent{
|
c := &commandsComponent{
|
||||||
app: app,
|
app: app,
|
||||||
background: nil,
|
background: nil,
|
||||||
showKeybinds: true,
|
showKeybinds: true,
|
||||||
|
showAll: false,
|
||||||
}
|
}
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
opt(c)
|
opt(c)
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case []CompletionItemI:
|
case []CompletionItemI:
|
||||||
c.list.SetItems(msg)
|
c.list.SetItems(msg)
|
||||||
case app.CompletionDialogTriggerdMsg:
|
case app.CompletionDialogTriggeredMsg:
|
||||||
c.pseudoSearchTextArea.SetValue(msg.InitialValue)
|
c.pseudoSearchTextArea.SetValue(msg.InitialValue)
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
if c.pseudoSearchTextArea.Focused() {
|
if c.pseudoSearchTextArea.Focused() {
|
||||||
|
|||||||
@@ -1,71 +1,62 @@
|
|||||||
package dialog
|
package dialog
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"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/sst/opencode/internal/app"
|
||||||
"github.com/sst/opencode/internal/commands"
|
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
|
||||||
commands []commands.Command
|
app *app.App
|
||||||
|
commandsComponent commandsComponent.CommandsComponent
|
||||||
|
viewport viewport.Model
|
||||||
}
|
}
|
||||||
|
|
||||||
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())
|
|
||||||
contentStyle := lipgloss.NewStyle().
|
|
||||||
PaddingLeft(1).Background(t.BackgroundElement())
|
|
||||||
|
|
||||||
lines := []string{}
|
|
||||||
for _, b := range h.commands {
|
|
||||||
// Only interested in slash commands
|
|
||||||
if b.Trigger == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
content := keyStyle.Render("/" + b.Trigger)
|
|
||||||
content += descStyle.Render(" " + b.Description)
|
|
||||||
// for i, key := range b.Keybindings {
|
|
||||||
// if i == 0 {
|
|
||||||
// keyString := " (" + key.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, contentStyle.Render(content))
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Join(lines, "\n")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *helpDialog) Render(background string) string {
|
func (h *helpDialog) Render(background string) string {
|
||||||
@@ -80,9 +71,16 @@ type HelpDialog interface {
|
|||||||
layout.Modal
|
layout.Modal
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHelpDialog(commands []commands.Command) HelpDialog {
|
func NewHelpDialog(app *app.App) HelpDialog {
|
||||||
|
vp := viewport.New(viewport.WithHeight(12))
|
||||||
return &helpDialog{
|
return &helpDialog{
|
||||||
commands: commands,
|
app: app,
|
||||||
|
commandsComponent: commandsComponent.New(app,
|
||||||
|
commandsComponent.WithBackground(theme.CurrentTheme().BackgroundElement()),
|
||||||
|
commandsComponent.WithShowAll(true),
|
||||||
|
commandsComponent.WithKeybinds(true),
|
||||||
|
),
|
||||||
modal: modal.New(modal.WithTitle("Help")),
|
modal: modal.New(modal.WithTitle("Help")),
|
||||||
|
viewport: vp,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ 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/styles"
|
"github.com/sst/opencode/internal/styles"
|
||||||
@@ -33,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
|
||||||
@@ -54,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"),
|
||||||
@@ -81,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,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(
|
||||||
app.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):
|
||||||
@@ -127,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 {
|
||||||
@@ -137,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
|
||||||
}
|
}
|
||||||
@@ -185,105 +143,46 @@ func (m *modelDialog) switchProvider(offset int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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())
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
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 {
|
||||||
indicator = "← " + indicator + "→"
|
indicator = "← → (switch provider) "
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
@@ -297,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(modal.WithTitle(fmt.Sprintf("Select %s Model", availableProviders[0].Name))),
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,17 @@ package dialog
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"slices"
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea/v2"
|
tea "github.com/charmbracelet/bubbletea/v2"
|
||||||
|
"github.com/charmbracelet/lipgloss/v2"
|
||||||
|
"github.com/muesli/reflow/truncate"
|
||||||
"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/list"
|
||||||
"github.com/sst/opencode/internal/components/modal"
|
"github.com/sst/opencode/internal/components/modal"
|
||||||
|
"github.com/sst/opencode/internal/components/toast"
|
||||||
"github.com/sst/opencode/internal/layout"
|
"github.com/sst/opencode/internal/layout"
|
||||||
"github.com/sst/opencode/internal/styles"
|
"github.com/sst/opencode/internal/styles"
|
||||||
"github.com/sst/opencode/internal/theme"
|
"github.com/sst/opencode/internal/theme"
|
||||||
@@ -19,33 +25,65 @@ type SessionDialog interface {
|
|||||||
layout.Modal
|
layout.Modal
|
||||||
}
|
}
|
||||||
|
|
||||||
type sessionItem client.SessionInfo
|
// sessionItem is a custom list item for sessions that can show delete confirmation
|
||||||
|
type sessionItem struct {
|
||||||
|
title string
|
||||||
|
isDeleteConfirming bool
|
||||||
|
}
|
||||||
|
|
||||||
func (s sessionItem) Render(selected bool, width int) string {
|
func (s sessionItem) Render(selected bool, width int) string {
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
baseStyle := styles.BaseStyle().
|
baseStyle := styles.BaseStyle()
|
||||||
Width(width - 4).
|
|
||||||
Background(t.BackgroundElement())
|
|
||||||
|
|
||||||
if selected {
|
var text string
|
||||||
baseStyle = baseStyle.
|
if s.isDeleteConfirming {
|
||||||
Background(t.Primary()).
|
text = "Press again to confirm delete"
|
||||||
Foreground(t.BackgroundElement()).
|
|
||||||
Bold(true)
|
|
||||||
} else {
|
} else {
|
||||||
baseStyle = baseStyle.
|
text = s.title
|
||||||
Foreground(t.Text())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return baseStyle.Padding(0, 1).Render(s.Title)
|
truncatedStr := truncate.StringWithTail(text, uint(width-1), "...")
|
||||||
|
|
||||||
|
var itemStyle lipgloss.Style
|
||||||
|
if selected {
|
||||||
|
if s.isDeleteConfirming {
|
||||||
|
// Red background for delete confirmation
|
||||||
|
itemStyle = baseStyle.
|
||||||
|
Background(t.Error()).
|
||||||
|
Foreground(t.Background()).
|
||||||
|
Width(width).
|
||||||
|
PaddingLeft(1)
|
||||||
|
} else {
|
||||||
|
// Normal selection
|
||||||
|
itemStyle = baseStyle.
|
||||||
|
Background(t.Primary()).
|
||||||
|
Foreground(t.Background()).
|
||||||
|
Width(width).
|
||||||
|
PaddingLeft(1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if s.isDeleteConfirming {
|
||||||
|
// Red text for delete confirmation when not selected
|
||||||
|
itemStyle = baseStyle.
|
||||||
|
Foreground(t.Error()).
|
||||||
|
PaddingLeft(1)
|
||||||
|
} else {
|
||||||
|
itemStyle = baseStyle.
|
||||||
|
PaddingLeft(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return itemStyle.Render(truncatedStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
type sessionDialog struct {
|
type sessionDialog struct {
|
||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
modal *modal.Modal
|
modal *modal.Modal
|
||||||
selectedSessionID string
|
sessions []client.SessionInfo
|
||||||
list list.List[sessionItem]
|
list list.List[sessionItem]
|
||||||
|
app *app.App
|
||||||
|
deleteConfirmation int // -1 means no confirmation, >= 0 means confirming deletion of session at this index
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sessionDialog) Init() tea.Cmd {
|
func (s *sessionDialog) Init() tea.Cmd {
|
||||||
@@ -61,13 +99,45 @@ func (s *sessionDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
case tea.KeyPressMsg:
|
case tea.KeyPressMsg:
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "enter":
|
case "enter":
|
||||||
if item, idx := s.list.GetSelectedItem(); idx >= 0 {
|
if s.deleteConfirmation >= 0 {
|
||||||
s.selectedSessionID = item.Id
|
s.deleteConfirmation = -1
|
||||||
|
s.updateListItems()
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
if _, idx := s.list.GetSelectedItem(); idx >= 0 && idx < len(s.sessions) {
|
||||||
|
selectedSession := s.sessions[idx]
|
||||||
return s, tea.Sequence(
|
return s, tea.Sequence(
|
||||||
util.CmdHandler(modal.CloseModalMsg{}),
|
util.CmdHandler(modal.CloseModalMsg{}),
|
||||||
util.CmdHandler(app.SessionSelectedMsg(&item)),
|
util.CmdHandler(app.SessionSelectedMsg(&selectedSession)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
case "x", "delete", "backspace":
|
||||||
|
if _, idx := s.list.GetSelectedItem(); idx >= 0 && idx < len(s.sessions) {
|
||||||
|
if s.deleteConfirmation == idx {
|
||||||
|
// Second press - actually delete the session
|
||||||
|
sessionToDelete := s.sessions[idx]
|
||||||
|
return s, tea.Sequence(
|
||||||
|
func() tea.Msg {
|
||||||
|
s.sessions = slices.Delete(s.sessions, idx, idx+1)
|
||||||
|
s.deleteConfirmation = -1
|
||||||
|
s.updateListItems()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
s.deleteSession(sessionToDelete.Id),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// First press - enter delete confirmation mode
|
||||||
|
s.deleteConfirmation = idx
|
||||||
|
s.updateListItems()
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "esc":
|
||||||
|
if s.deleteConfirmation >= 0 {
|
||||||
|
s.deleteConfirmation = -1
|
||||||
|
s.updateListItems()
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +148,42 @@ func (s *sessionDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *sessionDialog) Render(background string) string {
|
func (s *sessionDialog) Render(background string) string {
|
||||||
return s.modal.Render(s.list.View(), background)
|
listView := s.list.View()
|
||||||
|
|
||||||
|
t := theme.CurrentTheme()
|
||||||
|
helpStyle := styles.BaseStyle().PaddingLeft(1).PaddingTop(1)
|
||||||
|
helpText := styles.BaseStyle().Foreground(t.Text()).Render("x/del")
|
||||||
|
helpText = helpText + styles.BaseStyle().Background(t.BackgroundElement()).Foreground(t.TextMuted()).Render(" delete session")
|
||||||
|
helpText = helpStyle.Render(helpText)
|
||||||
|
|
||||||
|
content := strings.Join([]string{listView, helpText}, "\n")
|
||||||
|
|
||||||
|
return s.modal.Render(content, background)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sessionDialog) updateListItems() {
|
||||||
|
_, currentIdx := s.list.GetSelectedItem()
|
||||||
|
|
||||||
|
var items []sessionItem
|
||||||
|
for i, sess := range s.sessions {
|
||||||
|
item := sessionItem{
|
||||||
|
title: sess.Title,
|
||||||
|
isDeleteConfirming: s.deleteConfirmation == i,
|
||||||
|
}
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
s.list.SetItems(items)
|
||||||
|
s.list.SetSelectedIndex(currentIdx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sessionDialog) deleteSession(sessionID string) tea.Cmd {
|
||||||
|
return func() tea.Msg {
|
||||||
|
ctx := context.Background()
|
||||||
|
if err := s.app.DeleteSession(ctx, sessionID); err != nil {
|
||||||
|
return toast.NewErrorToast("Failed to delete session: " + err.Error())()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sessionDialog) Close() tea.Cmd {
|
func (s *sessionDialog) Close() tea.Cmd {
|
||||||
@@ -89,23 +194,36 @@ func (s *sessionDialog) Close() tea.Cmd {
|
|||||||
func NewSessionDialog(app *app.App) SessionDialog {
|
func NewSessionDialog(app *app.App) SessionDialog {
|
||||||
sessions, _ := app.ListSessions(context.Background())
|
sessions, _ := app.ListSessions(context.Background())
|
||||||
|
|
||||||
var sessionItems []sessionItem
|
var filteredSessions []client.SessionInfo
|
||||||
|
var items []sessionItem
|
||||||
for _, sess := range sessions {
|
for _, sess := range sessions {
|
||||||
if sess.ParentID != nil {
|
if sess.ParentID != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
sessionItems = append(sessionItems, sessionItem(sess))
|
filteredSessions = append(filteredSessions, sess)
|
||||||
|
items = append(items, sessionItem{
|
||||||
|
title: sess.Title,
|
||||||
|
isDeleteConfirming: false,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
list := list.NewListComponent(
|
// Create a generic list component
|
||||||
sessionItems,
|
listComponent := list.NewListComponent(
|
||||||
|
items,
|
||||||
10, // maxVisibleSessions
|
10, // maxVisibleSessions
|
||||||
"No sessions available",
|
"No sessions available",
|
||||||
true, // useAlphaNumericKeys
|
true, // useAlphaNumericKeys
|
||||||
)
|
)
|
||||||
|
listComponent.SetMaxWidth(layout.Current.Container.Width - 12)
|
||||||
|
|
||||||
return &sessionDialog{
|
return &sessionDialog{
|
||||||
list: list,
|
sessions: filteredSessions,
|
||||||
modal: modal.New(modal.WithTitle("Switch Session"), modal.WithMaxWidth(80)),
|
list: listComponent,
|
||||||
|
app: app,
|
||||||
|
deleteConfirmation: -1,
|
||||||
|
modal: modal.New(
|
||||||
|
modal.WithTitle("Switch Session"),
|
||||||
|
modal.WithMaxWidth(layout.Current.Container.Width-8),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
list "github.com/sst/opencode/internal/components/list"
|
list "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/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"
|
||||||
)
|
)
|
||||||
@@ -20,35 +19,12 @@ type ThemeDialog interface {
|
|||||||
layout.Modal
|
layout.Modal
|
||||||
}
|
}
|
||||||
|
|
||||||
type themeItem struct {
|
|
||||||
name string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t themeItem) Render(selected bool, width int) string {
|
|
||||||
th := theme.CurrentTheme()
|
|
||||||
baseStyle := styles.BaseStyle().
|
|
||||||
Width(width - 2).
|
|
||||||
Background(th.BackgroundElement())
|
|
||||||
|
|
||||||
if selected {
|
|
||||||
baseStyle = baseStyle.
|
|
||||||
Background(th.Primary()).
|
|
||||||
Foreground(th.BackgroundElement()).
|
|
||||||
Bold(true)
|
|
||||||
} else {
|
|
||||||
baseStyle = baseStyle.
|
|
||||||
Foreground(th.Text())
|
|
||||||
}
|
|
||||||
|
|
||||||
return baseStyle.Padding(0, 1).Render(t.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
type themeDialog struct {
|
type themeDialog struct {
|
||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
|
|
||||||
modal *modal.Modal
|
modal *modal.Modal
|
||||||
list list.List[themeItem]
|
list list.List[list.StringItem]
|
||||||
originalTheme string
|
originalTheme string
|
||||||
themeApplied bool
|
themeApplied bool
|
||||||
}
|
}
|
||||||
@@ -66,7 +42,7 @@ func (t *themeDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "enter":
|
case "enter":
|
||||||
if item, idx := t.list.GetSelectedItem(); idx >= 0 {
|
if item, idx := t.list.GetSelectedItem(); idx >= 0 {
|
||||||
selectedTheme := item.name
|
selectedTheme := string(item)
|
||||||
if err := theme.SetTheme(selectedTheme); err != nil {
|
if err := theme.SetTheme(selectedTheme); err != nil {
|
||||||
// status.Error(err.Error())
|
// status.Error(err.Error())
|
||||||
return t, nil
|
return t, nil
|
||||||
@@ -85,11 +61,11 @@ func (t *themeDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
|
|
||||||
var cmd tea.Cmd
|
var cmd tea.Cmd
|
||||||
listModel, cmd := t.list.Update(msg)
|
listModel, cmd := t.list.Update(msg)
|
||||||
t.list = listModel.(list.List[themeItem])
|
t.list = listModel.(list.List[list.StringItem])
|
||||||
|
|
||||||
if item, newIdx := t.list.GetSelectedItem(); newIdx >= 0 && newIdx != prevIdx {
|
if item, newIdx := t.list.GetSelectedItem(); newIdx >= 0 && newIdx != prevIdx {
|
||||||
theme.SetTheme(item.name)
|
theme.SetTheme(string(item))
|
||||||
return t, util.CmdHandler(ThemeSelectedMsg{ThemeName: item.name})
|
return t, util.CmdHandler(ThemeSelectedMsg{ThemeName: string(item)})
|
||||||
}
|
}
|
||||||
return t, cmd
|
return t, cmd
|
||||||
}
|
}
|
||||||
@@ -101,6 +77,7 @@ func (t *themeDialog) Render(background string) string {
|
|||||||
func (t *themeDialog) Close() tea.Cmd {
|
func (t *themeDialog) Close() tea.Cmd {
|
||||||
if !t.themeApplied {
|
if !t.themeApplied {
|
||||||
theme.SetTheme(t.originalTheme)
|
theme.SetTheme(t.originalTheme)
|
||||||
|
return util.CmdHandler(ThemeSelectedMsg{ThemeName: t.originalTheme})
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -110,17 +87,15 @@ func NewThemeDialog() ThemeDialog {
|
|||||||
themes := theme.AvailableThemes()
|
themes := theme.AvailableThemes()
|
||||||
currentTheme := theme.CurrentThemeName()
|
currentTheme := theme.CurrentThemeName()
|
||||||
|
|
||||||
var themeItems []themeItem
|
|
||||||
var selectedIdx int
|
var selectedIdx int
|
||||||
for i, name := range themes {
|
for i, name := range themes {
|
||||||
themeItems = append(themeItems, themeItem{name: name})
|
|
||||||
if name == currentTheme {
|
if name == currentTheme {
|
||||||
selectedIdx = i
|
selectedIdx = i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
list := list.NewListComponent(
|
list := list.NewStringList(
|
||||||
themeItems,
|
themes,
|
||||||
10, // maxVisibleThemes
|
10, // maxVisibleThemes
|
||||||
"No themes available",
|
"No themes available",
|
||||||
true,
|
true,
|
||||||
@@ -128,6 +103,9 @@ func NewThemeDialog() ThemeDialog {
|
|||||||
|
|
||||||
// Set the initial selection to the current theme
|
// Set the initial selection to the current theme
|
||||||
list.SetSelectedIndex(selectedIdx)
|
list.SetSelectedIndex(selectedIdx)
|
||||||
|
|
||||||
|
// Set the max width for the list to match the modal width
|
||||||
|
list.SetMaxWidth(36) // 40 (modal max width) - 4 (modal padding)
|
||||||
|
|
||||||
return &themeDialog{
|
return &themeDialog{
|
||||||
list: list,
|
list: list,
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ import (
|
|||||||
|
|
||||||
"github.com/charmbracelet/bubbles/v2/key"
|
"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/muesli/reflow/truncate"
|
||||||
|
"github.com/sst/opencode/internal/styles"
|
||||||
|
"github.com/sst/opencode/internal/theme"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ListItem interface {
|
type ListItem interface {
|
||||||
@@ -123,6 +127,9 @@ func (c *listComponent[T]) SetSelectedIndex(idx int) {
|
|||||||
func (c *listComponent[T]) View() string {
|
func (c *listComponent[T]) View() string {
|
||||||
items := c.items
|
items := c.items
|
||||||
maxWidth := c.maxWidth
|
maxWidth := c.maxWidth
|
||||||
|
if maxWidth == 0 {
|
||||||
|
maxWidth = 80 // Default width if not set
|
||||||
|
}
|
||||||
maxVisibleItems := min(c.maxVisibleItems, len(items))
|
maxVisibleItems := min(c.maxVisibleItems, len(items))
|
||||||
startIdx := 0
|
startIdx := 0
|
||||||
|
|
||||||
@@ -161,3 +168,36 @@ func NewListComponent[T ListItem](items []T, maxVisibleItems int, fallbackMsg st
|
|||||||
selectedIdx: 0,
|
selectedIdx: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StringItem is a simple implementation of ListItem for string values
|
||||||
|
type StringItem string
|
||||||
|
|
||||||
|
func (s StringItem) Render(selected bool, width int) string {
|
||||||
|
t := theme.CurrentTheme()
|
||||||
|
baseStyle := styles.BaseStyle()
|
||||||
|
|
||||||
|
truncatedStr := truncate.StringWithTail(string(s), uint(width-1), "...")
|
||||||
|
|
||||||
|
var itemStyle lipgloss.Style
|
||||||
|
if selected {
|
||||||
|
itemStyle = baseStyle.
|
||||||
|
Background(t.Primary()).
|
||||||
|
Foreground(t.Background()).
|
||||||
|
Width(width).
|
||||||
|
PaddingLeft(1)
|
||||||
|
} else {
|
||||||
|
itemStyle = baseStyle.
|
||||||
|
PaddingLeft(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return itemStyle.Render(truncatedStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStringList creates a new list component with string items
|
||||||
|
func NewStringList(items []string, maxVisibleItems int, fallbackMsg string, useAlphaNumericKeys bool) List[StringItem] {
|
||||||
|
stringItems := make([]StringItem, len(items))
|
||||||
|
for i, item := range items {
|
||||||
|
stringItems[i] = StringItem(item)
|
||||||
|
}
|
||||||
|
return NewListComponent(stringItems, maxVisibleItems, fallbackMsg, useAlphaNumericKeys)
|
||||||
|
}
|
||||||
|
|||||||
@@ -103,13 +103,13 @@ func (m *Modal) Render(contentView string, background string) string {
|
|||||||
Bold(true).
|
Bold(true).
|
||||||
Padding(0, 1)
|
Padding(0, 1)
|
||||||
|
|
||||||
escStyle := baseStyle.Foreground(t.TextMuted()).Bold(false)
|
escStyle := baseStyle.Foreground(t.TextMuted())
|
||||||
escText := escStyle.Render("esc")
|
escText := escStyle.Render("esc")
|
||||||
|
|
||||||
// Calculate position for esc text
|
// Calculate position for esc text
|
||||||
titleWidth := lipgloss.Width(m.title)
|
titleWidth := lipgloss.Width(m.title)
|
||||||
escWidth := lipgloss.Width(escText)
|
escWidth := lipgloss.Width(escText)
|
||||||
spacesNeeded := max(0, innerWidth-titleWidth-escWidth-3)
|
spacesNeeded := max(0, innerWidth-titleWidth-escWidth-2)
|
||||||
spacer := strings.Repeat(" ", spacesNeeded)
|
spacer := strings.Repeat(" ", spacesNeeded)
|
||||||
titleLine := m.title + spacer + escText
|
titleLine := m.title + spacer + escText
|
||||||
titleLine = titleStyle.Render(titleLine)
|
titleLine = titleStyle.Render(titleLine)
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ func formatTokensAndCost(tokens float32, contextWindow float32, cost float32) st
|
|||||||
formattedCost := fmt.Sprintf("$%.2f", cost)
|
formattedCost := fmt.Sprintf("$%.2f", cost)
|
||||||
percentage := (float64(tokens) / float64(contextWindow)) * 100
|
percentage := (float64(tokens) / float64(contextWindow)) * 100
|
||||||
|
|
||||||
return fmt.Sprintf("Tokens: %s (%d%%), Cost: %s", formattedTokens, int(percentage), formattedCost)
|
return fmt.Sprintf("Context: %s (%d%%), Cost: %s", formattedTokens, int(percentage), formattedCost)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m statusComponent) View() string {
|
func (m statusComponent) View() string {
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ package image
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
|
||||||
"github.com/atotto/clipboard"
|
"github.com/atotto/clipboard"
|
||||||
|
"image"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetImageFromClipboard() ([]byte, string, error) {
|
func GetImageFromClipboard() ([]byte, string, error) {
|
||||||
@@ -28,8 +28,6 @@ func GetImageFromClipboard() ([]byte, string, error) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func binaryToImage(data []byte) ([]byte, error) {
|
func binaryToImage(data []byte) ([]byte, error) {
|
||||||
reader := bytes.NewReader(data)
|
reader := bytes.NewReader(data)
|
||||||
img, _, err := image.Decode(reader)
|
img, _, err := image.Decode(reader)
|
||||||
@@ -40,7 +38,6 @@ func binaryToImage(data []byte) ([]byte, error) {
|
|||||||
return ImageToBytes(img)
|
return ImageToBytes(img)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func min(a, b int) int {
|
func min(a, b int) int {
|
||||||
if a < b {
|
if a < b {
|
||||||
return a
|
return a
|
||||||
|
|||||||
@@ -109,18 +109,26 @@ func PlaceOverlay(
|
|||||||
// Get the foreground line
|
// Get the foreground line
|
||||||
fgLine := fgLines[i-y]
|
fgLine := fgLines[i-y]
|
||||||
fgLineWidth := ansi.PrintableRuneWidth(fgLine)
|
fgLineWidth := ansi.PrintableRuneWidth(fgLine)
|
||||||
|
|
||||||
// Extract the styles at the border positions
|
// Extract the styles at the border positions
|
||||||
leftStyle := getStyleAtPosition(bgLine, pos)
|
// We need to get the style just before the border position to preserve background
|
||||||
rightStyle := getStyleAtPosition(bgLine, pos + 1 + fgLineWidth)
|
leftStyle := ansiStyle{}
|
||||||
|
if pos > 0 {
|
||||||
|
leftStyle = getStyleAtPosition(bgLine, pos-1)
|
||||||
|
} else {
|
||||||
|
leftStyle = getStyleAtPosition(bgLine, pos)
|
||||||
|
}
|
||||||
|
rightStyle := getStyleAtPosition(bgLine, pos+fgLineWidth)
|
||||||
|
|
||||||
// Left border - combine background from original with border foreground
|
// Left border - combine background from original with border foreground
|
||||||
leftSeq := combineStyles(leftStyle, options.borderColor)
|
leftSeq := combineStyles(leftStyle, options.borderColor)
|
||||||
if leftSeq != "" {
|
if leftSeq != "" {
|
||||||
b.WriteString(leftSeq)
|
b.WriteString(leftSeq)
|
||||||
}
|
}
|
||||||
b.WriteString("┃")
|
b.WriteString("┃")
|
||||||
b.WriteString("\x1b[0m") // Reset all styles
|
if leftSeq != "" {
|
||||||
|
b.WriteString("\x1b[0m") // Reset all styles only if we applied any
|
||||||
|
}
|
||||||
pos++
|
pos++
|
||||||
|
|
||||||
// Content
|
// Content
|
||||||
@@ -133,7 +141,9 @@ func PlaceOverlay(
|
|||||||
b.WriteString(rightSeq)
|
b.WriteString(rightSeq)
|
||||||
}
|
}
|
||||||
b.WriteString("┃")
|
b.WriteString("┃")
|
||||||
b.WriteString("\x1b[0m") // Reset all styles
|
if rightSeq != "" {
|
||||||
|
b.WriteString("\x1b[0m") // Reset all styles only if we applied any
|
||||||
|
}
|
||||||
pos++
|
pos++
|
||||||
} else {
|
} else {
|
||||||
// No border, just render the content
|
// No border, just render the content
|
||||||
@@ -172,23 +182,25 @@ type ansiStyle struct {
|
|||||||
// parseANSISequence parses an ANSI escape sequence into its components
|
// parseANSISequence parses an ANSI escape sequence into its components
|
||||||
func parseANSISequence(seq string) ansiStyle {
|
func parseANSISequence(seq string) ansiStyle {
|
||||||
style := ansiStyle{}
|
style := ansiStyle{}
|
||||||
|
|
||||||
// Extract the parameters from the sequence (e.g., \x1b[38;5;123;48;5;456m -> "38;5;123;48;5;456")
|
// Extract the parameters from the sequence (e.g., \x1b[38;5;123;48;5;456m -> "38;5;123;48;5;456")
|
||||||
if !strings.HasPrefix(seq, "\x1b[") || !strings.HasSuffix(seq, "m") {
|
if !strings.HasPrefix(seq, "\x1b[") || !strings.HasSuffix(seq, "m") {
|
||||||
return style
|
return style
|
||||||
}
|
}
|
||||||
|
|
||||||
params := seq[2 : len(seq)-1]
|
params := seq[2 : len(seq)-1]
|
||||||
if params == "" {
|
if params == "" {
|
||||||
return style
|
return style
|
||||||
}
|
}
|
||||||
|
|
||||||
parts := strings.Split(params, ";")
|
parts := strings.Split(params, ";")
|
||||||
i := 0
|
i := 0
|
||||||
for i < len(parts) {
|
for i < len(parts) {
|
||||||
switch parts[i] {
|
switch parts[i] {
|
||||||
case "0": // Reset
|
case "0": // Reset
|
||||||
style = ansiStyle{}
|
// Mark this as a reset by adding it to attrs
|
||||||
|
style.attrs = append(style.attrs, "0")
|
||||||
|
// Don't clear the style here, let the caller handle it
|
||||||
case "1", "2", "3", "4", "5", "6", "7", "8", "9": // Various attributes
|
case "1", "2", "3", "4", "5", "6", "7", "8", "9": // Various attributes
|
||||||
style.attrs = append(style.attrs, parts[i])
|
style.attrs = append(style.attrs, parts[i])
|
||||||
case "38": // Foreground color
|
case "38": // Foreground color
|
||||||
@@ -222,7 +234,7 @@ func parseANSISequence(seq string) ansiStyle {
|
|||||||
}
|
}
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
|
|
||||||
return style
|
return style
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,32 +243,30 @@ func combineStyles(bgStyle ansiStyle, fgColor *compat.AdaptiveColor) string {
|
|||||||
if fgColor == nil && bgStyle.bgColor == "" && len(bgStyle.attrs) == 0 {
|
if fgColor == nil && bgStyle.bgColor == "" && len(bgStyle.attrs) == 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
var parts []string
|
var parts []string
|
||||||
|
|
||||||
// Add attributes
|
// Add attributes
|
||||||
parts = append(parts, bgStyle.attrs...)
|
parts = append(parts, bgStyle.attrs...)
|
||||||
|
|
||||||
// Add background color from the original style
|
// Add background color from the original style
|
||||||
if bgStyle.bgColor != "" {
|
if bgStyle.bgColor != "" {
|
||||||
parts = append(parts, bgStyle.bgColor)
|
parts = append(parts, bgStyle.bgColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add foreground color if specified
|
// Add foreground color if specified
|
||||||
if fgColor != nil {
|
if fgColor != nil {
|
||||||
// Use the light color (could be improved to detect terminal background)
|
// Use the adaptive color which automatically selects based on terminal background
|
||||||
color := (*fgColor).Light
|
// The RGBA method already handles light/dark selection
|
||||||
|
r, g, b, _ := fgColor.RGBA()
|
||||||
// Use RGBA to get color components
|
|
||||||
r, g, b, _ := color.RGBA()
|
|
||||||
// RGBA returns 16-bit values, we need 8-bit
|
// RGBA returns 16-bit values, we need 8-bit
|
||||||
parts = append(parts, fmt.Sprintf("38;2;%d;%d;%d", r>>8, g>>8, b>>8))
|
parts = append(parts, fmt.Sprintf("38;2;%d;%d;%d", r>>8, g>>8, b>>8))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(parts) == 0 {
|
if len(parts) == 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("\x1b[%sm", strings.Join(parts, ";"))
|
return fmt.Sprintf("\x1b[%sm", strings.Join(parts, ";"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,10 +274,10 @@ func combineStyles(bgStyle ansiStyle, fgColor *compat.AdaptiveColor) string {
|
|||||||
func getStyleAtPosition(s string, targetPos int) ansiStyle {
|
func getStyleAtPosition(s string, targetPos int) ansiStyle {
|
||||||
// ANSI escape sequence regex
|
// ANSI escape sequence regex
|
||||||
ansiRegex := regexp.MustCompile(`\x1b\[[0-9;]*m`)
|
ansiRegex := regexp.MustCompile(`\x1b\[[0-9;]*m`)
|
||||||
|
|
||||||
visualPos := 0
|
visualPos := 0
|
||||||
currentStyle := ansiStyle{}
|
currentStyle := ansiStyle{}
|
||||||
|
|
||||||
i := 0
|
i := 0
|
||||||
for i < len(s) && visualPos <= targetPos {
|
for i < len(s) && visualPos <= targetPos {
|
||||||
// Check if we're at an ANSI escape sequence
|
// Check if we're at an ANSI escape sequence
|
||||||
@@ -275,18 +285,24 @@ func getStyleAtPosition(s string, targetPos int) ansiStyle {
|
|||||||
// Found an ANSI sequence at current position
|
// Found an ANSI sequence at current position
|
||||||
seq := s[i : i+match[1]]
|
seq := s[i : i+match[1]]
|
||||||
parsedStyle := parseANSISequence(seq)
|
parsedStyle := parseANSISequence(seq)
|
||||||
|
|
||||||
// Update current style (merge with existing)
|
// Check if this is a reset sequence
|
||||||
if parsedStyle.fgColor != "" {
|
if len(parsedStyle.attrs) > 0 && parsedStyle.attrs[0] == "0" {
|
||||||
currentStyle.fgColor = parsedStyle.fgColor
|
// Reset all styles
|
||||||
|
currentStyle = ansiStyle{}
|
||||||
|
} else {
|
||||||
|
// Update current style (merge with existing)
|
||||||
|
if parsedStyle.fgColor != "" {
|
||||||
|
currentStyle.fgColor = parsedStyle.fgColor
|
||||||
|
}
|
||||||
|
if parsedStyle.bgColor != "" {
|
||||||
|
currentStyle.bgColor = parsedStyle.bgColor
|
||||||
|
}
|
||||||
|
if len(parsedStyle.attrs) > 0 {
|
||||||
|
currentStyle.attrs = parsedStyle.attrs
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if parsedStyle.bgColor != "" {
|
|
||||||
currentStyle.bgColor = parsedStyle.bgColor
|
|
||||||
}
|
|
||||||
if len(parsedStyle.attrs) > 0 {
|
|
||||||
currentStyle.attrs = parsedStyle.attrs
|
|
||||||
}
|
|
||||||
|
|
||||||
i += match[1]
|
i += match[1]
|
||||||
} else if i < len(s) {
|
} else if i < len(s) {
|
||||||
// Regular character
|
// Regular character
|
||||||
@@ -298,7 +314,7 @@ func getStyleAtPosition(s string, targetPos int) ansiStyle {
|
|||||||
visualPos++
|
visualPos++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return currentStyle
|
return currentStyle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"image/color"
|
"image/color"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -42,7 +43,7 @@ func LoadThemesFromJSON() error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
themeName := strings.TrimSuffix(entry.Name(), ".json")
|
themeName := strings.TrimSuffix(entry.Name(), ".json")
|
||||||
data, err := themesFS.ReadFile(filepath.Join("themes", entry.Name()))
|
data, err := themesFS.ReadFile(path.Join("themes", entry.Name()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to read theme file %s: %w", entry.Name(), err)
|
return fmt.Errorf("failed to read theme file %s: %w", entry.Name(), err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,4 +133,3 @@ func TestLoadThemesFromDirectories(t *testing.T) {
|
|||||||
t.Error("Override theme not properly loaded")
|
t.Error("Override theme not properly loaded")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/theme.json",
|
||||||
|
"defs": {
|
||||||
|
"matrixInk0": "#0a0e0a",
|
||||||
|
"matrixInk1": "#0e130d",
|
||||||
|
"matrixInk2": "#141c12",
|
||||||
|
"matrixInk3": "#1e2a1b",
|
||||||
|
"rainGreen": "#2eff6a",
|
||||||
|
"rainGreenDim": "#1cc24b",
|
||||||
|
"rainGreenHi": "#62ff94",
|
||||||
|
"rainCyan": "#00efff",
|
||||||
|
"rainTeal": "#24f6d9",
|
||||||
|
"rainPurple": "#c770ff",
|
||||||
|
"rainOrange": "#ffa83d",
|
||||||
|
"alertRed": "#ff4b4b",
|
||||||
|
"alertYellow": "#e6ff57",
|
||||||
|
"alertBlue": "#30b3ff",
|
||||||
|
"rainGray": "#8ca391",
|
||||||
|
"lightBg": "#eef3ea",
|
||||||
|
"lightPaper": "#e4ebe1",
|
||||||
|
"lightInk1": "#dae1d7",
|
||||||
|
"lightText": "#203022",
|
||||||
|
"lightGray": "#748476"
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"primary": { "dark": "rainGreen", "light": "rainGreenDim" },
|
||||||
|
"secondary": { "dark": "rainCyan", "light": "rainTeal" },
|
||||||
|
"accent": { "dark": "rainPurple", "light": "rainPurple" },
|
||||||
|
"error": { "dark": "alertRed", "light": "alertRed" },
|
||||||
|
"warning": { "dark": "alertYellow", "light": "alertYellow" },
|
||||||
|
"success": { "dark": "rainGreenHi", "light": "rainGreenDim" },
|
||||||
|
"info": { "dark": "alertBlue", "light": "alertBlue" },
|
||||||
|
"text": { "dark": "rainGreenHi", "light": "lightText" },
|
||||||
|
"textMuted": { "dark": "rainGray", "light": "lightGray" },
|
||||||
|
"background": { "dark": "matrixInk0", "light": "lightBg" },
|
||||||
|
"backgroundPanel": { "dark": "matrixInk1", "light": "lightPaper" },
|
||||||
|
"backgroundElement": { "dark": "matrixInk2", "light": "lightInk1" },
|
||||||
|
"border": { "dark": "matrixInk3", "light": "lightGray" },
|
||||||
|
"borderActive": { "dark": "rainGreen", "light": "rainGreenDim" },
|
||||||
|
"borderSubtle": { "dark": "matrixInk2", "light": "lightInk1" },
|
||||||
|
"diffAdded": { "dark": "rainGreenDim", "light": "rainGreenDim" },
|
||||||
|
"diffRemoved": { "dark": "alertRed", "light": "alertRed" },
|
||||||
|
"diffContext": { "dark": "rainGray", "light": "lightGray" },
|
||||||
|
"diffHunkHeader": { "dark": "alertBlue", "light": "alertBlue" },
|
||||||
|
"diffHighlightAdded": { "dark": "#77ffaf", "light": "#5dac7e" },
|
||||||
|
"diffHighlightRemoved": { "dark": "#ff7171", "light": "#d53a3a" },
|
||||||
|
"diffAddedBg": { "dark": "#132616", "light": "#e0efde" },
|
||||||
|
"diffRemovedBg": { "dark": "#261212", "light": "#f9e5e5" },
|
||||||
|
"diffContextBg": { "dark": "matrixInk1", "light": "lightPaper" },
|
||||||
|
"diffLineNumber": { "dark": "matrixInk3", "light": "lightGray" },
|
||||||
|
"diffAddedLineNumberBg": { "dark": "#0f1b11", "light": "#d6e7d2" },
|
||||||
|
"diffRemovedLineNumberBg": { "dark": "#1b1414", "light": "#f2d2d2" },
|
||||||
|
"markdownText": { "dark": "rainGreenHi", "light": "lightText" },
|
||||||
|
"markdownHeading": { "dark": "rainCyan", "light": "rainTeal" },
|
||||||
|
"markdownLink": { "dark": "alertBlue", "light": "alertBlue" },
|
||||||
|
"markdownLinkText": { "dark": "rainTeal", "light": "rainTeal" },
|
||||||
|
"markdownCode": { "dark": "rainGreenDim", "light": "rainGreenDim" },
|
||||||
|
"markdownBlockQuote": { "dark": "rainGray", "light": "lightGray" },
|
||||||
|
"markdownEmph": { "dark": "rainOrange", "light": "rainOrange" },
|
||||||
|
"markdownStrong": { "dark": "alertYellow", "light": "alertYellow" },
|
||||||
|
"markdownHorizontalRule": { "dark": "rainGray", "light": "lightGray" },
|
||||||
|
"markdownListItem": { "dark": "alertBlue", "light": "alertBlue" },
|
||||||
|
"markdownListEnumeration": { "dark": "rainTeal", "light": "rainTeal" },
|
||||||
|
"markdownImage": { "dark": "alertBlue", "light": "alertBlue" },
|
||||||
|
"markdownImageText": { "dark": "rainTeal", "light": "rainTeal" },
|
||||||
|
"markdownCodeBlock": { "dark": "rainGreenHi", "light": "lightText" },
|
||||||
|
"syntaxComment": { "dark": "rainGray", "light": "lightGray" },
|
||||||
|
"syntaxKeyword": { "dark": "rainPurple", "light": "rainPurple" },
|
||||||
|
"syntaxFunction": { "dark": "alertBlue", "light": "alertBlue" },
|
||||||
|
"syntaxVariable": { "dark": "rainGreenHi", "light": "lightText" },
|
||||||
|
"syntaxString": { "dark": "rainGreenDim", "light": "rainGreenDim" },
|
||||||
|
"syntaxNumber": { "dark": "rainOrange", "light": "rainOrange" },
|
||||||
|
"syntaxType": { "dark": "alertYellow", "light": "alertYellow" },
|
||||||
|
"syntaxOperator": { "dark": "rainTeal", "light": "rainTeal" },
|
||||||
|
"syntaxPunctuation": { "dark": "rainGreenHi", "light": "lightText" }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,11 +48,11 @@
|
|||||||
"light": "nord10"
|
"light": "nord10"
|
||||||
},
|
},
|
||||||
"text": {
|
"text": {
|
||||||
"dark": "nord4",
|
"dark": "nord6",
|
||||||
"light": "nord0"
|
"light": "nord0"
|
||||||
},
|
},
|
||||||
"textMuted": {
|
"textMuted": {
|
||||||
"dark": "nord3",
|
"dark": "#8B95A7",
|
||||||
"light": "nord1"
|
"light": "nord1"
|
||||||
},
|
},
|
||||||
"background": {
|
"background": {
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
"light": "nord5"
|
"light": "nord5"
|
||||||
},
|
},
|
||||||
"backgroundElement": {
|
"backgroundElement": {
|
||||||
"dark": "nord1",
|
"dark": "nord2",
|
||||||
"light": "nord4"
|
"light": "nord4"
|
||||||
},
|
},
|
||||||
"border": {
|
"border": {
|
||||||
@@ -88,11 +88,11 @@
|
|||||||
"light": "nord11"
|
"light": "nord11"
|
||||||
},
|
},
|
||||||
"diffContext": {
|
"diffContext": {
|
||||||
"dark": "nord3",
|
"dark": "#8B95A7",
|
||||||
"light": "nord3"
|
"light": "nord3"
|
||||||
},
|
},
|
||||||
"diffHunkHeader": {
|
"diffHunkHeader": {
|
||||||
"dark": "nord3",
|
"dark": "#8B95A7",
|
||||||
"light": "nord3"
|
"light": "nord3"
|
||||||
},
|
},
|
||||||
"diffHighlightAdded": {
|
"diffHighlightAdded": {
|
||||||
@@ -148,7 +148,7 @@
|
|||||||
"light": "nord14"
|
"light": "nord14"
|
||||||
},
|
},
|
||||||
"markdownBlockQuote": {
|
"markdownBlockQuote": {
|
||||||
"dark": "nord3",
|
"dark": "#8B95A7",
|
||||||
"light": "nord3"
|
"light": "nord3"
|
||||||
},
|
},
|
||||||
"markdownEmph": {
|
"markdownEmph": {
|
||||||
@@ -160,7 +160,7 @@
|
|||||||
"light": "nord13"
|
"light": "nord13"
|
||||||
},
|
},
|
||||||
"markdownHorizontalRule": {
|
"markdownHorizontalRule": {
|
||||||
"dark": "nord3",
|
"dark": "#8B95A7",
|
||||||
"light": "nord3"
|
"light": "nord3"
|
||||||
},
|
},
|
||||||
"markdownListItem": {
|
"markdownListItem": {
|
||||||
@@ -184,7 +184,7 @@
|
|||||||
"light": "nord0"
|
"light": "nord0"
|
||||||
},
|
},
|
||||||
"syntaxComment": {
|
"syntaxComment": {
|
||||||
"dark": "nord3",
|
"dark": "#8B95A7",
|
||||||
"light": "nord3"
|
"light": "nord3"
|
||||||
},
|
},
|
||||||
"syntaxKeyword": {
|
"syntaxKeyword": {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/v2/key"
|
"github.com/charmbracelet/bubbles/v2/key"
|
||||||
tea "github.com/charmbracelet/bubbletea/v2"
|
tea "github.com/charmbracelet/bubbletea/v2"
|
||||||
@@ -25,6 +26,19 @@ import (
|
|||||||
"github.com/sst/opencode/pkg/client"
|
"github.com/sst/opencode/pkg/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// InterruptDebounceTimeoutMsg is sent when the interrupt key debounce timeout expires
|
||||||
|
type InterruptDebounceTimeoutMsg struct{}
|
||||||
|
|
||||||
|
// InterruptKeyState tracks the state of interrupt key presses for debouncing
|
||||||
|
type InterruptKeyState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
InterruptKeyIdle InterruptKeyState = iota
|
||||||
|
InterruptKeyFirstPress
|
||||||
|
)
|
||||||
|
|
||||||
|
const interruptDebounceTimeout = 1 * time.Second
|
||||||
|
|
||||||
type appModel struct {
|
type appModel struct {
|
||||||
width, height int
|
width, height int
|
||||||
app *app.App
|
app *app.App
|
||||||
@@ -40,6 +54,7 @@ type appModel struct {
|
|||||||
leaderBinding *key.Binding
|
leaderBinding *key.Binding
|
||||||
isLeaderSequence bool
|
isLeaderSequence bool
|
||||||
toastManager *toast.ToastManager
|
toastManager *toast.ToastManager
|
||||||
|
interruptKeyState InterruptKeyState
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a appModel) Init() tea.Cmd {
|
func (a appModel) Init() tea.Cmd {
|
||||||
@@ -117,7 +132,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updated, cmd := a.completions.Update(
|
updated, cmd := a.completions.Update(
|
||||||
app.CompletionDialogTriggerdMsg{
|
app.CompletionDialogTriggeredMsg{
|
||||||
InitialValue: initialValue,
|
InitialValue: initialValue,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -171,14 +186,37 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return a, nil
|
return a, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Check again for commands that don't require leader
|
// 6. Handle interrupt key debounce for session interrupt
|
||||||
|
interruptCommand := a.app.Commands[commands.SessionInterruptCommand]
|
||||||
|
if interruptCommand.Matches(msg, a.isLeaderSequence) && a.app.IsBusy() {
|
||||||
|
switch a.interruptKeyState {
|
||||||
|
case InterruptKeyIdle:
|
||||||
|
// First interrupt key press - start debounce timer
|
||||||
|
a.interruptKeyState = InterruptKeyFirstPress
|
||||||
|
a.editor.SetInterruptKeyInDebounce(true)
|
||||||
|
return a, tea.Tick(interruptDebounceTimeout, func(t time.Time) tea.Msg {
|
||||||
|
return InterruptDebounceTimeoutMsg{}
|
||||||
|
})
|
||||||
|
case InterruptKeyFirstPress:
|
||||||
|
// Second interrupt key press within timeout - actually interrupt
|
||||||
|
a.interruptKeyState = InterruptKeyIdle
|
||||||
|
a.editor.SetInterruptKeyInDebounce(false)
|
||||||
|
return a, util.CmdHandler(commands.ExecuteCommandMsg(interruptCommand))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Check again for commands that don't require leader (excluding interrupt when busy)
|
||||||
matches := a.app.Commands.Matches(msg, a.isLeaderSequence)
|
matches := a.app.Commands.Matches(msg, a.isLeaderSequence)
|
||||||
if len(matches) > 0 {
|
if len(matches) > 0 {
|
||||||
|
// Skip interrupt key if we're in debounce mode and app is busy
|
||||||
|
if interruptCommand.Matches(msg, a.isLeaderSequence) && a.app.IsBusy() && a.interruptKeyState != InterruptKeyIdle {
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
return a, util.CmdHandler(commands.ExecuteCommandsMsg(matches))
|
return a, util.CmdHandler(commands.ExecuteCommandsMsg(matches))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. Fallback to editor. This is for other characters
|
// 7. Fallback to editor. This is for other characters
|
||||||
// likek backspace, tab, etc.
|
// like backspace, tab, etc.
|
||||||
updatedEditor, cmd := a.editor.Update(msg)
|
updatedEditor, cmd := a.editor.Update(msg)
|
||||||
a.editor = updatedEditor.(chat.EditorComponent)
|
a.editor = updatedEditor.(chat.EditorComponent)
|
||||||
return a, cmd
|
return a, cmd
|
||||||
@@ -189,6 +227,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
updated, cmd := a.messages.Update(msg)
|
updated, cmd := a.messages.Update(msg)
|
||||||
a.messages = updated.(chat.MessagesComponent)
|
a.messages = updated.(chat.MessagesComponent)
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
|
return a, tea.Batch(cmds...)
|
||||||
case tea.BackgroundColorMsg:
|
case tea.BackgroundColorMsg:
|
||||||
styles.Terminal = &styles.TerminalInfo{
|
styles.Terminal = &styles.TerminalInfo{
|
||||||
BackgroundIsDark: msg.IsDark(),
|
BackgroundIsDark: msg.IsDark(),
|
||||||
@@ -222,6 +261,12 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
"opencode updated to "+msg.Properties.Version+", restart to apply.",
|
"opencode updated to "+msg.Properties.Version+", restart to apply.",
|
||||||
toast.WithTitle("New version installed"),
|
toast.WithTitle("New version installed"),
|
||||||
)
|
)
|
||||||
|
case client.EventSessionDeleted:
|
||||||
|
if a.app.Session != nil && msg.Properties.Info.Id == a.app.Session.Id {
|
||||||
|
a.app.Session = &client.SessionInfo{}
|
||||||
|
a.app.Messages = []client.MessageInfo{}
|
||||||
|
}
|
||||||
|
return a, toast.NewSuccessToast("Session deleted successfully")
|
||||||
case client.EventSessionUpdated:
|
case client.EventSessionUpdated:
|
||||||
if msg.Properties.Info.Id == a.app.Session.Id {
|
if msg.Properties.Info.Id == a.app.Session.Id {
|
||||||
a.app.Session = &msg.Properties.Info
|
a.app.Session = &msg.Properties.Info
|
||||||
@@ -229,12 +274,33 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
case client.EventMessageUpdated:
|
case client.EventMessageUpdated:
|
||||||
if msg.Properties.Info.Metadata.SessionID == a.app.Session.Id {
|
if msg.Properties.Info.Metadata.SessionID == a.app.Session.Id {
|
||||||
exists := false
|
exists := false
|
||||||
for i, m := range a.app.Messages {
|
optimisticReplaced := false
|
||||||
if m.Id == msg.Properties.Info.Id {
|
|
||||||
a.app.Messages[i] = msg.Properties.Info
|
// First check if this is replacing an optimistic message
|
||||||
exists = true
|
if msg.Properties.Info.Role == client.User {
|
||||||
|
// Look for optimistic messages to replace
|
||||||
|
for i, m := range a.app.Messages {
|
||||||
|
if strings.HasPrefix(m.Id, "optimistic-") && m.Role == client.User {
|
||||||
|
// Replace the optimistic message with the real one
|
||||||
|
a.app.Messages[i] = msg.Properties.Info
|
||||||
|
exists = true
|
||||||
|
optimisticReplaced = true
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If not replacing optimistic, check for existing message with same ID
|
||||||
|
if !optimisticReplaced {
|
||||||
|
for i, m := range a.app.Messages {
|
||||||
|
if m.Id == msg.Properties.Info.Id {
|
||||||
|
a.app.Messages[i] = msg.Properties.Info
|
||||||
|
exists = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
a.app.Messages = append(a.app.Messages, msg.Properties.Info)
|
a.app.Messages = append(a.app.Messages, msg.Properties.Info)
|
||||||
}
|
}
|
||||||
@@ -283,6 +349,10 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
tm, cmd := a.toastManager.Update(msg)
|
tm, cmd := a.toastManager.Update(msg)
|
||||||
a.toastManager = tm
|
a.toastManager = tm
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
|
case InterruptDebounceTimeoutMsg:
|
||||||
|
// Reset interrupt key state after timeout
|
||||||
|
a.interruptKeyState = InterruptKeyIdle
|
||||||
|
a.editor.SetInterruptKeyInDebounce(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// update status bar
|
// update status bar
|
||||||
@@ -363,7 +433,7 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
|
|||||||
}
|
}
|
||||||
switch command.Name {
|
switch command.Name {
|
||||||
case commands.AppHelpCommand:
|
case commands.AppHelpCommand:
|
||||||
helpDialog := dialog.NewHelpDialog(a.app.Commands.Sorted())
|
helpDialog := dialog.NewHelpDialog(a.app)
|
||||||
a.modal = helpDialog
|
a.modal = helpDialog
|
||||||
case commands.EditorOpenCommand:
|
case commands.EditorOpenCommand:
|
||||||
if a.app.IsBusy() {
|
if a.app.IsBusy() {
|
||||||
@@ -575,6 +645,7 @@ func NewModel(app *app.App) tea.Model {
|
|||||||
showCompletionDialog: false,
|
showCompletionDialog: false,
|
||||||
editorContainer: editorContainer,
|
editorContainer: editorContainer,
|
||||||
toastManager: toast.NewToastManager(),
|
toastManager: toast.NewToastManager(),
|
||||||
|
interruptKeyState: InterruptKeyIdle,
|
||||||
layout: layout.NewFlexLayout(
|
layout: layout.NewFlexLayout(
|
||||||
[]tea.ViewModel{messagesContainer, editorContainer},
|
[]tea.ViewModel{messagesContainer, editorContainer},
|
||||||
layout.WithDirection(layout.FlexDirectionVertical),
|
layout.WithDirection(layout.FlexDirectionVertical),
|
||||||
|
|||||||
@@ -230,6 +230,42 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/session_unshare": {
|
||||||
|
"post": {
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successfully unshared session",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/session.info"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"operationId": "postSession_unshare",
|
||||||
|
"parameters": [],
|
||||||
|
"description": "Unshare the session",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"sessionID": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"sessionID"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/session_messages": {
|
"/session_messages": {
|
||||||
"post": {
|
"post": {
|
||||||
"responses": {
|
"responses": {
|
||||||
@@ -327,6 +363,42 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/session_delete": {
|
||||||
|
"post": {
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successfully deleted session",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"operationId": "postSession_delete",
|
||||||
|
"parameters": [],
|
||||||
|
"description": "Delete a session and all its data",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"sessionID": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"sessionID"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/session_summarize": {
|
"/session_summarize": {
|
||||||
"post": {
|
"post": {
|
||||||
"responses": {
|
"responses": {
|
||||||
@@ -543,6 +615,9 @@
|
|||||||
{
|
{
|
||||||
"$ref": "#/components/schemas/Event.session.updated"
|
"$ref": "#/components/schemas/Event.session.updated"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/Event.session.deleted"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"$ref": "#/components/schemas/Event.session.error"
|
"$ref": "#/components/schemas/Event.session.error"
|
||||||
}
|
}
|
||||||
@@ -557,6 +632,7 @@
|
|||||||
"message.updated": "#/components/schemas/Event.message.updated",
|
"message.updated": "#/components/schemas/Event.message.updated",
|
||||||
"message.part.updated": "#/components/schemas/Event.message.part.updated",
|
"message.part.updated": "#/components/schemas/Event.message.part.updated",
|
||||||
"session.updated": "#/components/schemas/Event.session.updated",
|
"session.updated": "#/components/schemas/Event.session.updated",
|
||||||
|
"session.deleted": "#/components/schemas/Event.session.deleted",
|
||||||
"session.error": "#/components/schemas/Event.session.error"
|
"session.error": "#/components/schemas/Event.session.error"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -734,160 +810,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"type": "object",
|
"$ref": "#/components/schemas/Message.Metadata"
|
||||||
"properties": {
|
|
||||||
"time": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"created": {
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
"completed": {
|
|
||||||
"type": "number"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"created"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"error": {
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/ProviderAuthError"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/UnknownError"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"discriminator": {
|
|
||||||
"propertyName": "name",
|
|
||||||
"mapping": {
|
|
||||||
"ProviderAuthError": "#/components/schemas/ProviderAuthError",
|
|
||||||
"UnknownError": "#/components/schemas/UnknownError"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sessionID": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"tool": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"title": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"time": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"start": {
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
"end": {
|
|
||||||
"type": "number"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"start",
|
|
||||||
"end"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"title",
|
|
||||||
"time"
|
|
||||||
],
|
|
||||||
"additionalProperties": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"assistant": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"system": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"modelID": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"providerID": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"path": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"cwd": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"root": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"cwd",
|
|
||||||
"root"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"cost": {
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
"summary": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"tokens": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"input": {
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
"output": {
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
"reasoning": {
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
"cache": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"read": {
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
"write": {
|
|
||||||
"type": "number"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"read",
|
|
||||||
"write"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"input",
|
|
||||||
"output",
|
|
||||||
"reasoning",
|
|
||||||
"cache"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"system",
|
|
||||||
"modelID",
|
|
||||||
"providerID",
|
|
||||||
"path",
|
|
||||||
"cost",
|
|
||||||
"tokens"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"time",
|
|
||||||
"sessionID",
|
|
||||||
"tool"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -1141,6 +1064,162 @@
|
|||||||
"type"
|
"type"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"Message.Metadata": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"time": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"created": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"completed": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"created"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/ProviderAuthError"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/UnknownError"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"discriminator": {
|
||||||
|
"propertyName": "name",
|
||||||
|
"mapping": {
|
||||||
|
"ProviderAuthError": "#/components/schemas/ProviderAuthError",
|
||||||
|
"UnknownError": "#/components/schemas/UnknownError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sessionID": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"tool": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"title": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"start": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"start",
|
||||||
|
"end"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"title",
|
||||||
|
"time"
|
||||||
|
],
|
||||||
|
"additionalProperties": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"assistant": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"system": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"modelID": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"providerID": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"cwd": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"cwd",
|
||||||
|
"root"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"cost": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"summary": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"tokens": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"input": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"output": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"reasoning": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"cache": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"read": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"write": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"read",
|
||||||
|
"write"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"input",
|
||||||
|
"output",
|
||||||
|
"reasoning",
|
||||||
|
"cache"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"system",
|
||||||
|
"modelID",
|
||||||
|
"providerID",
|
||||||
|
"path",
|
||||||
|
"cost",
|
||||||
|
"tokens"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"time",
|
||||||
|
"sessionID",
|
||||||
|
"tool"
|
||||||
|
]
|
||||||
|
},
|
||||||
"ProviderAuthError": {
|
"ProviderAuthError": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -1300,6 +1379,30 @@
|
|||||||
"time"
|
"time"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"Event.session.deleted": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "session.deleted"
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"info": {
|
||||||
|
"$ref": "#/components/schemas/session.info"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"info"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"type",
|
||||||
|
"properties"
|
||||||
|
]
|
||||||
|
},
|
||||||
"Event.session.error": {
|
"Event.session.error": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -1461,6 +1564,9 @@
|
|||||||
"temperature": {
|
"temperature": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"tool_call": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"cost": {
|
"cost": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -1499,6 +1605,10 @@
|
|||||||
},
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1535,7 +1645,8 @@
|
|||||||
},
|
},
|
||||||
"description": "MCP (Model Context Protocol) server configurations"
|
"description": "MCP (Model Context Protocol) server configurations"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"Config.Keybinds": {
|
"Config.Keybinds": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -1648,7 +1759,8 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Exit the application"
|
"description": "Exit the application"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"Provider.Info": {
|
"Provider.Info": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -1700,6 +1812,9 @@
|
|||||||
"temperature": {
|
"temperature": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"tool_call": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"cost": {
|
"cost": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -1738,6 +1853,10 @@
|
|||||||
},
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -1745,9 +1864,11 @@
|
|||||||
"attachment",
|
"attachment",
|
||||||
"reasoning",
|
"reasoning",
|
||||||
"temperature",
|
"temperature",
|
||||||
|
"tool_call",
|
||||||
"cost",
|
"cost",
|
||||||
"limit",
|
"limit",
|
||||||
"id"
|
"id",
|
||||||
|
"options"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"Config.McpLocal": {
|
"Config.McpLocal": {
|
||||||
@@ -1776,7 +1897,8 @@
|
|||||||
"required": [
|
"required": [
|
||||||
"type",
|
"type",
|
||||||
"command"
|
"command"
|
||||||
]
|
],
|
||||||
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"Config.McpRemote": {
|
"Config.McpRemote": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -1794,7 +1916,8 @@
|
|||||||
"required": [
|
"required": [
|
||||||
"type",
|
"type",
|
||||||
"url"
|
"url"
|
||||||
]
|
],
|
||||||
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"Error": {
|
"Error": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|||||||
@@ -78,9 +78,11 @@ type ConfigInfo struct {
|
|||||||
Context float32 `json:"context"`
|
Context float32 `json:"context"`
|
||||||
Output float32 `json:"output"`
|
Output float32 `json:"output"`
|
||||||
} `json:"limit,omitempty"`
|
} `json:"limit,omitempty"`
|
||||||
Name *string `json:"name,omitempty"`
|
Name *string `json:"name,omitempty"`
|
||||||
Reasoning *bool `json:"reasoning,omitempty"`
|
Options *map[string]interface{} `json:"options,omitempty"`
|
||||||
Temperature *bool `json:"temperature,omitempty"`
|
Reasoning *bool `json:"reasoning,omitempty"`
|
||||||
|
Temperature *bool `json:"temperature,omitempty"`
|
||||||
|
ToolCall *bool `json:"tool_call,omitempty"`
|
||||||
} `json:"models"`
|
} `json:"models"`
|
||||||
Name *string `json:"name,omitempty"`
|
Name *string `json:"name,omitempty"`
|
||||||
Npm *string `json:"npm,omitempty"`
|
Npm *string `json:"npm,omitempty"`
|
||||||
@@ -252,6 +254,14 @@ type EventPermissionUpdated struct {
|
|||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EventSessionDeleted defines model for Event.session.deleted.
|
||||||
|
type EventSessionDeleted struct {
|
||||||
|
Properties struct {
|
||||||
|
Info SessionInfo `json:"info"`
|
||||||
|
} `json:"properties"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
// EventSessionError defines model for Event.session.error.
|
// EventSessionError defines model for Event.session.error.
|
||||||
type EventSessionError struct {
|
type EventSessionError struct {
|
||||||
Properties struct {
|
Properties struct {
|
||||||
@@ -290,47 +300,53 @@ type InstallationInfo struct {
|
|||||||
|
|
||||||
// MessageInfo defines model for Message.Info.
|
// MessageInfo defines model for Message.Info.
|
||||||
type MessageInfo struct {
|
type MessageInfo struct {
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
Metadata struct {
|
Metadata MessageMetadata `json:"metadata"`
|
||||||
Assistant *struct {
|
Parts []MessagePart `json:"parts"`
|
||||||
Cost float32 `json:"cost"`
|
Role MessageInfoRole `json:"role"`
|
||||||
ModelID string `json:"modelID"`
|
|
||||||
Path struct {
|
|
||||||
Cwd string `json:"cwd"`
|
|
||||||
Root string `json:"root"`
|
|
||||||
} `json:"path"`
|
|
||||||
ProviderID string `json:"providerID"`
|
|
||||||
Summary *bool `json:"summary,omitempty"`
|
|
||||||
System []string `json:"system"`
|
|
||||||
Tokens struct {
|
|
||||||
Cache struct {
|
|
||||||
Read float32 `json:"read"`
|
|
||||||
Write float32 `json:"write"`
|
|
||||||
} `json:"cache"`
|
|
||||||
Input float32 `json:"input"`
|
|
||||||
Output float32 `json:"output"`
|
|
||||||
Reasoning float32 `json:"reasoning"`
|
|
||||||
} `json:"tokens"`
|
|
||||||
} `json:"assistant,omitempty"`
|
|
||||||
Error *MessageInfo_Metadata_Error `json:"error,omitempty"`
|
|
||||||
SessionID string `json:"sessionID"`
|
|
||||||
Time struct {
|
|
||||||
Completed *float32 `json:"completed,omitempty"`
|
|
||||||
Created float32 `json:"created"`
|
|
||||||
} `json:"time"`
|
|
||||||
Tool map[string]MessageInfo_Metadata_Tool_AdditionalProperties `json:"tool"`
|
|
||||||
} `json:"metadata"`
|
|
||||||
Parts []MessagePart `json:"parts"`
|
|
||||||
Role MessageInfoRole `json:"role"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MessageInfo_Metadata_Error defines model for MessageInfo.Metadata.Error.
|
// MessageInfoRole defines model for MessageInfo.Role.
|
||||||
type MessageInfo_Metadata_Error struct {
|
type MessageInfoRole string
|
||||||
|
|
||||||
|
// MessageMetadata defines model for Message.Metadata.
|
||||||
|
type MessageMetadata struct {
|
||||||
|
Assistant *struct {
|
||||||
|
Cost float32 `json:"cost"`
|
||||||
|
ModelID string `json:"modelID"`
|
||||||
|
Path struct {
|
||||||
|
Cwd string `json:"cwd"`
|
||||||
|
Root string `json:"root"`
|
||||||
|
} `json:"path"`
|
||||||
|
ProviderID string `json:"providerID"`
|
||||||
|
Summary *bool `json:"summary,omitempty"`
|
||||||
|
System []string `json:"system"`
|
||||||
|
Tokens struct {
|
||||||
|
Cache struct {
|
||||||
|
Read float32 `json:"read"`
|
||||||
|
Write float32 `json:"write"`
|
||||||
|
} `json:"cache"`
|
||||||
|
Input float32 `json:"input"`
|
||||||
|
Output float32 `json:"output"`
|
||||||
|
Reasoning float32 `json:"reasoning"`
|
||||||
|
} `json:"tokens"`
|
||||||
|
} `json:"assistant,omitempty"`
|
||||||
|
Error *MessageMetadata_Error `json:"error,omitempty"`
|
||||||
|
SessionID string `json:"sessionID"`
|
||||||
|
Time struct {
|
||||||
|
Completed *float32 `json:"completed,omitempty"`
|
||||||
|
Created float32 `json:"created"`
|
||||||
|
} `json:"time"`
|
||||||
|
Tool map[string]MessageMetadata_Tool_AdditionalProperties `json:"tool"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MessageMetadata_Error defines model for MessageMetadata.Error.
|
||||||
|
type MessageMetadata_Error struct {
|
||||||
union json.RawMessage
|
union json.RawMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
// MessageInfo_Metadata_Tool_AdditionalProperties defines model for MessageInfo.Metadata.Tool.AdditionalProperties.
|
// MessageMetadata_Tool_AdditionalProperties defines model for Message.Metadata.tool.AdditionalProperties.
|
||||||
type MessageInfo_Metadata_Tool_AdditionalProperties struct {
|
type MessageMetadata_Tool_AdditionalProperties struct {
|
||||||
Time struct {
|
Time struct {
|
||||||
End float32 `json:"end"`
|
End float32 `json:"end"`
|
||||||
Start float32 `json:"start"`
|
Start float32 `json:"start"`
|
||||||
@@ -339,9 +355,6 @@ type MessageInfo_Metadata_Tool_AdditionalProperties struct {
|
|||||||
AdditionalProperties map[string]interface{} `json:"-"`
|
AdditionalProperties map[string]interface{} `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MessageInfoRole defines model for MessageInfo.Role.
|
|
||||||
type MessageInfoRole string
|
|
||||||
|
|
||||||
// MessagePart defines model for Message.Part.
|
// MessagePart defines model for Message.Part.
|
||||||
type MessagePart struct {
|
type MessagePart struct {
|
||||||
union json.RawMessage
|
union json.RawMessage
|
||||||
@@ -435,9 +448,11 @@ type ModelInfo struct {
|
|||||||
Context float32 `json:"context"`
|
Context float32 `json:"context"`
|
||||||
Output float32 `json:"output"`
|
Output float32 `json:"output"`
|
||||||
} `json:"limit"`
|
} `json:"limit"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Reasoning bool `json:"reasoning"`
|
Options map[string]interface{} `json:"options"`
|
||||||
Temperature bool `json:"temperature"`
|
Reasoning bool `json:"reasoning"`
|
||||||
|
Temperature bool `json:"temperature"`
|
||||||
|
ToolCall bool `json:"tool_call"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProviderInfo defines model for Provider.Info.
|
// ProviderInfo defines model for Provider.Info.
|
||||||
@@ -511,6 +526,11 @@ type PostSessionChatJSONBody struct {
|
|||||||
SessionID string `json:"sessionID"`
|
SessionID string `json:"sessionID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PostSessionDeleteJSONBody defines parameters for PostSessionDelete.
|
||||||
|
type PostSessionDeleteJSONBody struct {
|
||||||
|
SessionID string `json:"sessionID"`
|
||||||
|
}
|
||||||
|
|
||||||
// PostSessionInitializeJSONBody defines parameters for PostSessionInitialize.
|
// PostSessionInitializeJSONBody defines parameters for PostSessionInitialize.
|
||||||
type PostSessionInitializeJSONBody struct {
|
type PostSessionInitializeJSONBody struct {
|
||||||
ModelID string `json:"modelID"`
|
ModelID string `json:"modelID"`
|
||||||
@@ -535,6 +555,11 @@ type PostSessionSummarizeJSONBody struct {
|
|||||||
SessionID string `json:"sessionID"`
|
SessionID string `json:"sessionID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PostSessionUnshareJSONBody defines parameters for PostSessionUnshare.
|
||||||
|
type PostSessionUnshareJSONBody struct {
|
||||||
|
SessionID string `json:"sessionID"`
|
||||||
|
}
|
||||||
|
|
||||||
// PostFileSearchJSONRequestBody defines body for PostFileSearch for application/json ContentType.
|
// PostFileSearchJSONRequestBody defines body for PostFileSearch for application/json ContentType.
|
||||||
type PostFileSearchJSONRequestBody PostFileSearchJSONBody
|
type PostFileSearchJSONRequestBody PostFileSearchJSONBody
|
||||||
|
|
||||||
@@ -544,6 +569,9 @@ type PostSessionAbortJSONRequestBody PostSessionAbortJSONBody
|
|||||||
// PostSessionChatJSONRequestBody defines body for PostSessionChat for application/json ContentType.
|
// PostSessionChatJSONRequestBody defines body for PostSessionChat for application/json ContentType.
|
||||||
type PostSessionChatJSONRequestBody PostSessionChatJSONBody
|
type PostSessionChatJSONRequestBody PostSessionChatJSONBody
|
||||||
|
|
||||||
|
// PostSessionDeleteJSONRequestBody defines body for PostSessionDelete for application/json ContentType.
|
||||||
|
type PostSessionDeleteJSONRequestBody PostSessionDeleteJSONBody
|
||||||
|
|
||||||
// PostSessionInitializeJSONRequestBody defines body for PostSessionInitialize for application/json ContentType.
|
// PostSessionInitializeJSONRequestBody defines body for PostSessionInitialize for application/json ContentType.
|
||||||
type PostSessionInitializeJSONRequestBody PostSessionInitializeJSONBody
|
type PostSessionInitializeJSONRequestBody PostSessionInitializeJSONBody
|
||||||
|
|
||||||
@@ -556,25 +584,28 @@ type PostSessionShareJSONRequestBody PostSessionShareJSONBody
|
|||||||
// PostSessionSummarizeJSONRequestBody defines body for PostSessionSummarize for application/json ContentType.
|
// PostSessionSummarizeJSONRequestBody defines body for PostSessionSummarize for application/json ContentType.
|
||||||
type PostSessionSummarizeJSONRequestBody PostSessionSummarizeJSONBody
|
type PostSessionSummarizeJSONRequestBody PostSessionSummarizeJSONBody
|
||||||
|
|
||||||
// Getter for additional properties for MessageInfo_Metadata_Tool_AdditionalProperties. Returns the specified
|
// PostSessionUnshareJSONRequestBody defines body for PostSessionUnshare for application/json ContentType.
|
||||||
|
type PostSessionUnshareJSONRequestBody PostSessionUnshareJSONBody
|
||||||
|
|
||||||
|
// Getter for additional properties for MessageMetadata_Tool_AdditionalProperties. Returns the specified
|
||||||
// element and whether it was found
|
// element and whether it was found
|
||||||
func (a MessageInfo_Metadata_Tool_AdditionalProperties) Get(fieldName string) (value interface{}, found bool) {
|
func (a MessageMetadata_Tool_AdditionalProperties) Get(fieldName string) (value interface{}, found bool) {
|
||||||
if a.AdditionalProperties != nil {
|
if a.AdditionalProperties != nil {
|
||||||
value, found = a.AdditionalProperties[fieldName]
|
value, found = a.AdditionalProperties[fieldName]
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setter for additional properties for MessageInfo_Metadata_Tool_AdditionalProperties
|
// Setter for additional properties for MessageMetadata_Tool_AdditionalProperties
|
||||||
func (a *MessageInfo_Metadata_Tool_AdditionalProperties) Set(fieldName string, value interface{}) {
|
func (a *MessageMetadata_Tool_AdditionalProperties) Set(fieldName string, value interface{}) {
|
||||||
if a.AdditionalProperties == nil {
|
if a.AdditionalProperties == nil {
|
||||||
a.AdditionalProperties = make(map[string]interface{})
|
a.AdditionalProperties = make(map[string]interface{})
|
||||||
}
|
}
|
||||||
a.AdditionalProperties[fieldName] = value
|
a.AdditionalProperties[fieldName] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
// Override default JSON handling for MessageInfo_Metadata_Tool_AdditionalProperties to handle AdditionalProperties
|
// Override default JSON handling for MessageMetadata_Tool_AdditionalProperties to handle AdditionalProperties
|
||||||
func (a *MessageInfo_Metadata_Tool_AdditionalProperties) UnmarshalJSON(b []byte) error {
|
func (a *MessageMetadata_Tool_AdditionalProperties) UnmarshalJSON(b []byte) error {
|
||||||
object := make(map[string]json.RawMessage)
|
object := make(map[string]json.RawMessage)
|
||||||
err := json.Unmarshal(b, &object)
|
err := json.Unmarshal(b, &object)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -611,8 +642,8 @@ func (a *MessageInfo_Metadata_Tool_AdditionalProperties) UnmarshalJSON(b []byte)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Override default JSON handling for MessageInfo_Metadata_Tool_AdditionalProperties to handle AdditionalProperties
|
// Override default JSON handling for MessageMetadata_Tool_AdditionalProperties to handle AdditionalProperties
|
||||||
func (a MessageInfo_Metadata_Tool_AdditionalProperties) MarshalJSON() ([]byte, error) {
|
func (a MessageMetadata_Tool_AdditionalProperties) MarshalJSON() ([]byte, error) {
|
||||||
var err error
|
var err error
|
||||||
object := make(map[string]json.RawMessage)
|
object := make(map[string]json.RawMessage)
|
||||||
|
|
||||||
@@ -920,6 +951,34 @@ func (t *Event) MergeEventSessionUpdated(v EventSessionUpdated) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AsEventSessionDeleted returns the union data inside the Event as a EventSessionDeleted
|
||||||
|
func (t Event) AsEventSessionDeleted() (EventSessionDeleted, error) {
|
||||||
|
var body EventSessionDeleted
|
||||||
|
err := json.Unmarshal(t.union, &body)
|
||||||
|
return body, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromEventSessionDeleted overwrites any union data inside the Event as the provided EventSessionDeleted
|
||||||
|
func (t *Event) FromEventSessionDeleted(v EventSessionDeleted) error {
|
||||||
|
v.Type = "session.deleted"
|
||||||
|
b, err := json.Marshal(v)
|
||||||
|
t.union = b
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// MergeEventSessionDeleted performs a merge with any union data inside the Event, using the provided EventSessionDeleted
|
||||||
|
func (t *Event) MergeEventSessionDeleted(v EventSessionDeleted) error {
|
||||||
|
v.Type = "session.deleted"
|
||||||
|
b, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
merged, err := runtime.JSONMerge(t.union, b)
|
||||||
|
t.union = merged
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// AsEventSessionError returns the union data inside the Event as a EventSessionError
|
// AsEventSessionError returns the union data inside the Event as a EventSessionError
|
||||||
func (t Event) AsEventSessionError() (EventSessionError, error) {
|
func (t Event) AsEventSessionError() (EventSessionError, error) {
|
||||||
var body EventSessionError
|
var body EventSessionError
|
||||||
@@ -972,6 +1031,8 @@ func (t Event) ValueByDiscriminator() (interface{}, error) {
|
|||||||
return t.AsEventMessageUpdated()
|
return t.AsEventMessageUpdated()
|
||||||
case "permission.updated":
|
case "permission.updated":
|
||||||
return t.AsEventPermissionUpdated()
|
return t.AsEventPermissionUpdated()
|
||||||
|
case "session.deleted":
|
||||||
|
return t.AsEventSessionDeleted()
|
||||||
case "session.error":
|
case "session.error":
|
||||||
return t.AsEventSessionError()
|
return t.AsEventSessionError()
|
||||||
case "session.updated":
|
case "session.updated":
|
||||||
@@ -1082,23 +1143,23 @@ func (t *EventSessionError_Properties_Error) UnmarshalJSON(b []byte) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// AsProviderAuthError returns the union data inside the MessageInfo_Metadata_Error as a ProviderAuthError
|
// AsProviderAuthError returns the union data inside the MessageMetadata_Error as a ProviderAuthError
|
||||||
func (t MessageInfo_Metadata_Error) AsProviderAuthError() (ProviderAuthError, error) {
|
func (t MessageMetadata_Error) AsProviderAuthError() (ProviderAuthError, error) {
|
||||||
var body ProviderAuthError
|
var body ProviderAuthError
|
||||||
err := json.Unmarshal(t.union, &body)
|
err := json.Unmarshal(t.union, &body)
|
||||||
return body, err
|
return body, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// FromProviderAuthError overwrites any union data inside the MessageInfo_Metadata_Error as the provided ProviderAuthError
|
// FromProviderAuthError overwrites any union data inside the MessageMetadata_Error as the provided ProviderAuthError
|
||||||
func (t *MessageInfo_Metadata_Error) FromProviderAuthError(v ProviderAuthError) error {
|
func (t *MessageMetadata_Error) FromProviderAuthError(v ProviderAuthError) error {
|
||||||
v.Name = "ProviderAuthError"
|
v.Name = "ProviderAuthError"
|
||||||
b, err := json.Marshal(v)
|
b, err := json.Marshal(v)
|
||||||
t.union = b
|
t.union = b
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// MergeProviderAuthError performs a merge with any union data inside the MessageInfo_Metadata_Error, using the provided ProviderAuthError
|
// MergeProviderAuthError performs a merge with any union data inside the MessageMetadata_Error, using the provided ProviderAuthError
|
||||||
func (t *MessageInfo_Metadata_Error) MergeProviderAuthError(v ProviderAuthError) error {
|
func (t *MessageMetadata_Error) MergeProviderAuthError(v ProviderAuthError) error {
|
||||||
v.Name = "ProviderAuthError"
|
v.Name = "ProviderAuthError"
|
||||||
b, err := json.Marshal(v)
|
b, err := json.Marshal(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1110,23 +1171,23 @@ func (t *MessageInfo_Metadata_Error) MergeProviderAuthError(v ProviderAuthError)
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// AsUnknownError returns the union data inside the MessageInfo_Metadata_Error as a UnknownError
|
// AsUnknownError returns the union data inside the MessageMetadata_Error as a UnknownError
|
||||||
func (t MessageInfo_Metadata_Error) AsUnknownError() (UnknownError, error) {
|
func (t MessageMetadata_Error) AsUnknownError() (UnknownError, error) {
|
||||||
var body UnknownError
|
var body UnknownError
|
||||||
err := json.Unmarshal(t.union, &body)
|
err := json.Unmarshal(t.union, &body)
|
||||||
return body, err
|
return body, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// FromUnknownError overwrites any union data inside the MessageInfo_Metadata_Error as the provided UnknownError
|
// FromUnknownError overwrites any union data inside the MessageMetadata_Error as the provided UnknownError
|
||||||
func (t *MessageInfo_Metadata_Error) FromUnknownError(v UnknownError) error {
|
func (t *MessageMetadata_Error) FromUnknownError(v UnknownError) error {
|
||||||
v.Name = "UnknownError"
|
v.Name = "UnknownError"
|
||||||
b, err := json.Marshal(v)
|
b, err := json.Marshal(v)
|
||||||
t.union = b
|
t.union = b
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// MergeUnknownError performs a merge with any union data inside the MessageInfo_Metadata_Error, using the provided UnknownError
|
// MergeUnknownError performs a merge with any union data inside the MessageMetadata_Error, using the provided UnknownError
|
||||||
func (t *MessageInfo_Metadata_Error) MergeUnknownError(v UnknownError) error {
|
func (t *MessageMetadata_Error) MergeUnknownError(v UnknownError) error {
|
||||||
v.Name = "UnknownError"
|
v.Name = "UnknownError"
|
||||||
b, err := json.Marshal(v)
|
b, err := json.Marshal(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1138,7 +1199,7 @@ func (t *MessageInfo_Metadata_Error) MergeUnknownError(v UnknownError) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t MessageInfo_Metadata_Error) Discriminator() (string, error) {
|
func (t MessageMetadata_Error) Discriminator() (string, error) {
|
||||||
var discriminator struct {
|
var discriminator struct {
|
||||||
Discriminator string `json:"name"`
|
Discriminator string `json:"name"`
|
||||||
}
|
}
|
||||||
@@ -1146,7 +1207,7 @@ func (t MessageInfo_Metadata_Error) Discriminator() (string, error) {
|
|||||||
return discriminator.Discriminator, err
|
return discriminator.Discriminator, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t MessageInfo_Metadata_Error) ValueByDiscriminator() (interface{}, error) {
|
func (t MessageMetadata_Error) ValueByDiscriminator() (interface{}, error) {
|
||||||
discriminator, err := t.Discriminator()
|
discriminator, err := t.Discriminator()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1161,12 +1222,12 @@ func (t MessageInfo_Metadata_Error) ValueByDiscriminator() (interface{}, error)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t MessageInfo_Metadata_Error) MarshalJSON() ([]byte, error) {
|
func (t MessageMetadata_Error) MarshalJSON() ([]byte, error) {
|
||||||
b, err := t.union.MarshalJSON()
|
b, err := t.union.MarshalJSON()
|
||||||
return b, err
|
return b, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *MessageInfo_Metadata_Error) UnmarshalJSON(b []byte) error {
|
func (t *MessageMetadata_Error) UnmarshalJSON(b []byte) error {
|
||||||
err := t.union.UnmarshalJSON(b)
|
err := t.union.UnmarshalJSON(b)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1611,6 +1672,11 @@ type ClientInterface interface {
|
|||||||
// PostSessionCreate request
|
// PostSessionCreate request
|
||||||
PostSessionCreate(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error)
|
PostSessionCreate(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error)
|
||||||
|
|
||||||
|
// PostSessionDeleteWithBody request with any body
|
||||||
|
PostSessionDeleteWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error)
|
||||||
|
|
||||||
|
PostSessionDelete(ctx context.Context, body PostSessionDeleteJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error)
|
||||||
|
|
||||||
// PostSessionInitializeWithBody request with any body
|
// PostSessionInitializeWithBody request with any body
|
||||||
PostSessionInitializeWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error)
|
PostSessionInitializeWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error)
|
||||||
|
|
||||||
@@ -1633,6 +1699,11 @@ type ClientInterface interface {
|
|||||||
PostSessionSummarizeWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error)
|
PostSessionSummarizeWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error)
|
||||||
|
|
||||||
PostSessionSummarize(ctx context.Context, body PostSessionSummarizeJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error)
|
PostSessionSummarize(ctx context.Context, body PostSessionSummarizeJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error)
|
||||||
|
|
||||||
|
// PostSessionUnshareWithBody request with any body
|
||||||
|
PostSessionUnshareWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error)
|
||||||
|
|
||||||
|
PostSessionUnshare(ctx context.Context, body PostSessionUnshareJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) PostAppInfo(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) {
|
func (c *Client) PostAppInfo(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) {
|
||||||
@@ -1803,6 +1874,30 @@ func (c *Client) PostSessionCreate(ctx context.Context, reqEditors ...RequestEdi
|
|||||||
return c.Client.Do(req)
|
return c.Client.Do(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) PostSessionDeleteWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) {
|
||||||
|
req, err := NewPostSessionDeleteRequestWithBody(c.Server, contentType, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
if err := c.applyEditors(ctx, req, reqEditors); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c.Client.Do(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) PostSessionDelete(ctx context.Context, body PostSessionDeleteJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) {
|
||||||
|
req, err := NewPostSessionDeleteRequest(c.Server, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
if err := c.applyEditors(ctx, req, reqEditors); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c.Client.Do(req)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) PostSessionInitializeWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) {
|
func (c *Client) PostSessionInitializeWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) {
|
||||||
req, err := NewPostSessionInitializeRequestWithBody(c.Server, contentType, body)
|
req, err := NewPostSessionInitializeRequestWithBody(c.Server, contentType, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1911,6 +2006,30 @@ func (c *Client) PostSessionSummarize(ctx context.Context, body PostSessionSumma
|
|||||||
return c.Client.Do(req)
|
return c.Client.Do(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) PostSessionUnshareWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) {
|
||||||
|
req, err := NewPostSessionUnshareRequestWithBody(c.Server, contentType, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
if err := c.applyEditors(ctx, req, reqEditors); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c.Client.Do(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) PostSessionUnshare(ctx context.Context, body PostSessionUnshareJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) {
|
||||||
|
req, err := NewPostSessionUnshareRequest(c.Server, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
if err := c.applyEditors(ctx, req, reqEditors); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c.Client.Do(req)
|
||||||
|
}
|
||||||
|
|
||||||
// NewPostAppInfoRequest generates requests for PostAppInfo
|
// NewPostAppInfoRequest generates requests for PostAppInfo
|
||||||
func NewPostAppInfoRequest(server string) (*http.Request, error) {
|
func NewPostAppInfoRequest(server string) (*http.Request, error) {
|
||||||
var err error
|
var err error
|
||||||
@@ -2247,6 +2366,46 @@ func NewPostSessionCreateRequest(server string) (*http.Request, error) {
|
|||||||
return req, nil
|
return req, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewPostSessionDeleteRequest calls the generic PostSessionDelete builder with application/json body
|
||||||
|
func NewPostSessionDeleteRequest(server string, body PostSessionDeleteJSONRequestBody) (*http.Request, error) {
|
||||||
|
var bodyReader io.Reader
|
||||||
|
buf, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
bodyReader = bytes.NewReader(buf)
|
||||||
|
return NewPostSessionDeleteRequestWithBody(server, "application/json", bodyReader)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPostSessionDeleteRequestWithBody generates requests for PostSessionDelete with any type of body
|
||||||
|
func NewPostSessionDeleteRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
serverURL, err := url.Parse(server)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
operationPath := fmt.Sprintf("/session_delete")
|
||||||
|
if operationPath[0] == '/' {
|
||||||
|
operationPath = "." + operationPath
|
||||||
|
}
|
||||||
|
|
||||||
|
queryURL, err := serverURL.Parse(operationPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", queryURL.String(), body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Add("Content-Type", contentType)
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
// NewPostSessionInitializeRequest calls the generic PostSessionInitialize builder with application/json body
|
// NewPostSessionInitializeRequest calls the generic PostSessionInitialize builder with application/json body
|
||||||
func NewPostSessionInitializeRequest(server string, body PostSessionInitializeJSONRequestBody) (*http.Request, error) {
|
func NewPostSessionInitializeRequest(server string, body PostSessionInitializeJSONRequestBody) (*http.Request, error) {
|
||||||
var bodyReader io.Reader
|
var bodyReader io.Reader
|
||||||
@@ -2434,6 +2593,46 @@ func NewPostSessionSummarizeRequestWithBody(server string, contentType string, b
|
|||||||
return req, nil
|
return req, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewPostSessionUnshareRequest calls the generic PostSessionUnshare builder with application/json body
|
||||||
|
func NewPostSessionUnshareRequest(server string, body PostSessionUnshareJSONRequestBody) (*http.Request, error) {
|
||||||
|
var bodyReader io.Reader
|
||||||
|
buf, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
bodyReader = bytes.NewReader(buf)
|
||||||
|
return NewPostSessionUnshareRequestWithBody(server, "application/json", bodyReader)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPostSessionUnshareRequestWithBody generates requests for PostSessionUnshare with any type of body
|
||||||
|
func NewPostSessionUnshareRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
serverURL, err := url.Parse(server)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
operationPath := fmt.Sprintf("/session_unshare")
|
||||||
|
if operationPath[0] == '/' {
|
||||||
|
operationPath = "." + operationPath
|
||||||
|
}
|
||||||
|
|
||||||
|
queryURL, err := serverURL.Parse(operationPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", queryURL.String(), body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Add("Content-Type", contentType)
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error {
|
func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error {
|
||||||
for _, r := range c.RequestEditors {
|
for _, r := range c.RequestEditors {
|
||||||
if err := r(ctx, req); err != nil {
|
if err := r(ctx, req); err != nil {
|
||||||
@@ -2516,6 +2715,11 @@ type ClientWithResponsesInterface interface {
|
|||||||
// PostSessionCreateWithResponse request
|
// PostSessionCreateWithResponse request
|
||||||
PostSessionCreateWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*PostSessionCreateResponse, error)
|
PostSessionCreateWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*PostSessionCreateResponse, error)
|
||||||
|
|
||||||
|
// PostSessionDeleteWithBodyWithResponse request with any body
|
||||||
|
PostSessionDeleteWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostSessionDeleteResponse, error)
|
||||||
|
|
||||||
|
PostSessionDeleteWithResponse(ctx context.Context, body PostSessionDeleteJSONRequestBody, reqEditors ...RequestEditorFn) (*PostSessionDeleteResponse, error)
|
||||||
|
|
||||||
// PostSessionInitializeWithBodyWithResponse request with any body
|
// PostSessionInitializeWithBodyWithResponse request with any body
|
||||||
PostSessionInitializeWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostSessionInitializeResponse, error)
|
PostSessionInitializeWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostSessionInitializeResponse, error)
|
||||||
|
|
||||||
@@ -2538,6 +2742,11 @@ type ClientWithResponsesInterface interface {
|
|||||||
PostSessionSummarizeWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostSessionSummarizeResponse, error)
|
PostSessionSummarizeWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostSessionSummarizeResponse, error)
|
||||||
|
|
||||||
PostSessionSummarizeWithResponse(ctx context.Context, body PostSessionSummarizeJSONRequestBody, reqEditors ...RequestEditorFn) (*PostSessionSummarizeResponse, error)
|
PostSessionSummarizeWithResponse(ctx context.Context, body PostSessionSummarizeJSONRequestBody, reqEditors ...RequestEditorFn) (*PostSessionSummarizeResponse, error)
|
||||||
|
|
||||||
|
// PostSessionUnshareWithBodyWithResponse request with any body
|
||||||
|
PostSessionUnshareWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostSessionUnshareResponse, error)
|
||||||
|
|
||||||
|
PostSessionUnshareWithResponse(ctx context.Context, body PostSessionUnshareJSONRequestBody, reqEditors ...RequestEditorFn) (*PostSessionUnshareResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type PostAppInfoResponse struct {
|
type PostAppInfoResponse struct {
|
||||||
@@ -2791,6 +3000,28 @@ func (r PostSessionCreateResponse) StatusCode() int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PostSessionDeleteResponse struct {
|
||||||
|
Body []byte
|
||||||
|
HTTPResponse *http.Response
|
||||||
|
JSON200 *bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status returns HTTPResponse.Status
|
||||||
|
func (r PostSessionDeleteResponse) Status() string {
|
||||||
|
if r.HTTPResponse != nil {
|
||||||
|
return r.HTTPResponse.Status
|
||||||
|
}
|
||||||
|
return http.StatusText(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatusCode returns HTTPResponse.StatusCode
|
||||||
|
func (r PostSessionDeleteResponse) StatusCode() int {
|
||||||
|
if r.HTTPResponse != nil {
|
||||||
|
return r.HTTPResponse.StatusCode
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
type PostSessionInitializeResponse struct {
|
type PostSessionInitializeResponse struct {
|
||||||
Body []byte
|
Body []byte
|
||||||
HTTPResponse *http.Response
|
HTTPResponse *http.Response
|
||||||
@@ -2901,6 +3132,28 @@ func (r PostSessionSummarizeResponse) StatusCode() int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PostSessionUnshareResponse struct {
|
||||||
|
Body []byte
|
||||||
|
HTTPResponse *http.Response
|
||||||
|
JSON200 *SessionInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status returns HTTPResponse.Status
|
||||||
|
func (r PostSessionUnshareResponse) Status() string {
|
||||||
|
if r.HTTPResponse != nil {
|
||||||
|
return r.HTTPResponse.Status
|
||||||
|
}
|
||||||
|
return http.StatusText(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatusCode returns HTTPResponse.StatusCode
|
||||||
|
func (r PostSessionUnshareResponse) StatusCode() int {
|
||||||
|
if r.HTTPResponse != nil {
|
||||||
|
return r.HTTPResponse.StatusCode
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
// PostAppInfoWithResponse request returning *PostAppInfoResponse
|
// PostAppInfoWithResponse request returning *PostAppInfoResponse
|
||||||
func (c *ClientWithResponses) PostAppInfoWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*PostAppInfoResponse, error) {
|
func (c *ClientWithResponses) PostAppInfoWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*PostAppInfoResponse, error) {
|
||||||
rsp, err := c.PostAppInfo(ctx, reqEditors...)
|
rsp, err := c.PostAppInfo(ctx, reqEditors...)
|
||||||
@@ -3024,6 +3277,23 @@ func (c *ClientWithResponses) PostSessionCreateWithResponse(ctx context.Context,
|
|||||||
return ParsePostSessionCreateResponse(rsp)
|
return ParsePostSessionCreateResponse(rsp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PostSessionDeleteWithBodyWithResponse request with arbitrary body returning *PostSessionDeleteResponse
|
||||||
|
func (c *ClientWithResponses) PostSessionDeleteWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostSessionDeleteResponse, error) {
|
||||||
|
rsp, err := c.PostSessionDeleteWithBody(ctx, contentType, body, reqEditors...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ParsePostSessionDeleteResponse(rsp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClientWithResponses) PostSessionDeleteWithResponse(ctx context.Context, body PostSessionDeleteJSONRequestBody, reqEditors ...RequestEditorFn) (*PostSessionDeleteResponse, error) {
|
||||||
|
rsp, err := c.PostSessionDelete(ctx, body, reqEditors...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ParsePostSessionDeleteResponse(rsp)
|
||||||
|
}
|
||||||
|
|
||||||
// PostSessionInitializeWithBodyWithResponse request with arbitrary body returning *PostSessionInitializeResponse
|
// PostSessionInitializeWithBodyWithResponse request with arbitrary body returning *PostSessionInitializeResponse
|
||||||
func (c *ClientWithResponses) PostSessionInitializeWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostSessionInitializeResponse, error) {
|
func (c *ClientWithResponses) PostSessionInitializeWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostSessionInitializeResponse, error) {
|
||||||
rsp, err := c.PostSessionInitializeWithBody(ctx, contentType, body, reqEditors...)
|
rsp, err := c.PostSessionInitializeWithBody(ctx, contentType, body, reqEditors...)
|
||||||
@@ -3101,6 +3371,23 @@ func (c *ClientWithResponses) PostSessionSummarizeWithResponse(ctx context.Conte
|
|||||||
return ParsePostSessionSummarizeResponse(rsp)
|
return ParsePostSessionSummarizeResponse(rsp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PostSessionUnshareWithBodyWithResponse request with arbitrary body returning *PostSessionUnshareResponse
|
||||||
|
func (c *ClientWithResponses) PostSessionUnshareWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostSessionUnshareResponse, error) {
|
||||||
|
rsp, err := c.PostSessionUnshareWithBody(ctx, contentType, body, reqEditors...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ParsePostSessionUnshareResponse(rsp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClientWithResponses) PostSessionUnshareWithResponse(ctx context.Context, body PostSessionUnshareJSONRequestBody, reqEditors ...RequestEditorFn) (*PostSessionUnshareResponse, error) {
|
||||||
|
rsp, err := c.PostSessionUnshare(ctx, body, reqEditors...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ParsePostSessionUnshareResponse(rsp)
|
||||||
|
}
|
||||||
|
|
||||||
// ParsePostAppInfoResponse parses an HTTP response from a PostAppInfoWithResponse call
|
// ParsePostAppInfoResponse parses an HTTP response from a PostAppInfoWithResponse call
|
||||||
func ParsePostAppInfoResponse(rsp *http.Response) (*PostAppInfoResponse, error) {
|
func ParsePostAppInfoResponse(rsp *http.Response) (*PostAppInfoResponse, error) {
|
||||||
bodyBytes, err := io.ReadAll(rsp.Body)
|
bodyBytes, err := io.ReadAll(rsp.Body)
|
||||||
@@ -3402,6 +3689,32 @@ func ParsePostSessionCreateResponse(rsp *http.Response) (*PostSessionCreateRespo
|
|||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParsePostSessionDeleteResponse parses an HTTP response from a PostSessionDeleteWithResponse call
|
||||||
|
func ParsePostSessionDeleteResponse(rsp *http.Response) (*PostSessionDeleteResponse, error) {
|
||||||
|
bodyBytes, err := io.ReadAll(rsp.Body)
|
||||||
|
defer func() { _ = rsp.Body.Close() }()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &PostSessionDeleteResponse{
|
||||||
|
Body: bodyBytes,
|
||||||
|
HTTPResponse: rsp,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
|
||||||
|
var dest bool
|
||||||
|
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
response.JSON200 = &dest
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ParsePostSessionInitializeResponse parses an HTTP response from a PostSessionInitializeWithResponse call
|
// ParsePostSessionInitializeResponse parses an HTTP response from a PostSessionInitializeWithResponse call
|
||||||
func ParsePostSessionInitializeResponse(rsp *http.Response) (*PostSessionInitializeResponse, error) {
|
func ParsePostSessionInitializeResponse(rsp *http.Response) (*PostSessionInitializeResponse, error) {
|
||||||
bodyBytes, err := io.ReadAll(rsp.Body)
|
bodyBytes, err := io.ReadAll(rsp.Body)
|
||||||
@@ -3531,3 +3844,29 @@ func ParsePostSessionSummarizeResponse(rsp *http.Response) (*PostSessionSummariz
|
|||||||
|
|
||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParsePostSessionUnshareResponse parses an HTTP response from a PostSessionUnshareWithResponse call
|
||||||
|
func ParsePostSessionUnshareResponse(rsp *http.Response) (*PostSessionUnshareResponse, error) {
|
||||||
|
bodyBytes, err := io.ReadAll(rsp.Body)
|
||||||
|
defer func() { _ = rsp.Body.Close() }()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &PostSessionUnshareResponse{
|
||||||
|
Body: bodyBytes,
|
||||||
|
HTTPResponse: rsp,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
|
||||||
|
var dest SessionInfo
|
||||||
|
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
response.JSON200 = &dest
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -113,29 +113,83 @@ const DiffView: Component<DiffViewProps> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={`${styles.diff} ${props.class ?? ""}`}>
|
<div class={`${styles.diff} ${props.class ?? ""}`}>
|
||||||
<div class={styles.column}>
|
{rows().map((r) => (
|
||||||
{rows().map((r) => (
|
<div class={styles.row}>
|
||||||
<CodeBlock
|
<div class={styles.beforeColumn}>
|
||||||
code={r.left}
|
<CodeBlock
|
||||||
lang={props.lang}
|
code={r.left}
|
||||||
data-section="cell"
|
lang={props.lang}
|
||||||
data-diff-type={r.type === "removed" || r.type === "modified" ? "removed" : ""}
|
data-section="cell"
|
||||||
/>
|
data-diff-type={r.type === "removed" || r.type === "modified" ? "removed" : ""}
|
||||||
))}
|
data-display-mobile={r.type === "added" && !r.left ? "false" : undefined}
|
||||||
</div>
|
/>
|
||||||
|
{(r.type === "added" || r.type === "modified") && r.right !== undefined && (
|
||||||
|
<CodeBlock
|
||||||
|
code={r.right}
|
||||||
|
lang={props.lang}
|
||||||
|
data-section="cell"
|
||||||
|
data-diff-type="added"
|
||||||
|
data-display-mobile="true"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class={styles.column}>
|
<div class={styles.afterColumn}>
|
||||||
{rows().map((r) => (
|
<CodeBlock
|
||||||
<CodeBlock
|
code={r.right}
|
||||||
code={r.right}
|
lang={props.lang}
|
||||||
lang={props.lang}
|
data-section="cell"
|
||||||
data-section="cell"
|
data-diff-type={r.type === "added" || r.type === "modified" ? "added" : ""}
|
||||||
data-diff-type={r.type === "added" || r.type === "modified" ? "added" : ""}
|
/>
|
||||||
/>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DiffView
|
export default DiffView
|
||||||
|
|
||||||
|
// String to test diff viewer with
|
||||||
|
const testDiff = `--- combined_before.txt 2025-06-24 16:38:08
|
||||||
|
+++ combined_after.txt 2025-06-24 16:38:12
|
||||||
|
@@ -1,21 +1,25 @@
|
||||||
|
unchanged line
|
||||||
|
-deleted line
|
||||||
|
-old content
|
||||||
|
+added line
|
||||||
|
+new content
|
||||||
|
|
||||||
|
-removed empty line below
|
||||||
|
+added empty line above
|
||||||
|
|
||||||
|
- tab indented
|
||||||
|
-trailing spaces
|
||||||
|
-very long line that will definitely wrap in most editors and cause potential alignment issues when displayed in a two column diff view
|
||||||
|
-unicode content: 🚀 ✨ 中文
|
||||||
|
-mixed content with tabs and spaces
|
||||||
|
+ space indented
|
||||||
|
+no trailing spaces
|
||||||
|
+short line
|
||||||
|
+very long replacement line that will also wrap and test how the diff viewer handles long line additions after short line removals
|
||||||
|
+different unicode: 🎉 💻 日本語
|
||||||
|
+normalized content with consistent spacing
|
||||||
|
+newline to content
|
||||||
|
|
||||||
|
-content to remove
|
||||||
|
-whitespace only:
|
||||||
|
-multiple
|
||||||
|
-consecutive
|
||||||
|
-deletions
|
||||||
|
-single deletion
|
||||||
|
+
|
||||||
|
+single addition
|
||||||
|
+first addition
|
||||||
|
+second addition
|
||||||
|
+third addition
|
||||||
|
line before addition
|
||||||
|
+first added line
|
||||||
|
+
|
||||||
|
+third added line
|
||||||
|
line after addition
|
||||||
|
final unchanged line`
|
||||||
|
|||||||
@@ -444,7 +444,7 @@ function MarkdownPart(props: MarkdownPartProps) {
|
|||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
<MarkdownView
|
<MarkdownView
|
||||||
data-elment-markdown
|
data-element-markdown
|
||||||
markdown={local.text}
|
markdown={local.text}
|
||||||
ref={(el) => (divEl = el)}
|
ref={(el) => (divEl = el)}
|
||||||
/>
|
/>
|
||||||
@@ -463,6 +463,7 @@ function MarkdownPart(props: MarkdownPartProps) {
|
|||||||
|
|
||||||
interface TerminalPartProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
interface TerminalPartProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
||||||
command: string
|
command: string
|
||||||
|
error?: string
|
||||||
result?: string
|
result?: string
|
||||||
desc?: string
|
desc?: string
|
||||||
expand?: boolean
|
expand?: boolean
|
||||||
@@ -470,6 +471,7 @@ interface TerminalPartProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|||||||
function TerminalPart(props: TerminalPartProps) {
|
function TerminalPart(props: TerminalPartProps) {
|
||||||
const [local, rest] = splitProps(props, [
|
const [local, rest] = splitProps(props, [
|
||||||
"command",
|
"command",
|
||||||
|
"error",
|
||||||
"result",
|
"result",
|
||||||
"desc",
|
"desc",
|
||||||
"expand",
|
"expand",
|
||||||
@@ -508,12 +510,25 @@ function TerminalPart(props: TerminalPartProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div data-section="content">
|
<div data-section="content">
|
||||||
<CodeBlock lang="bash" code={local.command} />
|
<CodeBlock lang="bash" code={local.command} />
|
||||||
<CodeBlock
|
<Switch>
|
||||||
lang="console"
|
<Match when={local.error}>
|
||||||
onRendered={checkOverflow}
|
<CodeBlock
|
||||||
ref={(el) => (preEl = el)}
|
data-section="error"
|
||||||
code={local.result || ""}
|
lang="text"
|
||||||
/>
|
onRendered={checkOverflow}
|
||||||
|
ref={(el) => (preEl = el)}
|
||||||
|
code={local.error || ""}
|
||||||
|
/>
|
||||||
|
</Match>
|
||||||
|
<Match when={local.result}>
|
||||||
|
<CodeBlock
|
||||||
|
lang="console"
|
||||||
|
onRendered={checkOverflow}
|
||||||
|
ref={(el) => (preEl = el)}
|
||||||
|
code={local.result || ""}
|
||||||
|
/>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{((!local.expand && overflowed()) || expanded()) && (
|
{((!local.expand && overflowed()) || expanded()) && (
|
||||||
@@ -726,7 +741,7 @@ export default function Share(props: {
|
|||||||
for (let i = 0; i < messages().length; i++) {
|
for (let i = 0; i < messages().length; i++) {
|
||||||
const msg = messages()[i]
|
const msg = messages()[i]
|
||||||
|
|
||||||
// TODO: Cleaup
|
// TODO: Cleanup
|
||||||
// const system = result.messages.length === 0 && msg.role === "system"
|
// const system = result.messages.length === 0 && msg.role === "system"
|
||||||
const assistant = msg.metadata?.assistant
|
const assistant = msg.metadata?.assistant
|
||||||
|
|
||||||
@@ -1601,8 +1616,10 @@ export default function Share(props: {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{(_part) => {
|
{(_part) => {
|
||||||
const command = () => toolData()?.args.command
|
const command = () => toolData()?.metadata?.title
|
||||||
const desc = () => toolData()?.args.description
|
const desc = () => toolData()?.metadata?.description
|
||||||
|
const result = () => toolData()?.metadata?.stdout
|
||||||
|
const error = () => toolData()?.metadata?.stderr
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -1617,14 +1634,17 @@ export default function Share(props: {
|
|||||||
<div></div>
|
<div></div>
|
||||||
</div>
|
</div>
|
||||||
<div data-section="content">
|
<div data-section="content">
|
||||||
<div data-part-tool-body>
|
{command() && (
|
||||||
<TerminalPart
|
<div data-part-tool-body>
|
||||||
desc={desc()}
|
<TerminalPart
|
||||||
data-size="sm"
|
desc={desc()}
|
||||||
command={command()}
|
data-size="sm"
|
||||||
result={toolData()?.result}
|
command={command()!}
|
||||||
/>
|
result={result()}
|
||||||
</div>
|
error={error()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<ToolFooter
|
<ToolFooter
|
||||||
time={toolData()?.duration || 0}
|
time={toolData()?.duration || 0}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
pre {
|
pre {
|
||||||
--shiki-dark-bg: var(--sl-color-bg-surface) !important;
|
--shiki-dark-bg: var(--sl-color-bg-surface) !important;
|
||||||
background-color: var(--sl-color-bg-surface) !important;
|
background-color: var(--sl-color-bg-surface) !important;
|
||||||
|
|
||||||
|
span {
|
||||||
|
white-space: break-spaces;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,39 +1,52 @@
|
|||||||
.diff {
|
.diff {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: 1fr 1fr;
|
flex-direction: column;
|
||||||
border: 1px solid var(--sl-color-divider);
|
border: 1px solid var(--sl-color-divider);
|
||||||
background-color: var(--sl-color-bg-surface);
|
background-color: var(--sl-color-bg-surface);
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.column {
|
.row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beforeColumn,
|
||||||
|
.afterColumn {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
overflow-x: visible;
|
overflow-x: visible;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
&:first-child {
|
.beforeColumn {
|
||||||
border-right: 1px solid var(--sl-color-divider);
|
border-right: 1px solid var(--sl-color-divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
& > [data-section="cell"]:first-child {
|
.diff > .row:first-child [data-section="cell"]:first-child {
|
||||||
padding-top: 0.5rem;
|
padding-top: 0.5rem;
|
||||||
}
|
}
|
||||||
& > [data-section="cell"]:last-child {
|
|
||||||
padding-bottom: 0.5rem;
|
.diff > .row:last-child [data-section="cell"]:last-child {
|
||||||
}
|
padding-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-section="cell"] {
|
[data-section="cell"] {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: none;
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.1875rem 0.5rem 0.1875rem 2.2ch;
|
padding: 0.1875rem 0.5rem 0.1875rem 2.2ch;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
|
&[data-display-mobile="true"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
--shiki-dark-bg: var(--sl-color-bg-surface) !important;
|
--shiki-dark-bg: var(--sl-color-bg-surface) !important;
|
||||||
background-color: var(--sl-color-bg-surface) !important;
|
background-color: var(--sl-color-bg-surface) !important;
|
||||||
@@ -83,3 +96,27 @@
|
|||||||
color: var(--sl-color-green-high);
|
color: var(--sl-color-green-high);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 40rem) {
|
||||||
|
.row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.afterColumn {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beforeColumn {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-section="cell"] {
|
||||||
|
&[data-display-mobile="true"] {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-display-mobile="false"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -37,16 +37,26 @@
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
||||||
&::before {
|
&:not(pre code) {
|
||||||
content: "`";
|
&::before {
|
||||||
font-weight: 600;
|
content: "`";
|
||||||
}
|
font-weight: 700;
|
||||||
&::after {
|
}
|
||||||
content: "`";
|
&::after {
|
||||||
font-weight: 600;
|
content: "`";
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -616,6 +616,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-section="error"] {
|
||||||
|
pre {
|
||||||
|
color: var(--sl-color-red) !important;
|
||||||
|
--shiki-dark: var(--sl-color-red) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&[data-expanded="true"] {
|
&[data-expanded="true"] {
|
||||||
pre {
|
pre {
|
||||||
display: block;
|
display: block;
|
||||||
@@ -659,12 +666,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&[data-expanded="true"] {
|
&[data-expanded="true"] {
|
||||||
[data-elment-markdown] {
|
[data-element-markdown] {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&[data-expanded="false"] {
|
&[data-expanded="false"] {
|
||||||
[data-elment-markdown] {
|
[data-element-markdown] {
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
-webkit-line-clamp: 3;
|
-webkit-line-clamp: 3;
|
||||||
|
|||||||
@@ -89,3 +89,21 @@ You can configure MCP servers you want to use through the `mcp` option.
|
|||||||
```
|
```
|
||||||
|
|
||||||
[Learn more here](/docs/mcp-servers).
|
[Learn more here](/docs/mcp-servers).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Disabled providers
|
||||||
|
|
||||||
|
You can disable providers that are loaded automatically through the `disabled_providers` option. This is useful when you want to prevent certain providers from being loaded even if their credentials are available.
|
||||||
|
|
||||||
|
```json title="opencode.json"
|
||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"disabled_providers": ["openai", "gemini"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `disabled_providers` option accepts an array of provider IDs. When a provider is disabled:
|
||||||
|
- It won't be loaded even if environment variables are set
|
||||||
|
- It won't be loaded even if API keys are configured through `opencode auth login`
|
||||||
|
- The provider's models won't appear in the model selection list
|
||||||
|
|||||||
@@ -64,6 +64,10 @@ paru -S opencode-bin
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
##### Windows
|
||||||
|
|
||||||
|
Right now the automatic installation methods do not work properly on Windows. However you can grab the binary from the [Releases](https://github.com/sst/opencode/releases).
|
||||||
|
|
||||||
## Providers
|
## Providers
|
||||||
|
|
||||||
We recommend signing up for Claude Pro or Max, running `opencode auth login` and selecting Anthropic. It's the most cost-effective way to use opencode.
|
We recommend signing up for Claude Pro or Max, running `opencode auth login` and selecting Anthropic. It's the most cost-effective way to use opencode.
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
@echo off
|
||||||
|
|
||||||
|
if not exist ".git" (
|
||||||
|
exit /b 0
|
||||||
|
)
|
||||||
|
|
||||||
|
if not exist ".git\hooks" (
|
||||||
|
mkdir ".git\hooks"
|
||||||
|
)
|
||||||
|
|
||||||
|
(
|
||||||
|
echo #!/bin/sh
|
||||||
|
echo bun run typecheck
|
||||||
|
) > ".git\hooks\pre-push"
|
||||||
|
|
||||||
|
echo ✅ Pre-push hook installed
|
||||||
Reference in New Issue
Block a user