mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-29 03:32:08 +08:00
Merge remote-tracking branch 'origin/main' into feat/cli-local-run
# Conflicts: # package.json
This commit is contained in:
@@ -129,6 +129,7 @@
|
||||
"@solid-primitives/storage": "catalog:",
|
||||
"@solidjs/meta": "catalog:",
|
||||
"@solidjs/router": "0.15.4",
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"electron-context-menu": "4.1.2",
|
||||
"electron-log": "^5",
|
||||
@@ -152,7 +153,7 @@
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"@valibot/to-json-schema": "1.6.0",
|
||||
"electron": "40.8.5",
|
||||
"electron": "41.2.1",
|
||||
"electron-builder": "^26",
|
||||
"electron-vite": "^5",
|
||||
"solid-js": "catalog:",
|
||||
@@ -412,6 +413,7 @@
|
||||
"@modelcontextprotocol/sdk": "1.29.0",
|
||||
"@morphllm/morphsdk": "0.2.166",
|
||||
"@npmcli/arborist": "9.4.0",
|
||||
"@npmcli/config": "10.8.1",
|
||||
"@octokit/graphql": "9.0.2",
|
||||
"@octokit/rest": "catalog:",
|
||||
"@openauthjs/openauth": "catalog:",
|
||||
@@ -422,8 +424,8 @@
|
||||
"@opentelemetry/exporter-trace-otlp-http": "0.214.0",
|
||||
"@opentelemetry/sdk-trace-base": "2.6.1",
|
||||
"@opentelemetry/sdk-trace-node": "2.6.1",
|
||||
"@opentui/core": "catalog:",
|
||||
"@opentui/solid": "catalog:",
|
||||
"@opentui/core": "0.1.99",
|
||||
"@opentui/solid": "0.1.99",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@pierre/diffs": "catalog:",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
@@ -527,8 +529,8 @@
|
||||
"zod": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@opentui/core": "catalog:",
|
||||
"@opentui/solid": "catalog:",
|
||||
"@opentui/core": "0.1.99",
|
||||
"@opentui/solid": "0.1.99",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
@@ -677,6 +679,7 @@
|
||||
],
|
||||
"patchedDependencies": {
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||
"@npmcli/agent@4.0.0": "patches/@npmcli%2Fagent@4.0.0.patch",
|
||||
"stream-chat@9.38.0": "patches/stream-chat@9.38.0.patch",
|
||||
},
|
||||
"overrides": {
|
||||
@@ -716,7 +719,7 @@
|
||||
"@tailwindcss/vite": "4.1.11",
|
||||
"@tsconfig/bun": "1.0.9",
|
||||
"@tsconfig/node22": "22.0.2",
|
||||
"@types/bun": "1.3.11",
|
||||
"@types/bun": "1.3.12",
|
||||
"@types/cross-spawn": "6.0.6",
|
||||
"@types/luxon": "3.7.1",
|
||||
"@types/node": "22.13.9",
|
||||
@@ -1504,6 +1507,8 @@
|
||||
|
||||
"@npmcli/arborist": ["@npmcli/arborist@9.4.0", "", { "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", "@npmcli/fs": "^5.0.0", "@npmcli/installed-package-contents": "^4.0.0", "@npmcli/map-workspaces": "^5.0.0", "@npmcli/metavuln-calculator": "^9.0.2", "@npmcli/name-from-folder": "^4.0.0", "@npmcli/node-gyp": "^5.0.0", "@npmcli/package-json": "^7.0.0", "@npmcli/query": "^5.0.0", "@npmcli/redact": "^4.0.0", "@npmcli/run-script": "^10.0.0", "bin-links": "^6.0.0", "cacache": "^20.0.1", "common-ancestor-path": "^2.0.0", "hosted-git-info": "^9.0.0", "json-stringify-nice": "^1.1.4", "lru-cache": "^11.2.1", "minimatch": "^10.0.3", "nopt": "^9.0.0", "npm-install-checks": "^8.0.0", "npm-package-arg": "^13.0.0", "npm-pick-manifest": "^11.0.1", "npm-registry-fetch": "^19.0.0", "pacote": "^21.0.2", "parse-conflict-json": "^5.0.1", "proc-log": "^6.0.0", "proggy": "^4.0.0", "promise-all-reject-late": "^1.0.0", "promise-call-limit": "^3.0.1", "semver": "^7.3.7", "ssri": "^13.0.0", "treeverse": "^3.0.0", "walk-up-path": "^4.0.0" }, "bin": { "arborist": "bin/index.js" } }, "sha512-4Bm8hNixJG/sii1PMnag0V9i/sGOX9VRzFrUiZMSBJpGlLR38f+Btl85d07G9GL56xO0l0OZjvrGNYsDYp0xKA=="],
|
||||
|
||||
"@npmcli/config": ["@npmcli/config@10.8.1", "", { "dependencies": { "@npmcli/map-workspaces": "^5.0.0", "@npmcli/package-json": "^7.0.0", "ci-info": "^4.0.0", "ini": "^6.0.0", "nopt": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "walk-up-path": "^4.0.0" } }, "sha512-MAYk9IlIGiyC0c9fnjdBSQfIFPZT0g1MfeSiD1UXTq2zJOLX55jS9/sETJHqw/7LN18JjITrhYfgCfapbmZHiQ=="],
|
||||
|
||||
"@npmcli/fs": ["@npmcli/fs@5.0.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og=="],
|
||||
|
||||
"@npmcli/git": ["@npmcli/git@7.0.2", "", { "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/promise-spawn": "^9.0.0", "ini": "^6.0.0", "lru-cache": "^11.2.1", "npm-pick-manifest": "^11.0.1", "proc-log": "^6.0.0", "semver": "^7.3.5", "which": "^6.0.0" } }, "sha512-oeolHDjExNAJAnlYP2qzNjMX/Xi9bmu78C9dIGr4xjobrSKbuMYCph8lTzn4vnW3NjIqVmw/f8BCfouqyJXlRg=="],
|
||||
@@ -2214,7 +2219,7 @@
|
||||
|
||||
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
|
||||
"@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="],
|
||||
|
||||
"@types/cacache": ["@types/cacache@20.0.1", "", { "dependencies": { "@types/node": "*", "minipass": "*" } }, "sha512-QlKW3AFoFr/hvPHwFHMIVUH/ZCYeetBNou3PCmxu5LaNDvrtBlPJtIA6uhmU9JRt9oxj7IYoqoLcpxtzpPiTcw=="],
|
||||
|
||||
@@ -2698,7 +2703,7 @@
|
||||
|
||||
"bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
|
||||
"bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="],
|
||||
|
||||
"bun-webgpu": ["bun-webgpu@0.1.5", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.5", "bun-webgpu-darwin-x64": "^0.1.5", "bun-webgpu-linux-x64": "^0.1.5", "bun-webgpu-win32-x64": "^0.1.5" } }, "sha512-91/K6S5whZKX7CWAm9AylhyKrLGRz6BUiiPiM/kXadSnD4rffljCD/q9cNFftm5YXhx4MvLqw33yEilxogJvwA=="],
|
||||
|
||||
@@ -3074,7 +3079,7 @@
|
||||
|
||||
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
|
||||
|
||||
"electron": ["electron@40.8.5", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^24.9.0", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" } }, "sha512-pgTY/VPQKaiU4sTjfU96iyxCXrFm4htVPCMRT4b7q9ijNTRgtLmLvcmzp2G4e7xDrq9p7OLHSmu1rBKFf6Y1/A=="],
|
||||
"electron": ["electron@41.2.1", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^24.9.0", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" } }, "sha512-teeRThiYGTPKf/2yOW7zZA1bhb91KEQ4yLBPOg7GxpmnkLFLugKgQaAKOrCgdzwsXh/5mFIfmkm+4+wACJKwaA=="],
|
||||
|
||||
"electron-builder": ["electron-builder@26.8.1", "", { "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.8.1", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { "electron-builder": "cli.js", "install-app-deps": "install-app-deps.js" } }, "sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw=="],
|
||||
|
||||
|
||||
Generated
+3
-3
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1775823930,
|
||||
"narHash": "sha256-ALT447J7FcxP/97J01A/gp/hgdO5lXRsm+zLMt+gIjc=",
|
||||
"lastModified": 1776683584,
|
||||
"narHash": "sha256-NuTLMrr10Tng72hurYG8jYQ4XKK8wnpJmOGcPiis96g=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "8c11f88bb9573a10a7d6bf87161ef08455ac70b9",
|
||||
"rev": "9dd5558b06dbdacbf635a3dd36dce1b1a7ee3a89",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-uDu9FY2G8j6AzpXAQ3WoWLJ9uyh5R7xqcAVzij6dpdU=",
|
||||
"aarch64-linux": "sha256-GtVSse+wWwmFA9e3XkAp+1spPH2u4C2jqJ9HUKQRJ44=",
|
||||
"aarch64-darwin": "sha256-UrZIY7v59gB0k0+/CIXA/NFrR3FSE+5Lcsz9qZffsqM=",
|
||||
"x86_64-darwin": "sha256-/thTDanC0f1rczDj7RbgeFyaRfuhMdbRXrgfH6KRejs="
|
||||
"x86_64-linux": "sha256-w2yWlMGc9MVtbgLGZ77y7CzlV+FHFEIBMuyY2+SVKzQ=",
|
||||
"aarch64-linux": "sha256-dNczn+1Gdm6MQNsEHiBuWHO9pFg/wkZ8DndRScFllyA=",
|
||||
"aarch64-darwin": "sha256-flKz2Hejb6VL4Ygm2PHMJWTNpPxEWDX4lwZkUqWiggo=",
|
||||
"x86_64-darwin": "sha256-pZmMQD73/t+mPjwKXjQVQ6guyrXZP9X1vsWoxZXdYWs="
|
||||
}
|
||||
}
|
||||
|
||||
+18
-17
@@ -7,6 +7,7 @@
|
||||
sysctl,
|
||||
makeBinaryWrapper,
|
||||
models-dev,
|
||||
ripgrep,
|
||||
installShellFiles,
|
||||
versionCheckHook,
|
||||
writableTmpDirAsHomeHook,
|
||||
@@ -51,25 +52,25 @@ stdenvNoCC.mkDerivation (finalAttrs: {
|
||||
runHook postBuild
|
||||
'';
|
||||
|
||||
installPhase =
|
||||
''
|
||||
runHook preInstall
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
|
||||
install -Dm755 dist/@kilocode/cli-*/bin/kilo $out/bin/kilo
|
||||
install -Dm644 schema.json $out/share/kilo/schema.json
|
||||
''
|
||||
# bun runs sysctl to detect if dunning on rosetta2
|
||||
+ lib.optionalString stdenvNoCC.hostPlatform.isDarwin ''
|
||||
wrapProgram $out/bin/kilo \
|
||||
--prefix PATH : ${
|
||||
lib.makeBinPath [
|
||||
sysctl
|
||||
install -Dm755 dist/@kilocode/cli-*/bin/kilo $out/bin/kilo
|
||||
install -Dm644 schema.json $out/share/kilo/schema.json
|
||||
|
||||
wrapProgram $out/bin/kilo \
|
||||
--prefix PATH : ${
|
||||
lib.makeBinPath (
|
||||
[
|
||||
ripgrep
|
||||
]
|
||||
}
|
||||
''
|
||||
+ ''
|
||||
runHook postInstall
|
||||
'';
|
||||
# bun runs sysctl to detect if dunning on rosetta2
|
||||
++ lib.optional stdenvNoCC.hostPlatform.isDarwin sysctl
|
||||
)
|
||||
}
|
||||
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
postInstall = lib.optionalString (stdenvNoCC.buildPlatform.canExecute stdenvNoCC.hostPlatform) ''
|
||||
# trick yargs into also generating zsh completions
|
||||
|
||||
+4
-3
@@ -4,11 +4,11 @@
|
||||
"description": "AI-powered development tool",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.3.11",
|
||||
"packageManager": "bun@1.3.13",
|
||||
"scripts": {
|
||||
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
||||
"dev-setup": "bin/kilodev dev-setup",
|
||||
"dev:desktop": "bun --cwd packages/desktop tauri dev",
|
||||
"dev:desktop": "bun --cwd packages/desktop-electron dev",
|
||||
"dev:web": "bun --cwd packages/app dev",
|
||||
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
|
||||
"dev:storybook": "bun --cwd packages/storybook storybook",
|
||||
@@ -30,7 +30,7 @@
|
||||
"@effect/opentelemetry": "4.0.0-beta.48",
|
||||
"@effect/platform-node": "4.0.0-beta.48",
|
||||
"@npmcli/arborist": "9.4.0",
|
||||
"@types/bun": "1.3.11",
|
||||
"@types/bun": "1.3.12",
|
||||
"@types/cross-spawn": "6.0.6",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
@@ -142,6 +142,7 @@
|
||||
"happy-dom": ">=20.8.9"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"@npmcli/agent@4.0.0": "patches/@npmcli%2Fagent@4.0.0.patch",
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
||||
"stream-chat@9.38.0": "patches/stream-chat@9.38.0.patch"
|
||||
|
||||
Binary file not shown.
+21
-21
@@ -141,13 +141,11 @@ export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
|
||||
<LanguageProvider locale={props.locale}>
|
||||
<UiI18nBridge>
|
||||
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
|
||||
<QueryProvider>
|
||||
<DialogProvider>
|
||||
<MarkedProvider>
|
||||
<FileComponentProvider component={File}>{props.children}</FileComponentProvider>
|
||||
</MarkedProvider>
|
||||
</DialogProvider>
|
||||
</QueryProvider>
|
||||
<DialogProvider>
|
||||
<MarkedProvider>
|
||||
<FileComponentProvider component={File}>{props.children}</FileComponentProvider>
|
||||
</MarkedProvider>
|
||||
</DialogProvider>
|
||||
</ErrorBoundary>
|
||||
</UiI18nBridge>
|
||||
</LanguageProvider>
|
||||
@@ -293,20 +291,22 @@ export function AppInterface(props: {
|
||||
>
|
||||
<ConnectionGate disableHealthCheck={props.disableHealthCheck}>
|
||||
<ServerKey>
|
||||
<GlobalSDKProvider>
|
||||
<GlobalSyncProvider>
|
||||
<Dynamic
|
||||
component={props.router ?? Router}
|
||||
root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>}
|
||||
>
|
||||
<Route path="/" component={HomeRoute} />
|
||||
<Route path="/:dir" component={DirectoryLayout}>
|
||||
<Route path="/" component={SessionIndexRoute} />
|
||||
<Route path="/session/:id?" component={SessionRoute} />
|
||||
</Route>
|
||||
</Dynamic>
|
||||
</GlobalSyncProvider>
|
||||
</GlobalSDKProvider>
|
||||
<QueryProvider>
|
||||
<GlobalSDKProvider>
|
||||
<GlobalSyncProvider>
|
||||
<Dynamic
|
||||
component={props.router ?? Router}
|
||||
root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>}
|
||||
>
|
||||
<Route path="/" component={HomeRoute} />
|
||||
<Route path="/:dir" component={DirectoryLayout}>
|
||||
<Route path="/" component={SessionIndexRoute} />
|
||||
<Route path="/session/:id?" component={SessionRoute} />
|
||||
</Route>
|
||||
</Dynamic>
|
||||
</GlobalSyncProvider>
|
||||
</GlobalSDKProvider>
|
||||
</QueryProvider>
|
||||
</ServerKey>
|
||||
</ConnectionGate>
|
||||
</ServerProvider>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { type LocalProject, getAvatarColors } from "@/context/layout"
|
||||
import { getFilename } from "@opencode-ai/shared/util/path"
|
||||
import { Avatar } from "@opencode-ai/ui/avatar"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { getProjectAvatarSource } from "@/pages/layout/sidebar-items"
|
||||
|
||||
const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
|
||||
|
||||
@@ -26,8 +27,8 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
|
||||
const [store, setStore] = createStore({
|
||||
name: defaultName(),
|
||||
color: props.project.icon?.color || "pink",
|
||||
iconUrl: props.project.icon?.override || "",
|
||||
color: props.project.icon?.color,
|
||||
iconOverride: props.project.icon?.override,
|
||||
startup: props.project.commands?.start ?? "",
|
||||
dragOver: false,
|
||||
iconHover: false,
|
||||
@@ -39,7 +40,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
if (!file.type.startsWith("image/")) return
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
setStore("iconUrl", e.target?.result as string)
|
||||
setStore("iconOverride", e.target?.result as string)
|
||||
setStore("iconHover", false)
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
@@ -68,7 +69,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
}
|
||||
|
||||
function clearIcon() {
|
||||
setStore("iconUrl", "")
|
||||
setStore("iconOverride", "")
|
||||
}
|
||||
|
||||
const saveMutation = useMutation(() => ({
|
||||
@@ -81,17 +82,17 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
projectID: props.project.id,
|
||||
directory: props.project.worktree,
|
||||
name,
|
||||
icon: { color: store.color, override: store.iconUrl },
|
||||
icon: { color: store.color || "", override: store.iconOverride || "" },
|
||||
commands: { start },
|
||||
})
|
||||
globalSync.project.icon(props.project.worktree, store.iconUrl || undefined)
|
||||
globalSync.project.icon(props.project.worktree, store.iconOverride || undefined)
|
||||
dialog.close()
|
||||
return
|
||||
}
|
||||
|
||||
globalSync.project.meta(props.project.worktree, {
|
||||
name,
|
||||
icon: { color: store.color, override: store.iconUrl || undefined },
|
||||
icon: { color: store.color || undefined, override: store.iconOverride || undefined },
|
||||
commands: { start: start || undefined },
|
||||
})
|
||||
dialog.close()
|
||||
@@ -130,13 +131,13 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
classList={{
|
||||
"border-text-interactive-base bg-surface-info-base/20": store.dragOver,
|
||||
"border-border-base hover:border-border-strong": !store.dragOver,
|
||||
"overflow-hidden": !!store.iconUrl,
|
||||
"overflow-hidden": !!store.iconOverride,
|
||||
}}
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onClick={() => {
|
||||
if (store.iconUrl && store.iconHover) {
|
||||
if (store.iconOverride && store.iconHover) {
|
||||
clearIcon()
|
||||
} else {
|
||||
iconInput?.click()
|
||||
@@ -144,7 +145,11 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
}}
|
||||
>
|
||||
<Show
|
||||
when={store.iconUrl}
|
||||
when={getProjectAvatarSource(props.project.id, {
|
||||
color: store.color,
|
||||
url: props.project.icon?.url,
|
||||
override: store.iconOverride,
|
||||
})}
|
||||
fallback={
|
||||
<div class="size-full flex items-center justify-center">
|
||||
<Avatar
|
||||
@@ -155,18 +160,20 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<img
|
||||
src={store.iconUrl}
|
||||
alt={language.t("dialog.project.edit.icon.alt")}
|
||||
class="size-full object-cover"
|
||||
/>
|
||||
{(src) => (
|
||||
<img
|
||||
src={src()}
|
||||
alt={language.t("dialog.project.edit.icon.alt")}
|
||||
class="size-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
<div
|
||||
class="absolute inset-0 size-16 bg-surface-raised-stronger-non-alpha/90 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
|
||||
classList={{
|
||||
"opacity-100": store.iconHover && !store.iconUrl,
|
||||
"opacity-0": !(store.iconHover && !store.iconUrl),
|
||||
"opacity-100": store.iconHover && !store.iconOverride,
|
||||
"opacity-0": !(store.iconHover && !store.iconOverride),
|
||||
}}
|
||||
>
|
||||
<Icon name="cloud-upload" size="large" class="text-icon-on-interactive-base drop-shadow-sm" />
|
||||
@@ -174,8 +181,8 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
<div
|
||||
class="absolute inset-0 size-16 bg-surface-raised-stronger-non-alpha/90 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
|
||||
classList={{
|
||||
"opacity-100": store.iconHover && !!store.iconUrl,
|
||||
"opacity-0": !(store.iconHover && !!store.iconUrl),
|
||||
"opacity-100": store.iconHover && !!store.iconOverride,
|
||||
"opacity-0": !(store.iconHover && !!store.iconOverride),
|
||||
}}
|
||||
>
|
||||
<Icon name="trash" size="large" class="text-icon-on-interactive-base drop-shadow-sm" />
|
||||
@@ -198,7 +205,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={!store.iconUrl}>
|
||||
<Show when={!store.iconOverride}>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-12-medium text-text-weak">{language.t("dialog.project.edit.color")}</label>
|
||||
<div class="flex gap-1.5">
|
||||
@@ -215,7 +222,10 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
"bg-transparent border border-transparent hover:bg-surface-base-hover hover:border-border-weak-base":
|
||||
store.color !== color,
|
||||
}}
|
||||
onClick={() => setStore("color", color)}
|
||||
onClick={() => {
|
||||
if (store.color === color && !props.project.icon?.url) return
|
||||
setStore("color", store.color === color ? undefined : color)
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
fallback={store.name || defaultName()}
|
||||
|
||||
@@ -504,7 +504,7 @@ export function DialogSelectServer() {
|
||||
|
||||
return (
|
||||
<Dialog title={formTitle()}>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-1 min-h-0 flex-col gap-2">
|
||||
<Show
|
||||
when={!isFormMode()}
|
||||
fallback={
|
||||
@@ -539,7 +539,7 @@ export function DialogSelectServer() {
|
||||
if (x) void select(x)
|
||||
}}
|
||||
divider={true}
|
||||
class="px-5 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]h-[300px] [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:min-h-14 [&_[data-slot=list-item]]:p-3 [&_[data-slot=list-item]]:!bg-transparent"
|
||||
class="flex-1 min-h-0 px-5 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:min-h-14 [&_[data-slot=list-item]]:p-3 [&_[data-slot=list-item]]:!bg-transparent"
|
||||
>
|
||||
{(i) => {
|
||||
const key = ServerConnection.key(i)
|
||||
@@ -619,7 +619,7 @@ export function DialogSelectServer() {
|
||||
</List>
|
||||
</Show>
|
||||
|
||||
<div class="px-5 pb-5">
|
||||
<div class="shrink-0 px-5 pb-5">
|
||||
<Show
|
||||
when={isFormMode()}
|
||||
fallback={
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
||||
import { useSpring } from "@opencode-ai/ui/motion-spring"
|
||||
import { createEffect, on, Component, Show, onCleanup, createMemo, createSignal } from "solid-js"
|
||||
import { createEffect, on, Component, Show, onCleanup, createMemo, createSignal, createResource } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useLocal } from "@/context/local"
|
||||
import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file"
|
||||
@@ -54,7 +54,7 @@ import { PromptImageAttachments } from "./prompt-input/image-attachments"
|
||||
import { PromptDragOverlay } from "./prompt-input/drag-overlay"
|
||||
import { promptPlaceholder } from "./prompt-input/placeholder"
|
||||
import { ImagePreview } from "@opencode-ai/ui/image-preview"
|
||||
import { useQuery } from "@tanstack/solid-query"
|
||||
import { useQueries } from "@tanstack/solid-query"
|
||||
import { loadAgentsQuery, loadProvidersQuery } from "@/context/global-sync/bootstrap"
|
||||
|
||||
interface PromptInputProps {
|
||||
@@ -1252,16 +1252,23 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
const agentsQuery = useQuery(() => loadAgentsQuery(sdk.directory))
|
||||
const [agentsQuery, globalProvidersQuery, providersQuery] = useQueries(() => ({
|
||||
queries: [loadAgentsQuery(sdk.directory), loadProvidersQuery(null), loadProvidersQuery(sdk.directory)],
|
||||
}))
|
||||
|
||||
const agentsLoading = () => agentsQuery.isLoading
|
||||
|
||||
const globalProvidersQuery = useQuery(() => loadProvidersQuery(null))
|
||||
const providersQuery = useQuery(() => loadProvidersQuery(sdk.directory))
|
||||
|
||||
const agentsShouldFadeIn = createMemo((prev) => prev ?? agentsLoading())
|
||||
const providersLoading = () => agentsLoading() || providersQuery.isLoading || globalProvidersQuery.isLoading
|
||||
const providersShouldFadeIn = createMemo((prev) => prev ?? providersLoading())
|
||||
|
||||
const [promptReady] = createResource(
|
||||
() => prompt.ready().promise,
|
||||
(p) => p,
|
||||
)
|
||||
|
||||
return (
|
||||
<div class="relative size-full _max-h-[320px] flex flex-col gap-0">
|
||||
{(promptReady(), null)}
|
||||
<PromptPopover
|
||||
popover={store.popover}
|
||||
setSlashPopoverRef={(el) => (slashPopoverRef = el)}
|
||||
@@ -1359,15 +1366,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
}}
|
||||
style={{ "padding-bottom": space }}
|
||||
/>
|
||||
<Show when={!prompt.dirty()}>
|
||||
<div
|
||||
class="absolute top-0 inset-x-0 pl-3 pr-2 pt-2 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate"
|
||||
classList={{ "font-mono!": store.mode === "shell" }}
|
||||
style={{ "padding-bottom": space }}
|
||||
>
|
||||
{placeholder()}
|
||||
</div>
|
||||
</Show>
|
||||
<div
|
||||
class="absolute top-0 inset-x-0 pl-3 pr-2 pt-2 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate"
|
||||
classList={{ "font-mono!": store.mode === "shell" }}
|
||||
style={{ "padding-bottom": space, display: prompt.dirty() ? "none" : undefined }}
|
||||
>
|
||||
{placeholder()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -1461,7 +1466,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 min-w-0 flex-1 h-7">
|
||||
<Show when={!agentsLoading()}>
|
||||
<div data-component="prompt-agent-control">
|
||||
<div
|
||||
data-component="prompt-agent-control"
|
||||
style={agentsShouldFadeIn() ? { animation: "fade-in 0.3s" } : undefined}
|
||||
>
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={4}
|
||||
@@ -1487,7 +1495,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
</Show>
|
||||
<Show when={!providersLoading()}>
|
||||
<Show when={store.mode !== "shell"}>
|
||||
<div data-component="prompt-model-control">
|
||||
<div
|
||||
data-component="prompt-model-control"
|
||||
style={providersShouldFadeIn() ? { animation: "fade-in 0.3s" } : undefined}
|
||||
>
|
||||
<Show
|
||||
when={providers.paid().length > 0}
|
||||
fallback={
|
||||
@@ -1558,7 +1569,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
</TooltipKeybind>
|
||||
</Show>
|
||||
</div>
|
||||
<div data-component="prompt-variant-control">
|
||||
<div
|
||||
data-component="prompt-variant-control"
|
||||
style={providersShouldFadeIn() ? { animation: "fade-in 0.3s" } : undefined}
|
||||
>
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={4}
|
||||
|
||||
@@ -19,6 +19,9 @@ import {
|
||||
sansDefault,
|
||||
sansFontFamily,
|
||||
sansInput,
|
||||
terminalDefault,
|
||||
terminalFontFamily,
|
||||
terminalInput,
|
||||
useSettings,
|
||||
} from "@/context/settings"
|
||||
import { decode64 } from "@/utils/base64"
|
||||
@@ -181,6 +184,7 @@ export const SettingsGeneral: Component = () => {
|
||||
const soundOptions = [noneSound, ...SOUND_OPTIONS]
|
||||
const mono = () => monoInput(settings.appearance.font())
|
||||
const sans = () => sansInput(settings.appearance.uiFont())
|
||||
const terminal = () => terminalInput(settings.appearance.terminalFont())
|
||||
|
||||
const soundSelectProps = (
|
||||
enabled: () => boolean,
|
||||
@@ -276,6 +280,18 @@ export const SettingsGeneral: Component = () => {
|
||||
/>
|
||||
</div>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
title={language.t("settings.general.row.showSessionProgressBar.title")}
|
||||
description={language.t("settings.general.row.showSessionProgressBar.description")}
|
||||
>
|
||||
<div data-action="settings-show-session-progress-bar">
|
||||
<Switch
|
||||
checked={settings.general.showSessionProgressBar()}
|
||||
onChange={(checked) => settings.general.setShowSessionProgressBar(checked)}
|
||||
/>
|
||||
</div>
|
||||
</SettingsRow>
|
||||
</SettingsList>
|
||||
</div>
|
||||
)
|
||||
@@ -451,6 +467,29 @@ export const SettingsGeneral: Component = () => {
|
||||
/>
|
||||
</div>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
title={language.t("settings.general.row.terminalFont.title")}
|
||||
description={language.t("settings.general.row.terminalFont.description")}
|
||||
>
|
||||
<div class="w-full sm:w-[220px]">
|
||||
<TextField
|
||||
data-action="settings-terminal-font"
|
||||
label={language.t("settings.general.row.terminalFont.title")}
|
||||
hideLabel
|
||||
type="text"
|
||||
value={terminal()}
|
||||
onChange={(value) => settings.appearance.setTerminalFont(value)}
|
||||
placeholder={terminalDefault}
|
||||
spellcheck={false}
|
||||
autocorrect="off"
|
||||
autocomplete="off"
|
||||
autocapitalize="off"
|
||||
class="text-12-regular"
|
||||
style={{ "font-family": terminalFontFamily(settings.appearance.terminalFont()) }}
|
||||
/>
|
||||
</div>
|
||||
</SettingsRow>
|
||||
</SettingsList>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useLanguage } from "@/context/language"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { useServer } from "@/context/server"
|
||||
import { monoFontFamily, useSettings } from "@/context/settings"
|
||||
import { terminalFontFamily, useSettings } from "@/context/settings"
|
||||
import type { LocalPTY } from "@/context/terminal"
|
||||
import { disposeIfDisposable, getHoveredLinkText, setOptionIfSupported } from "@/utils/runtime-adapters"
|
||||
import { terminalWriter } from "@/utils/terminal-writer"
|
||||
@@ -300,7 +300,7 @@ export const Terminal = (props: TerminalProps) => {
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const font = monoFontFamily(settings.appearance.font())
|
||||
const font = terminalFontFamily(settings.appearance.terminalFont())
|
||||
if (!term) return
|
||||
setOptionIfSupported(term, "fontFamily", font)
|
||||
scheduleFit()
|
||||
@@ -360,7 +360,7 @@ export const Terminal = (props: TerminalProps) => {
|
||||
cols: restoreSize?.cols,
|
||||
rows: restoreSize?.rows,
|
||||
fontSize: 14,
|
||||
fontFamily: monoFontFamily(settings.appearance.font()),
|
||||
fontFamily: terminalFontFamily(settings.appearance.terminalFont()),
|
||||
allowTransparency: false,
|
||||
convertEol: false,
|
||||
theme: terminalColors(),
|
||||
|
||||
@@ -9,10 +9,9 @@ import type {
|
||||
} from "@kilocode/sdk/v2/client"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { getFilename } from "@opencode-ai/shared/util/path"
|
||||
import { createContext, getOwner, onCleanup, onMount, type ParentProps, untrack, useContext } from "solid-js"
|
||||
import { batch, createContext, getOwner, onCleanup, onMount, type ParentProps, untrack, useContext } from "solid-js"
|
||||
import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import type { InitError } from "../pages/error"
|
||||
import { useGlobalSDK } from "./global-sdk"
|
||||
import { bootstrapDirectory, bootstrapGlobal, clearProviderRev } from "./global-sync/bootstrap"
|
||||
@@ -24,7 +23,6 @@ import { estimateRootSessionTotal, loadRootSessionsWithFallback } from "./global
|
||||
import { trimSessions } from "./global-sync/session-trim"
|
||||
import type { ProjectMeta } from "./global-sync/types"
|
||||
import { SESSION_RECENT_LIMIT } from "./global-sync/types"
|
||||
import { sanitizeProject } from "./global-sync/utils"
|
||||
import { formatServerError } from "@/utils/server-errors"
|
||||
import { queryOptions, skipToken, useQueryClient } from "@tanstack/solid-query"
|
||||
|
||||
@@ -56,15 +54,10 @@ function createGlobalSync() {
|
||||
const sessionLoads = new Map<string, Promise<void>>()
|
||||
const sessionMeta = new Map<string, { limit: number }>()
|
||||
|
||||
const [projectCache, setProjectCache, projectInit] = persisted(
|
||||
Persist.global("globalSync.project", ["globalSync.project.v1"]),
|
||||
createStore({ value: [] as Project[] }),
|
||||
)
|
||||
|
||||
const [globalStore, setGlobalStore] = createStore<GlobalStore>({
|
||||
ready: false,
|
||||
path: { state: "", config: "", worktree: "", directory: "", home: "" },
|
||||
project: projectCache.value,
|
||||
project: [],
|
||||
session_todo: {},
|
||||
provider: { all: [], connected: [], default: {} },
|
||||
provider_auth: {},
|
||||
@@ -73,37 +66,18 @@ function createGlobalSync() {
|
||||
})
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
let active = true
|
||||
let projectWritten = false
|
||||
let bootedAt = 0
|
||||
let bootingRoot = false
|
||||
let eventFrame: number | undefined
|
||||
let eventTimer: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
onCleanup(() => {
|
||||
active = false
|
||||
})
|
||||
onCleanup(() => {
|
||||
if (eventFrame !== undefined) cancelAnimationFrame(eventFrame)
|
||||
if (eventTimer !== undefined) clearTimeout(eventTimer)
|
||||
})
|
||||
|
||||
const cacheProjects = () => {
|
||||
setProjectCache(
|
||||
"value",
|
||||
untrack(() => globalStore.project.map(sanitizeProject)),
|
||||
)
|
||||
}
|
||||
|
||||
const setProjects = (next: Project[] | ((draft: Project[]) => void)) => {
|
||||
projectWritten = true
|
||||
if (typeof next === "function") {
|
||||
setGlobalStore("project", produce(next))
|
||||
cacheProjects()
|
||||
return
|
||||
}
|
||||
const setProjects = (next: Project[] | ((draft: Project[]) => Project[])) => {
|
||||
setGlobalStore("project", next)
|
||||
cacheProjects()
|
||||
}
|
||||
|
||||
const setBootStore = ((...input: unknown[]) => {
|
||||
@@ -116,22 +90,12 @@ function createGlobalSync() {
|
||||
|
||||
const set = ((...input: unknown[]) => {
|
||||
if (input[0] === "project" && (Array.isArray(input[1]) || typeof input[1] === "function")) {
|
||||
setProjects(input[1] as Project[] | ((draft: Project[]) => void))
|
||||
setProjects(input[1] as Project[] | ((draft: Project[]) => Project[]))
|
||||
return input[1]
|
||||
}
|
||||
return (setGlobalStore as (...args: unknown[]) => unknown)(...input)
|
||||
}) as typeof setGlobalStore
|
||||
|
||||
if (projectInit instanceof Promise) {
|
||||
void projectInit.then(() => {
|
||||
if (!active) return
|
||||
if (projectWritten) return
|
||||
const cached = projectCache.value
|
||||
if (cached.length === 0) return
|
||||
setGlobalStore("project", cached)
|
||||
})
|
||||
}
|
||||
|
||||
const setSessionTodo = (sessionID: string, todos: Todo[] | undefined) => {
|
||||
if (!sessionID) return
|
||||
if (!todos) {
|
||||
@@ -223,16 +187,18 @@ function createGlobalSync() {
|
||||
limit,
|
||||
permission: store.permission,
|
||||
})
|
||||
setStore(
|
||||
"sessionTotal",
|
||||
estimateRootSessionTotal({
|
||||
count: nonArchived.length,
|
||||
limit: x.limit,
|
||||
limited: x.limited,
|
||||
}),
|
||||
)
|
||||
setStore("session", reconcile(sessions, { key: "id" }))
|
||||
cleanupDroppedSessionCaches(store, setStore, sessions, setSessionTodo)
|
||||
batch(() => {
|
||||
setStore(
|
||||
"sessionTotal",
|
||||
estimateRootSessionTotal({
|
||||
count: nonArchived.length,
|
||||
limit: x.limit,
|
||||
limited: x.limited,
|
||||
}),
|
||||
)
|
||||
setStore("session", reconcile(sessions, { key: "id" }))
|
||||
cleanupDroppedSessionCaches(store, setStore, sessions, setSessionTodo)
|
||||
})
|
||||
sessionMeta.set(directory, { limit })
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -298,6 +264,19 @@ function createGlobalSync() {
|
||||
const event = e.details
|
||||
const recent = bootingRoot || Date.now() - bootedAt < 1500
|
||||
|
||||
if (event.type === "session.error") {
|
||||
const error = event.properties.error
|
||||
if (error?.name !== "MessageAbortedError") {
|
||||
console.error("[global-sync] session error", {
|
||||
scope: directory === "global" ? "global" : "workspace",
|
||||
directory: directory === "global" ? undefined : directory,
|
||||
project: directory === "global" ? undefined : getFilename(directory),
|
||||
sessionID: event.properties.sessionID,
|
||||
error,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (directory === "global") {
|
||||
applyGlobalEvent({
|
||||
event,
|
||||
|
||||
@@ -19,7 +19,6 @@ import type { State, VcsCache } from "./types"
|
||||
import { cmp, normalizeAgentList, normalizeProviderList } from "./utils"
|
||||
import { formatServerError } from "@/utils/server-errors"
|
||||
import { QueryClient, queryOptions, skipToken } from "@tanstack/solid-query"
|
||||
import { loadSessionsQuery } from "../global-sync"
|
||||
|
||||
type GlobalStore = {
|
||||
ready: boolean
|
||||
@@ -82,6 +81,9 @@ export async function bootstrapGlobal(input: {
|
||||
input.setGlobalStore("config", x.data!)
|
||||
}),
|
||||
),
|
||||
]
|
||||
|
||||
const slow = [
|
||||
() =>
|
||||
input.queryClient.fetchQuery({
|
||||
...loadProvidersQuery(null),
|
||||
@@ -93,9 +95,6 @@ export async function bootstrapGlobal(input: {
|
||||
}),
|
||||
),
|
||||
}),
|
||||
]
|
||||
|
||||
const slow = [
|
||||
() =>
|
||||
retry(() =>
|
||||
input.globalSDK.path.get().then((x) => {
|
||||
@@ -183,8 +182,43 @@ function warmSessions(input: {
|
||||
export const loadProvidersQuery = (directory: string | null) =>
|
||||
queryOptions<null>({ queryKey: [directory, "providers"], queryFn: skipToken })
|
||||
|
||||
export const loadAgentsQuery = (directory: string | null) =>
|
||||
queryOptions<null>({ queryKey: [directory, "agents"], queryFn: skipToken })
|
||||
export const loadAgentsQuery = (
|
||||
directory: string | null,
|
||||
sdk?: KiloClient,
|
||||
transform?: (x: Awaited<ReturnType<KiloClient["app"]["agents"]>>) => void,
|
||||
) =>
|
||||
queryOptions<null>({
|
||||
queryKey: [directory, "agents"],
|
||||
queryFn:
|
||||
sdk && transform
|
||||
? () =>
|
||||
retry(() =>
|
||||
sdk.app
|
||||
.agents()
|
||||
.then(transform)
|
||||
.then(() => null),
|
||||
)
|
||||
: skipToken,
|
||||
})
|
||||
|
||||
export const loadPathQuery = (
|
||||
directory: string | null,
|
||||
sdk?: KiloClient,
|
||||
transform?: (x: Awaited<ReturnType<KiloClient["path"]["get"]>>) => void,
|
||||
) =>
|
||||
queryOptions<Path>({
|
||||
queryKey: [directory, "path"],
|
||||
queryFn:
|
||||
sdk && transform
|
||||
? () =>
|
||||
retry(() =>
|
||||
sdk.path.get().then(async (x) => {
|
||||
transform(x)
|
||||
return x.data!
|
||||
}),
|
||||
)
|
||||
: skipToken,
|
||||
})
|
||||
|
||||
export async function bootstrapDirectory(input: {
|
||||
directory: string
|
||||
@@ -222,45 +256,27 @@ export async function bootstrapDirectory(input: {
|
||||
input.setStore("lsp", [])
|
||||
if (loading) input.setStore("status", "partial")
|
||||
|
||||
const fast = [() => Promise.resolve(input.loadSessions(input.directory))]
|
||||
|
||||
const errs = errors(await runAll(fast))
|
||||
if (errs.length > 0) {
|
||||
console.error("Failed to bootstrap instance", errs[0])
|
||||
const project = getFilename(input.directory)
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: input.translate("toast.project.reloadFailed.title", { project }),
|
||||
description: formatServerError(errs[0], input.translate),
|
||||
})
|
||||
}
|
||||
|
||||
const rev = (providerRev.get(input.directory) ?? 0) + 1
|
||||
providerRev.set(input.directory, rev)
|
||||
;(async () => {
|
||||
const slow = [
|
||||
() => Promise.resolve(input.loadSessions(input.directory)),
|
||||
() =>
|
||||
input.queryClient.ensureQueryData({
|
||||
...loadAgentsQuery(input.directory),
|
||||
queryFn: () =>
|
||||
retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", normalizeAgentList(x.data)))).then(
|
||||
() => null,
|
||||
),
|
||||
}),
|
||||
input.queryClient.ensureQueryData(
|
||||
loadAgentsQuery(input.directory, input.sdk, (x) => input.setStore("agent", normalizeAgentList(x.data))),
|
||||
),
|
||||
() => retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))),
|
||||
() => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))),
|
||||
() =>
|
||||
seededProject
|
||||
? Promise.resolve()
|
||||
: retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id)),
|
||||
() =>
|
||||
seededPath
|
||||
? Promise.resolve()
|
||||
: retry(() =>
|
||||
input.sdk.path.get().then((x) => {
|
||||
input.setStore("path", x.data!)
|
||||
const next = projectID(x.data?.directory ?? input.directory, input.global.project)
|
||||
if (next) input.setStore("project", next)
|
||||
}),
|
||||
),
|
||||
!seededProject &&
|
||||
(() => retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id))),
|
||||
!seededPath &&
|
||||
(() =>
|
||||
input.queryClient.ensureQueryData(
|
||||
loadPathQuery(input.directory, input.sdk, (x) => {
|
||||
const next = projectID(x.data?.directory ?? input.directory, input.global.project)
|
||||
if (next) input.setStore("project", next)
|
||||
}),
|
||||
)),
|
||||
() =>
|
||||
retry(() =>
|
||||
input.sdk.vcs.get().then((x) => {
|
||||
@@ -330,7 +346,28 @@ export async function bootstrapDirectory(input: {
|
||||
input.setStore("mcp_ready", true)
|
||||
}),
|
||||
),
|
||||
]
|
||||
() =>
|
||||
input.queryClient.ensureQueryData({
|
||||
...loadProvidersQuery(input.directory),
|
||||
queryFn: () =>
|
||||
retry(() => input.sdk.provider.list())
|
||||
.then((x) => {
|
||||
if (providerRev.get(input.directory) !== rev) return
|
||||
input.setStore("provider", normalizeProviderList(x.data!))
|
||||
input.setStore("provider_ready", true)
|
||||
})
|
||||
.catch((err) => {
|
||||
if (providerRev.get(input.directory) !== rev) console.error("Failed to refresh provider list", err)
|
||||
const project = getFilename(input.directory)
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: input.translate("toast.project.reloadFailed.title", { project }),
|
||||
description: formatServerError(err, input.translate),
|
||||
})
|
||||
})
|
||||
.then(() => null),
|
||||
}),
|
||||
].filter(Boolean) as (() => Promise<any>)[]
|
||||
|
||||
await waitForPaint()
|
||||
const slowErrs = errors(await runAll(slow))
|
||||
@@ -344,29 +381,6 @@ export async function bootstrapDirectory(input: {
|
||||
})
|
||||
}
|
||||
|
||||
if (loading && errs.length === 0 && slowErrs.length === 0) input.setStore("status", "complete")
|
||||
|
||||
const rev = (providerRev.get(input.directory) ?? 0) + 1
|
||||
providerRev.set(input.directory, rev)
|
||||
void input.queryClient.ensureQueryData({
|
||||
...loadSessionsQuery(input.directory),
|
||||
queryFn: () =>
|
||||
retry(() => input.sdk.provider.list())
|
||||
.then((x) => {
|
||||
if (providerRev.get(input.directory) !== rev) return
|
||||
input.setStore("provider", normalizeProviderList(x.data!))
|
||||
input.setStore("provider_ready", true)
|
||||
})
|
||||
.catch((err) => {
|
||||
if (providerRev.get(input.directory) !== rev) console.error("Failed to refresh provider list", err)
|
||||
const project = getFilename(input.directory)
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: input.translate("toast.project.reloadFailed.title", { project }),
|
||||
description: formatServerError(err, input.translate),
|
||||
})
|
||||
})
|
||||
.then(() => null),
|
||||
})
|
||||
if (loading && slowErrs.length === 0) input.setStore("status", "complete")
|
||||
})()
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
type VcsCache,
|
||||
} from "./types"
|
||||
import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction"
|
||||
import { useQuery } from "@tanstack/solid-query"
|
||||
import { loadPathQuery } from "./bootstrap"
|
||||
|
||||
export function createChildStoreManager(input: {
|
||||
owner: Owner
|
||||
@@ -154,16 +156,21 @@ export function createChildStoreManager(input: {
|
||||
|
||||
const init = () =>
|
||||
createRoot((dispose) => {
|
||||
const initialMeta = meta[0].value
|
||||
const initialIcon = icon[0].value
|
||||
|
||||
const pathQuery = useQuery(() => loadPathQuery(directory))
|
||||
const child = createStore<State>({
|
||||
project: "",
|
||||
projectMeta: initialMeta,
|
||||
projectMeta: undefined,
|
||||
icon: initialIcon,
|
||||
provider_ready: false,
|
||||
provider: { all: [], connected: [], default: {} },
|
||||
config: {},
|
||||
path: { state: "", config: "", worktree: "", directory: "", home: "" },
|
||||
get path() {
|
||||
if (pathQuery.isLoading || !pathQuery.data)
|
||||
return { state: "", config: "", worktree: "", directory: "", home: "" }
|
||||
return pathQuery.data
|
||||
},
|
||||
status: "loading" as const,
|
||||
agent: [],
|
||||
command: [],
|
||||
@@ -200,11 +207,6 @@ export function createChildStoreManager(input: {
|
||||
child[1]("vcs", (value) => value ?? cached)
|
||||
})
|
||||
|
||||
onPersistedInit(meta[2], () => {
|
||||
if (child[0].projectMeta !== initialMeta) return
|
||||
child[1]("projectMeta", meta[0].value)
|
||||
})
|
||||
|
||||
onPersistedInit(icon[2], () => {
|
||||
if (child[0].icon !== initialIcon) return
|
||||
child[1]("icon", icon[0].value)
|
||||
|
||||
@@ -21,7 +21,7 @@ const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
||||
export function applyGlobalEvent(input: {
|
||||
event: { type: string; properties?: unknown }
|
||||
project: Project[]
|
||||
setGlobalProject: (next: Project[] | ((draft: Project[]) => void)) => void
|
||||
setGlobalProject: (next: Project[] | ((draft: Project[]) => Project[])) => void
|
||||
refresh: () => void
|
||||
}) {
|
||||
if (input.event.type === "global.disposed" || input.event.type === "server.connected") {
|
||||
@@ -33,14 +33,18 @@ export function applyGlobalEvent(input: {
|
||||
const properties = input.event.properties as Project
|
||||
const result = Binary.search(input.project, properties.id, (s) => s.id)
|
||||
if (result.found) {
|
||||
input.setGlobalProject((draft) => {
|
||||
draft[result.index] = { ...draft[result.index], ...properties }
|
||||
})
|
||||
input.setGlobalProject(
|
||||
produce((draft) => {
|
||||
draft[result.index] = { ...draft[result.index], ...properties }
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
input.setGlobalProject((draft) => {
|
||||
draft.splice(result.index, 0, properties)
|
||||
})
|
||||
input.setGlobalProject(
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 0, properties)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function cleanupSessionCaches(
|
||||
|
||||
@@ -391,37 +391,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
? globalSync.data.project.find((x) => x.id === projectID)
|
||||
: globalSync.data.project.find((x) => x.worktree === project.worktree)
|
||||
|
||||
const local = childStore.projectMeta
|
||||
const localOverride =
|
||||
local?.name !== undefined ||
|
||||
local?.commands?.start !== undefined ||
|
||||
local?.icon?.override !== undefined ||
|
||||
local?.icon?.color !== undefined
|
||||
|
||||
const base = {
|
||||
...metadata,
|
||||
...project,
|
||||
icon: {
|
||||
url: metadata?.icon?.url,
|
||||
override: metadata?.icon?.override ?? childStore.icon,
|
||||
color: metadata?.icon?.color,
|
||||
},
|
||||
}
|
||||
|
||||
const isGlobal = projectID === "global" || (metadata?.id === undefined && localOverride)
|
||||
if (!isGlobal) return base
|
||||
|
||||
return {
|
||||
...base,
|
||||
id: base.id ?? "global",
|
||||
name: local?.name,
|
||||
commands: local?.commands,
|
||||
icon: {
|
||||
url: base.icon?.url,
|
||||
override: local?.icon?.override,
|
||||
color: local?.icon?.color,
|
||||
},
|
||||
}
|
||||
return { ...metadata, ...project }
|
||||
}
|
||||
|
||||
const roots = createMemo(() => {
|
||||
@@ -516,7 +486,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
}
|
||||
|
||||
for (const project of projects) {
|
||||
if (project.icon?.color) continue
|
||||
if (project.icon?.color || project.icon?.override || project.icon?.url) continue
|
||||
const worktree = project.worktree
|
||||
const existing = colors[worktree]
|
||||
const color = existing ?? pickAvailableColor(used)
|
||||
|
||||
@@ -185,9 +185,9 @@ function createPromptSession(dir: string, id: string | undefined) {
|
||||
|
||||
return {
|
||||
ready,
|
||||
current: createMemo(() => store.prompt),
|
||||
current: () => store.prompt,
|
||||
cursor: createMemo(() => store.cursor),
|
||||
dirty: createMemo(() => !isPromptEqual(store.prompt, DEFAULT_PROMPT)),
|
||||
dirty: () => !isPromptEqual(store.prompt, DEFAULT_PROMPT),
|
||||
context: {
|
||||
items: createMemo(() => store.context.items),
|
||||
add(item: ContextItem) {
|
||||
@@ -277,7 +277,7 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
|
||||
const pick = (scope?: Scope) => (scope ? load(scope.dir, scope.id) : session())
|
||||
|
||||
return {
|
||||
ready: () => session().ready(),
|
||||
ready: () => session().ready,
|
||||
current: () => session().current(),
|
||||
cursor: () => session().cursor(),
|
||||
dirty: () => session().dirty(),
|
||||
|
||||
@@ -31,6 +31,7 @@ export interface Settings {
|
||||
showReasoningSummaries: boolean
|
||||
shellToolPartsExpanded: boolean
|
||||
editToolPartsExpanded: boolean
|
||||
showSessionProgressBar: boolean
|
||||
}
|
||||
updates: {
|
||||
startup: boolean
|
||||
@@ -39,6 +40,7 @@ export interface Settings {
|
||||
fontSize: number
|
||||
mono: string
|
||||
sans: string
|
||||
terminal: string
|
||||
}
|
||||
keybinds: Record<string, string>
|
||||
permissions: {
|
||||
@@ -50,13 +52,17 @@ export interface Settings {
|
||||
|
||||
export const monoDefault = "System Mono"
|
||||
export const sansDefault = "System Sans"
|
||||
export const terminalDefault = "JetBrainsMono Nerd Font Mono"
|
||||
|
||||
const monoFallback =
|
||||
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
|
||||
const sansFallback = 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
|
||||
const terminalFallback =
|
||||
'"JetBrainsMono Nerd Font Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
|
||||
|
||||
const monoBase = monoFallback
|
||||
const sansBase = sansFallback
|
||||
const terminalBase = terminalFallback
|
||||
|
||||
function input(font: string | undefined) {
|
||||
return font ?? ""
|
||||
@@ -89,6 +95,14 @@ export function sansFontFamily(font: string | undefined) {
|
||||
return stack(font, sansBase)
|
||||
}
|
||||
|
||||
export function terminalInput(font: string | undefined) {
|
||||
return input(font)
|
||||
}
|
||||
|
||||
export function terminalFontFamily(font: string | undefined) {
|
||||
return stack(font, terminalBase)
|
||||
}
|
||||
|
||||
const defaultSettings: Settings = {
|
||||
general: {
|
||||
autoSave: true,
|
||||
@@ -102,6 +116,7 @@ const defaultSettings: Settings = {
|
||||
showReasoningSummaries: false,
|
||||
shellToolPartsExpanded: false,
|
||||
editToolPartsExpanded: false,
|
||||
showSessionProgressBar: true,
|
||||
},
|
||||
updates: {
|
||||
startup: true,
|
||||
@@ -110,6 +125,7 @@ const defaultSettings: Settings = {
|
||||
fontSize: 14,
|
||||
mono: "",
|
||||
sans: "",
|
||||
terminal: "",
|
||||
},
|
||||
keybinds: {},
|
||||
permissions: {
|
||||
@@ -213,6 +229,13 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
||||
setEditToolPartsExpanded(value: boolean) {
|
||||
setStore("general", "editToolPartsExpanded", value)
|
||||
},
|
||||
showSessionProgressBar: withFallback(
|
||||
() => store.general?.showSessionProgressBar,
|
||||
defaultSettings.general.showSessionProgressBar,
|
||||
),
|
||||
setShowSessionProgressBar(value: boolean) {
|
||||
setStore("general", "showSessionProgressBar", value)
|
||||
},
|
||||
},
|
||||
updates: {
|
||||
startup: withFallback(() => store.updates?.startup, defaultSettings.updates.startup),
|
||||
@@ -233,6 +256,10 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
||||
setUIFont(value: string) {
|
||||
setStore("appearance", "sans", value.trim() ? value : "")
|
||||
},
|
||||
terminalFont: withFallback(() => store.appearance?.terminal, defaultSettings.appearance.terminal),
|
||||
setTerminalFont(value: string) {
|
||||
setStore("appearance", "terminal", value.trim() ? value : "")
|
||||
},
|
||||
},
|
||||
keybinds: {
|
||||
get: (action: string) => store.keybinds?.[action],
|
||||
|
||||
Generated
+5
-1
@@ -564,7 +564,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "السمة",
|
||||
"settings.general.row.theme.description": "تخصيص سمة Kilo.",
|
||||
"settings.general.row.font.title": "خط الكود",
|
||||
"settings.general.row.font.description": "خصّص الخط المستخدم في كتل التعليمات البرمجية والطرفيات",
|
||||
"settings.general.row.font.description": "خصّص الخط المستخدم في كتل التعليمات البرمجية",
|
||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||
"settings.general.row.uiFont.title": "خط الواجهة",
|
||||
"settings.general.row.uiFont.description": "خصّص الخط المستخدم في الواجهة بأكملها",
|
||||
"settings.general.row.followup.title": "سلوك المتابعة",
|
||||
@@ -579,6 +581,8 @@ export const dict = {
|
||||
"settings.general.row.editToolPartsExpanded.title": "توسيع أجزاء أداة edit",
|
||||
"settings.general.row.editToolPartsExpanded.description":
|
||||
"إظهار أجزاء أدوات edit و write و patch موسعة بشكل افتراضي في الشريط الزمني",
|
||||
"settings.general.row.showSessionProgressBar.title": "إظهار شريط تقدم الجلسة",
|
||||
"settings.general.row.showSessionProgressBar.description": "عرض شريط التقدم المتحرك أعلى الجلسة أثناء عمل الوكيل",
|
||||
"settings.general.row.wayland.title": "استخدام Wayland الأصلي",
|
||||
"settings.general.row.wayland.description": "تعطيل التراجع إلى X11 على Wayland. يتطلب إعادة التشغيل.",
|
||||
"settings.general.row.wayland.tooltip":
|
||||
|
||||
Generated
+6
-1
@@ -572,7 +572,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "Tema",
|
||||
"settings.general.row.theme.description": "Personalize como o Kilo é tematizado.",
|
||||
"settings.general.row.font.title": "Fonte de código",
|
||||
"settings.general.row.font.description": "Personalize a fonte usada em blocos de código e terminais",
|
||||
"settings.general.row.font.description": "Personalize a fonte usada em blocos de código",
|
||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||
"settings.general.row.uiFont.title": "Fonte da interface",
|
||||
"settings.general.row.uiFont.description": "Personalize a fonte usada em toda a interface",
|
||||
"settings.general.row.followup.title": "Comportamento de acompanhamento",
|
||||
@@ -588,6 +590,9 @@ export const dict = {
|
||||
"settings.general.row.editToolPartsExpanded.title": "Expandir partes da ferramenta de edição",
|
||||
"settings.general.row.editToolPartsExpanded.description":
|
||||
"Mostrar partes das ferramentas de edição, escrita e patch expandidas por padrão na linha do tempo",
|
||||
"settings.general.row.showSessionProgressBar.title": "Mostrar barra de progresso da sessão",
|
||||
"settings.general.row.showSessionProgressBar.description":
|
||||
"Exibir a barra de progresso animada no topo da sessão quando o agente estiver trabalhando",
|
||||
"settings.general.row.wayland.title": "Usar Wayland nativo",
|
||||
"settings.general.row.wayland.description": "Desabilitar fallback X11 no Wayland. Requer reinicialização.",
|
||||
"settings.general.row.wayland.tooltip":
|
||||
|
||||
Generated
+6
-1
@@ -637,7 +637,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "Tema",
|
||||
"settings.general.row.theme.description": "Prilagodi temu Kilo-a.",
|
||||
"settings.general.row.font.title": "Font za kod",
|
||||
"settings.general.row.font.description": "Prilagodi font koji se koristi u blokovima koda i terminalima",
|
||||
"settings.general.row.font.description": "Prilagodi font koji se koristi u blokovima koda",
|
||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||
"settings.general.row.uiFont.title": "UI font",
|
||||
"settings.general.row.uiFont.description": "Prilagodi font koji se koristi u cijelom interfejsu",
|
||||
"settings.general.row.followup.title": "Ponašanje nadovezivanja",
|
||||
@@ -653,6 +655,9 @@ export const dict = {
|
||||
"settings.general.row.editToolPartsExpanded.title": "Proširi dijelove alata za uređivanje",
|
||||
"settings.general.row.editToolPartsExpanded.description":
|
||||
"Prikaži dijelove alata za uređivanje, pisanje i patch podrazumijevano proširene na vremenskoj traci",
|
||||
"settings.general.row.showSessionProgressBar.title": "Prikaži traku napretka sesije",
|
||||
"settings.general.row.showSessionProgressBar.description":
|
||||
"Prikaži animiranu traku napretka na vrhu sesije kada agent radi",
|
||||
"settings.general.row.wayland.title": "Koristi nativni Wayland",
|
||||
"settings.general.row.wayland.description": "Onemogući X11 fallback na Waylandu. Zahtijeva restart.",
|
||||
"settings.general.row.wayland.tooltip":
|
||||
|
||||
Generated
+6
-1
@@ -632,7 +632,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "Tema",
|
||||
"settings.general.row.theme.description": "Tilpas hvordan Kilo er temabestemt.",
|
||||
"settings.general.row.font.title": "Kode-skrifttype",
|
||||
"settings.general.row.font.description": "Tilpas skrifttypen, der bruges i kodeblokke og terminaler",
|
||||
"settings.general.row.font.description": "Tilpas skrifttypen, der bruges i kodeblokke",
|
||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||
"settings.general.row.uiFont.title": "UI-skrifttype",
|
||||
"settings.general.row.uiFont.description": "Tilpas skrifttypen, der bruges i hele brugerfladen",
|
||||
"settings.general.row.followup.title": "Opfølgningsadfærd",
|
||||
@@ -647,6 +649,9 @@ export const dict = {
|
||||
"settings.general.row.editToolPartsExpanded.title": "Udvid edit-værktøjsdele",
|
||||
"settings.general.row.editToolPartsExpanded.description":
|
||||
"Vis edit-, write- og patch-værktøjsdele udvidet som standard i tidslinjen",
|
||||
"settings.general.row.showSessionProgressBar.title": "Vis sessionens fremdriftslinje",
|
||||
"settings.general.row.showSessionProgressBar.description":
|
||||
"Vis den animerede fremdriftslinje øverst i sessionen, når agenten arbejder",
|
||||
"settings.general.row.wayland.title": "Brug native Wayland",
|
||||
"settings.general.row.wayland.description": "Deaktiver X11-fallback på Wayland. Kræver genstart.",
|
||||
"settings.general.row.wayland.tooltip":
|
||||
|
||||
Generated
+6
-1
@@ -581,7 +581,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "Thema",
|
||||
"settings.general.row.theme.description": "Das Thema von Kilo anpassen.",
|
||||
"settings.general.row.font.title": "Code-Schriftart",
|
||||
"settings.general.row.font.description": "Die in Codeblöcken und Terminals verwendete Schriftart anpassen",
|
||||
"settings.general.row.font.description": "Die in Codeblöcken verwendete Schriftart anpassen",
|
||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||
"settings.general.row.uiFont.title": "UI-Schriftart",
|
||||
"settings.general.row.uiFont.description": "Die im gesamten Interface verwendete Schriftart anpassen",
|
||||
"settings.general.row.followup.title": "Verhalten bei Folgefragen",
|
||||
@@ -598,6 +600,9 @@ export const dict = {
|
||||
"settings.general.row.editToolPartsExpanded.title": "Edit-Tool-Abschnitte ausklappen",
|
||||
"settings.general.row.editToolPartsExpanded.description":
|
||||
"Edit-, Write- und Patch-Tool-Abschnitte standardmäßig in der Timeline ausgeklappt anzeigen",
|
||||
"settings.general.row.showSessionProgressBar.title": "Sitzungsfortschrittsleiste anzeigen",
|
||||
"settings.general.row.showSessionProgressBar.description":
|
||||
"Die animierte Fortschrittsleiste oben in der Sitzung anzeigen, wenn der Agent arbeitet",
|
||||
"settings.general.row.wayland.title": "Natives Wayland verwenden",
|
||||
"settings.general.row.wayland.description": "X11-Fallback unter Wayland deaktivieren. Erfordert Neustart.",
|
||||
"settings.general.row.wayland.tooltip":
|
||||
|
||||
@@ -735,7 +735,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "Theme",
|
||||
"settings.general.row.theme.description": "Customise how Kilo is themed.",
|
||||
"settings.general.row.font.title": "Code Font",
|
||||
"settings.general.row.font.description": "Customise the font used in code blocks and terminals",
|
||||
"settings.general.row.font.description": "Customise the font used in code blocks",
|
||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||
"settings.general.row.uiFont.title": "UI Font",
|
||||
"settings.general.row.uiFont.description": "Customise the font used throughout the interface",
|
||||
"settings.general.row.followup.title": "Follow-up behavior",
|
||||
@@ -760,6 +762,9 @@ export const dict = {
|
||||
"settings.general.row.editToolPartsExpanded.title": "Expand edit tool parts",
|
||||
"settings.general.row.editToolPartsExpanded.description":
|
||||
"Show edit, write, and patch tool parts expanded by default in the timeline",
|
||||
"settings.general.row.showSessionProgressBar.title": "Show session progress bar",
|
||||
"settings.general.row.showSessionProgressBar.description":
|
||||
"Display the animated progress bar at the top of the session when the agent is working",
|
||||
|
||||
"settings.general.row.wayland.title": "Use native Wayland",
|
||||
"settings.general.row.wayland.description": "Disable X11 fallback on Wayland. Requires restart.",
|
||||
|
||||
Generated
+6
-1
@@ -640,7 +640,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "Tema",
|
||||
"settings.general.row.theme.description": "Personaliza el tema de Kilo.",
|
||||
"settings.general.row.font.title": "Fuente de código",
|
||||
"settings.general.row.font.description": "Personaliza la fuente usada en bloques de código y terminales",
|
||||
"settings.general.row.font.description": "Personaliza la fuente usada en bloques de código",
|
||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||
"settings.general.row.uiFont.title": "Fuente de la interfaz",
|
||||
"settings.general.row.uiFont.description": "Personaliza la fuente usada en toda la interfaz",
|
||||
"settings.general.row.followup.title": "Comportamiento de seguimiento",
|
||||
@@ -657,6 +659,9 @@ export const dict = {
|
||||
"settings.general.row.editToolPartsExpanded.title": "Expandir partes de la herramienta de edición",
|
||||
"settings.general.row.editToolPartsExpanded.description":
|
||||
"Mostrar las partes de las herramientas de edición, escritura y parcheado expandidas por defecto en la línea de tiempo",
|
||||
"settings.general.row.showSessionProgressBar.title": "Mostrar barra de progreso de la sesión",
|
||||
"settings.general.row.showSessionProgressBar.description":
|
||||
"Mostrar la barra de progreso animada en la parte superior de la sesión cuando el agente esté trabajando",
|
||||
"settings.general.row.wayland.title": "Usar Wayland nativo",
|
||||
"settings.general.row.wayland.description": "Deshabilitar fallback a X11 en Wayland. Requiere reinicio.",
|
||||
"settings.general.row.wayland.tooltip":
|
||||
|
||||
Generated
+6
-1
@@ -579,7 +579,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "Thème",
|
||||
"settings.general.row.theme.description": "Personnaliser le thème d'Kilo.",
|
||||
"settings.general.row.font.title": "Police de code",
|
||||
"settings.general.row.font.description": "Personnaliser la police utilisée dans les blocs de code et les terminaux",
|
||||
"settings.general.row.font.description": "Personnaliser la police utilisée dans les blocs de code",
|
||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||
"settings.general.row.uiFont.title": "Police de l'interface",
|
||||
"settings.general.row.uiFont.description": "Personnaliser la police utilisée dans toute l'interface",
|
||||
"settings.general.row.followup.title": "Comportement de suivi",
|
||||
@@ -596,6 +598,9 @@ export const dict = {
|
||||
"settings.general.row.editToolPartsExpanded.title": "Développer les parties de l'outil edit",
|
||||
"settings.general.row.editToolPartsExpanded.description":
|
||||
"Afficher les parties des outils edit, write et patch développées par défaut dans la chronologie",
|
||||
"settings.general.row.showSessionProgressBar.title": "Afficher la barre de progression de la session",
|
||||
"settings.general.row.showSessionProgressBar.description":
|
||||
"Afficher la barre de progression animée en haut de la session lorsque l'agent travaille",
|
||||
"settings.general.row.wayland.title": "Utiliser Wayland natif",
|
||||
"settings.general.row.wayland.description": "Désactiver le repli X11 sur Wayland. Nécessite un redémarrage.",
|
||||
"settings.general.row.wayland.tooltip":
|
||||
|
||||
Generated
+6
-1
@@ -569,7 +569,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "テーマ",
|
||||
"settings.general.row.theme.description": "Kiloのテーマをカスタマイズします。",
|
||||
"settings.general.row.font.title": "コードフォント",
|
||||
"settings.general.row.font.description": "コードブロックとターミナルで使用するフォントをカスタマイズします",
|
||||
"settings.general.row.font.description": "コードブロックで使用するフォントをカスタマイズします",
|
||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||
"settings.general.row.uiFont.title": "UIフォント",
|
||||
"settings.general.row.uiFont.description": "インターフェース全体で使用するフォントをカスタマイズします",
|
||||
"settings.general.row.followup.title": "フォローアップの動作",
|
||||
@@ -585,6 +587,9 @@ export const dict = {
|
||||
"settings.general.row.editToolPartsExpanded.title": "edit ツールパーツを展開",
|
||||
"settings.general.row.editToolPartsExpanded.description":
|
||||
"タイムラインで edit、write、patch ツールパーツをデフォルトで展開して表示します",
|
||||
"settings.general.row.showSessionProgressBar.title": "セッション進行状況バーを表示",
|
||||
"settings.general.row.showSessionProgressBar.description":
|
||||
"エージェントの作業中に、セッション上部にアニメーション付きの進行状況バーを表示します",
|
||||
"settings.general.row.wayland.title": "ネイティブWaylandを使用",
|
||||
"settings.general.row.wayland.description": "WaylandでのX11フォールバックを無効にします。再起動が必要です。",
|
||||
"settings.general.row.wayland.tooltip":
|
||||
|
||||
Generated
+6
-1
@@ -566,7 +566,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "테마",
|
||||
"settings.general.row.theme.description": "Kilo 테마 사용자 지정",
|
||||
"settings.general.row.font.title": "코드 글꼴",
|
||||
"settings.general.row.font.description": "코드 블록과 터미널에 사용되는 글꼴을 사용자 지정",
|
||||
"settings.general.row.font.description": "코드 블록에 사용되는 글꼴을 사용자 지정",
|
||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||
"settings.general.row.uiFont.title": "UI 글꼴",
|
||||
"settings.general.row.uiFont.description": "인터페이스 전반에 사용되는 글꼴을 사용자 지정",
|
||||
"settings.general.row.followup.title": "후속 조치 동작",
|
||||
@@ -581,6 +583,9 @@ export const dict = {
|
||||
"settings.general.row.editToolPartsExpanded.title": "edit 도구 파트 펼치기",
|
||||
"settings.general.row.editToolPartsExpanded.description":
|
||||
"타임라인에서 기본적으로 edit, write, patch 도구 파트를 펼친 상태로 표시합니다",
|
||||
"settings.general.row.showSessionProgressBar.title": "세션 진행 표시줄 표시",
|
||||
"settings.general.row.showSessionProgressBar.description":
|
||||
"에이전트가 작업 중일 때 세션 상단에 애니메이션 진행 표시줄을 표시합니다",
|
||||
"settings.general.row.wayland.title": "네이티브 Wayland 사용",
|
||||
"settings.general.row.wayland.description": "Wayland에서 X11 폴백을 비활성화합니다. 다시 시작해야 합니다.",
|
||||
"settings.general.row.wayland.tooltip":
|
||||
|
||||
Generated
+6
-1
@@ -640,7 +640,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "Tema",
|
||||
"settings.general.row.theme.description": "Tilpass hvordan Kilo er tematisert.",
|
||||
"settings.general.row.font.title": "Kodefont",
|
||||
"settings.general.row.font.description": "Tilpass skrifttypen som brukes i kodeblokker og terminaler",
|
||||
"settings.general.row.font.description": "Tilpass skrifttypen som brukes i kodeblokker",
|
||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||
"settings.general.row.uiFont.title": "UI-skrift",
|
||||
"settings.general.row.uiFont.description": "Tilpass skrifttypen som brukes i hele grensesnittet",
|
||||
"settings.general.row.followup.title": "Oppfølgingsadferd",
|
||||
@@ -654,6 +656,9 @@ export const dict = {
|
||||
"settings.general.row.editToolPartsExpanded.title": "Utvid edit-verktøydeler",
|
||||
"settings.general.row.editToolPartsExpanded.description":
|
||||
"Vis edit-, write- og patch-verktøydeler utvidet som standard i tidslinjen",
|
||||
"settings.general.row.showSessionProgressBar.title": "Vis fremdriftslinje for sesjonen",
|
||||
"settings.general.row.showSessionProgressBar.description":
|
||||
"Vis den animerte fremdriftslinjen øverst i sesjonen når agenten jobber",
|
||||
"settings.general.row.wayland.title": "Bruk innebygd Wayland",
|
||||
"settings.general.row.wayland.description": "Deaktiver X11-fallback på Wayland. Krever omstart.",
|
||||
"settings.general.row.wayland.tooltip":
|
||||
|
||||
Generated
+6
-1
@@ -571,7 +571,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "Motyw",
|
||||
"settings.general.row.theme.description": "Dostosuj motyw Kilo.",
|
||||
"settings.general.row.font.title": "Czcionka kodu",
|
||||
"settings.general.row.font.description": "Dostosuj czcionkę używaną w blokach kodu i terminalach",
|
||||
"settings.general.row.font.description": "Dostosuj czcionkę używaną w blokach kodu",
|
||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||
"settings.general.row.uiFont.title": "Czcionka interfejsu",
|
||||
"settings.general.row.uiFont.description": "Dostosuj czcionkę używaną w całym interfejsie",
|
||||
"settings.general.row.followup.title": "Zachowanie kontynuacji",
|
||||
@@ -586,6 +588,9 @@ export const dict = {
|
||||
"settings.general.row.editToolPartsExpanded.title": "Rozwijaj elementy narzędzia edit",
|
||||
"settings.general.row.editToolPartsExpanded.description":
|
||||
"Domyślnie pokazuj rozwinięte elementy narzędzi edit, write i patch na osi czasu",
|
||||
"settings.general.row.showSessionProgressBar.title": "Pokazuj pasek postępu sesji",
|
||||
"settings.general.row.showSessionProgressBar.description":
|
||||
"Wyświetlaj animowany pasek postępu u góry sesji, gdy agent pracuje",
|
||||
"settings.general.row.wayland.title": "Użyj natywnego Wayland",
|
||||
"settings.general.row.wayland.description": "Wyłącz fallback X11 na Wayland. Wymaga restartu.",
|
||||
"settings.general.row.wayland.tooltip":
|
||||
|
||||
Generated
+6
-1
@@ -637,7 +637,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "Тема",
|
||||
"settings.general.row.theme.description": "Настройте оформление Kilo.",
|
||||
"settings.general.row.font.title": "Шрифт кода",
|
||||
"settings.general.row.font.description": "Настройте шрифт, используемый в блоках кода и терминалах",
|
||||
"settings.general.row.font.description": "Настройте шрифт, используемый в блоках кода",
|
||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||
"settings.general.row.uiFont.title": "Шрифт интерфейса",
|
||||
"settings.general.row.uiFont.description": "Настройте шрифт, используемый во всем интерфейсе",
|
||||
"settings.general.row.followup.title": "Поведение уточняющих вопросов",
|
||||
@@ -654,6 +656,9 @@ export const dict = {
|
||||
"settings.general.row.editToolPartsExpanded.title": "Разворачивать элементы инструмента edit",
|
||||
"settings.general.row.editToolPartsExpanded.description":
|
||||
"Показывать элементы инструментов edit, write и patch в ленте развернутыми по умолчанию",
|
||||
"settings.general.row.showSessionProgressBar.title": "Показывать индикатор прогресса сессии",
|
||||
"settings.general.row.showSessionProgressBar.description":
|
||||
"Показывать анимированный индикатор прогресса вверху сессии, когда агент работает",
|
||||
"settings.general.row.wayland.title": "Использовать нативный Wayland",
|
||||
"settings.general.row.wayland.description": "Отключить X11 fallback на Wayland. Требуется перезапуск.",
|
||||
"settings.general.row.wayland.tooltip":
|
||||
|
||||
Generated
+6
-1
@@ -631,7 +631,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "ธีม",
|
||||
"settings.general.row.theme.description": "ปรับแต่งวิธีการที่ Kilo มีธีม",
|
||||
"settings.general.row.font.title": "ฟอนต์โค้ด",
|
||||
"settings.general.row.font.description": "ปรับแต่งฟอนต์ที่ใช้ในบล็อกโค้ดและเทอร์มินัล",
|
||||
"settings.general.row.font.description": "ปรับแต่งฟอนต์ที่ใช้ในบล็อกโค้ด",
|
||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||
"settings.general.row.uiFont.title": "ฟอนต์ UI",
|
||||
"settings.general.row.uiFont.description": "ปรับแต่งฟอนต์ที่ใช้ทั่วทั้งอินเทอร์เฟซ",
|
||||
"settings.general.row.followup.title": "พฤติกรรมการติดตามผล",
|
||||
@@ -645,6 +647,9 @@ export const dict = {
|
||||
"settings.general.row.editToolPartsExpanded.title": "ขยายส่วนเครื่องมือ edit",
|
||||
"settings.general.row.editToolPartsExpanded.description":
|
||||
"แสดงส่วนเครื่องมือ edit, write และ patch แบบขยายตามค่าเริ่มต้นในไทม์ไลน์",
|
||||
"settings.general.row.showSessionProgressBar.title": "แสดงแถบความคืบหน้าของเซสชัน",
|
||||
"settings.general.row.showSessionProgressBar.description":
|
||||
"แสดงแถบความคืบหน้าแบบเคลื่อนไหวที่ด้านบนของเซสชันเมื่อเอเจนต์กำลังทำงาน",
|
||||
"settings.general.row.wayland.title": "ใช้ Wayland แบบเนทีฟ",
|
||||
"settings.general.row.wayland.description": "ปิดใช้งาน X11 fallback บน Wayland ต้องรีสตาร์ท",
|
||||
"settings.general.row.wayland.tooltip": "บน Linux ที่มีจอภาพรีเฟรชเรตแบบผสม Wayland แบบเนทีฟอาจเสถียรกว่า",
|
||||
|
||||
Generated
+7
-1
@@ -642,7 +642,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "Tema",
|
||||
"settings.general.row.theme.description": "Kilo'un temasını özelleştirin.",
|
||||
"settings.general.row.font.title": "Kod Yazı Tipi",
|
||||
"settings.general.row.font.description": "Kod bloklarında ve terminallerde kullanılan yazı tipini özelleştirin",
|
||||
"settings.general.row.font.description": "Kod bloklarında kullanılan yazı tipini özelleştirin",
|
||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||
"settings.general.row.uiFont.title": "Arayüz Yazı Tipi",
|
||||
"settings.general.row.uiFont.description": "Arayüz genelinde kullanılan yazı tipini özelleştirin",
|
||||
"settings.general.row.followup.title": "Takip davranışı",
|
||||
@@ -659,6 +661,10 @@ export const dict = {
|
||||
"settings.general.row.editToolPartsExpanded.description":
|
||||
"Zaman çizelgesinde düzenleme, yazma ve yama araç bileşenlerini varsayılan olarak genişletilmiş göster",
|
||||
|
||||
"settings.general.row.showSessionProgressBar.title": "Oturum ilerleme çubuğunu göster",
|
||||
"settings.general.row.showSessionProgressBar.description":
|
||||
"Ajan çalışırken oturumun üst kısmında animasyonlu ilerleme çubuğunu göster",
|
||||
|
||||
"settings.general.row.wayland.title": "Yerel Wayland kullan",
|
||||
"settings.general.row.wayland.description":
|
||||
"Wayland'da X11 geri dönüşünü devre dışı bırak. Yeniden başlatma gerektirir.",
|
||||
|
||||
Generated
+5
-1
@@ -631,7 +631,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "主题",
|
||||
"settings.general.row.theme.description": "自定义 Kilo 的主题。",
|
||||
"settings.general.row.font.title": "代码字体",
|
||||
"settings.general.row.font.description": "自定义代码块和终端使用的字体",
|
||||
"settings.general.row.font.description": "自定义代码块使用的字体",
|
||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||
"settings.general.row.uiFont.title": "界面字体",
|
||||
"settings.general.row.uiFont.description": "自定义整个界面使用的字体",
|
||||
"settings.general.row.followup.title": "跟进消息行为",
|
||||
@@ -644,6 +646,8 @@ export const dict = {
|
||||
"settings.general.row.shellToolPartsExpanded.description": "默认在时间线中展开 shell 工具部分",
|
||||
"settings.general.row.editToolPartsExpanded.title": "展开编辑工具部分",
|
||||
"settings.general.row.editToolPartsExpanded.description": "默认在时间线中展开 edit、write 和 patch 工具部分",
|
||||
"settings.general.row.showSessionProgressBar.title": "显示会话进度条",
|
||||
"settings.general.row.showSessionProgressBar.description": "当智能体正在工作时,在会话顶部显示动画进度条",
|
||||
"settings.general.row.wayland.title": "使用原生 Wayland",
|
||||
"settings.general.row.wayland.description": "在 Wayland 上禁用 X11 回退。需要重启。",
|
||||
"settings.general.row.wayland.tooltip": "在混合刷新率显示器的 Linux 系统上,原生 Wayland 可能更稳定。",
|
||||
|
||||
Generated
+5
-1
@@ -626,7 +626,9 @@ export const dict = {
|
||||
"settings.general.row.theme.title": "主題",
|
||||
"settings.general.row.theme.description": "自訂 Kilo 的主題。",
|
||||
"settings.general.row.font.title": "程式碼字型",
|
||||
"settings.general.row.font.description": "自訂程式碼區塊和終端機使用的字型",
|
||||
"settings.general.row.font.description": "自訂程式碼區塊使用的字型",
|
||||
"settings.general.row.terminalFont.title": "Terminal Font",
|
||||
"settings.general.row.terminalFont.description": "Customise the font used in the terminal",
|
||||
"settings.general.row.uiFont.title": "介面字型",
|
||||
"settings.general.row.uiFont.description": "自訂整個介面使用的字型",
|
||||
"settings.general.row.followup.title": "後續追問行為",
|
||||
@@ -640,6 +642,8 @@ export const dict = {
|
||||
"settings.general.row.shellToolPartsExpanded.description": "在時間軸中預設展開 shell 工具區塊",
|
||||
"settings.general.row.editToolPartsExpanded.title": "展開 edit 工具區塊",
|
||||
"settings.general.row.editToolPartsExpanded.description": "在時間軸中預設展開 edit、write 和 patch 工具區塊",
|
||||
"settings.general.row.showSessionProgressBar.title": "顯示工作階段進度列",
|
||||
"settings.general.row.showSessionProgressBar.description": "當代理程式正在運作時,在工作階段頂部顯示動畫進度列",
|
||||
"settings.general.row.wayland.title": "使用原生 Wayland",
|
||||
"settings.general.row.wayland.description": "在 Wayland 上停用 X11 後備模式。需要重新啟動。",
|
||||
"settings.general.row.wayland.tooltip": "在混合更新率螢幕的 Linux 系統上,原生 Wayland 可能更穩定。",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
@import "@opencode-ai/ui/styles/tailwind";
|
||||
|
||||
@font-face {
|
||||
font-family: "JetBrainsMono Nerd Font Mono";
|
||||
src: url("/assets/JetBrainsMonoNerdFontMono-Regular.woff2") format("woff2");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@layer components {
|
||||
@keyframes session-progress-whip {
|
||||
0% {
|
||||
@@ -66,4 +73,13 @@
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { DataProvider } from "@opencode-ai/ui/context"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { base64Encode } from "@opencode-ai/shared/util/encode"
|
||||
import { useLocation, useNavigate, useParams } from "@solidjs/router"
|
||||
import { createEffect, createMemo, type ParentProps, Show } from "solid-js"
|
||||
import { createEffect, createMemo, createResource, type ParentProps, Show } from "solid-js"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { LocalProvider } from "@/context/local"
|
||||
import { SDKProvider } from "@/context/sdk"
|
||||
@@ -23,11 +23,10 @@ function DirectoryDataProvider(props: ParentProps<{ directory: string }>) {
|
||||
navigate(`/${base64Encode(next)}${path}${location.search}${location.hash}`, { replace: true })
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const id = params.id
|
||||
if (!id) return
|
||||
void sync.session.sync(id)
|
||||
})
|
||||
createResource(
|
||||
() => params.id,
|
||||
(id) => sync.session.sync(id),
|
||||
)
|
||||
|
||||
return (
|
||||
<DataProvider
|
||||
|
||||
@@ -31,7 +31,7 @@ function sortSessions(now: number) {
|
||||
const isRootVisibleSession = (session: Session, directory: string) =>
|
||||
workspaceKey(session.directory) === workspaceKey(directory) && !session.parentID && !session.time?.archived
|
||||
|
||||
const roots = (store: SessionStore) =>
|
||||
export const roots = (store: SessionStore) =>
|
||||
(store.session ?? []).filter((session) => isRootVisibleSession(session, store.path.directory))
|
||||
|
||||
export const sortedRootSessions = (store: SessionStore, now: number) => roots(store).sort(sortSessions(now))
|
||||
|
||||
@@ -19,6 +19,12 @@ import { childSessionOnPath, hasProjectPermissions } from "./helpers"
|
||||
|
||||
const KILO_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
|
||||
|
||||
export function getProjectAvatarSource(id?: string, icon?: { color?: string; url?: string; override?: string }) {
|
||||
return id === KILO_PROJECT_ID
|
||||
? "https://kilo.ai/favicon.svg"
|
||||
: (icon?.override ?? (icon?.color ? undefined : icon?.url))
|
||||
}
|
||||
|
||||
export const ProjectIcon = (props: { project: LocalProject; class?: string; notify?: boolean }): JSX.Element => {
|
||||
const globalSync = useGlobalSync()
|
||||
const notification = useNotification()
|
||||
@@ -42,9 +48,7 @@ export const ProjectIcon = (props: { project: LocalProject; class?: string; noti
|
||||
<div class="size-full rounded overflow-clip">
|
||||
<Avatar
|
||||
fallback={name()}
|
||||
src={
|
||||
props.project.id === KILO_PROJECT_ID ? "https://kilo.ai/favicon.svg" : props.project.icon?.override
|
||||
}
|
||||
src={getProjectAvatarSource(props.project.id, props.project.icon)}
|
||||
{...getAvatarColors(props.project.icon?.color)}
|
||||
class="size-full rounded"
|
||||
classList={{ "badge-mask": notify() }}
|
||||
@@ -263,10 +267,10 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={currentChild()}>
|
||||
<Show when={currentChild()} keyed>
|
||||
{(child) => (
|
||||
<div class="w-full">
|
||||
<SessionItem {...props} session={child()} level={(props.level ?? 0) + 1} />
|
||||
<SessionItem {...props} session={child} level={(props.level ?? 0) + 1} />
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
@@ -321,7 +321,7 @@ export const SortableWorkspace = (props: {
|
||||
const hasMore = createMemo(() => workspaceStore.sessionTotal > count())
|
||||
const query = useQuery(() => ({ ...loadSessionsQuery(props.project.worktree) }))
|
||||
const busy = createMemo(() => props.ctx.isBusy(props.directory))
|
||||
const loading = () => query.isLoading
|
||||
const loading = () => query.isLoading && count() === 0
|
||||
const touch = createMediaQuery("(hover: none)")
|
||||
const showNew = createMemo(() => !loading() && (touch() || count() === 0 || (active() && !params.id)))
|
||||
const loadMore = async () => {
|
||||
|
||||
@@ -259,7 +259,7 @@ export function MessageTimeline(props: {
|
||||
if (!id) return idle
|
||||
return sync.data.session_status[id] ?? idle
|
||||
})
|
||||
const working = createMemo(() => !!pending() || sessionStatus().type !== "idle")
|
||||
const working = createMemo(() => sessionStatus().type !== "idle")
|
||||
const tint = createMemo(() => messageAgentColor(sessionMessages(), sync.data.agent))
|
||||
|
||||
const [timeoutDone, setTimeoutDone] = createSignal(true)
|
||||
@@ -721,7 +721,7 @@ export function MessageTimeline(props: {
|
||||
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
|
||||
}}
|
||||
>
|
||||
<Show when={workingStatus() !== "hidden"}>
|
||||
<Show when={workingStatus() !== "hidden" && settings.general.showSessionProgressBar()}>
|
||||
<div
|
||||
data-component="session-progress"
|
||||
data-state={workingStatus()}
|
||||
@@ -812,7 +812,7 @@ export function MessageTimeline(props: {
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={sessionID()}>
|
||||
<Show when={sessionID()} keyed>
|
||||
{(id) => (
|
||||
<div class="shrink-0 flex items-center gap-3">
|
||||
<SessionContextUsage placement="bottom" />
|
||||
@@ -878,12 +878,12 @@ export function MessageTimeline(props: {
|
||||
</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</Show>
|
||||
<DropdownMenu.Item onSelect={() => void archiveSession(id())}>
|
||||
<DropdownMenu.Item onSelect={() => void archiveSession(id)}>
|
||||
<DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id()} />)}
|
||||
onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id} />)}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
@@ -6,7 +6,7 @@ FROM ${REGISTRY}/build/base:24.04
|
||||
SHELL ["/bin/bash", "-lc"]
|
||||
|
||||
ARG NODE_VERSION=24.4.0
|
||||
ARG BUN_VERSION=1.3.11
|
||||
ARG BUN_VERSION=1.3.13
|
||||
|
||||
ENV BUN_INSTALL=/opt/bun
|
||||
ENV PATH=/opt/bun/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
|
||||
@@ -53,6 +53,10 @@ export default defineConfig({
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: { index: "src/preload/index.ts" },
|
||||
output: {
|
||||
format: "cjs",
|
||||
entryFileNames: "[name].js",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"electron-store": "^10",
|
||||
"electron-updater": "^6",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"drizzle-orm": "catalog:",
|
||||
"marked": "^15",
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -53,7 +54,7 @@
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"@valibot/to-json-schema": "1.6.0",
|
||||
"electron": "40.8.5",
|
||||
"electron": "41.2.1",
|
||||
"electron-builder": "^26",
|
||||
"electron-vite": "^5",
|
||||
"solid-js": "catalog:",
|
||||
|
||||
@@ -28,8 +28,10 @@ const APP_IDS: Record<string, string> = {
|
||||
beta: "ai.opencode.desktop.beta",
|
||||
prod: "ai.opencode.desktop",
|
||||
}
|
||||
const appId = app.isPackaged ? APP_IDS[CHANNEL] : "ai.opencode.desktop.dev"
|
||||
app.setName(app.isPackaged ? APP_NAMES[CHANNEL] : "OpenCode Dev")
|
||||
app.setPath("userData", join(app.getPath("appData"), app.isPackaged ? APP_IDS[CHANNEL] : "ai.opencode.desktop.dev"))
|
||||
app.setAppUserModelId(appId)
|
||||
app.setPath("userData", join(app.getPath("appData"), appId))
|
||||
const { autoUpdater } = pkg
|
||||
|
||||
import type { InitStep, ServerReadyData, SqliteMigrationProgress, WslConfig } from "../preload/types"
|
||||
@@ -40,7 +42,14 @@ import { initLogging } from "./logging"
|
||||
import { parseMarkdown } from "./markdown"
|
||||
import { createMenu } from "./menu"
|
||||
import { getDefaultServerUrl, getWslConfig, setDefaultServerUrl, setWslConfig, spawnLocalServer } from "./server"
|
||||
import { createLoadingWindow, createMainWindow, setBackgroundColor, setDockIcon } from "./windows"
|
||||
import {
|
||||
createLoadingWindow,
|
||||
createMainWindow,
|
||||
registerRendererProtocol,
|
||||
setBackgroundColor,
|
||||
setDockIcon,
|
||||
} from "./windows"
|
||||
import { drizzle } from "drizzle-orm/node-sqlite/driver"
|
||||
import type { Server } from "virtual:opencode-server"
|
||||
|
||||
const initEmitter = new EventEmitter()
|
||||
@@ -103,6 +112,7 @@ function setupApp() {
|
||||
|
||||
void app.whenReady().then(async () => {
|
||||
app.setAsDefaultProtocolClient("opencode")
|
||||
registerRendererProtocol()
|
||||
setDockIcon()
|
||||
setupAutoUpdater()
|
||||
await initialize()
|
||||
@@ -137,15 +147,6 @@ async function initialize() {
|
||||
const url = `http://${hostname}:${port}`
|
||||
const password = randomUUID()
|
||||
|
||||
logger.log("spawning sidecar", { url })
|
||||
const { listener, health } = await spawnLocalServer(hostname, port, password)
|
||||
server = listener
|
||||
serverReady.resolve({
|
||||
url,
|
||||
username: "kilo", // kilocode_change
|
||||
password,
|
||||
})
|
||||
|
||||
const loadingTask = (async () => {
|
||||
logger.log("sidecar connection started", { url })
|
||||
|
||||
@@ -156,10 +157,32 @@ async function initialize() {
|
||||
if (progress.type === "Done") sqliteDone?.resolve()
|
||||
})
|
||||
|
||||
if (needsMigration) {
|
||||
const { Database, JsonMigration } = await import("virtual:opencode-server")
|
||||
await JsonMigration.run(drizzle({ client: Database.Client().$client }), {
|
||||
progress: (event: { current: number; total: number }) => {
|
||||
const percent = Math.round(event.current / event.total) * 100
|
||||
initEmitter.emit("sqlite", { type: "InProgress", value: percent })
|
||||
},
|
||||
})
|
||||
initEmitter.emit("sqlite", { type: "Done" })
|
||||
|
||||
sqliteDone?.resolve()
|
||||
}
|
||||
|
||||
if (needsMigration) {
|
||||
await sqliteDone?.promise
|
||||
}
|
||||
|
||||
logger.log("spawning sidecar", { url })
|
||||
const { listener, health } = await spawnLocalServer(hostname, port, password)
|
||||
server = listener
|
||||
serverReady.resolve({
|
||||
url,
|
||||
username: "kilo", // kilocode_change
|
||||
password,
|
||||
})
|
||||
|
||||
await Promise.race([
|
||||
health.wait,
|
||||
delay(30_000).then(() => {
|
||||
@@ -172,15 +195,10 @@ async function initialize() {
|
||||
logger.log("loading task finished")
|
||||
})()
|
||||
|
||||
const globals = {
|
||||
updaterEnabled: UPDATER_ENABLED,
|
||||
deepLinks: pendingDeepLinks,
|
||||
}
|
||||
|
||||
if (needsMigration) {
|
||||
const show = await Promise.race([loadingTask.then(() => false), delay(1_000).then(() => true)])
|
||||
if (show) {
|
||||
overlay = createLoadingWindow(globals)
|
||||
overlay = createLoadingWindow()
|
||||
await delay(1_000)
|
||||
}
|
||||
}
|
||||
@@ -192,7 +210,7 @@ async function initialize() {
|
||||
await loadingComplete.promise
|
||||
}
|
||||
|
||||
mainWindow = createMainWindow(globals)
|
||||
mainWindow = createMainWindow()
|
||||
wireMenu()
|
||||
|
||||
overlay?.close()
|
||||
@@ -229,6 +247,8 @@ registerIpcHandlers({
|
||||
initEmitter.off("step", listener)
|
||||
}
|
||||
},
|
||||
getWindowConfig: () => ({ updaterEnabled: UPDATER_ENABLED }),
|
||||
consumeInitialDeepLinks: () => pendingDeepLinks.splice(0),
|
||||
getDefaultServerUrl: () => getDefaultServerUrl(),
|
||||
setDefaultServerUrl: (url) => setDefaultServerUrl(url),
|
||||
getWslConfig: () => Promise.resolve(getWslConfig()),
|
||||
|
||||
@@ -2,7 +2,14 @@ import { execFile } from "node:child_process"
|
||||
import { BrowserWindow, Notification, app, clipboard, dialog, ipcMain, shell } from "electron"
|
||||
import type { IpcMainEvent, IpcMainInvokeEvent } from "electron"
|
||||
|
||||
import type { InitStep, ServerReadyData, SqliteMigrationProgress, TitlebarTheme, WslConfig } from "../preload/types"
|
||||
import type {
|
||||
InitStep,
|
||||
ServerReadyData,
|
||||
SqliteMigrationProgress,
|
||||
TitlebarTheme,
|
||||
WindowConfig,
|
||||
WslConfig,
|
||||
} from "../preload/types"
|
||||
import { getStore } from "./store"
|
||||
import { setTitlebar } from "./windows"
|
||||
|
||||
@@ -14,6 +21,8 @@ const pickerFilters = (ext?: string[]) => {
|
||||
type Deps = {
|
||||
killSidecar: () => void
|
||||
awaitInitialization: (sendStep: (step: InitStep) => void) => Promise<ServerReadyData>
|
||||
getWindowConfig: () => Promise<WindowConfig> | WindowConfig
|
||||
consumeInitialDeepLinks: () => Promise<string[]> | string[]
|
||||
getDefaultServerUrl: () => Promise<string | null> | string | null
|
||||
setDefaultServerUrl: (url: string | null) => Promise<void> | void
|
||||
getWslConfig: () => Promise<WslConfig>
|
||||
@@ -37,6 +46,8 @@ export function registerIpcHandlers(deps: Deps) {
|
||||
const send = (step: InitStep) => event.sender.send("init-step", step)
|
||||
return deps.awaitInitialization(send)
|
||||
})
|
||||
ipcMain.handle("get-window-config", () => deps.getWindowConfig())
|
||||
ipcMain.handle("consume-initial-deep-links", () => deps.consumeInitialDeepLinks())
|
||||
ipcMain.handle("get-default-server-url", () => deps.getDefaultServerUrl())
|
||||
ipcMain.handle("set-default-server-url", (_event: IpcMainInvokeEvent, url: string | null) =>
|
||||
deps.setDefaultServerUrl(url),
|
||||
|
||||
@@ -47,7 +47,7 @@ export function createMenu(deps: Deps) {
|
||||
{
|
||||
label: "New Window",
|
||||
accelerator: "Cmd+Shift+N",
|
||||
click: () => createMainWindow({ updaterEnabled: UPDATER_ENABLED }),
|
||||
click: () => createMainWindow(),
|
||||
},
|
||||
{ type: "separator" },
|
||||
{ role: "close" },
|
||||
|
||||
@@ -4,7 +4,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs"
|
||||
import { homedir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { CHANNEL } from "./constants"
|
||||
import { getStore, store } from "./store"
|
||||
import { getStore } from "./store"
|
||||
|
||||
const TAURI_MIGRATED_KEY = "tauriMigrated"
|
||||
|
||||
@@ -67,7 +67,7 @@ function migrateFile(datPath: string, filename: string) {
|
||||
}
|
||||
|
||||
export function migrate() {
|
||||
if (store.get(TAURI_MIGRATED_KEY)) {
|
||||
if (getStore().get(TAURI_MIGRATED_KEY)) {
|
||||
log.log("tauri migration: already done, skipping")
|
||||
return
|
||||
}
|
||||
@@ -77,7 +77,7 @@ export function migrate() {
|
||||
|
||||
if (!existsSync(dir)) {
|
||||
log.log("tauri migration: no tauri data directory found, nothing to migrate")
|
||||
store.set(TAURI_MIGRATED_KEY, true)
|
||||
getStore().set(TAURI_MIGRATED_KEY, true)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -87,5 +87,5 @@ export function migrate() {
|
||||
}
|
||||
|
||||
log.log("tauri migration: complete")
|
||||
store.set(TAURI_MIGRATED_KEY, true)
|
||||
getStore().set(TAURI_MIGRATED_KEY, true)
|
||||
}
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import { app } from "electron"
|
||||
import { DEFAULT_SERVER_URL_KEY, WSL_ENABLED_KEY } from "./constants"
|
||||
import { getUserShell, loadShellEnv } from "./shell-env"
|
||||
import { store } from "./store"
|
||||
import { getStore } from "./store"
|
||||
|
||||
export type WslConfig = { enabled: boolean }
|
||||
|
||||
export type HealthCheck = { wait: Promise<void> }
|
||||
|
||||
export function getDefaultServerUrl(): string | null {
|
||||
const value = store.get(DEFAULT_SERVER_URL_KEY)
|
||||
const value = getStore().get(DEFAULT_SERVER_URL_KEY)
|
||||
return typeof value === "string" ? value : null
|
||||
}
|
||||
|
||||
export function setDefaultServerUrl(url: string | null) {
|
||||
if (url) {
|
||||
store.set(DEFAULT_SERVER_URL_KEY, url)
|
||||
getStore().set(DEFAULT_SERVER_URL_KEY, url)
|
||||
return
|
||||
}
|
||||
|
||||
store.delete(DEFAULT_SERVER_URL_KEY)
|
||||
getStore().delete(DEFAULT_SERVER_URL_KEY)
|
||||
}
|
||||
|
||||
export function getWslConfig(): WslConfig {
|
||||
const value = store.get(WSL_ENABLED_KEY)
|
||||
const value = getStore().get(WSL_ENABLED_KEY)
|
||||
return { enabled: typeof value === "boolean" ? value : false }
|
||||
}
|
||||
|
||||
export function setWslConfig(config: WslConfig) {
|
||||
store.set(WSL_ENABLED_KEY, config.enabled)
|
||||
getStore().set(WSL_ENABLED_KEY, config.enabled)
|
||||
}
|
||||
|
||||
export async function spawnLocalServer(hostname: string, port: number, password: string) {
|
||||
@@ -39,6 +39,7 @@ export async function spawnLocalServer(hostname: string, port: number, password:
|
||||
hostname,
|
||||
username: "opencode",
|
||||
password,
|
||||
cors: ["oc://renderer"],
|
||||
})
|
||||
|
||||
const wait = (async () => {
|
||||
|
||||
@@ -4,6 +4,10 @@ import { SETTINGS_STORE } from "./constants"
|
||||
|
||||
const cache = new Map<string, Store>()
|
||||
|
||||
// We cannot instantiate the electron-store at module load time because
|
||||
// module import hoisting causes this to run before app.setPath("userData", ...)
|
||||
// in index.ts has executed, which would result in files being written to the default directory
|
||||
// (e.g. bad: %APPDATA%\@opencode-ai\desktop-electron\opencode.settings vs good: %APPDATA%\ai.opencode.desktop.dev\opencode.settings).
|
||||
export function getStore(name = SETTINGS_STORE) {
|
||||
const cached = cache.get(name)
|
||||
if (cached) return cached
|
||||
@@ -11,5 +15,3 @@ export function getStore(name = SETTINGS_STORE) {
|
||||
cache.set(name, next)
|
||||
return next
|
||||
}
|
||||
|
||||
export const store = getStore(SETTINGS_STORE)
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
import windowState from "electron-window-state"
|
||||
import { app, BrowserWindow, nativeImage, nativeTheme } from "electron"
|
||||
import { dirname, join } from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { app, BrowserWindow, net, nativeImage, nativeTheme, protocol } from "electron"
|
||||
import { dirname, isAbsolute, join, relative, resolve } from "node:path"
|
||||
import { fileURLToPath, pathToFileURL } from "node:url"
|
||||
import type { TitlebarTheme } from "../preload/types"
|
||||
|
||||
type Globals = {
|
||||
updaterEnabled: boolean
|
||||
deepLinks?: string[]
|
||||
}
|
||||
|
||||
const root = dirname(fileURLToPath(import.meta.url))
|
||||
const rendererRoot = join(root, "../renderer")
|
||||
const rendererProtocol = "oc"
|
||||
const rendererHost = "renderer"
|
||||
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{
|
||||
scheme: rendererProtocol,
|
||||
privileges: {
|
||||
secure: true,
|
||||
standard: true,
|
||||
supportFetchAPI: true,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
let backgroundColor: string | undefined
|
||||
|
||||
@@ -54,7 +63,7 @@ export function setDockIcon() {
|
||||
if (!icon.isEmpty()) app.dock?.setIcon(icon)
|
||||
}
|
||||
|
||||
export function createMainWindow(globals: Globals) {
|
||||
export function createMainWindow() {
|
||||
const state = windowState({
|
||||
defaultWidth: 1280,
|
||||
defaultHeight: 800,
|
||||
@@ -84,15 +93,29 @@ export function createMainWindow(globals: Globals) {
|
||||
}
|
||||
: {}),
|
||||
webPreferences: {
|
||||
preload: join(root, "../preload/index.mjs"),
|
||||
sandbox: false,
|
||||
preload: join(root, "../preload/index.js"),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
sandbox: true,
|
||||
},
|
||||
})
|
||||
|
||||
win.webContents.session.webRequest.onBeforeSendHeaders((details, callback) => {
|
||||
const { requestHeaders } = details
|
||||
upsertKeyValue(requestHeaders, "Access-Control-Allow-Origin", ["*"])
|
||||
callback({ requestHeaders })
|
||||
})
|
||||
|
||||
win.webContents.session.webRequest.onHeadersReceived((details, callback) => {
|
||||
const { responseHeaders = {} } = details
|
||||
upsertKeyValue(responseHeaders, "Access-Control-Allow-Origin", ["*"])
|
||||
upsertKeyValue(responseHeaders, "Access-Control-Allow-Headers", ["*"])
|
||||
callback({ responseHeaders })
|
||||
})
|
||||
|
||||
state.manage(win)
|
||||
loadWindow(win, "index.html")
|
||||
wireZoom(win)
|
||||
injectGlobals(win, globals)
|
||||
|
||||
win.once("ready-to-show", () => {
|
||||
win.show()
|
||||
@@ -101,7 +124,7 @@ export function createMainWindow(globals: Globals) {
|
||||
return win
|
||||
}
|
||||
|
||||
export function createLoadingWindow(globals: Globals) {
|
||||
export function createLoadingWindow() {
|
||||
const mode = tone()
|
||||
const win = new BrowserWindow({
|
||||
width: 640,
|
||||
@@ -120,17 +143,37 @@ export function createLoadingWindow(globals: Globals) {
|
||||
}
|
||||
: {}),
|
||||
webPreferences: {
|
||||
preload: join(root, "../preload/index.mjs"),
|
||||
sandbox: false,
|
||||
preload: join(root, "../preload/index.js"),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
sandbox: true,
|
||||
},
|
||||
})
|
||||
|
||||
loadWindow(win, "loading.html")
|
||||
injectGlobals(win, globals)
|
||||
|
||||
return win
|
||||
}
|
||||
|
||||
export function registerRendererProtocol() {
|
||||
if (protocol.isProtocolHandled(rendererProtocol)) return
|
||||
|
||||
protocol.handle(rendererProtocol, (request) => {
|
||||
const url = new URL(request.url)
|
||||
if (url.host !== rendererHost) {
|
||||
return new Response("Not found", { status: 404 })
|
||||
}
|
||||
|
||||
const file = resolve(rendererRoot, `.${decodeURIComponent(url.pathname)}`)
|
||||
const rel = relative(rendererRoot, file)
|
||||
if (rel.startsWith("..") || isAbsolute(rel)) {
|
||||
return new Response("Not found", { status: 404 })
|
||||
}
|
||||
|
||||
return net.fetch(pathToFileURL(file).toString())
|
||||
})
|
||||
}
|
||||
|
||||
function loadWindow(win: BrowserWindow, html: string) {
|
||||
const devUrl = process.env.ELECTRON_RENDERER_URL
|
||||
if (devUrl) {
|
||||
@@ -139,25 +182,25 @@ function loadWindow(win: BrowserWindow, html: string) {
|
||||
return
|
||||
}
|
||||
|
||||
void win.loadFile(join(root, `../renderer/${html}`))
|
||||
void win.loadURL(`${rendererProtocol}://${rendererHost}/${html}`)
|
||||
}
|
||||
|
||||
function injectGlobals(win: BrowserWindow, globals: Globals) {
|
||||
win.webContents.on("dom-ready", () => {
|
||||
const deepLinks = globals.deepLinks ?? []
|
||||
const data = {
|
||||
updaterEnabled: globals.updaterEnabled,
|
||||
deepLinks: Array.isArray(deepLinks) ? deepLinks.splice(0) : deepLinks,
|
||||
}
|
||||
void win.webContents.executeJavaScript(
|
||||
`window.__KILO__ = Object.assign(window.__KILO__ ?? {}, ${JSON.stringify(data)})`,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function wireZoom(win: BrowserWindow) {
|
||||
win.webContents.setZoomFactor(1)
|
||||
win.webContents.on("zoom-changed", () => {
|
||||
win.webContents.setZoomFactor(1)
|
||||
})
|
||||
}
|
||||
|
||||
function upsertKeyValue(obj: Record<string, any>, keyToChange: string, value: any) {
|
||||
const keyToChangeLower = keyToChange.toLowerCase()
|
||||
for (const key of Object.keys(obj)) {
|
||||
if (key.toLowerCase() === keyToChangeLower) {
|
||||
// Reassign old key
|
||||
obj[key] = value
|
||||
// Done
|
||||
return
|
||||
}
|
||||
}
|
||||
// Insert at end instead
|
||||
obj[keyToChange] = value
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ const api: ElectronAPI = {
|
||||
ipcRenderer.removeListener("init-step", handler)
|
||||
})
|
||||
},
|
||||
getWindowConfig: () => ipcRenderer.invoke("get-window-config"),
|
||||
consumeInitialDeepLinks: () => ipcRenderer.invoke("consume-initial-deep-links"),
|
||||
getDefaultServerUrl: () => ipcRenderer.invoke("get-default-server-url"),
|
||||
setDefaultServerUrl: (url) => ipcRenderer.invoke("set-default-server-url", url),
|
||||
getWslConfig: () => ipcRenderer.invoke("get-wsl-config"),
|
||||
|
||||
@@ -15,10 +15,16 @@ export type TitlebarTheme = {
|
||||
mode: "light" | "dark"
|
||||
}
|
||||
|
||||
export type WindowConfig = {
|
||||
updaterEnabled: boolean
|
||||
}
|
||||
|
||||
export type ElectronAPI = {
|
||||
killSidecar: () => Promise<void>
|
||||
installCli: () => Promise<string>
|
||||
awaitInitialization: (onStep: (step: InitStep) => void) => Promise<ServerReadyData>
|
||||
getWindowConfig: () => Promise<WindowConfig>
|
||||
consumeInitialDeepLinks: () => Promise<string[]>
|
||||
getDefaultServerUrl: () => Promise<string | null>
|
||||
setDefaultServerUrl: (url: string | null) => Promise<void>
|
||||
getWslConfig: () => Promise<WslConfig>
|
||||
|
||||
@@ -4,8 +4,6 @@ declare global {
|
||||
interface Window {
|
||||
api: ElectronAPI
|
||||
__OPENCODE__?: {
|
||||
updaterEnabled?: boolean
|
||||
wsl?: boolean
|
||||
deepLinks?: string[]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ const root = resolve(dir, "../..")
|
||||
const html = async (name: string) => Bun.file(join(dir, name)).text()
|
||||
|
||||
/**
|
||||
* Electron loads renderer HTML via `win.loadFile()` which uses the `file://`
|
||||
* protocol. Absolute paths like `src="/foo.js"` resolve to the filesystem root
|
||||
* (e.g. `file:///C:/foo.js` on Windows) instead of relative to the app bundle.
|
||||
* Packaged Electron windows load renderer HTML via the privileged `oc://`
|
||||
* protocol. Root-relative asset paths like `src="/foo.js"` would resolve from
|
||||
* the protocol origin root instead of relative to the current HTML entrypoint.
|
||||
*
|
||||
* All local resource references must use relative paths (`./`).
|
||||
*/
|
||||
|
||||
@@ -20,7 +20,6 @@ import { createEffect, createResource, onCleanup, onMount, Show } from "solid-js
|
||||
import { render } from "solid-js/web"
|
||||
import pkg from "../../package.json"
|
||||
import { initI18n, t } from "./i18n"
|
||||
import { UPDATER_ENABLED } from "./updater"
|
||||
import { webviewZoom } from "./webview-zoom"
|
||||
import "./styles.css"
|
||||
import { useTheme } from "@opencode-ai/ui/theme"
|
||||
@@ -43,8 +42,7 @@ const emitDeepLinks = (urls: string[]) => {
|
||||
}
|
||||
|
||||
const listenForDeepLinks = () => {
|
||||
const startUrls = window.__KILO__?.deepLinks ?? []
|
||||
if (startUrls.length) emitDeepLinks(startUrls)
|
||||
void window.api.consumeInitialDeepLinks().then((urls) => emitDeepLinks(urls))
|
||||
return window.api.onDeepLink((urls) => emitDeepLinks(urls))
|
||||
}
|
||||
|
||||
@@ -57,13 +55,21 @@ const createPlatform = (): Platform => {
|
||||
return undefined
|
||||
})()
|
||||
|
||||
const isWslEnabled = async () => {
|
||||
if (os !== "windows") return false
|
||||
return window.api
|
||||
.getWslConfig()
|
||||
.then((config) => config.enabled)
|
||||
.catch(() => false)
|
||||
}
|
||||
|
||||
const wslHome = async () => {
|
||||
if (os !== "windows" || !window.__KILO__?.wsl) return undefined
|
||||
if (!(await isWslEnabled())) return undefined
|
||||
return window.api.wslPath("~", "windows").catch(() => undefined)
|
||||
}
|
||||
|
||||
const handleWslPicker = async <T extends string | string[]>(result: T | null): Promise<T | null> => {
|
||||
if (!result || !window.__KILO__?.wsl) return result
|
||||
if (!result || !(await isWslEnabled())) return result
|
||||
if (Array.isArray(result)) {
|
||||
return Promise.all(result.map((path) => window.api.wslPath(path, "linux").catch(() => path))) as any
|
||||
}
|
||||
@@ -137,7 +143,7 @@ const createPlatform = (): Platform => {
|
||||
if (os === "windows") {
|
||||
const resolvedApp = app ? await window.api.resolveAppPath(app).catch(() => null) : null
|
||||
const resolvedPath = await (async () => {
|
||||
if (window.__KILO__?.wsl) {
|
||||
if (await isWslEnabled()) {
|
||||
const converted = await window.api.wslPath(path, "windows").catch(() => null)
|
||||
if (converted) return converted
|
||||
}
|
||||
@@ -159,12 +165,14 @@ const createPlatform = (): Platform => {
|
||||
storage,
|
||||
|
||||
checkUpdate: async () => {
|
||||
if (!UPDATER_ENABLED()) return { updateAvailable: false }
|
||||
const config = await window.api.getWindowConfig().catch(() => ({ updaterEnabled: false }))
|
||||
if (!config.updaterEnabled) return { updateAvailable: false }
|
||||
return window.api.checkUpdate()
|
||||
},
|
||||
|
||||
update: async () => {
|
||||
if (!UPDATER_ENABLED()) return
|
||||
const config = await window.api.getWindowConfig().catch(() => ({ updaterEnabled: false }))
|
||||
if (!config.updaterEnabled) return
|
||||
await window.api.installUpdate()
|
||||
},
|
||||
|
||||
@@ -194,11 +202,7 @@ const createPlatform = (): Platform => {
|
||||
return fetch(input, init)
|
||||
},
|
||||
|
||||
getWslEnabled: async () => {
|
||||
const next = await window.api.getWslConfig().catch(() => null)
|
||||
if (next) return next.enabled
|
||||
return window.__KILO__!.wsl ?? false
|
||||
},
|
||||
getWslEnabled: () => isWslEnabled(),
|
||||
|
||||
setWslEnabled: async (enabled) => {
|
||||
await window.api.setWslConfig({ enabled })
|
||||
@@ -249,6 +253,7 @@ listenForDeepLinks()
|
||||
|
||||
render(() => {
|
||||
const platform = createPlatform()
|
||||
const [windowConfig] = createResource(() => window.api.getWindowConfig().catch(() => ({ updaterEnabled: false })))
|
||||
const loadLocale = async () => {
|
||||
const current = await platform.storage?.("opencode.global.dat").getItem("language")
|
||||
const legacy = current ? undefined : await platform.storage?.().getItem("language.v1")
|
||||
@@ -325,7 +330,15 @@ render(() => {
|
||||
return (
|
||||
<PlatformProvider value={platform}>
|
||||
<AppBaseProviders locale={locale.latest}>
|
||||
<Show when={!defaultServer.loading && !sidecar.loading && !windowCount.loading && !locale.loading}>
|
||||
<Show
|
||||
when={
|
||||
!defaultServer.loading &&
|
||||
!sidecar.loading &&
|
||||
!windowConfig.loading &&
|
||||
!windowCount.loading &&
|
||||
!locale.loading
|
||||
}
|
||||
>
|
||||
{(_) => {
|
||||
return (
|
||||
<AppInterface
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { initI18n, t } from "./i18n"
|
||||
|
||||
export const UPDATER_ENABLED = () => window.__KILO__?.updaterEnabled ?? false
|
||||
|
||||
export async function runUpdater({ alertOnFail }: { alertOnFail: boolean }) {
|
||||
await initI18n()
|
||||
try {
|
||||
|
||||
@@ -33,6 +33,9 @@
|
||||
</screenshots>
|
||||
|
||||
<releases>
|
||||
<release version="1.4.0" date="2026-04-08">
|
||||
<url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.4.0</url>
|
||||
</release>
|
||||
<release version="1.0.223" date="2026-01-01">
|
||||
<url type="details">https://github.com/Kilo-Org/kilocode/releases/tag/v1.0.223</url>
|
||||
</release>
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"icons/dev/icon.ico"
|
||||
],
|
||||
"active": true,
|
||||
"category": "DeveloperTool",
|
||||
"targets": ["deb", "rpm", "dmg", "nsis", "app"],
|
||||
"externalBin": ["sidecars/kilo-cli"],
|
||||
"linux": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "kilo"
|
||||
name = "Kilo"
|
||||
description = "The open source coding agent."
|
||||
version = "7.2.25"
|
||||
version = "1.14.22"
|
||||
schema_version = 1
|
||||
authors = ["Anomaly"]
|
||||
repository = "https://github.com/Kilo-Org/kilocode"
|
||||
@@ -11,26 +11,26 @@ name = "Kilo"
|
||||
icon = "./icons/opencode.svg"
|
||||
|
||||
[agent_servers.opencode.targets.darwin-aarch64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.25/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.14.22/opencode-darwin-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.darwin-x86_64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.25/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.14.22/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.25/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.14.22/opencode-linux-arm64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-x86_64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.25/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.14.22/opencode-linux-x64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.windows-x86_64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.25/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.14.22/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -12,7 +12,12 @@ Kilo Code's autocomplete feature provides intelligent code suggestions and compl
|
||||
|
||||
## How Autocomplete Works
|
||||
|
||||
The extension uses **Fill-in-the-Middle (FIM)** completion powered by Codestral (`mistralai/codestral-2508`) via the **Kilo Gateway**. It analyzes the code before and after your cursor to generate contextually accurate inline suggestions.
|
||||
The extension uses **Fill-in-the-Middle (FIM)** completion routed through the **Kilo Gateway**. It analyzes the code before and after your cursor to generate contextually accurate inline suggestions.
|
||||
|
||||
You can choose between two FIM models:
|
||||
|
||||
- **Codestral** (`mistralai/codestral-2508`) by Mistral AI — the default, billed through your Kilo account.
|
||||
- **Mercury Edit** (`inception/mercury-edit`) by Inception — temporarily available via **BYOK** (Bring Your Own Key) only; Kilo Gateway support is coming soon.
|
||||
|
||||
## Triggering Options
|
||||
|
||||
@@ -30,9 +35,14 @@ This keybinding requires `kilo-code.new.autocomplete.enableSmartInlineTaskKeybin
|
||||
|
||||
## Provider and Model
|
||||
|
||||
Autocomplete currently uses **Codestral** (`mistralai/codestral-2508`) routed through the **Kilo Gateway**. Codestral is optimized for Fill-in-the-Middle (FIM) completions, and there is no option to select a different model at this time. Support for additional FIM models is planned for future releases.
|
||||
Autocomplete requests are routed through the **Kilo Gateway**. You can pick the FIM model under **Settings → Models → Autocomplete model**:
|
||||
|
||||
Requests are billed through your Kilo account. To use your own Mistral API key instead, see [Setting Up Mistral for Free Autocomplete](/docs/code-with-ai/features/autocomplete/mistral-setup).
|
||||
- **Codestral** (`mistralai/codestral-2508`) — the default. Billed through your Kilo account, or free when you add your own Mistral Codestral key via BYOK. See [Setting Up Mistral for Free Autocomplete](/docs/code-with-ai/features/autocomplete/mistral-setup).
|
||||
- **Mercury Edit** (`inception/mercury-edit`) — a fast diffusion-based FIM model by Inception. Temporarily requires an **Inception BYOK key** until Kilo Gateway support lands. Add one from the [BYOK page](https://app.kilo.ai/byok) in the Kilo platform. See [Bring Your Own Key (BYOK)](/docs/getting-started/byok) for setup details.
|
||||
|
||||
{% callout type="note" %}
|
||||
Mercury Edit is only available through BYOK for now — Kilo Gateway support is coming soon. If you select Mercury Edit without a valid Inception BYOK key configured, autocomplete requests will fail — switch back to Codestral or add an Inception key to continue.
|
||||
{% /callout %}
|
||||
|
||||
## Status Bar
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Source Code Links
|
||||
|
||||
<!-- Auto-generated by script/extract-source-links.ts — DO NOT EDIT -->
|
||||
<!-- 81 unique URLs extracted from extension and CLI source -->
|
||||
<!-- 82 unique URLs extracted from extension and CLI source -->
|
||||
|
||||
- <https://api.apertis.ai/v1>
|
||||
<!-- packages/opencode/src/provider/model-cache.ts -->
|
||||
@@ -34,6 +34,8 @@
|
||||
<!-- packages/opencode/src/provider/sdk/copilot/responses/openai-responses-language-model.ts -->
|
||||
- <https://docs.github.com/en/actions/how-tos/security-for-github-actions/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services>
|
||||
<!-- packages/opencode/src/cli/cmd/github.ts -->
|
||||
- <https://docs.mistral.ai/capabilities/reasoning/adjustable>
|
||||
<!-- packages/opencode/src/provider/transform.ts -->
|
||||
- <https://docs.venice.ai/overview/guides/reasoning-models#reasoning-effort>
|
||||
<!-- packages/opencode/src/provider/transform.ts -->
|
||||
- <https://docs.x.ai/docs/guides/reasoning#control-how-hard-the-model-thinks>
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE `project` ADD `icon_url_override` text;
|
||||
UPDATE `project` SET `icon_url_override` = `icon_url` WHERE `icon_url` IS NOT NULL;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -117,6 +117,7 @@
|
||||
"@modelcontextprotocol/sdk": "1.29.0",
|
||||
"@morphllm/morphsdk": "0.2.166",
|
||||
"@npmcli/arborist": "9.4.0",
|
||||
"@npmcli/config": "10.8.1",
|
||||
"@octokit/graphql": "9.0.2",
|
||||
"@octokit/rest": "catalog:",
|
||||
"@openauthjs/openauth": "catalog:",
|
||||
@@ -127,8 +128,8 @@
|
||||
"@opentelemetry/exporter-trace-otlp-http": "0.214.0",
|
||||
"@opentelemetry/sdk-trace-base": "2.6.1",
|
||||
"@opentelemetry/sdk-trace-node": "2.6.1",
|
||||
"@opentui/core": "catalog:",
|
||||
"@opentui/solid": "catalog:",
|
||||
"@opentui/core": "0.1.99",
|
||||
"@opentui/solid": "0.1.99",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@pierre/diffs": "catalog:",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
|
||||
@@ -126,12 +126,10 @@ const allTargets: {
|
||||
arch: "x64",
|
||||
avx2: false,
|
||||
},
|
||||
// kilocode_change start - Windows ARM64 target
|
||||
{
|
||||
os: "win32",
|
||||
arch: "arm64",
|
||||
},
|
||||
// kilocode_change end
|
||||
{
|
||||
os: "win32",
|
||||
arch: "x64",
|
||||
@@ -141,12 +139,6 @@ const allTargets: {
|
||||
arch: "x64",
|
||||
avx2: false,
|
||||
},
|
||||
// kilocode_change start - added Windows ARM64 target
|
||||
{
|
||||
os: "win32",
|
||||
arch: "arm64",
|
||||
},
|
||||
// kilocode_change end
|
||||
]
|
||||
|
||||
const targets = singleFlag
|
||||
@@ -195,7 +187,6 @@ for (const item of targets) {
|
||||
const rootPath = path.resolve(dir, "../../node_modules/@opentui/core/parser.worker.js")
|
||||
const parserWorker = fs.realpathSync(fs.existsSync(localPath) ? localPath : rootPath)
|
||||
const workerPath = "./src/cli/cmd/tui/worker.ts"
|
||||
const rgPath = "./src/file/ripgrep.worker.ts"
|
||||
|
||||
// Use platform-specific bunfs root path based on target OS
|
||||
const bunfsRoot = item.os === "win32" ? "B:/~BUN/root/" : "/$bunfs/root/"
|
||||
@@ -220,19 +211,12 @@ for (const item of targets) {
|
||||
windows: {},
|
||||
},
|
||||
files: embeddedFileMap ? { "opencode-web-ui.gen.ts": embeddedFileMap } : {},
|
||||
entrypoints: [
|
||||
"./src/index.ts",
|
||||
parserWorker,
|
||||
workerPath,
|
||||
rgPath,
|
||||
...(embeddedFileMap ? ["opencode-web-ui.gen.ts"] : []),
|
||||
],
|
||||
entrypoints: ["./src/index.ts", parserWorker, workerPath, ...(embeddedFileMap ? ["opencode-web-ui.gen.ts"] : [])],
|
||||
define: {
|
||||
KILO_VERSION: `'${Script.version}'`,
|
||||
KILO_MIGRATIONS: JSON.stringify(migrations),
|
||||
OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + workerRelativePath,
|
||||
KILO_WORKER_PATH: workerPath,
|
||||
KILO_RIPGREP_WORKER_PATH: rgPath,
|
||||
KILO_CHANNEL: `'${Script.channel}'`,
|
||||
KILO_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "",
|
||||
KILO_BUILD_KIND: Script.release ? `'release'` : `'source'`, // kilocode_change
|
||||
|
||||
@@ -77,10 +77,10 @@ const image = "ghcr.io/kilo-org/kilo" // kilocode_change
|
||||
const platforms = "linux/amd64,linux/arm64"
|
||||
const tags = [`${image}:${version}`, `${image}:${Script.channel}`]
|
||||
const tagFlags = tags.flatMap((t) => ["-t", t])
|
||||
await $`docker buildx build --platform ${platforms} ${tagFlags} --push .`
|
||||
|
||||
// registries
|
||||
if (!Script.preview) {
|
||||
await $`docker buildx build --platform ${platforms} ${tagFlags} --push .`
|
||||
// Calculate SHA values
|
||||
// kilocode_change start
|
||||
const arm64Sha = await $`sha256sum ./dist/kilo-linux-arm64.tar.gz | cut -d' ' -f1`.text().then((x) => x.trim())
|
||||
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
// This script runs a separate OpenCode server to be used as a remote
|
||||
// workspace, simulating a remote environment but all local to make
|
||||
// debugger easier
|
||||
//
|
||||
// *Important*: make sure you add the debug workspace plugin first.
|
||||
// In `.opencode/opencode.jsonc` in the root of this project add:
|
||||
//
|
||||
// "plugin": ["../packages/opencode/src/control-plane/dev/debug-workspace-plugin.ts"]
|
||||
//
|
||||
// Afterwards, run `./packages/opencode/script/run-workspace-server`
|
||||
|
||||
import { stat } from "node:fs/promises"
|
||||
import { setTimeout as sleep } from "node:timers/promises"
|
||||
|
||||
const DEV_DATA_FILE = "/tmp/opencode-workspace-dev-data.json"
|
||||
const RESTART_POLL_INTERVAL = 250
|
||||
|
||||
async function readData() {
|
||||
return await Bun.file(DEV_DATA_FILE).json()
|
||||
}
|
||||
|
||||
async function readDataMtime() {
|
||||
return await stat(DEV_DATA_FILE)
|
||||
.then((info) => info.mtimeMs)
|
||||
.catch((error) => {
|
||||
if (typeof error === "object" && error && "code" in error && error.code === "ENOENT") {
|
||||
return undefined
|
||||
}
|
||||
|
||||
throw error
|
||||
})
|
||||
}
|
||||
|
||||
async function readSnapshot() {
|
||||
while (true) {
|
||||
try {
|
||||
const before = await readDataMtime()
|
||||
if (before === undefined) {
|
||||
await sleep(RESTART_POLL_INTERVAL)
|
||||
continue
|
||||
}
|
||||
|
||||
const data = await readData()
|
||||
const after = await readDataMtime()
|
||||
|
||||
if (before === after) {
|
||||
return { data, mtime: after }
|
||||
}
|
||||
} catch (error) {
|
||||
if (typeof error === "object" && error && "code" in error && error.code === "ENOENT") {
|
||||
await sleep(RESTART_POLL_INTERVAL)
|
||||
continue
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function startDevServer(data: any) {
|
||||
const env = Object.fromEntries(Object.entries(data.env ?? {}).filter(([, value]) => value !== undefined))
|
||||
|
||||
return Bun.spawn(["bun", "run", "dev", "serve", "--port", String(data.port), "--print-logs"], {
|
||||
env: {
|
||||
...process.env,
|
||||
...env,
|
||||
XDG_DATA_HOME: "/tmp/data",
|
||||
},
|
||||
stdin: "inherit",
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
})
|
||||
}
|
||||
|
||||
async function waitForRestartSignal(mtime: number, signal: AbortSignal) {
|
||||
while (!signal.aborted) {
|
||||
await sleep(RESTART_POLL_INTERVAL)
|
||||
if (signal.aborted) return false
|
||||
if ((await readDataMtime()) !== mtime) return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
while (true) {
|
||||
const { data, mtime } = await readSnapshot()
|
||||
const proc = startDevServer(data)
|
||||
const restartAbort = new AbortController()
|
||||
|
||||
const result = await Promise.race([
|
||||
proc.exited.then((code) => ({ type: "exit" as const, code })),
|
||||
waitForRestartSignal(mtime, restartAbort.signal).then((restart) => ({ type: "restart" as const, restart })),
|
||||
])
|
||||
|
||||
restartAbort.abort()
|
||||
|
||||
if (result.type === "restart" && result.restart) {
|
||||
proc.kill()
|
||||
await proc.exited
|
||||
continue
|
||||
}
|
||||
|
||||
process.exit(result.code)
|
||||
}
|
||||
@@ -55,7 +55,7 @@ const configFile = process.argv[2]
|
||||
const tuiFile = process.argv[3]
|
||||
|
||||
console.log(configFile)
|
||||
await Bun.write(configFile, JSON.stringify(generate(Config.Info), null, 2))
|
||||
await Bun.write(configFile, JSON.stringify(generate(Config.Info.zod), null, 2))
|
||||
|
||||
if (tuiFile) {
|
||||
console.log(tuiFile)
|
||||
|
||||
@@ -224,7 +224,7 @@ When to use each:
|
||||
|
||||
Promoting a previously-anonymous schema to Schema.Class is acceptable when it is top-level or endpoint-facing, but call it out in the PR — it is an additive SDK change (`export type Foo = ...` newly appears) even if it preserves the JSON shape.
|
||||
|
||||
Schemas that are **not** pure objects (enums, unions, records, tuples) cannot use Schema.Class. For those, add `.annotate({ identifier: "FooName" })` to get the same named-ref behavior:
|
||||
Schemas that are **not** pure objects (enums, unions, records, tuples) cannot use Schema.Class. For those — and for pure-object schemas where handlers populate plain objects rather than class instances — add `.annotate({ identifier: "FooName" })` to get the same named-ref behavior without the `instanceof` requirement:
|
||||
|
||||
```ts
|
||||
export const Action = Schema.Literals(["ask", "allow", "deny"]).annotate({ identifier: "PermissionActionConfig" })
|
||||
@@ -373,9 +373,9 @@ The first slice is successful if:
|
||||
|
||||
- `Schema.Class` works well for route DTOs such as `Question.Request`, `Question.Info`, and `Question.Reply`.
|
||||
- scalar or collection schemas such as `Question.Answer` should stay as schemas and use helpers like `withStatics(...)` instead of being forced into classes.
|
||||
- if an `HttpApi` success schema uses `Schema.Class`, the handler or underlying service needs to return real schema instances rather than plain objects.
|
||||
- if an `HttpApi` success schema uses `Schema.Class`, the handler or underlying service needs to return real schema instances rather than plain objects. `Schema.Class`'s Declaration AST enforces `input instanceof self || input.[ClassTypeId]` during encode (see effect-smol `Schema.ts:10479-10484`). Plain objects from zod parse fail with `Expected Foo, got {...}`. This surfaced on `GET /config` where the service returns zod-parsed plain objects and `Config.InfoSchema` referenced `ConfigProvider.Info` (class). The fix was to convert pure-object classes to `Schema.Struct(...).annotate({ identifier: "..." })` — same named SDK `$ref`, no instance requirement. Verified byte-identical `types.gen.ts` vs `dev`.
|
||||
- internal event payloads can stay anonymous when we want to avoid adding extra named OpenAPI component churn for non-route shapes.
|
||||
- `Schema.Class` emits named `$ref` in OpenAPI — only use it for types that already had `.meta({ ref })` in the old Zod schema. Inner/nested types should stay as `Schema.Struct` to avoid SDK shape changes.
|
||||
- `Schema.Class` emits named `$ref` in OpenAPI — only use it for types that already had `.meta({ ref })` in the old Zod schema **and** when the handler/service returns real instances. For schemas that need a named `$ref` but are populated from plain objects, use `Schema.Struct(...).annotate({ identifier: "..." })` instead. Inner/nested types should stay as `Schema.Struct` to avoid SDK shape changes.
|
||||
|
||||
### Integration
|
||||
|
||||
@@ -404,8 +404,7 @@ Current instance route inventory:
|
||||
- `provider` - `bridged`
|
||||
endpoints: `GET /provider`, `GET /provider/auth`, `POST /provider/:providerID/oauth/authorize`, `POST /provider/:providerID/oauth/callback`
|
||||
- `config` - `bridged` (partial)
|
||||
bridged endpoint: `GET /config/providers`
|
||||
later endpoint: `GET /config`
|
||||
bridged endpoints: `GET /config`, `GET /config/providers`
|
||||
defer `PATCH /config` for now
|
||||
- `project` - `bridged` (partial)
|
||||
bridged endpoints: `GET /project`, `GET /project/current`
|
||||
@@ -431,9 +430,8 @@ Current instance route inventory:
|
||||
Recommended near-term sequence:
|
||||
|
||||
1. `workspace` read endpoints (`GET /experimental/workspace/adaptor`, `GET /experimental/workspace`, `GET /experimental/workspace/status`)
|
||||
2. `config` full read endpoint (`GET /config`)
|
||||
3. `file` JSON read endpoints
|
||||
4. `mcp` JSON read endpoints
|
||||
2. `file` JSON read endpoints
|
||||
3. `mcp` JSON read endpoints
|
||||
|
||||
## Checklist
|
||||
|
||||
@@ -449,8 +447,8 @@ Recommended near-term sequence:
|
||||
- [x] port remaining provider endpoints (`GET /provider`, OAuth mutations)
|
||||
- [x] port `config` providers read endpoint
|
||||
- [x] port `project` read endpoints (`GET /project`, `GET /project/current`)
|
||||
- [x] port `GET /config` full read endpoint
|
||||
- [ ] port `workspace` read endpoints
|
||||
- [ ] port `GET /config` full read endpoint
|
||||
- [ ] port `file` JSON read endpoints
|
||||
- [ ] decide when to remove the flag and make Effect routes the default
|
||||
|
||||
|
||||
@@ -224,7 +224,6 @@ These tools mostly use direct getters for path resolution and repo-relative disp
|
||||
- `src/tool/bash.ts`
|
||||
- `src/tool/edit.ts`
|
||||
- `src/tool/lsp.ts`
|
||||
- `src/tool/multiedit.ts`
|
||||
- `src/tool/plan.ts`
|
||||
- `src/tool/read.ts`
|
||||
- `src/tool/write.ts`
|
||||
|
||||
@@ -97,7 +97,7 @@ creating a parallel schema source of truth.
|
||||
|
||||
## Escape hatches
|
||||
|
||||
The walker in `@/util/effect-zod` exposes three explicit escape hatches for
|
||||
The walker in `@/util/effect-zod` exposes two explicit escape hatches for
|
||||
cases the pure-Schema path cannot express. Each one stays in the codebase
|
||||
only as long as its upstream or local dependency requires it — inline
|
||||
comments document when each can be deleted.
|
||||
@@ -109,19 +109,7 @@ Replaces the entire derivation with a hand-crafted zod schema. Used when:
|
||||
- the target carries external `$ref` metadata (e.g.
|
||||
`config/model-id.ts` points at `https://models.dev/...`)
|
||||
- the target is a zod-only schema that cannot yet be expressed as Schema
|
||||
(e.g. `ConfigAgent.Info`, `ConfigPermission.Info`, `Log.Level`)
|
||||
|
||||
### `ZodPreprocess` annotation
|
||||
|
||||
Wraps the derived zod schema with `z.preprocess(fn, inner)`. Used by
|
||||
`config/permission.ts` to inject `__originalKeys` before parsing, because
|
||||
`Schema.StructWithRest` canonicalises output (known fields first, catchall
|
||||
after) and destroys the user's original property order — which permission
|
||||
rule precedence depends on.
|
||||
|
||||
Tracked upstream as `effect:core/wlh553`: "Schema: add preserveInputOrder
|
||||
(or pre-parse hook) for open structs." Once that lands, `ZodPreprocess` and
|
||||
the `__originalKeys` hack can both be deleted.
|
||||
(e.g. `ConfigAgent.Info`, `Log.Level`)
|
||||
|
||||
### Local `DeepMutable<T>` in `config/config.ts`
|
||||
|
||||
@@ -171,21 +159,95 @@ Schema at source.
|
||||
These are the highest-priority next targets. Each is a small, self-contained
|
||||
schema module with a clear domain.
|
||||
|
||||
- [ ] `src/control-plane/schema.ts`
|
||||
- [ ] `src/permission/schema.ts`
|
||||
- [ ] `src/project/schema.ts`
|
||||
- [ ] `src/provider/schema.ts`
|
||||
- [ ] `src/pty/schema.ts`
|
||||
- [ ] `src/question/schema.ts`
|
||||
- [ ] `src/session/schema.ts`
|
||||
- [ ] `src/sync/schema.ts`
|
||||
- [ ] `src/tool/schema.ts`
|
||||
- [x] `src/control-plane/schema.ts`
|
||||
- [x] `src/permission/schema.ts`
|
||||
- [x] `src/project/schema.ts`
|
||||
- [x] `src/provider/schema.ts`
|
||||
- [x] `src/pty/schema.ts`
|
||||
- [x] `src/question/schema.ts`
|
||||
- [x] `src/session/schema.ts`
|
||||
- [x] `src/sync/schema.ts`
|
||||
- [x] `src/tool/schema.ts`
|
||||
|
||||
### Session domain
|
||||
|
||||
Major cluster. Message + event types flow through the SSE API and every SDK
|
||||
output, so byte-identical SDK surface is critical.
|
||||
|
||||
Suggested order for this cluster, starting from the leaves that `session.ts`
|
||||
and the SSE/event surface depend on:
|
||||
|
||||
1. `src/session/schema.ts` ✅ already migrated
|
||||
2. `src/provider/schema.ts` if `message-v2.ts` still relies on zod-first IDs
|
||||
3. `src/lsp/*` schema leaves needed by `LSP.Range`
|
||||
4. `src/snapshot/*` leaves used by `Snapshot.FileDiff`
|
||||
5. `src/session/message-v2.ts`
|
||||
6. `src/session/message.ts`
|
||||
7. `src/session/prompt.ts`
|
||||
8. `src/session/revert.ts`
|
||||
9. `src/session/summary.ts`
|
||||
10. `src/session/status.ts`
|
||||
11. `src/session/todo.ts`
|
||||
12. `src/session/session.ts`
|
||||
13. `src/session/compaction.ts`
|
||||
|
||||
Dependency sketch:
|
||||
|
||||
```text
|
||||
session.ts
|
||||
|- project/schema.ts
|
||||
|- control-plane/schema.ts
|
||||
|- permission/schema.ts
|
||||
|- snapshot/*
|
||||
|- message-v2.ts
|
||||
| |- provider/schema.ts
|
||||
| |- lsp/*
|
||||
| |- snapshot/*
|
||||
| |- sync/index.ts
|
||||
| `- bus/bus-event.ts
|
||||
|- sync/index.ts
|
||||
|- bus/bus-event.ts
|
||||
`- util/update-schema.ts
|
||||
```
|
||||
|
||||
Working rule for this cluster:
|
||||
|
||||
- migrate reusable leaf schemas and nested payload objects first
|
||||
- migrate aggregate DTOs like `Session.Info` after their nested pieces exist as
|
||||
named Schema values
|
||||
- leave zod-only event/update helpers in place temporarily when converting
|
||||
them would force unrelated churn across sync/bus boundaries
|
||||
|
||||
`message-v2.ts` first-pass outline:
|
||||
|
||||
1. Schema-backed imports already available
|
||||
- `SessionID`, `MessageID`, `PartID`
|
||||
- `ProviderID`, `ModelID`
|
||||
2. Local leaf objects to extract and migrate first
|
||||
- output format payloads
|
||||
- common part bases like `PartBase`
|
||||
- timestamp/range helper objects like `time.start/end`
|
||||
- file/source helper objects
|
||||
- token/cost/model helper objects
|
||||
3. Part variants built from those leaves
|
||||
- `SnapshotPart`, `PatchPart`, `TextPart`, `ReasoningPart`
|
||||
- `FilePart`, `AgentPart`, `CompactionPart`, `SubtaskPart`
|
||||
- retry/step/tool related parts
|
||||
4. Higher-level unions and DTOs
|
||||
- `FilePartSource`
|
||||
- part unions
|
||||
- message unions and assistant/user payloads
|
||||
5. Errors and event payloads last
|
||||
- `NamedError.create(...)` shapes can stay temporarily if converting them to
|
||||
`Schema.TaggedErrorClass` would force unrelated churn
|
||||
- `SyncEvent.define(...)` and `BusEvent.define(...)` payloads can keep using
|
||||
derived `.zod` until the sync/bus layers are migrated
|
||||
|
||||
Possible later tightening after the Schema-first migration is stable:
|
||||
|
||||
- promote repeated opaque strings and timestamp numbers into branded/newtype
|
||||
leaf schemas where that adds domain value without changing the wire format
|
||||
|
||||
- [ ] `src/session/compaction.ts`
|
||||
- [ ] `src/session/message-v2.ts`
|
||||
- [ ] `src/session/message.ts`
|
||||
@@ -216,7 +278,6 @@ emitted JSON Schema must stay byte-identical.
|
||||
- [ ] `src/tool/grep.ts`
|
||||
- [ ] `src/tool/invalid.ts`
|
||||
- [ ] `src/tool/lsp.ts`
|
||||
- [ ] `src/tool/multiedit.ts`
|
||||
- [ ] `src/tool/plan.ts`
|
||||
- [ ] `src/tool/question.ts`
|
||||
- [ ] `src/tool/read.ts`
|
||||
|
||||
@@ -46,7 +46,6 @@ These exported tool definitions currently use `Tool.define(...)` in `src/tool`:
|
||||
- [x] `grep.ts`
|
||||
- [x] `invalid.ts`
|
||||
- [x] `lsp.ts`
|
||||
- [x] `multiedit.ts`
|
||||
- [x] `plan.ts`
|
||||
- [x] `question.ts`
|
||||
- [x] `read.ts`
|
||||
@@ -82,7 +81,6 @@ Notable items that are already effectively on the target path and do not need se
|
||||
- `write.ts`
|
||||
- `codesearch.ts`
|
||||
- `websearch.ts`
|
||||
- `multiedit.ts`
|
||||
- `edit.ts`
|
||||
|
||||
## Filesystem notes
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
You are a helpful AI assistant tasked with summarizing conversations.
|
||||
You are an anchored context summarization assistant for coding sessions.
|
||||
|
||||
When asked to summarize, provide a detailed but concise summary of the conversation.
|
||||
Focus on information that would be helpful for continuing the conversation, including:
|
||||
- What was done
|
||||
- What is currently being worked on
|
||||
- Which files are being modified
|
||||
- What needs to be done next
|
||||
- Key user requests, constraints, or preferences that should persist
|
||||
- Important technical decisions and why they were made
|
||||
Summarize only the conversation history you are given. The newest turns may be kept verbatim outside your summary, so focus on the older context that still matters for continuing the work.
|
||||
|
||||
Your summary should be comprehensive enough to provide context but concise enough to be quickly understood.
|
||||
If the prompt includes a <previous-summary> block, treat it as the current anchored summary. Update it with the new history by preserving still-true details, removing stale details, and merging in new facts.
|
||||
|
||||
Do not respond to any questions in the conversation, only output the summary.
|
||||
Respond in the same language the user used in the conversation.
|
||||
Always follow the exact output structure requested by the user prompt. Keep every section, preserve exact file paths and identifiers when known, and prefer terse bullets over paragraphs.
|
||||
|
||||
Do not answer the conversation itself. Do not mention that you are summarizing, compacting, or merging context. Respond in the same language as the conversation.
|
||||
|
||||
@@ -23,8 +23,7 @@ const DiagnosticsCommand = cmd({
|
||||
const out = await AppRuntime.runPromise(
|
||||
LSP.Service.use((lsp) =>
|
||||
Effect.gen(function* () {
|
||||
yield* lsp.touchFile(args.file, true)
|
||||
yield* Effect.sleep(1000)
|
||||
yield* lsp.touchFile(args.file, "full")
|
||||
return yield* lsp.diagnostics()
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -992,7 +992,8 @@ export const GithubRunCommand = cmd({
|
||||
const err = result.info.error
|
||||
console.error("Agent error:", err)
|
||||
if (err.name === "ContextOverflowError") throw new Error(formatPromptTooLargeError(files))
|
||||
throw new Error(`${err.name}: ${err.data?.message || ""}`)
|
||||
const message = "message" in err.data ? err.data.message : ""
|
||||
throw new Error(`${err.name}: ${message}`)
|
||||
}
|
||||
|
||||
const text = extractResponseText(result.parts)
|
||||
@@ -1021,7 +1022,8 @@ export const GithubRunCommand = cmd({
|
||||
const err = summary.info.error
|
||||
console.error("Summary agent error:", err)
|
||||
if (err.name === "ContextOverflowError") throw new Error(formatPromptTooLargeError(files))
|
||||
throw new Error(`${err.name}: ${err.data?.message || ""}`)
|
||||
const message = "message" in err.data ? err.data.message : ""
|
||||
throw new Error(`${err.name}: ${message}`)
|
||||
}
|
||||
|
||||
const summaryText = extractResponseText(summary.parts)
|
||||
|
||||
@@ -197,7 +197,7 @@ export const ImportCommand = cmd({
|
||||
)
|
||||
|
||||
for (const msg of exportData.messages) {
|
||||
const msgInfo = MessageV2.Info.parse(msg.info)
|
||||
const msgInfo = MessageV2.Info.zod.parse(msg.info)
|
||||
const { id, sessionID: _, ...msgData } = msgInfo
|
||||
Database.use((db) =>
|
||||
db
|
||||
@@ -213,7 +213,7 @@ export const ImportCommand = cmd({
|
||||
)
|
||||
|
||||
for (const part of msg.parts) {
|
||||
const partInfo = MessageV2.Part.parse(part)
|
||||
const partInfo = MessageV2.Part.zod.parse(part)
|
||||
const { id: partId, sessionID: _s, messageID, ...partData } = partInfo
|
||||
Database.use((db) =>
|
||||
db
|
||||
|
||||
@@ -4,9 +4,30 @@ import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { Git } from "@/git"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { Process } from "@/util"
|
||||
import { existsSync } from "node:fs" // kilocode_change
|
||||
|
||||
const subcommand = "pr" // kilocode_change
|
||||
|
||||
// kilocode_change start - resolve the currently running CLI instead of hardcoding opencode
|
||||
export function cliCommand(
|
||||
input = {
|
||||
execPath: process.execPath,
|
||||
argv: process.argv,
|
||||
exists: existsSync,
|
||||
},
|
||||
) {
|
||||
const script = input.argv[1]
|
||||
if (!script) return [input.execPath]
|
||||
if (script === subcommand) return [input.execPath] // kilocode_change
|
||||
if (script.startsWith("/$bunfs/root/")) return [input.execPath]
|
||||
if (script.startsWith("B:/~BUN/root/")) return [input.execPath]
|
||||
if (input.exists(script)) return [input.execPath, script]
|
||||
return [input.execPath]
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
export const PrCommand = cmd({
|
||||
command: "pr <number>",
|
||||
command: `${subcommand} <number>`, // kilocode_change
|
||||
describe: "fetch and checkout a GitHub PR branch, then run kilo", // kilocode_change
|
||||
builder: (yargs) =>
|
||||
yargs.positional("number", {
|
||||
@@ -26,6 +47,7 @@ export const PrCommand = cmd({
|
||||
|
||||
const prNumber = args.number
|
||||
const localBranchName = `pr/${prNumber}`
|
||||
const cli = cliCommand() // kilocode_change
|
||||
UI.println(`Fetching and checking out PR #${prNumber}...`)
|
||||
|
||||
// Use gh pr checkout with custom branch name
|
||||
@@ -102,9 +124,7 @@ export const PrCommand = cmd({
|
||||
UI.println(`Found session: ${sessionUrl}`)
|
||||
UI.println(`Importing session...`)
|
||||
|
||||
const importResult = await Process.text(["kilo", "import", sessionUrl], {
|
||||
nothrow: true,
|
||||
})
|
||||
const importResult = await Process.text([...cli, "import", sessionUrl], { nothrow: true })
|
||||
// kilocode_change end
|
||||
if (importResult.code === 0) {
|
||||
const importOutput = importResult.text.trim()
|
||||
@@ -122,13 +142,12 @@ export const PrCommand = cmd({
|
||||
|
||||
UI.println(`Successfully checked out PR #${prNumber} as branch '${localBranchName}'`)
|
||||
UI.println()
|
||||
const bin = "kilo" // kilocode_change
|
||||
UI.println(`Starting ${bin}...`) // kilocode_change
|
||||
UI.println("Starting kilo...") // kilocode_change
|
||||
UI.println()
|
||||
|
||||
const opencodeArgs = sessionId ? ["-s", sessionId] : []
|
||||
const run = sessionId ? [...cli, "-s", sessionId] : cli // kilocode_change
|
||||
// kilocode_change start
|
||||
const opencodeProcess = Process.spawn([bin, ...opencodeArgs], {
|
||||
const opencodeProcess = Process.spawn(run, {
|
||||
// kilocode_change end
|
||||
stdin: "inherit",
|
||||
stdout: "inherit",
|
||||
@@ -136,7 +155,7 @@ export const PrCommand = cmd({
|
||||
cwd: process.cwd(),
|
||||
})
|
||||
const code = await opencodeProcess.exited
|
||||
if (code !== 0) throw new Error(`${bin} exited with code ${code}`) // kilocode_change
|
||||
if (code !== 0) throw new Error(`kilo exited with code ${code}`) // kilocode_change
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
@@ -126,8 +126,8 @@ export function tui(input: {
|
||||
|
||||
const mode = await Terminal.getTerminalBackgroundColor()
|
||||
|
||||
// Re-clear after getTerminalBackgroundColor() — setRawMode(false) restores
|
||||
// the original console mode which re-enables ENABLE_PROCESSED_INPUT.
|
||||
// Re-clear after getTerminalBackgroundColor() because setRawMode(false)
|
||||
// restores the original console mode, including processed input on Windows.
|
||||
win32DisableProcessedInput()
|
||||
|
||||
const onExit = async () => {
|
||||
|
||||
@@ -5,6 +5,8 @@ import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
|
||||
import { TuiConfig } from "@/cli/cmd/tui/config/tui"
|
||||
import { createKiloClient } from "@kilocode/sdk/v2" // kilocode_change
|
||||
import { importCloudSession, validateCloudFork } from "@/kilocode/cloud-session" // kilocode_change
|
||||
import { errorMessage } from "@/util/error"
|
||||
import { validateSession } from "./validate-session"
|
||||
|
||||
export const AttachCommand = cmd({
|
||||
command: "attach <url>",
|
||||
@@ -98,6 +100,20 @@ export const AttachCommand = cmd({
|
||||
}
|
||||
// kilocode_change end
|
||||
const config = await TuiConfig.get()
|
||||
|
||||
try {
|
||||
await validateSession({
|
||||
url: args.url,
|
||||
sessionID: args.session,
|
||||
directory,
|
||||
headers,
|
||||
})
|
||||
} catch (error) {
|
||||
UI.error(errorMessage(error))
|
||||
process.exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
await tui({
|
||||
url: args.url,
|
||||
config,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Global } from "@/global"
|
||||
import { Filesystem } from "@/util"
|
||||
import { Flock } from "@opencode-ai/shared/util/flock"
|
||||
import { rename, rm } from "fs/promises"
|
||||
import { createSignal, type Setter } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createStore, unwrap } from "solid-js/store"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import path from "path"
|
||||
|
||||
@@ -11,12 +13,29 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({
|
||||
const [ready, setReady] = createSignal(false)
|
||||
const [store, setStore] = createStore<Record<string, any>>()
|
||||
const filePath = path.join(Global.Path.state, "kv.json")
|
||||
const lock = `tui-kv:${filePath}`
|
||||
// Queue same-process writes so rapid updates persist in order.
|
||||
let write = Promise.resolve()
|
||||
|
||||
Filesystem.readJson<Record<string, any>>(filePath)
|
||||
// Write to a temp file first so kv.json is only replaced once the JSON is complete, avoiding partial writes if shutdown interrupts persistence.
|
||||
function writeSnapshot(snapshot: Record<string, any>) {
|
||||
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`
|
||||
return Filesystem.writeJson(tempPath, snapshot)
|
||||
.then(() => rename(tempPath, filePath))
|
||||
.catch(async (error) => {
|
||||
await rm(tempPath, { force: true }).catch(() => undefined)
|
||||
throw error
|
||||
})
|
||||
}
|
||||
|
||||
// Read under the same lock used for writes because kv.json is shared across processes.
|
||||
Flock.withLock(lock, () => Filesystem.readJson<Record<string, any>>(filePath))
|
||||
.then((x) => {
|
||||
setStore(x)
|
||||
})
|
||||
.catch(() => {})
|
||||
.catch((error) => {
|
||||
console.error("Failed to read KV state", { filePath, error })
|
||||
})
|
||||
.finally(() => {
|
||||
setReady(true)
|
||||
})
|
||||
@@ -44,7 +63,12 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({
|
||||
},
|
||||
set(key: string, value: any) {
|
||||
setStore(key, value)
|
||||
void Filesystem.writeJson(filePath, store)
|
||||
const snapshot = structuredClone(unwrap(store))
|
||||
write = write
|
||||
.then(() => Flock.withLock(lock, () => writeSnapshot(snapshot)))
|
||||
.catch((error) => {
|
||||
console.error("Failed to write KV state", { filePath, error })
|
||||
})
|
||||
},
|
||||
}
|
||||
return result
|
||||
|
||||
@@ -327,8 +327,11 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
const lock = pick(kv.get("theme_mode_lock"))
|
||||
const mode = pick(kv.get("theme_mode", props.mode))
|
||||
draft.mode = lock ?? mode ?? props.mode
|
||||
const mode = lock ?? props.mode
|
||||
if (!lock && pick(kv.get("theme_mode")) !== undefined) {
|
||||
kv.set("theme_mode", undefined)
|
||||
}
|
||||
draft.mode = mode
|
||||
draft.lock = lock
|
||||
const active = config.theme ?? kv.get("theme", "kilo") // kilocode_change
|
||||
draft.active = typeof active === "string" ? active : "kilo" // kilocode_change
|
||||
@@ -386,7 +389,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
}
|
||||
|
||||
function apply(mode: "dark" | "light") {
|
||||
kv.set("theme_mode", mode)
|
||||
if (store.lock !== undefined) kv.set("theme_mode", mode)
|
||||
if (store.mode === mode) return
|
||||
setStore("mode", mode)
|
||||
renderer.clearPaletteCache()
|
||||
@@ -402,6 +405,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
function free() {
|
||||
setStore("lock", undefined)
|
||||
kv.set("theme_mode_lock", undefined)
|
||||
kv.set("theme_mode", undefined)
|
||||
const mode = renderer.themeMode
|
||||
if (mode) apply(mode)
|
||||
}
|
||||
@@ -410,7 +414,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
if (store.lock) return
|
||||
apply(mode)
|
||||
}
|
||||
// renderer.on(CliRenderEvents.THEME_MODE, handle)
|
||||
renderer.on(CliRenderEvents.THEME_MODE, handle)
|
||||
|
||||
const refresh = () => {
|
||||
renderer.clearPaletteCache()
|
||||
|
||||
@@ -63,6 +63,7 @@ import { Flag } from "@/flag/flag"
|
||||
import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
|
||||
import parsers from "../../../../../../parsers-config.ts"
|
||||
import * as Clipboard from "../../util/clipboard"
|
||||
import { errorMessage } from "@/util/error"
|
||||
import { Toast, useToast } from "../../ui/toast"
|
||||
import { useKV } from "../../context/kv.tsx"
|
||||
import * as Editor from "../../util/editor"
|
||||
@@ -261,31 +262,43 @@ export function Session() {
|
||||
const toast = useToast()
|
||||
const sdk = useSDK()
|
||||
|
||||
createEffect(async () => {
|
||||
const previousWorkspace = project.workspace.current()
|
||||
const result = await sdk.client.session.get({ sessionID: route.sessionID }, { throwOnError: true })
|
||||
if (!result.data) {
|
||||
createEffect(() => {
|
||||
const sessionID = route.sessionID
|
||||
void (async () => {
|
||||
const previousWorkspace = project.workspace.current()
|
||||
const result = await sdk.client.session.get({ sessionID }, { throwOnError: true })
|
||||
if (!result.data) {
|
||||
toast.show({
|
||||
message: `Session not found: ${sessionID}`,
|
||||
variant: "error",
|
||||
duration: 5000,
|
||||
})
|
||||
navigate({ type: "home" })
|
||||
return
|
||||
}
|
||||
|
||||
if (result.data.workspaceID !== previousWorkspace) {
|
||||
project.workspace.set(result.data.workspaceID)
|
||||
|
||||
// Sync all the data for this workspace. Note that this
|
||||
// workspace may not exist anymore which is why this is not
|
||||
// fatal. If it doesn't we still want to show the session
|
||||
// (which will be non-interactive)
|
||||
try {
|
||||
await sync.bootstrap({ fatal: false })
|
||||
} catch {}
|
||||
}
|
||||
await sync.session.sync(sessionID)
|
||||
if (route.sessionID === sessionID && scroll) scroll.scrollBy(100_000)
|
||||
})().catch((error) => {
|
||||
if (route.sessionID !== sessionID) return
|
||||
toast.show({
|
||||
message: `Session not found: ${route.sessionID}`,
|
||||
message: errorMessage(error),
|
||||
variant: "error",
|
||||
duration: 5000,
|
||||
})
|
||||
navigate({ type: "home" })
|
||||
return
|
||||
}
|
||||
|
||||
if (result.data.workspaceID !== previousWorkspace) {
|
||||
project.workspace.set(result.data.workspaceID)
|
||||
|
||||
// Sync all the data for this workspace. Note that this
|
||||
// workspace may not exist anymore which is why this is not
|
||||
// fatal. If it doesn't we still want to show the session
|
||||
// (which will be non-interactive)
|
||||
try {
|
||||
await sync.bootstrap({ fatal: false })
|
||||
} catch (e) {}
|
||||
}
|
||||
await sync.session.sync(route.sessionID)
|
||||
if (scroll) scroll.scrollBy(100_000)
|
||||
})
|
||||
})
|
||||
|
||||
let lastSwitch: string | undefined = undefined
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useSDK } from "../../context/sdk"
|
||||
import { SplitBorder } from "../../component/border"
|
||||
import { useSync } from "../../context/sync"
|
||||
import { useTextareaKeybindings } from "../../component/textarea-keybindings"
|
||||
import { useProject } from "../../context/project"
|
||||
import path from "path"
|
||||
import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
|
||||
import { Keybind } from "@/util"
|
||||
@@ -132,6 +133,7 @@ function TextBody(props: { title: string; description?: string; icon?: string })
|
||||
|
||||
export function PermissionPrompt(props: { request: PermissionRequest }) {
|
||||
const sdk = useSDK()
|
||||
const project = useProject()
|
||||
const sync = useSync()
|
||||
const [store, setStore] = createStore({
|
||||
stage: "permission" as PermissionStage,
|
||||
@@ -191,6 +193,7 @@ export function PermissionPrompt(props: { request: PermissionRequest }) {
|
||||
void sdk.client.permission.reply({
|
||||
reply: "always",
|
||||
requestID: props.request.id,
|
||||
workspace: project.workspace.current(),
|
||||
})
|
||||
}}
|
||||
/>
|
||||
@@ -202,6 +205,7 @@ export function PermissionPrompt(props: { request: PermissionRequest }) {
|
||||
reply: "reject",
|
||||
requestID: props.request.id,
|
||||
message: message || undefined,
|
||||
workspace: project.workspace.current(),
|
||||
})
|
||||
}}
|
||||
onCancel={() => {
|
||||
@@ -467,12 +471,14 @@ export function PermissionPrompt(props: { request: PermissionRequest }) {
|
||||
void sdk.client.permission.reply({
|
||||
reply: "reject",
|
||||
requestID: props.request.id,
|
||||
workspace: project.workspace.current(),
|
||||
})
|
||||
return
|
||||
}
|
||||
void sdk.client.permission.reply({
|
||||
reply: "once",
|
||||
requestID: props.request.id,
|
||||
workspace: project.workspace.current(),
|
||||
})
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -19,6 +19,7 @@ import { createKiloClient } from "@kilocode/sdk/v2" // kilocode_change
|
||||
import { writeHeapSnapshot } from "v8"
|
||||
import { TuiConfig } from "./config/tui"
|
||||
import { KILO_PROCESS_ROLE, KILO_RUN_ID, ensureRunID, sanitizedProcessEnv } from "@/util/opencode-process"
|
||||
import { validateSession } from "./validate-session"
|
||||
|
||||
declare global {
|
||||
const KILO_WORKER_PATH: string
|
||||
@@ -285,6 +286,19 @@ export const TuiThreadCommand = cmd({
|
||||
events: createEventSource(client),
|
||||
}
|
||||
|
||||
try {
|
||||
await validateSession({
|
||||
url: transport.url,
|
||||
sessionID: args.session,
|
||||
directory: cwd,
|
||||
fetch: transport.fetch,
|
||||
})
|
||||
} catch (error) {
|
||||
UI.error(errorMessage(error))
|
||||
process.exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
client.call("checkUpgrade", { directory: cwd }).catch(() => {})
|
||||
}, 1000).unref?.()
|
||||
|
||||
@@ -41,9 +41,9 @@ function parse(color: string): RGBA | null {
|
||||
return null
|
||||
}
|
||||
|
||||
function mode(bg: RGBA | null): "dark" | "light" {
|
||||
if (!bg) return "dark"
|
||||
const luminance = (0.299 * bg.r + 0.587 * bg.g + 0.114 * bg.b) / 255
|
||||
function mode(background: RGBA | null): "dark" | "light" {
|
||||
if (!background) return "dark"
|
||||
const luminance = (0.299 * background.r + 0.587 * background.g + 0.114 * background.b) / 255
|
||||
return luminance > 0.5 ? "light" : "dark"
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { createKiloClient } from "@kilocode/sdk/v2"
|
||||
import { SessionID } from "@/session/schema"
|
||||
|
||||
export async function validateSession(input: {
|
||||
url: string
|
||||
sessionID?: string
|
||||
directory?: string
|
||||
fetch?: typeof fetch
|
||||
headers?: RequestInit["headers"]
|
||||
}) {
|
||||
if (!input.sessionID) return
|
||||
|
||||
const result = SessionID.zod.safeParse(input.sessionID)
|
||||
if (!result.success) {
|
||||
throw new Error(`Invalid session ID: ${result.error.issues.at(0)?.message ?? "unknown error"}`)
|
||||
}
|
||||
|
||||
await createKiloClient({
|
||||
baseUrl: input.url,
|
||||
directory: input.directory,
|
||||
fetch: input.fetch,
|
||||
headers: input.headers,
|
||||
}).session.get({ sessionID: result.data }, { throwOnError: true })
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { InstallationVersion } from "@/installation/version"
|
||||
|
||||
export async function upgrade() {
|
||||
const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.getGlobal()))
|
||||
if (config.autoupdate === false || Flag.KILO_DISABLE_AUTOUPDATE) return
|
||||
const method = await AppRuntime.runPromise(Installation.Service.use((svc) => svc.method()))
|
||||
// kilocode_change start - only auto-upgrade for npm/pnpm/bun (we only publish @kilocode/cli via npm registry)
|
||||
if (method !== "npm" && method !== "pnpm" && method !== "bun") return
|
||||
@@ -20,7 +21,6 @@ export async function upgrade() {
|
||||
}
|
||||
|
||||
if (InstallationVersion === latest) return
|
||||
if (config.autoupdate === false || Flag.KILO_DISABLE_AUTOUPDATE) return
|
||||
|
||||
const kind = Installation.getReleaseType(InstallationVersion, latest)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ export * as ConfigAgent from "./agent"
|
||||
import { Schema } from "effect"
|
||||
import z from "zod"
|
||||
import { Bus } from "@/bus"
|
||||
import { zod, ZodOverride } from "@/util/effect-zod"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { Log } from "../util"
|
||||
import { NamedError } from "@opencode-ai/shared/util/error"
|
||||
import { Glob } from "@opencode-ai/shared/util/glob"
|
||||
@@ -25,12 +25,6 @@ const Color = Schema.Union([
|
||||
Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]),
|
||||
])
|
||||
|
||||
// ConfigPermission.Info is a zod schema (its `.preprocess(...).transform(...)`
|
||||
// shape lives outside the Effect Schema type system), so the walker reaches it
|
||||
// via ZodOverride rather than a pure Schema reference. This preserves the
|
||||
// `$ref: PermissionConfig` emitted in openapi.json.
|
||||
const PermissionRef = Schema.Any.annotate({ [ZodOverride]: ConfigPermission.Info })
|
||||
|
||||
const AgentSchema = Schema.StructWithRest(
|
||||
Schema.Struct({
|
||||
model: Schema.optional(Schema.NullOr(ConfigModelID)), // kilocode_change - nullable for delete sentinel
|
||||
@@ -57,7 +51,7 @@ const AgentSchema = Schema.StructWithRest(
|
||||
description: "Maximum number of agentic iterations before forcing text-only response",
|
||||
}),
|
||||
maxSteps: Schema.optional(PositiveInt).annotate({ description: "@deprecated Use 'steps' field instead." }),
|
||||
permission: Schema.optional(PermissionRef),
|
||||
permission: Schema.optional(ConfigPermission.Info),
|
||||
}),
|
||||
[Schema.Record(Schema.String, Schema.Any)],
|
||||
)
|
||||
@@ -96,7 +90,7 @@ const normalize = (agent: z.infer<typeof Info>) => {
|
||||
const permission: ConfigPermission.Info = {}
|
||||
for (const [tool, enabled] of Object.entries(agent.tools ?? {})) {
|
||||
const action = enabled ? "allow" : "deny"
|
||||
if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") {
|
||||
if (tool === "write" || tool === "edit" || tool === "patch") {
|
||||
permission.edit = action
|
||||
continue
|
||||
}
|
||||
@@ -104,7 +98,8 @@ const normalize = (agent: z.infer<typeof Info>) => {
|
||||
}
|
||||
globalThis.Object.assign(permission, agent.permission)
|
||||
|
||||
return { ...agent, options, permission, steps: agent.steps ?? agent.maxSteps }
|
||||
const steps = agent.steps ?? agent.maxSteps
|
||||
return { ...agent, options, permission, ...(steps !== undefined ? { steps } : {}) }
|
||||
}
|
||||
|
||||
export const Info = zod(AgentSchema).transform(normalize).meta({ ref: "AgentConfig" }) as unknown as z.ZodType<
|
||||
|
||||
@@ -25,6 +25,7 @@ import { Context, Duration, Effect, Exit, Fiber, Layer, Option, Schema } from "e
|
||||
import { EffectFlock } from "@opencode-ai/shared/util/effect-flock"
|
||||
import { InstanceRef } from "@/effect/instance-ref"
|
||||
import { zod, ZodOverride } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
import { ConfigAgent } from "./agent"
|
||||
import { ConfigCommand } from "./command"
|
||||
import { ConfigFormatter } from "./formatter"
|
||||
@@ -101,13 +102,20 @@ export type Layout = ConfigLayout.Layout
|
||||
// ZodOverride-annotated Schema.Any. Walker sees the annotation and emits the
|
||||
// exact zod directly, preserving component $refs.
|
||||
const AgentRef = Schema.Any.annotate({ [ZodOverride]: ConfigAgent.Info })
|
||||
const PermissionRef = Schema.Any.annotate({ [ZodOverride]: ConfigPermission.Info })
|
||||
const LogLevelRef = Schema.Any.annotate({ [ZodOverride]: Log.Level })
|
||||
|
||||
const PositiveInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0))
|
||||
const NonNegativeInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0))
|
||||
|
||||
const InfoSchema = Schema.Struct({
|
||||
// The Effect Schema is the canonical source of truth. The `.zod` compatibility
|
||||
// surface is derived so existing Hono validators keep working without a parallel
|
||||
// Zod definition.
|
||||
//
|
||||
// The walker emits `z.object({...})` which is non-strict by default. Config
|
||||
// historically uses `.strict()` (additionalProperties: false in openapi.json),
|
||||
// so layer that on after derivation. Re-apply the Config ref afterward
|
||||
// since `.strict()` strips the walker's meta annotation.
|
||||
export const Info = Schema.Struct({
|
||||
$schema: Schema.optional(Schema.String).annotate({
|
||||
description: "JSON schema reference for configuration validation",
|
||||
}),
|
||||
@@ -220,7 +228,7 @@ const InfoSchema = Schema.Struct({
|
||||
description: "Additional instruction files or patterns to include",
|
||||
}),
|
||||
layout: Schema.optional(ConfigLayout.Layout).annotate({ description: "@deprecated Always uses stretch layout." }),
|
||||
permission: Schema.optional(PermissionRef),
|
||||
permission: Schema.optional(ConfigPermission.Info),
|
||||
tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)),
|
||||
enterprise: Schema.optional(
|
||||
Schema.Struct({
|
||||
@@ -236,6 +244,13 @@ const InfoSchema = Schema.Struct({
|
||||
prune: Schema.optional(Schema.Boolean).annotate({
|
||||
description: "Enable pruning of old tool outputs (default: true)",
|
||||
}),
|
||||
tail_turns: Schema.optional(NonNegativeInt).annotate({
|
||||
description:
|
||||
"Number of recent user turns, including their following assistant/tool responses, to keep verbatim during compaction (default: 2)",
|
||||
}),
|
||||
preserve_recent_tokens: Schema.optional(NonNegativeInt).annotate({
|
||||
description: "Maximum number of tokens from recent turns to preserve verbatim after compaction",
|
||||
}),
|
||||
reserved: Schema.optional(NonNegativeInt).annotate({
|
||||
description: "Token buffer for compaction. Leaves enough window to avoid overflow during compaction.",
|
||||
}),
|
||||
@@ -263,6 +278,14 @@ const InfoSchema = Schema.Struct({
|
||||
}),
|
||||
),
|
||||
})
|
||||
.annotate({ identifier: "Config" })
|
||||
.pipe(
|
||||
withStatics((s) => ({
|
||||
zod: (zod(s) as unknown as z.ZodObject<any>).strict().meta({ ref: "Config" }) as unknown as z.ZodType<
|
||||
DeepMutable<Schema.Schema.Type<typeof s>>
|
||||
>,
|
||||
})),
|
||||
)
|
||||
|
||||
// Schema.Struct produces readonly types by default, but the service code
|
||||
// below mutates Info objects directly (e.g. `config.mode = ...`). Strip the
|
||||
@@ -284,15 +307,7 @@ type DeepMutable<T> = T extends readonly [unknown, ...unknown[]]
|
||||
? { -readonly [K in keyof T]: DeepMutable<T[K]> }
|
||||
: T
|
||||
|
||||
// The walker emits `z.object({...})` which is non-strict by default. Config
|
||||
// historically uses `.strict()` (additionalProperties: false in openapi.json),
|
||||
// so layer that on after derivation. Re-apply the Config ref afterward
|
||||
// since `.strict()` strips the walker's meta annotation.
|
||||
export const Info = (zod(InfoSchema) as unknown as z.ZodObject<any>)
|
||||
.strict()
|
||||
.meta({ ref: "Config" }) as unknown as z.ZodType<DeepMutable<Schema.Schema.Type<typeof InfoSchema>>>
|
||||
|
||||
export type Info = z.output<typeof Info> & {
|
||||
export type Info = DeepMutable<Schema.Schema.Type<typeof Info>> & {
|
||||
// plugin_origins is derived state, not a persisted config field. It keeps each winning plugin spec together
|
||||
// with the file and scope it came from so later runtime code can make location-sensitive decisions.
|
||||
plugin_origins?: ConfigPlugin.Origin[]
|
||||
@@ -414,7 +429,7 @@ export const layer = Layer.effect(
|
||||
),
|
||||
)
|
||||
const parsed = ConfigParse.jsonc(expanded, source)
|
||||
const data = ConfigParse.schema(Info, normalizeLoadedConfig(parsed, source), source)
|
||||
const data = ConfigParse.schema(Info.zod, normalizeLoadedConfig(parsed, source), source)
|
||||
if (!("path" in options)) return data
|
||||
|
||||
yield* Effect.promise(() => resolveLoadedPlugins(data, options.path))
|
||||
@@ -666,9 +681,8 @@ export const layer = Layer.effect(
|
||||
|
||||
const deps: Fiber.Fiber<void, never>[] = []
|
||||
|
||||
// kilocode_change start
|
||||
for (const dir of unique(directories)) {
|
||||
// kilocode_change
|
||||
// kilocode_change start
|
||||
if (KilocodeConfig.isConfigDir(dir, Flag.KILO_CONFIG_DIR)) {
|
||||
for (const file of KilocodeConfig.ALL_CONFIG_FILES) {
|
||||
const source = path.join(dir, file)
|
||||
@@ -826,7 +840,7 @@ export const layer = Layer.effect(
|
||||
const perms: Record<string, ConfigPermission.Action> = {}
|
||||
for (const [tool, enabled] of Object.entries(result.tools)) {
|
||||
const action: ConfigPermission.Action = enabled ? "allow" : "deny"
|
||||
if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") {
|
||||
if (tool === "write" || tool === "edit" || tool === "patch") {
|
||||
perms.edit = action
|
||||
continue
|
||||
}
|
||||
@@ -932,13 +946,13 @@ export const layer = Layer.effect(
|
||||
|
||||
let next: Info
|
||||
if (!file.endsWith(".jsonc")) {
|
||||
const existing = ConfigParse.schema(Info, ConfigParse.jsonc(before, file), file)
|
||||
const existing = ConfigParse.schema(Info.zod, ConfigParse.jsonc(before, file), file)
|
||||
const merged = KilocodeConfig.mergeConfig(writable(existing), writable(config)) // kilocode_change
|
||||
yield* fs.writeFileString(file, JSON.stringify(merged, null, 2)).pipe(Effect.orDie)
|
||||
next = merged
|
||||
} else {
|
||||
const updated = patchJsonc(before, writable(config))
|
||||
next = ConfigParse.schema(Info, ConfigParse.jsonc(updated, file), file)
|
||||
next = ConfigParse.schema(Info.zod, ConfigParse.jsonc(updated, file), file)
|
||||
yield* fs.writeFileString(file, updated).pipe(Effect.orDie)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Schema } from "effect"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
|
||||
export class Local extends Schema.Class<Local>("McpLocalConfig")({
|
||||
export const Local = Schema.Struct({
|
||||
type: Schema.Literal("local").annotate({ description: "Type of MCP server connection" }),
|
||||
command: Schema.mutable(Schema.Array(Schema.String)).annotate({
|
||||
description: "Command and arguments to run the MCP server",
|
||||
@@ -16,11 +16,12 @@ export class Local extends Schema.Class<Local>("McpLocalConfig")({
|
||||
timeout: Schema.optional(Schema.Number).annotate({
|
||||
description: "Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified.",
|
||||
}),
|
||||
}) {
|
||||
static readonly zod = zod(this)
|
||||
}
|
||||
})
|
||||
.annotate({ identifier: "McpLocalConfig" })
|
||||
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type Local = Schema.Schema.Type<typeof Local>
|
||||
|
||||
export class OAuth extends Schema.Class<OAuth>("McpOAuthConfig")({
|
||||
export const OAuth = Schema.Struct({
|
||||
clientId: Schema.optional(Schema.String).annotate({
|
||||
description: "OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted.",
|
||||
}),
|
||||
@@ -31,11 +32,12 @@ export class OAuth extends Schema.Class<OAuth>("McpOAuthConfig")({
|
||||
redirectUri: Schema.optional(Schema.String).annotate({
|
||||
description: "OAuth redirect URI (default: http://127.0.0.1:19876/mcp/oauth/callback).",
|
||||
}),
|
||||
}) {
|
||||
static readonly zod = zod(this)
|
||||
}
|
||||
})
|
||||
.annotate({ identifier: "McpOAuthConfig" })
|
||||
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type OAuth = Schema.Schema.Type<typeof OAuth>
|
||||
|
||||
export class Remote extends Schema.Class<Remote>("McpRemoteConfig")({
|
||||
export const Remote = Schema.Struct({
|
||||
type: Schema.Literal("remote").annotate({ description: "Type of MCP server connection" }),
|
||||
url: Schema.String.annotate({ description: "URL of the remote MCP server" }),
|
||||
enabled: Schema.optional(Schema.Boolean).annotate({
|
||||
@@ -50,9 +52,10 @@ export class Remote extends Schema.Class<Remote>("McpRemoteConfig")({
|
||||
timeout: Schema.optional(Schema.Number).annotate({
|
||||
description: "Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified.",
|
||||
}),
|
||||
}) {
|
||||
static readonly zod = zod(this)
|
||||
}
|
||||
})
|
||||
.annotate({ identifier: "McpRemoteConfig" })
|
||||
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type Remote = Schema.Schema.Type<typeof Remote>
|
||||
|
||||
export const Info = Schema.Union([Local, Remote])
|
||||
.annotate({ discriminator: "type" })
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as ConfigPermission from "./permission"
|
||||
import { Schema } from "effect"
|
||||
import { zod, ZodPreprocess } from "@/util/effect-zod"
|
||||
import { Schema, SchemaGetter } from "effect"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
|
||||
export const Action = Schema.NullOr(Schema.Literals(["ask", "allow", "deny"])) // kilocode_change - nullable allows null as a delete sentinel
|
||||
@@ -18,21 +18,19 @@ export const Rule = Schema.Union([Action, Object])
|
||||
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type Rule = Schema.Schema.Type<typeof Rule>
|
||||
|
||||
// Captures the user's original property insertion order before Schema.Struct
|
||||
// canonicalises the object. See the `ZodPreprocess` comment in
|
||||
// `util/effect-zod.ts` for the full rationale — in short: rule precedence is
|
||||
// encoded in JSON key order (`evaluate.ts` uses `findLast`, so later keys win)
|
||||
// and `Schema.StructWithRest` would otherwise drop that order.
|
||||
const permissionPreprocess = (val: unknown) => {
|
||||
if (typeof val === "object" && val !== null && !Array.isArray(val)) {
|
||||
return { __originalKeys: globalThis.Object.keys(val), ...val }
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
const ObjectShape = Schema.StructWithRest(
|
||||
// Known permission keys get explicit types — most are full Rule (either a
|
||||
// single Action or a per-pattern object), but a handful of tools take no
|
||||
// sub-target patterns and are Action-only. Unknown keys fall through the
|
||||
// Record rest signature as Rule.
|
||||
//
|
||||
// StructWithRest canonicalises key order on decode (known first, then rest),
|
||||
// which used to require the `__originalKeys` preprocess hack because
|
||||
// `Permission.fromConfig` depended on the user's insertion order. That
|
||||
// dependency is gone — `fromConfig` now sorts top-level keys so wildcard
|
||||
// permissions come before specifics, making the final precedence
|
||||
// order-independent.
|
||||
const InputObject = Schema.StructWithRest(
|
||||
Schema.Struct({
|
||||
__originalKeys: Schema.optional(Schema.mutable(Schema.Array(Schema.String))),
|
||||
read: Schema.optional(Rule),
|
||||
edit: Schema.optional(Rule),
|
||||
glob: Schema.optional(Rule),
|
||||
@@ -53,24 +51,29 @@ const ObjectShape = Schema.StructWithRest(
|
||||
[Schema.Record(Schema.String, Rule)],
|
||||
)
|
||||
|
||||
const InnerSchema = Schema.Union([ObjectShape, Action]).annotate({
|
||||
[ZodPreprocess]: permissionPreprocess,
|
||||
})
|
||||
// Input the user writes in config: either a single Action (shorthand for "*")
|
||||
// or an object of per-target rules.
|
||||
const InputSchema = Schema.Union([Action, InputObject])
|
||||
|
||||
// Post-parse: drop the __originalKeys metadata and rebuild the rule map in the
|
||||
// user's original insertion order. A plain string input (the Action branch of
|
||||
// the union) becomes `{ "*": action }`.
|
||||
const transform = (x: unknown): Record<string, Rule> => {
|
||||
if (typeof x === "string") return { "*": x as Action }
|
||||
const obj = x as { __originalKeys?: string[] } & Record<string, unknown>
|
||||
const { __originalKeys, ...rest } = obj
|
||||
if (!__originalKeys) return rest as Record<string, Rule>
|
||||
const result: Record<string, Rule> = {}
|
||||
for (const key of __originalKeys) {
|
||||
if (key in rest) result[key] = rest[key] as Rule
|
||||
}
|
||||
return result
|
||||
}
|
||||
// Normalise the Action shorthand into `{ "*": action }`. Object inputs pass
|
||||
// through untouched.
|
||||
const normalizeInput = (input: Schema.Schema.Type<typeof InputSchema>): Schema.Schema.Type<typeof InputObject> =>
|
||||
input === null || typeof input === "string" ? { "*": input } : input // kilocode_change
|
||||
|
||||
export const Info = zod(InnerSchema).transform(transform).meta({ ref: "PermissionConfig" })
|
||||
export type Info = Record<string, Rule>
|
||||
export const Info = InputSchema.pipe(
|
||||
Schema.decodeTo(InputObject, {
|
||||
decode: SchemaGetter.transform(normalizeInput),
|
||||
// Not perfectly invertible (we lose whether the user originally typed an
|
||||
// Action shorthand), but the object form is always a valid representation
|
||||
// of the same rules.
|
||||
encode: SchemaGetter.passthrough({ strict: false }),
|
||||
}),
|
||||
)
|
||||
.annotate({ identifier: "PermissionConfig" })
|
||||
.pipe(
|
||||
// Walker already emits the decodeTo transform into the derived zod (see
|
||||
// `encoded()` in effect-zod.ts), so just expose that directly.
|
||||
withStatics((s) => ({ zod: zod(s) })),
|
||||
)
|
||||
type _Info = Schema.Schema.Type<typeof InputObject>
|
||||
export type Info = { -readonly [K in keyof _Info]: _Info[K] }
|
||||
|
||||
@@ -60,7 +60,8 @@ export const Model = Schema.Struct({
|
||||
variants: Schema.optional(
|
||||
Schema.Record(
|
||||
Schema.String,
|
||||
Schema.NullOr( // kilocode_change - allow null values so removed variants can be deleted via stripNulls on save
|
||||
Schema.NullOr(
|
||||
// kilocode_change - allow null values so removed variants can be deleted via stripNulls on save
|
||||
Schema.StructWithRest(
|
||||
Schema.Struct({
|
||||
disabled: Schema.optional(Schema.Boolean).annotate({ description: "Disable this variant for the model" }),
|
||||
@@ -72,7 +73,7 @@ export const Model = Schema.Struct({
|
||||
),
|
||||
}).pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
|
||||
export class Info extends Schema.Class<Info>("ProviderConfig")({
|
||||
export const Info = Schema.Struct({
|
||||
api: Schema.optional(Schema.String),
|
||||
name: Schema.optional(Schema.String),
|
||||
env: Schema.optional(Schema.mutable(Schema.Array(Schema.String))),
|
||||
@@ -109,8 +110,9 @@ export class Info extends Schema.Class<Info>("ProviderConfig")({
|
||||
),
|
||||
),
|
||||
models: Schema.optional(Schema.Record(Schema.String, Schema.NullOr(Model))), // kilocode_change - allow null values so removed models can be deleted via stripNulls on save
|
||||
}) {
|
||||
static readonly zod = zod(this)
|
||||
}
|
||||
})
|
||||
.annotate({ identifier: "ProviderConfig" })
|
||||
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type Info = Schema.Schema.Type<typeof Info>
|
||||
|
||||
export * as ConfigProvider from "./provider"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Schema } from "effect"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
|
||||
export class Server extends Schema.Class<Server>("ServerConfig")({
|
||||
export const Server = Schema.Struct({
|
||||
port: Schema.optional(Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0))).annotate({
|
||||
description: "Port to listen on",
|
||||
}),
|
||||
@@ -13,8 +14,9 @@ export class Server extends Schema.Class<Server>("ServerConfig")({
|
||||
cors: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({
|
||||
description: "Additional domains to allow for CORS",
|
||||
}),
|
||||
}) {
|
||||
static readonly zod = zod(this)
|
||||
}
|
||||
})
|
||||
.annotate({ identifier: "ServerConfig" })
|
||||
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type Server = Schema.Schema.Type<typeof Server>
|
||||
|
||||
export * as ConfigServer from "./server"
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import type { Plugin } from "@kilocode/plugin"
|
||||
import { rename, writeFile } from "node:fs/promises"
|
||||
import { randomInt } from "node:crypto"
|
||||
import { setTimeout as sleep } from "node:timers/promises"
|
||||
|
||||
const DEV_DATA_FILE = "/tmp/opencode-workspace-dev-data.json"
|
||||
const DEV_DATA_TEMP_FILE = `${DEV_DATA_FILE}.tmp`
|
||||
|
||||
async function waitForHealth(port: number) {
|
||||
const url = `http://127.0.0.1:${port}/global/health`
|
||||
const started = Date.now()
|
||||
|
||||
while (Date.now() - started < 30_000) {
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
if (response.ok) {
|
||||
return
|
||||
}
|
||||
} catch {}
|
||||
|
||||
await sleep(250)
|
||||
}
|
||||
|
||||
throw new Error(`Timed out waiting for debug server health check at ${url}`)
|
||||
}
|
||||
|
||||
let PORT: number | undefined
|
||||
|
||||
async function writeDebugData(port: number, id: string, env: Record<string, string | undefined>) {
|
||||
await writeFile(
|
||||
DEV_DATA_TEMP_FILE,
|
||||
JSON.stringify(
|
||||
{
|
||||
port,
|
||||
id,
|
||||
env,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
|
||||
await rename(DEV_DATA_TEMP_FILE, DEV_DATA_FILE)
|
||||
}
|
||||
|
||||
export const DebugWorkspacePlugin: Plugin = async ({ experimental_workspace }) => {
|
||||
experimental_workspace.register("debug", {
|
||||
name: "Debug",
|
||||
description: "Create a debugging server",
|
||||
configure(config) {
|
||||
return config
|
||||
},
|
||||
async create(config, env) {
|
||||
const port = randomInt(5000, 9001)
|
||||
PORT = port
|
||||
|
||||
await writeDebugData(port, config.id, env)
|
||||
|
||||
await waitForHealth(port)
|
||||
},
|
||||
async remove(_config) {},
|
||||
target(_config) {
|
||||
return {
|
||||
type: "remote",
|
||||
url: `http://localhost:${PORT!}/`,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
export default DebugWorkspacePlugin
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Schema } from "effect"
|
||||
import z from "zod"
|
||||
|
||||
import { Identifier } from "@/id/id"
|
||||
import { ZodOverride } from "@/util/effect-zod"
|
||||
import { zod, ZodOverride } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
|
||||
const workspaceIdSchema = Schema.String.annotate({ [ZodOverride]: Identifier.schema("workspace") }).pipe(
|
||||
@@ -14,6 +13,6 @@ export type WorkspaceID = typeof workspaceIdSchema.Type
|
||||
export const WorkspaceID = workspaceIdSchema.pipe(
|
||||
withStatics((schema: typeof workspaceIdSchema) => ({
|
||||
ascending: (id?: string) => schema.make(Identifier.ascending("workspace", id)),
|
||||
zod: Identifier.schema("workspace").pipe(z.custom<WorkspaceID>()),
|
||||
zod: zod(schema),
|
||||
})),
|
||||
)
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
import z from "zod"
|
||||
import { Cause, Context, Effect, Layer, Queue, Stream } from "effect"
|
||||
import { ripgrep } from "ripgrep"
|
||||
import { Filesystem } from "@/util"
|
||||
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
||||
import { Cause, Context, Effect, Fiber, Layer, Queue, Stream } from "effect"
|
||||
import type { PlatformError } from "effect/PlatformError"
|
||||
import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http"
|
||||
import { ChildProcess } from "effect/unstable/process"
|
||||
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
|
||||
|
||||
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
|
||||
import { Global } from "@/global"
|
||||
import { Log } from "@/util"
|
||||
import { sanitizedProcessEnv } from "@/util/opencode-process"
|
||||
import { KiloRipgrepStream } from "../kilocode/kilo-ripgrep-stream" // kilocode_change - UTF-8 safe stream decoder shared with worker
|
||||
import { which } from "@/util/which"
|
||||
|
||||
const log = Log.create({ service: "ripgrep" })
|
||||
const VERSION = "15.1.0"
|
||||
const PLATFORM = {
|
||||
"arm64-darwin": { platform: "aarch64-apple-darwin", extension: "tar.gz" },
|
||||
"arm64-linux": { platform: "aarch64-unknown-linux-gnu", extension: "tar.gz" },
|
||||
"x64-darwin": { platform: "x86_64-apple-darwin", extension: "tar.gz" },
|
||||
"x64-linux": { platform: "x86_64-unknown-linux-musl", extension: "tar.gz" },
|
||||
"arm64-win32": { platform: "aarch64-pc-windows-msvc", extension: "zip" },
|
||||
"ia32-win32": { platform: "i686-pc-windows-msvc", extension: "zip" },
|
||||
"x64-win32": { platform: "x86_64-pc-windows-msvc", extension: "zip" },
|
||||
} as const
|
||||
|
||||
const Stats = z.object({
|
||||
elapsed: z.object({
|
||||
@@ -121,62 +135,20 @@ export interface TreeInput {
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly files: (input: FilesInput) => Stream.Stream<string, Error>
|
||||
readonly tree: (input: TreeInput) => Effect.Effect<string, Error>
|
||||
readonly search: (input: SearchInput) => Effect.Effect<SearchResult, Error>
|
||||
readonly files: (input: FilesInput) => Stream.Stream<string, PlatformError | Error>
|
||||
readonly tree: (input: TreeInput) => Effect.Effect<string, PlatformError | Error>
|
||||
readonly search: (input: SearchInput) => Effect.Effect<SearchResult, PlatformError | Error>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Ripgrep") {}
|
||||
|
||||
type Run = { kind: "files" | "search"; cwd: string; args: string[] }
|
||||
|
||||
type WorkerResult = {
|
||||
type: "result"
|
||||
code: number
|
||||
stdout: string
|
||||
stderr: string
|
||||
}
|
||||
|
||||
type WorkerLine = {
|
||||
type: "line"
|
||||
line: string
|
||||
}
|
||||
|
||||
type WorkerDone = {
|
||||
type: "done"
|
||||
code: number
|
||||
stderr: string
|
||||
}
|
||||
|
||||
type WorkerError = {
|
||||
type: "error"
|
||||
error: {
|
||||
message: string
|
||||
name?: string
|
||||
stack?: string
|
||||
}
|
||||
}
|
||||
|
||||
function env() {
|
||||
const env = sanitizedProcessEnv()
|
||||
delete env.RIPGREP_CONFIG_PATH
|
||||
return env
|
||||
}
|
||||
|
||||
function text(input: unknown) {
|
||||
if (typeof input === "string") return input
|
||||
if (input instanceof ArrayBuffer) return Buffer.from(input).toString()
|
||||
if (ArrayBuffer.isView(input)) return Buffer.from(input.buffer, input.byteOffset, input.byteLength).toString()
|
||||
return String(input)
|
||||
}
|
||||
|
||||
function toError(input: unknown) {
|
||||
if (input instanceof Error) return input
|
||||
if (typeof input === "string") return new Error(input)
|
||||
return new Error(String(input))
|
||||
}
|
||||
|
||||
function abort(signal?: AbortSignal) {
|
||||
function aborted(signal?: AbortSignal) {
|
||||
const err = signal?.reason
|
||||
if (err instanceof Error) return err
|
||||
const out = new Error("Aborted")
|
||||
@@ -184,6 +156,16 @@ function abort(signal?: AbortSignal) {
|
||||
return out
|
||||
}
|
||||
|
||||
function waitForAbort(signal?: AbortSignal) {
|
||||
if (!signal) return Effect.never
|
||||
if (signal.aborted) return Effect.fail(aborted(signal))
|
||||
return Effect.callback<never, Error>((resume) => {
|
||||
const onabort = () => resume(Effect.fail(aborted(signal)))
|
||||
signal.addEventListener("abort", onabort, { once: true })
|
||||
return Effect.sync(() => signal.removeEventListener("abort", onabort))
|
||||
})
|
||||
}
|
||||
|
||||
function error(stderr: string, code: number) {
|
||||
const err = new Error(stderr.trim() || `ripgrep failed with code ${code}`)
|
||||
err.name = "RipgrepError"
|
||||
@@ -204,376 +186,300 @@ function row(data: Row): Row {
|
||||
}
|
||||
}
|
||||
|
||||
function opts(cwd: string) {
|
||||
return {
|
||||
env: env(),
|
||||
preopens: { ".": cwd },
|
||||
}
|
||||
function parse(line: string) {
|
||||
return Effect.try({
|
||||
try: () => Result.parse(JSON.parse(line)),
|
||||
catch: (cause) => new Error("invalid ripgrep output", { cause }),
|
||||
})
|
||||
}
|
||||
|
||||
function check(cwd: string) {
|
||||
return Effect.tryPromise({
|
||||
try: () => fs.stat(cwd).catch(() => undefined),
|
||||
catch: toError,
|
||||
}).pipe(
|
||||
Effect.flatMap((stat) =>
|
||||
stat?.isDirectory()
|
||||
? Effect.void
|
||||
: Effect.fail(
|
||||
Object.assign(new Error(`No such file or directory: '${cwd}'`), {
|
||||
code: "ENOENT",
|
||||
errno: -2,
|
||||
path: cwd,
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
function fail(queue: Queue.Queue<string, PlatformError | Error | Cause.Done>, err: PlatformError | Error) {
|
||||
Queue.failCauseUnsafe(queue, Cause.fail(err))
|
||||
}
|
||||
|
||||
function filesArgs(input: FilesInput) {
|
||||
const args = ["--files", "--glob=!.git/*"]
|
||||
const args = ["--no-config", "--files", "--glob=!.git/*"]
|
||||
if (input.follow) args.push("--follow")
|
||||
if (input.hidden !== false) args.push("--hidden")
|
||||
if (input.hidden === false) args.push("--glob=!.*")
|
||||
if (input.maxDepth !== undefined) args.push(`--max-depth=${input.maxDepth}`)
|
||||
if (input.glob) {
|
||||
for (const glob of input.glob) {
|
||||
args.push(`--glob=${glob}`)
|
||||
}
|
||||
for (const glob of input.glob) args.push(`--glob=${glob}`)
|
||||
}
|
||||
args.push(".")
|
||||
return args
|
||||
}
|
||||
|
||||
function searchArgs(input: SearchInput) {
|
||||
const args = ["--json", "--hidden", "--glob=!.git/*", "--no-messages"]
|
||||
const args = ["--no-config", "--json", "--hidden", "--glob=!.git/*", "--no-messages"]
|
||||
if (input.follow) args.push("--follow")
|
||||
if (input.glob) {
|
||||
for (const glob of input.glob) {
|
||||
args.push(`--glob=${glob}`)
|
||||
}
|
||||
for (const glob of input.glob) args.push(`--glob=${glob}`)
|
||||
}
|
||||
if (input.limit) args.push(`--max-count=${input.limit}`)
|
||||
args.push("--", input.pattern, ...(input.file ?? ["."]))
|
||||
return args
|
||||
}
|
||||
|
||||
function parse(stdout: string) {
|
||||
return stdout
|
||||
.trim()
|
||||
.split(/\r?\n/)
|
||||
.filter(Boolean)
|
||||
.map((line) => Result.parse(JSON.parse(line)))
|
||||
.flatMap((item) => (item.type === "match" ? [row(item.data)] : []))
|
||||
function raceAbort<A, E, R>(effect: Effect.Effect<A, E, R>, signal?: AbortSignal) {
|
||||
return signal ? effect.pipe(Effect.raceFirst(waitForAbort(signal))) : effect
|
||||
}
|
||||
|
||||
declare const KILO_RIPGREP_WORKER_PATH: string
|
||||
export const layer: Layer.Layer<Service, never, AppFileSystem.Service | ChildProcessSpawner | HttpClient.HttpClient> =
|
||||
Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* AppFileSystem.Service
|
||||
const http = HttpClient.filterStatusOk(yield* HttpClient.HttpClient)
|
||||
const spawner = yield* ChildProcessSpawner
|
||||
|
||||
function target(): Effect.Effect<string | URL, Error> {
|
||||
if (typeof KILO_RIPGREP_WORKER_PATH !== "undefined") {
|
||||
return Effect.succeed(KILO_RIPGREP_WORKER_PATH)
|
||||
}
|
||||
const js = new URL("./ripgrep.worker.js", import.meta.url)
|
||||
return Effect.tryPromise({
|
||||
try: () => Filesystem.exists(fileURLToPath(js)),
|
||||
catch: toError,
|
||||
}).pipe(Effect.map((exists) => (exists ? js : new URL("./ripgrep.worker.ts", import.meta.url))))
|
||||
}
|
||||
const run = Effect.fnUntraced(function* (command: string, args: string[], opts?: { cwd?: string }) {
|
||||
const handle = yield* spawner.spawn(
|
||||
ChildProcess.make(command, args, { cwd: opts?.cwd, extendEnv: true, stdin: "ignore" }),
|
||||
)
|
||||
const [stdout, stderr, code] = yield* Effect.all(
|
||||
[
|
||||
Stream.mkString(Stream.decodeText(handle.stdout)),
|
||||
Stream.mkString(Stream.decodeText(handle.stderr)),
|
||||
handle.exitCode,
|
||||
],
|
||||
{ concurrency: "unbounded" },
|
||||
)
|
||||
return { stdout, stderr, code }
|
||||
}, Effect.scoped)
|
||||
|
||||
function worker() {
|
||||
return target().pipe(Effect.flatMap((file) => Effect.sync(() => new Worker(file, { env: env() }))))
|
||||
}
|
||||
const extract = Effect.fnUntraced(function* (
|
||||
archive: string,
|
||||
config: (typeof PLATFORM)[keyof typeof PLATFORM],
|
||||
target: string,
|
||||
) {
|
||||
const dir = yield* fs.makeTempDirectoryScoped({ directory: Global.Path.bin, prefix: "ripgrep-" })
|
||||
|
||||
// kilocode_change start - delegate to KiloRipgrepStream for UTF-8 safe decoding
|
||||
function drain(
|
||||
dec: ReturnType<typeof KiloRipgrepStream.decoder>,
|
||||
buf: string,
|
||||
chunk: unknown,
|
||||
push: (line: string) => void,
|
||||
) {
|
||||
return KiloRipgrepStream.drain(dec, buf, chunk, push)
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
function fail(queue: Queue.Queue<string, Error | Cause.Done>, err: Error) {
|
||||
Queue.failCauseUnsafe(queue, Cause.fail(err))
|
||||
}
|
||||
|
||||
function searchDirect(input: SearchInput) {
|
||||
return Effect.tryPromise({
|
||||
try: () =>
|
||||
ripgrep(searchArgs(input), {
|
||||
buffer: true,
|
||||
...opts(input.cwd),
|
||||
}),
|
||||
catch: toError,
|
||||
}).pipe(
|
||||
Effect.flatMap((ret) => {
|
||||
const out = ret.stdout ?? ""
|
||||
if (ret.code !== 0 && ret.code !== 1 && ret.code !== 2) {
|
||||
return Effect.fail(error(ret.stderr ?? "", ret.code ?? 1))
|
||||
}
|
||||
return Effect.sync(() => ({
|
||||
items: ret.code === 1 ? [] : parse(out),
|
||||
partial: ret.code === 2,
|
||||
}))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function searchWorker(input: SearchInput) {
|
||||
if (input.signal?.aborted) return Effect.fail(abort(input.signal))
|
||||
|
||||
return Effect.acquireUseRelease(
|
||||
worker(),
|
||||
(w) =>
|
||||
Effect.callback<SearchResult, Error>((resume, signal) => {
|
||||
let open = true
|
||||
const done = (effect: Effect.Effect<SearchResult, Error>) => {
|
||||
if (!open) return
|
||||
open = false
|
||||
resume(effect)
|
||||
if (config.extension === "zip") {
|
||||
const shell = (yield* Effect.sync(() => which("powershell.exe") ?? which("pwsh.exe"))) ?? "powershell.exe"
|
||||
const result = yield* run(shell, [
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-Command",
|
||||
`$global:ProgressPreference = 'SilentlyContinue'; Expand-Archive -LiteralPath '${archive.replaceAll("'", "''")}' -DestinationPath '${dir.replaceAll("'", "''")}' -Force`,
|
||||
])
|
||||
if (result.code !== 0) {
|
||||
return yield* Effect.fail(error(result.stderr || result.stdout, result.code))
|
||||
}
|
||||
}
|
||||
const onabort = () => done(Effect.fail(abort(input.signal)))
|
||||
|
||||
w.onerror = (evt) => {
|
||||
done(Effect.fail(toError(evt.error ?? evt.message)))
|
||||
if (config.extension === "tar.gz") {
|
||||
const result = yield* run("tar", ["-xzf", archive, "-C", dir])
|
||||
if (result.code !== 0) {
|
||||
return yield* Effect.fail(error(result.stderr || result.stdout, result.code))
|
||||
}
|
||||
}
|
||||
w.onmessage = (evt: MessageEvent<WorkerResult | WorkerError>) => {
|
||||
const msg = evt.data
|
||||
if (msg.type === "error") {
|
||||
done(Effect.fail(Object.assign(new Error(msg.error.message), msg.error)))
|
||||
return
|
||||
|
||||
const extracted = path.join(
|
||||
dir,
|
||||
`ripgrep-${VERSION}-${config.platform}`,
|
||||
process.platform === "win32" ? "rg.exe" : "rg",
|
||||
)
|
||||
if (!(yield* fs.isFile(extracted))) {
|
||||
return yield* Effect.fail(new Error(`ripgrep archive did not contain executable: ${extracted}`))
|
||||
}
|
||||
|
||||
yield* fs.copyFile(extracted, target)
|
||||
if (process.platform === "win32") return
|
||||
yield* fs.chmod(target, 0o755)
|
||||
}, Effect.scoped)
|
||||
|
||||
const filepath = yield* Effect.cached(
|
||||
Effect.gen(function* () {
|
||||
const system = yield* Effect.sync(() => which(process.platform === "win32" ? "rg.exe" : "rg"))
|
||||
if (system && (yield* fs.isFile(system).pipe(Effect.orDie))) return system
|
||||
|
||||
const target = path.join(Global.Path.bin, `rg${process.platform === "win32" ? ".exe" : ""}`)
|
||||
if (yield* fs.isFile(target).pipe(Effect.orDie)) return target
|
||||
|
||||
const platformKey = `${process.arch}-${process.platform}` as keyof typeof PLATFORM
|
||||
const config = PLATFORM[platformKey]
|
||||
if (!config) {
|
||||
return yield* Effect.fail(new Error(`unsupported platform for ripgrep: ${platformKey}`))
|
||||
}
|
||||
if (msg.code === 1) {
|
||||
done(Effect.succeed({ items: [], partial: false }))
|
||||
return
|
||||
|
||||
const filename = `ripgrep-${VERSION}-${config.platform}.${config.extension}`
|
||||
const url = `https://github.com/BurntSushi/ripgrep/releases/download/${VERSION}/${filename}`
|
||||
const archive = path.join(Global.Path.bin, filename)
|
||||
|
||||
log.info("downloading ripgrep", { url })
|
||||
yield* fs.ensureDir(Global.Path.bin).pipe(Effect.orDie)
|
||||
|
||||
const bytes = yield* HttpClientRequest.get(url).pipe(
|
||||
http.execute,
|
||||
Effect.flatMap((response) => response.arrayBuffer),
|
||||
Effect.mapError((cause) => (cause instanceof Error ? cause : new Error(String(cause)))),
|
||||
)
|
||||
if (bytes.byteLength === 0) {
|
||||
return yield* Effect.fail(new Error(`failed to download ripgrep from ${url}`))
|
||||
}
|
||||
if (msg.code !== 0 && msg.code !== 1 && msg.code !== 2) {
|
||||
done(Effect.fail(error(msg.stderr, msg.code)))
|
||||
return
|
||||
|
||||
yield* fs.writeWithDirs(archive, new Uint8Array(bytes))
|
||||
yield* extract(archive, config, target)
|
||||
yield* fs.remove(archive, { force: true }).pipe(Effect.ignore)
|
||||
return target
|
||||
}),
|
||||
)
|
||||
|
||||
const check = Effect.fnUntraced(function* (cwd: string) {
|
||||
if (yield* fs.isDir(cwd).pipe(Effect.orDie)) return
|
||||
return yield* Effect.fail(
|
||||
Object.assign(new Error(`No such file or directory: '${cwd}'`), {
|
||||
code: "ENOENT",
|
||||
errno: -2,
|
||||
path: cwd,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
const command = Effect.fnUntraced(function* (cwd: string, args: string[]) {
|
||||
const binary = yield* filepath
|
||||
return ChildProcess.make(binary, args, {
|
||||
cwd,
|
||||
env: env(),
|
||||
extendEnv: true,
|
||||
stdin: "ignore",
|
||||
})
|
||||
})
|
||||
|
||||
const files: Interface["files"] = (input) =>
|
||||
Stream.callback<string, PlatformError | Error>((queue) =>
|
||||
Effect.gen(function* () {
|
||||
yield* Effect.forkScoped(
|
||||
Effect.gen(function* () {
|
||||
yield* check(input.cwd)
|
||||
const handle = yield* spawner.spawn(yield* command(input.cwd, filesArgs(input)))
|
||||
const stderr = yield* Stream.mkString(Stream.decodeText(handle.stderr)).pipe(Effect.forkScoped)
|
||||
const stdout = yield* Stream.decodeText(handle.stdout).pipe(
|
||||
Stream.splitLines,
|
||||
Stream.filter((line) => line.length > 0),
|
||||
Stream.runForEach((line) => Effect.sync(() => Queue.offerUnsafe(queue, clean(line)))),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
const code = yield* raceAbort(handle.exitCode, input.signal)
|
||||
yield* Fiber.join(stdout)
|
||||
if (code === 0 || code === 1) {
|
||||
Queue.endUnsafe(queue)
|
||||
return
|
||||
}
|
||||
fail(queue, error(yield* Fiber.join(stderr), code))
|
||||
}).pipe(
|
||||
Effect.catch((err) =>
|
||||
Effect.sync(() => {
|
||||
fail(queue, err)
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
const search: Interface["search"] = Effect.fn("Ripgrep.search")(function* (input: SearchInput) {
|
||||
yield* check(input.cwd)
|
||||
|
||||
const program = Effect.scoped(
|
||||
Effect.gen(function* () {
|
||||
const handle = yield* spawner.spawn(yield* command(input.cwd, searchArgs(input)))
|
||||
|
||||
const [items, stderr, code] = yield* Effect.all(
|
||||
[
|
||||
Stream.decodeText(handle.stdout).pipe(
|
||||
Stream.splitLines,
|
||||
Stream.filter((line) => line.length > 0),
|
||||
Stream.mapEffect(parse),
|
||||
Stream.filter((item): item is Match => item.type === "match"),
|
||||
Stream.map((item) => row(item.data)),
|
||||
Stream.runCollect,
|
||||
Effect.map((chunk) => [...chunk]),
|
||||
),
|
||||
Stream.mkString(Stream.decodeText(handle.stderr)),
|
||||
handle.exitCode,
|
||||
],
|
||||
{ concurrency: "unbounded" },
|
||||
)
|
||||
|
||||
if (code !== 0 && code !== 1 && code !== 2) {
|
||||
return yield* Effect.fail(error(stderr, code))
|
||||
}
|
||||
|
||||
return {
|
||||
items: code === 1 ? [] : items,
|
||||
partial: code === 2,
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
return yield* raceAbort(program, input.signal)
|
||||
})
|
||||
|
||||
const tree: Interface["tree"] = Effect.fn("Ripgrep.tree")(function* (input: TreeInput) {
|
||||
log.info("tree", input)
|
||||
const list = Array.from(yield* files({ cwd: input.cwd, signal: input.signal }).pipe(Stream.runCollect))
|
||||
|
||||
interface Node {
|
||||
name: string
|
||||
children: Map<string, Node>
|
||||
}
|
||||
|
||||
function child(node: Node, name: string) {
|
||||
const item = node.children.get(name)
|
||||
if (item) return item
|
||||
const next = { name, children: new Map() }
|
||||
node.children.set(name, next)
|
||||
return next
|
||||
}
|
||||
|
||||
function count(node: Node): number {
|
||||
return Array.from(node.children.values()).reduce((sum, child) => sum + 1 + count(child), 0)
|
||||
}
|
||||
|
||||
const root: Node = { name: "", children: new Map() }
|
||||
for (const file of list) {
|
||||
if (file.includes(".kilo") || file.includes(".opencode")) continue // kilocode_change
|
||||
const parts = file.split(path.sep)
|
||||
if (parts.length < 2) continue
|
||||
let node = root
|
||||
for (const part of parts.slice(0, -1)) {
|
||||
node = child(node, part)
|
||||
}
|
||||
done(
|
||||
Effect.sync(() => ({
|
||||
items: parse(msg.stdout),
|
||||
partial: msg.code === 2,
|
||||
})),
|
||||
}
|
||||
|
||||
const total = count(root)
|
||||
const limit = input.limit ?? total
|
||||
const lines: string[] = []
|
||||
const queue: Array<{ node: Node; path: string }> = Array.from(root.children.values())
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((node) => ({ node, path: node.name }))
|
||||
|
||||
let used = 0
|
||||
for (let i = 0; i < queue.length && used < limit; i++) {
|
||||
const item = queue[i]
|
||||
lines.push(item.path)
|
||||
used++
|
||||
queue.push(
|
||||
...Array.from(item.node.children.values())
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((node) => ({ node, path: `${item.path}/${node.name}` })),
|
||||
)
|
||||
}
|
||||
|
||||
input.signal?.addEventListener("abort", onabort, { once: true })
|
||||
signal.addEventListener("abort", onabort, { once: true })
|
||||
w.postMessage({
|
||||
kind: "search",
|
||||
cwd: input.cwd,
|
||||
args: searchArgs(input),
|
||||
} satisfies Run)
|
||||
if (total > used) lines.push(`[${total - used} truncated]`)
|
||||
return lines.join("\n")
|
||||
})
|
||||
|
||||
return Effect.sync(() => {
|
||||
input.signal?.removeEventListener("abort", onabort)
|
||||
signal.removeEventListener("abort", onabort)
|
||||
w.onerror = null
|
||||
w.onmessage = null
|
||||
})
|
||||
}),
|
||||
(w) => Effect.sync(() => w.terminate()),
|
||||
)
|
||||
}
|
||||
|
||||
function filesDirect(input: FilesInput) {
|
||||
return Stream.callback<string, Error>(
|
||||
Effect.fnUntraced(function* (queue: Queue.Queue<string, Error | Cause.Done>) {
|
||||
let buf = ""
|
||||
let err = ""
|
||||
// kilocode_change start
|
||||
const decoder = KiloRipgrepStream.decoder()
|
||||
const out = {
|
||||
write(chunk: unknown) {
|
||||
buf = drain(decoder, buf, chunk, (line) => {
|
||||
Queue.offerUnsafe(queue, clean(line))
|
||||
})
|
||||
},
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
const stderr = {
|
||||
write(chunk: unknown) {
|
||||
err += text(chunk)
|
||||
},
|
||||
}
|
||||
|
||||
yield* Effect.forkScoped(
|
||||
Effect.gen(function* () {
|
||||
yield* check(input.cwd)
|
||||
const ret = yield* Effect.tryPromise({
|
||||
try: () =>
|
||||
ripgrep(filesArgs(input), {
|
||||
stdout: out,
|
||||
stderr,
|
||||
...opts(input.cwd),
|
||||
}),
|
||||
catch: toError,
|
||||
})
|
||||
buf += decoder.end() // kilocode_change
|
||||
if (buf) Queue.offerUnsafe(queue, clean(buf))
|
||||
if (ret.code === 0 || ret.code === 1) {
|
||||
Queue.endUnsafe(queue)
|
||||
return
|
||||
}
|
||||
fail(queue, error(err, ret.code ?? 1))
|
||||
}).pipe(
|
||||
Effect.catch((err) =>
|
||||
Effect.sync(() => {
|
||||
fail(queue, err)
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
return Service.of({ files, tree, search })
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function filesWorker(input: FilesInput) {
|
||||
return Stream.callback<string, Error>(
|
||||
Effect.fnUntraced(function* (queue: Queue.Queue<string, Error | Cause.Done>) {
|
||||
if (input.signal?.aborted) {
|
||||
fail(queue, abort(input.signal))
|
||||
return
|
||||
}
|
||||
|
||||
const w = yield* Effect.acquireRelease(worker(), (w) => Effect.sync(() => w.terminate()))
|
||||
let open = true
|
||||
const close = () => {
|
||||
if (!open) return false
|
||||
open = false
|
||||
return true
|
||||
}
|
||||
const onabort = () => {
|
||||
if (!close()) return
|
||||
fail(queue, abort(input.signal))
|
||||
}
|
||||
|
||||
w.onerror = (evt) => {
|
||||
if (!close()) return
|
||||
fail(queue, toError(evt.error ?? evt.message))
|
||||
}
|
||||
w.onmessage = (evt: MessageEvent<WorkerLine | WorkerDone | WorkerError>) => {
|
||||
const msg = evt.data
|
||||
if (msg.type === "line") {
|
||||
if (open) Queue.offerUnsafe(queue, msg.line)
|
||||
return
|
||||
}
|
||||
if (!close()) return
|
||||
if (msg.type === "error") {
|
||||
fail(queue, Object.assign(new Error(msg.error.message), msg.error))
|
||||
return
|
||||
}
|
||||
if (msg.code === 0 || msg.code === 1) {
|
||||
Queue.endUnsafe(queue)
|
||||
return
|
||||
}
|
||||
fail(queue, error(msg.stderr, msg.code))
|
||||
}
|
||||
|
||||
yield* Effect.acquireRelease(
|
||||
Effect.sync(() => {
|
||||
input.signal?.addEventListener("abort", onabort, { once: true })
|
||||
w.postMessage({
|
||||
kind: "files",
|
||||
cwd: input.cwd,
|
||||
args: filesArgs(input),
|
||||
} satisfies Run)
|
||||
}),
|
||||
() =>
|
||||
Effect.sync(() => {
|
||||
input.signal?.removeEventListener("abort", onabort)
|
||||
w.onerror = null
|
||||
w.onmessage = null
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const source = (input: FilesInput) => {
|
||||
const useWorker = !!input.signal && typeof Worker !== "undefined"
|
||||
if (!useWorker && input.signal) {
|
||||
log.warn("worker unavailable, ripgrep abort disabled")
|
||||
}
|
||||
return useWorker ? filesWorker(input) : filesDirect(input)
|
||||
}
|
||||
|
||||
const files: Interface["files"] = (input) => source(input)
|
||||
|
||||
const tree: Interface["tree"] = Effect.fn("Ripgrep.tree")(function* (input: TreeInput) {
|
||||
log.info("tree", input)
|
||||
const list = Array.from(yield* source({ cwd: input.cwd, signal: input.signal }).pipe(Stream.runCollect))
|
||||
|
||||
interface Node {
|
||||
name: string
|
||||
children: Map<string, Node>
|
||||
}
|
||||
|
||||
function child(node: Node, name: string) {
|
||||
const item = node.children.get(name)
|
||||
if (item) return item
|
||||
const next = { name, children: new Map() }
|
||||
node.children.set(name, next)
|
||||
return next
|
||||
}
|
||||
|
||||
function count(node: Node): number {
|
||||
return Array.from(node.children.values()).reduce((sum, child) => sum + 1 + count(child), 0)
|
||||
}
|
||||
|
||||
const root: Node = { name: "", children: new Map() }
|
||||
for (const file of list) {
|
||||
if (file.includes(".kilo") || file.includes(".opencode")) continue // kilocode_change
|
||||
const parts = file.split(path.sep)
|
||||
if (parts.length < 2) continue
|
||||
let node = root
|
||||
for (const part of parts.slice(0, -1)) {
|
||||
node = child(node, part)
|
||||
}
|
||||
}
|
||||
|
||||
const total = count(root)
|
||||
const limit = input.limit ?? total
|
||||
const lines: string[] = []
|
||||
const queue: Array<{ node: Node; path: string }> = Array.from(root.children.values())
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((node) => ({ node, path: node.name }))
|
||||
|
||||
let used = 0
|
||||
for (let i = 0; i < queue.length && used < limit; i++) {
|
||||
const item = queue[i]
|
||||
lines.push(item.path)
|
||||
used++
|
||||
queue.push(
|
||||
...Array.from(item.node.children.values())
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((node) => ({ node, path: `${item.path}/${node.name}` })),
|
||||
)
|
||||
}
|
||||
|
||||
if (total > used) lines.push(`[${total - used} truncated]`)
|
||||
return lines.join("\n")
|
||||
})
|
||||
|
||||
const search: Interface["search"] = Effect.fn("Ripgrep.search")(function* (input: SearchInput) {
|
||||
const useWorker = !!input.signal && typeof Worker !== "undefined"
|
||||
if (!useWorker && input.signal) {
|
||||
log.warn("worker unavailable, ripgrep abort disabled")
|
||||
}
|
||||
return yield* useWorker ? searchWorker(input) : searchDirect(input)
|
||||
})
|
||||
|
||||
return Service.of({ files, tree, search })
|
||||
}),
|
||||
export const defaultLayer = layer.pipe(
|
||||
Layer.provide(FetchHttpClient.layer),
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
Layer.provide(CrossSpawnSpawner.defaultLayer),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer
|
||||
|
||||
export * as Ripgrep from "./ripgrep"
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
import { ripgrep } from "ripgrep"
|
||||
import { sanitizedProcessEnv } from "@/util/opencode-process"
|
||||
import { KiloRipgrepStream } from "../kilocode/kilo-ripgrep-stream" // kilocode_change - share UTF-8 stream decoding
|
||||
|
||||
function env() {
|
||||
const env = sanitizedProcessEnv()
|
||||
delete env.RIPGREP_CONFIG_PATH
|
||||
return env
|
||||
}
|
||||
|
||||
function opts(cwd: string) {
|
||||
return {
|
||||
env: env(),
|
||||
preopens: { ".": cwd },
|
||||
}
|
||||
}
|
||||
|
||||
type Run = {
|
||||
kind: "files" | "search"
|
||||
cwd: string
|
||||
args: string[]
|
||||
}
|
||||
|
||||
function text(input: unknown) {
|
||||
if (typeof input === "string") return input
|
||||
if (input instanceof ArrayBuffer) return Buffer.from(input).toString()
|
||||
if (ArrayBuffer.isView(input)) return Buffer.from(input.buffer, input.byteOffset, input.byteLength).toString()
|
||||
return String(input)
|
||||
}
|
||||
|
||||
function error(input: unknown) {
|
||||
if (input instanceof Error) {
|
||||
return {
|
||||
message: input.message,
|
||||
name: input.name,
|
||||
stack: input.stack,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
message: String(input),
|
||||
}
|
||||
}
|
||||
|
||||
function clean(file: string) {
|
||||
return file.replace(/^\.[\\/]/, "")
|
||||
}
|
||||
|
||||
onmessage = async (evt: MessageEvent<Run>) => {
|
||||
const msg = evt.data
|
||||
|
||||
try {
|
||||
if (msg.kind === "search") {
|
||||
const ret = await ripgrep(msg.args, {
|
||||
buffer: true,
|
||||
...opts(msg.cwd),
|
||||
})
|
||||
postMessage({
|
||||
type: "result",
|
||||
code: ret.code ?? 0,
|
||||
stdout: ret.stdout ?? "",
|
||||
stderr: ret.stderr ?? "",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
let buf = ""
|
||||
let err = ""
|
||||
// kilocode_change start - keep decoder state across stdout chunks
|
||||
const decoder = KiloRipgrepStream.decoder()
|
||||
const out = {
|
||||
write(chunk: unknown) {
|
||||
buf = KiloRipgrepStream.drain(decoder, buf, chunk, (line) => postMessage({ type: "line", line: clean(line) }))
|
||||
},
|
||||
}
|
||||
// kilocode_change end
|
||||
const stderr = {
|
||||
write(chunk: unknown) {
|
||||
err += text(chunk)
|
||||
},
|
||||
}
|
||||
|
||||
const ret = await ripgrep(msg.args, {
|
||||
stdout: out,
|
||||
stderr,
|
||||
...opts(msg.cwd),
|
||||
})
|
||||
|
||||
buf += decoder.end() // kilocode_change - flush any trailing buffered bytes
|
||||
if (buf) postMessage({ type: "line", line: clean(buf) })
|
||||
postMessage({
|
||||
type: "done",
|
||||
code: ret.code ?? 0,
|
||||
stderr: err,
|
||||
})
|
||||
} catch (err) {
|
||||
postMessage({
|
||||
type: "error",
|
||||
error: error(err),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export type Status = z.infer<typeof Status>
|
||||
export interface Interface {
|
||||
readonly init: () => Effect.Effect<void>
|
||||
readonly status: () => Effect.Effect<Status[]>
|
||||
readonly file: (filepath: string) => Effect.Effect<void>
|
||||
readonly file: (filepath: string) => Effect.Effect<boolean>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Format") {}
|
||||
@@ -70,16 +70,19 @@ export const layer = Layer.effect(
|
||||
}
|
||||
}),
|
||||
)
|
||||
return checks.filter((x) => x.cmd).map((x) => ({ item: x.item, cmd: x.cmd! }))
|
||||
return checks
|
||||
.filter((x): x is { item: Formatter.Info; cmd: string[] } => x.cmd !== false)
|
||||
.map((x) => ({ item: x.item, cmd: x.cmd }))
|
||||
}
|
||||
|
||||
function formatFile(filepath: string) {
|
||||
return Effect.gen(function* () {
|
||||
log.info("formatting", { file: filepath })
|
||||
const ext = path.extname(filepath)
|
||||
const formatters = yield* Effect.promise(() => getFormatter(path.extname(filepath)))
|
||||
|
||||
for (const { item, cmd } of yield* Effect.promise(() => getFormatter(ext))) {
|
||||
if (cmd === false) continue
|
||||
if (!formatters.length) return false
|
||||
|
||||
for (const { item, cmd } of formatters) {
|
||||
log.info("running", { command: cmd })
|
||||
const replaced = cmd.map((x) => x.replace("$FILE", filepath))
|
||||
const dir = yield* InstanceState.directory
|
||||
@@ -113,6 +116,8 @@ export const layer = Layer.effect(
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
@@ -188,7 +193,7 @@ export const layer = Layer.effect(
|
||||
|
||||
const file = Effect.fn("Format.file")(function* (filepath: string) {
|
||||
const { formatFile } = yield* InstanceState.get(state)
|
||||
yield* formatFile(filepath)
|
||||
return yield* formatFile(filepath)
|
||||
})
|
||||
|
||||
return Service.of({ init, status, file })
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user