Resolve merge conflicts

This commit is contained in:
Kevin van Dijk
2026-02-18 13:02:19 +01:00
52 changed files with 1513 additions and 238 deletions
+3
View File
@@ -32,6 +32,9 @@ description: Use this when you are working on file operations like reading, writ
- Decode tool stderr with `Bun.readableStreamToText`. - Decode tool stderr with `Bun.readableStreamToText`.
- For large writes, use `Bun.write(Bun.file(path), text)`. - For large writes, use `Bun.write(Bun.file(path), text)`.
NOTE: Bun.file(...).exists() will return `false` if the value is a directory.
Use Filesystem.exists(...) instead if path can be file or directory
## Quick checklist ## Quick checklist
- Use Bun APIs first. - Use Bun APIs first.
+13 -13
View File
@@ -268,8 +268,8 @@
"@opencode-ai/script": "workspace:*", "@opencode-ai/script": "workspace:*",
"@opencode-ai/util": "workspace:*", "@opencode-ai/util": "workspace:*",
"@openrouter/ai-sdk-provider": "1.5.4", "@openrouter/ai-sdk-provider": "1.5.4",
"@opentui/core": "0.1.77", "@opentui/core": "0.1.79",
"@opentui/solid": "0.1.77", "@opentui/solid": "0.1.79",
"@parcel/watcher": "2.5.1", "@parcel/watcher": "2.5.1",
"@pierre/diffs": "catalog:", "@pierre/diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2", "@solid-primitives/event-bus": "1.1.2",
@@ -444,7 +444,7 @@
"@tailwindcss/vite": "4.1.11", "@tailwindcss/vite": "4.1.11",
"@tsconfig/bun": "1.0.9", "@tsconfig/bun": "1.0.9",
"@tsconfig/node22": "22.0.2", "@tsconfig/node22": "22.0.2",
"@types/bun": "1.3.8", "@types/bun": "1.3.9",
"@types/luxon": "3.7.1", "@types/luxon": "3.7.1",
"@types/node": "22.13.9", "@types/node": "22.13.9",
"@types/semver": "7.7.1", "@types/semver": "7.7.1",
@@ -1013,21 +1013,21 @@
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
"@opentui/core": ["@opentui/core@0.1.77", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.77", "@opentui/core-darwin-x64": "0.1.77", "@opentui/core-linux-arm64": "0.1.77", "@opentui/core-linux-x64": "0.1.77", "@opentui/core-win32-arm64": "0.1.77", "@opentui/core-win32-x64": "0.1.77", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-lE3kabm6jdqK3AuBq+O0zZrXdxt6ulmibTc57sf+AsPny6cmwYHnWI4wD6hcreFiYoQVNVvdiJchVgPtowMlEg=="], "@opentui/core": ["@opentui/core@0.1.79", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.79", "@opentui/core-darwin-x64": "0.1.79", "@opentui/core-linux-arm64": "0.1.79", "@opentui/core-linux-x64": "0.1.79", "@opentui/core-win32-arm64": "0.1.79", "@opentui/core-win32-x64": "0.1.79", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-job/t09w8A/aHb/WuaVbimu5fIffyN+PCuVO5cYhXEg/NkOkC/WdFi80B8bwncR/DBPyLAh6oJ3EG86grOVo5g=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.77", "", { "os": "darwin", "cpu": "arm64" }, "sha512-SNqmygCMEsPCW7xWjzCZ5caBf36xaprwVdAnFijGDOuIzLA4iaDa6um8cj3TJh7awenN3NTRsuRc7OuH42UH+g=="], "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.79", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kgsGniV+DM5G1P3GideyJhvfnthNKcVCAm2mPTIr9InQ3L0gS/Feh7zgwOS/jxDvdlQbOWGKMk2Z3JApeC1MLw=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.77", "", { "os": "darwin", "cpu": "x64" }, "sha512-/8fsa03swEHTQt/9NrGm98kemlU+VuTURI/OFZiH53vPDRrOYIYoa4Jyga/H7ZMcG+iFhkq97zIe+0Kw95LGmA=="], "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.79", "", { "os": "darwin", "cpu": "x64" }, "sha512-OpyAmFqAAKQ2CeFmf/oLWcNksmP6Ryx/3R5dbKXThOudMCeQvfvInJTRbc2jTn9VFpf+Qj4BgHkJg1h90tf/EA=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.77", "", { "os": "linux", "cpu": "arm64" }, "sha512-QfUXZJPc69OvqoMu+AlLgjqXrwu4IeqcBuUWYMuH8nPTeLsVUc3CBbXdV2lv9UDxWzxzrxdS4ALPaxvmEv9lsQ=="], "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.79", "", { "os": "linux", "cpu": "arm64" }, "sha512-DCa5YaknS4bWhFt8TMEGH+qmTinyzuY8hoZbO4crtWXAxofPP7Pas76Cwxlvis/PyLffA+pPxAl1l5sUZpsvqw=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.77", "", { "os": "linux", "cpu": "x64" }, "sha512-Kmfx0yUKnPj67AoXYIgL7qQo0QVsUG5Iw8aRtv6XFzXqa5SzBPhaKkKZ9yHPjOmTalZquUs+9zcCRNKpYYuL7A=="], "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.79", "", { "os": "linux", "cpu": "x64" }, "sha512-V6xjvFfHh3NGvsuuDae1KHPRZXHMEE8XL0A/GM6v4I4OCC23kDmkK60Vn6OptQwAzwwbz0X0IX+Ut/GQU9qGgA=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.77", "", { "os": "win32", "cpu": "arm64" }, "sha512-HGTscPXc7gdd23Nh1DbzUNjog1I+5IZp95XPtLftGTpjrWs60VcetXcyJqK2rQcXNxewJK5yDyaa5QyMjfEhCQ=="], "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.79", "", { "os": "win32", "cpu": "arm64" }, "sha512-sPRKnVzOdT5szI59tte7pxwwkYA+07EQN+6miFAvkFuiLmRUngONUD8HVjL7nCnxcPFqxaU4Rvl1y40ST86g8g=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.77", "", { "os": "win32", "cpu": "x64" }, "sha512-c7GijsbvVgnlzd2murIbwuwrGbcv76KdUw6WlVv7a0vex50z6xJCpv1keGzpe0QfxrZ/6fFEFX7JnwGLno0wjA=="], "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.79", "", { "os": "win32", "cpu": "x64" }, "sha512-vmQcFTvKf9fqajnDtgU6/uAsiTGwx8//khqHVBmiTEXUsiT792Ki9l8sgNughbuldqG5iZOiF6IaAWU1H67UpA=="],
"@opentui/solid": ["@opentui/solid@0.1.77", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.77", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-JY+hUbXVV+XCk6bC8dvcwawWCEmC3Gid6GDs23AJWBgHZ3TU2kRKrgwTdltm45DOq2cZXrYCt690/yE8bP+Gxg=="], "@opentui/solid": ["@opentui/solid@0.1.79", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.79", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-c5+0jexKxb8GwRDDkQ/U6isZZqClAzHccXmYiLYmSnqdoQQp2lIGHLartL+K8lfIQrsKClzP2ZHumN6nexRfRg=="],
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
@@ -1401,7 +1401,7 @@
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], "@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.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
"@types/diff": ["@types/diff@6.0.0", "", {}, "sha512-dhVCYGv3ZSbzmQaBSagrv1WJ6rXCdkyTcDyoNu1MD8JohI7pR7k8wdZEm+mvdxRKXyHVwckFzWU1vJc+Z29MlA=="], "@types/diff": ["@types/diff@6.0.0", "", {}, "sha512-dhVCYGv3ZSbzmQaBSagrv1WJ6rXCdkyTcDyoNu1MD8JohI7pR7k8wdZEm+mvdxRKXyHVwckFzWU1vJc+Z29MlA=="],
@@ -1613,7 +1613,7 @@
"bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="], "bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="],
"bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
"bun-webgpu": ["bun-webgpu@0.1.4", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.4", "bun-webgpu-darwin-x64": "^0.1.4", "bun-webgpu-linux-x64": "^0.1.4", "bun-webgpu-win32-x64": "^0.1.4" } }, "sha512-Kw+HoXl1PMWJTh9wvh63SSRofTA8vYBFCw0XEP1V1fFdQEDhI8Sgf73sdndE/oDpN/7CMx0Yv/q8FCvO39ROMQ=="], "bun-webgpu": ["bun-webgpu@0.1.4", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.4", "bun-webgpu-darwin-x64": "^0.1.4", "bun-webgpu-linux-x64": "^0.1.4", "bun-webgpu-win32-x64": "^0.1.4" } }, "sha512-Kw+HoXl1PMWJTh9wvh63SSRofTA8vYBFCw0XEP1V1fFdQEDhI8Sgf73sdndE/oDpN/7CMx0Yv/q8FCvO39ROMQ=="],
@@ -1,15 +0,0 @@
{
"keep": {
"days": true,
"amount": 14
},
"auditLog": "/home/thdxr/dev/projects/sst/opencode/logs/.2c5480b3b2480f80fa29b850af461dce619c0b2f-audit.json",
"files": [
{
"date": 1759827172859,
"name": "/home/thdxr/dev/projects/sst/opencode/logs/mcp-puppeteer-2025-10-07.log",
"hash": "a3d98b26edd793411b968a0d24cfeee8332138e282023c3b83ec169d55c67f16"
}
],
"hashType": "sha256"
}
+2 -2
View File
@@ -4,7 +4,7 @@
"description": "AI-powered development tool", "description": "AI-powered development tool",
"private": true, "private": true,
"type": "module", "type": "module",
"packageManager": "bun@1.3.8", "packageManager": "bun@1.3.9",
"scripts": { "scripts": {
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts", "dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
"dev:desktop": "bun --cwd packages/desktop tauri dev", "dev:desktop": "bun --cwd packages/desktop tauri dev",
@@ -21,7 +21,7 @@
"packages/sdk/js" "packages/sdk/js"
], ],
"catalog": { "catalog": {
"@types/bun": "1.3.8", "@types/bun": "1.3.9",
"@octokit/rest": "22.0.0", "@octokit/rest": "22.0.0",
"@hono/zod-validator": "0.4.2", "@hono/zod-validator": "0.4.2",
"ulid": "3.0.1", "ulid": "3.0.1",
@@ -2,6 +2,7 @@ import { onCleanup, onMount } from "solid-js"
import { showToast } from "@opencode-ai/ui/toast" import { showToast } from "@opencode-ai/ui/toast"
import { usePrompt, type ContentPart, type ImageAttachmentPart } from "@/context/prompt" import { usePrompt, type ContentPart, type ImageAttachmentPart } from "@/context/prompt"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { uuid } from "@/utils/uuid"
import { getCursorPosition } from "./editor-dom" import { getCursorPosition } from "./editor-dom"
export const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"] export const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]
@@ -31,7 +32,7 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
const dataUrl = reader.result as string const dataUrl = reader.result as string
const attachment: ImageAttachmentPart = { const attachment: ImageAttachmentPart = {
type: "image", type: "image",
id: crypto.randomUUID?.() ?? Math.random().toString(16).slice(2), id: uuid(),
filename: file.name, filename: file.name,
mime: file.type, mime: file.type,
dataUrl, dataUrl,
+2 -1
View File
@@ -4,6 +4,7 @@ import { createSimpleContext } from "@opencode-ai/ui/context"
import { useParams } from "@solidjs/router" import { useParams } from "@solidjs/router"
import { Persist, persisted } from "@/utils/persist" import { Persist, persisted } from "@/utils/persist"
import { createScopedCache } from "@/utils/scoped-cache" import { createScopedCache } from "@/utils/scoped-cache"
import { uuid } from "@/utils/uuid"
import type { SelectedLineRange } from "@/context/file" import type { SelectedLineRange } from "@/context/file"
export type LineComment = { export type LineComment = {
@@ -53,7 +54,7 @@ function createCommentSessionState(store: Store<CommentStore>, setStore: SetStor
const add = (input: Omit<LineComment, "id" | "time">) => { const add = (input: Omit<LineComment, "id" | "time">) => {
const next: LineComment = { const next: LineComment = {
id: crypto.randomUUID?.() ?? Math.random().toString(16).slice(2), id: uuid(),
time: Date.now(), time: Date.now(),
...input, ...input,
} }
+4 -2
View File
@@ -4,6 +4,7 @@ import { createSimpleContext } from "@opencode-ai/ui/context"
import { useGlobalSync } from "./global-sync" import { useGlobalSync } from "./global-sync"
import { useGlobalSDK } from "./global-sdk" import { useGlobalSDK } from "./global-sdk"
import { useServer } from "./server" import { useServer } from "./server"
import { usePlatform } from "./platform"
import { Project } from "@kilocode/sdk/v2" import { Project } from "@kilocode/sdk/v2"
import { Persist, persisted, removePersisted } from "@/utils/persist" import { Persist, persisted, removePersisted } from "@/utils/persist"
import { same } from "@/utils/same" import { same } from "@/utils/same"
@@ -90,6 +91,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
const globalSdk = useGlobalSDK() const globalSdk = useGlobalSDK()
const globalSync = useGlobalSync() const globalSync = useGlobalSync()
const server = useServer() const server = useServer()
const platform = usePlatform()
const isRecord = (value: unknown): value is Record<string, unknown> => const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value) typeof value === "object" && value !== null && !Array.isArray(value)
@@ -200,10 +202,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
for (const entry of SESSION_STATE_KEYS) { for (const entry of SESSION_STATE_KEYS) {
const target = session ? Persist.session(dir, session, entry.key) : Persist.workspace(dir, entry.key) const target = session ? Persist.session(dir, session, entry.key) : Persist.workspace(dir, entry.key)
void removePersisted(target) void removePersisted(target, platform)
const legacyKey = `${dir}/${entry.legacy}${session ? "/" + session : ""}.${entry.version}` const legacyKey = `${dir}/${entry.legacy}${session ? "/" + session : ""}.${entry.version}`
void removePersisted({ key: legacyKey }) void removePersisted({ key: legacyKey }, platform)
} }
} }
} }
+4 -3
View File
@@ -3,6 +3,7 @@ import { createSimpleContext } from "@opencode-ai/ui/context"
import { batch, createEffect, createMemo, createRoot, onCleanup } from "solid-js" import { batch, createEffect, createMemo, createRoot, onCleanup } from "solid-js"
import { useParams } from "@solidjs/router" import { useParams } from "@solidjs/router"
import { useSDK } from "./sdk" import { useSDK } from "./sdk"
import type { Platform } from "./platform"
import { Persist, persisted, removePersisted } from "@/utils/persist" import { Persist, persisted, removePersisted } from "@/utils/persist"
export type LocalPTY = { export type LocalPTY = {
@@ -37,14 +38,14 @@ type TerminalCacheEntry = {
const caches = new Set<Map<string, TerminalCacheEntry>>() const caches = new Set<Map<string, TerminalCacheEntry>>()
export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[]) { export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[], platform?: Platform) {
const key = getWorkspaceTerminalCacheKey(dir) const key = getWorkspaceTerminalCacheKey(dir)
for (const cache of caches) { for (const cache of caches) {
const entry = cache.get(key) const entry = cache.get(key)
entry?.value.clear() entry?.value.clear()
} }
removePersisted(Persist.workspace(dir, "terminal")) removePersisted(Persist.workspace(dir, "terminal"), platform)
const legacy = new Set(getLegacyTerminalStorageKeys(dir)) const legacy = new Set(getLegacyTerminalStorageKeys(dir))
for (const id of sessionIDs ?? []) { for (const id of sessionIDs ?? []) {
@@ -53,7 +54,7 @@ export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[]) {
} }
} }
for (const key of legacy) { for (const key of legacy) {
removePersisted({ key }) removePersisted({ key }, platform)
} }
} }
+2 -1
View File
@@ -132,7 +132,8 @@ export const dict = {
"provider.connect.oauth.code.invalid": "رمز التفويض غير صالح", "provider.connect.oauth.code.invalid": "رمز التفويض غير صالح",
"provider.connect.oauth.auto.visit.prefix": "قم بزيارة ", "provider.connect.oauth.auto.visit.prefix": "قم بزيارة ",
"provider.connect.oauth.auto.visit.link": "هذا الرابط", "provider.connect.oauth.auto.visit.link": "هذا الرابط",
"provider.connect.oauth.auto.visit.suffix": " وأدخل الرمز أدناه لتوصيل حسابك واستخدام نماذج {{provider}} في Kilo.", "provider.connect.oauth.auto.visit.suffix":
" وأدخل الرمز أدناه لتوصيل حسابك واستخدام نماذج {{provider}} في Kilo.",
"provider.connect.oauth.auto.confirmationCode": "رمز التأكيد", "provider.connect.oauth.auto.confirmationCode": "رمز التأكيد",
"provider.connect.toast.connected.title": "تم توصيل {{provider}}", "provider.connect.toast.connected.title": "تم توصيل {{provider}}",
"provider.connect.toast.connected.description": "نماذج {{provider}} متاحة الآن للاستخدام.", "provider.connect.toast.connected.description": "نماذج {{provider}} متاحة الآن للاستخدام.",
+4 -2
View File
@@ -385,7 +385,8 @@ export const dict = {
"toast.session.unshare.failed.description": "Une erreur s'est produite lors de l'annulation du partage de la session", "toast.session.unshare.failed.description": "Une erreur s'est produite lors de l'annulation du partage de la session",
"toast.session.listFailed.title": "Échec du chargement des sessions pour {{project}}", "toast.session.listFailed.title": "Échec du chargement des sessions pour {{project}}",
"toast.update.title": "Mise à jour disponible", "toast.update.title": "Mise à jour disponible",
"toast.update.description": "Une nouvelle version d'Kilo ({{version}}) est maintenant disponible pour installation.", "toast.update.description":
"Une nouvelle version d'Kilo ({{version}}) est maintenant disponible pour installation.",
"toast.update.action.installRestart": "Installer et redémarrer", "toast.update.action.installRestart": "Installer et redémarrer",
"toast.update.action.notYet": "Pas encore", "toast.update.action.notYet": "Pas encore",
"error.page.title": "Quelque chose s'est mal passé", "error.page.title": "Quelque chose s'est mal passé",
@@ -516,7 +517,8 @@ export const dict = {
"sidebar.workspaces.enable": "Activer les espaces de travail", "sidebar.workspaces.enable": "Activer les espaces de travail",
"sidebar.workspaces.disable": "Désactiver les espaces de travail", "sidebar.workspaces.disable": "Désactiver les espaces de travail",
"sidebar.gettingStarted.title": "Commencer", "sidebar.gettingStarted.title": "Commencer",
"sidebar.gettingStarted.line1": "Kilo inclut des modèles gratuits pour que vous puissiez commencer immédiatement.", "sidebar.gettingStarted.line1":
"Kilo inclut des modèles gratuits pour que vous puissiez commencer immédiatement.",
"sidebar.gettingStarted.line2": "sidebar.gettingStarted.line2":
"Connectez n'importe quel fournisseur pour utiliser des modèles, y compris Claude, GPT, Gemini etc.", "Connectez n'importe quel fournisseur pour utiliser des modèles, y compris Claude, GPT, Gemini etc.",
"sidebar.project.recentSessions": "Sessions récentes", "sidebar.project.recentSessions": "Sessions récentes",
+2 -1
View File
@@ -144,7 +144,8 @@ export const dict = {
"provider.connect.oauth.code.invalid": "รหัสการอนุญาตไม่ถูกต้อง", "provider.connect.oauth.code.invalid": "รหัสการอนุญาตไม่ถูกต้อง",
"provider.connect.oauth.auto.visit.prefix": "เยี่ยมชม ", "provider.connect.oauth.auto.visit.prefix": "เยี่ยมชม ",
"provider.connect.oauth.auto.visit.link": "ลิงก์นี้", "provider.connect.oauth.auto.visit.link": "ลิงก์นี้",
"provider.connect.oauth.auto.visit.suffix": " และป้อนรหัสด้านล่างเพื่อเชื่อมต่อบัญชีและใช้โมเดล {{provider}} ใน Kilo", "provider.connect.oauth.auto.visit.suffix":
" และป้อนรหัสด้านล่างเพื่อเชื่อมต่อบัญชีและใช้โมเดล {{provider}} ใน Kilo",
"provider.connect.oauth.auto.confirmationCode": "รหัสยืนยัน", "provider.connect.oauth.auto.confirmationCode": "รหัสยืนยัน",
"provider.connect.toast.connected.title": "{{provider}} ที่เชื่อมต่อแล้ว", "provider.connect.toast.connected.title": "{{provider}} ที่เชื่อมต่อแล้ว",
"provider.connect.toast.connected.description": "โมเดล {{provider}} พร้อมใช้งานแล้ว", "provider.connect.toast.connected.description": "โมเดล {{provider}} พร้อมใช้งานแล้ว",
+2 -1
View File
@@ -145,7 +145,8 @@ export const dict = {
"provider.connect.oauth.code.invalid": "授權碼無效", "provider.connect.oauth.code.invalid": "授權碼無效",
"provider.connect.oauth.auto.visit.prefix": "造訪 ", "provider.connect.oauth.auto.visit.prefix": "造訪 ",
"provider.connect.oauth.auto.visit.link": "此連結", "provider.connect.oauth.auto.visit.link": "此連結",
"provider.connect.oauth.auto.visit.suffix": " 並輸入以下程式碼,以連線你的帳戶並在 Kilo 中使用 {{provider}} 模型。", "provider.connect.oauth.auto.visit.suffix":
" 並輸入以下程式碼,以連線你的帳戶並在 Kilo 中使用 {{provider}} 模型。",
"provider.connect.oauth.auto.confirmationCode": "確認碼", "provider.connect.oauth.auto.confirmationCode": "確認碼",
"provider.connect.toast.connected.title": "{{provider}} 已連線", "provider.connect.toast.connected.title": "{{provider}} 已連線",
"provider.connect.toast.connected.description": "現在可以使用 {{provider}} 模型了。", "provider.connect.toast.connected.description": "現在可以使用 {{provider}} 模型了。",
+11
View File
@@ -1203,6 +1203,16 @@ export default function Layout(props: ParentProps) {
if (!result) return if (!result) return
globalSync.set(
"project",
produce((draft) => {
const project = draft.find((item) => item.worktree === root)
if (!project) return
project.sandboxes = (project.sandboxes ?? []).filter((sandbox) => sandbox !== directory)
}),
)
setStore("workspaceOrder", root, (order) => (order ?? []).filter((workspace) => workspace !== directory))
layout.projects.close(directory) layout.projects.close(directory)
layout.projects.open(root) layout.projects.open(root)
@@ -1230,6 +1240,7 @@ export default function Layout(props: ParentProps) {
clearWorkspaceTerminals( clearWorkspaceTerminals(
directory, directory,
sessions.map((s) => s.id), sessions.map((s) => s.id),
platform,
) )
await globalSDK.client.instance.dispose({ directory }).catch(() => undefined) await globalSDK.client.instance.dispose({ directory }).catch(() => undefined)
@@ -32,7 +32,9 @@ export const ProjectIcon = (props: { project: LocalProject; class?: string; noti
<div class="size-full rounded overflow-clip"> <div class="size-full rounded overflow-clip">
<Avatar <Avatar
fallback={name()} fallback={name()}
src={props.project.id === KILO_PROJECT_ID ? "https://kilo.ai/favicon.svg" : props.project.icon?.override} src={
props.project.id === KILO_PROJECT_ID ? "https://kilo.ai/favicon.svg" : props.project.icon?.override
}
{...getAvatarColors(props.project.icon?.color)} {...getAvatarColors(props.project.icon?.color)}
class="size-full rounded" class="size-full rounded"
classList={{ "badge-mask": unseenCount() > 0 && props.notify }} classList={{ "badge-mask": unseenCount() > 0 && props.notify }}
+4 -4
View File
@@ -1,3 +1,5 @@
import { uuid } from "@/utils/uuid"
type Nav = { type Nav = {
id: string id: string
dir?: string dir?: string
@@ -16,8 +18,6 @@ const key = (dir: string | undefined, to: string) => `${dir ?? ""}:${to}`
const now = () => performance.now() const now = () => performance.now()
const uid = () => crypto.randomUUID?.() ?? Math.random().toString(16).slice(2)
const navs = new Map<string, Nav>() const navs = new Map<string, Nav>()
const pending = new Map<string, string>() const pending = new Map<string, string>()
const active = new Map<string, string>() const active = new Map<string, string>()
@@ -94,7 +94,7 @@ function ensure(id: string, data: Omit<Nav, "marks" | "logged" | "timer">) {
export function navStart(input: { dir?: string; from?: string; to: string; trigger?: string }) { export function navStart(input: { dir?: string; from?: string; to: string; trigger?: string }) {
if (!dev) return if (!dev) return
const id = uid() const id = uuid()
const start = now() const start = now()
const nav = ensure(id, { ...input, id, start }) const nav = ensure(id, { ...input, id, start })
nav.marks["navigate:start"] = start nav.marks["navigate:start"] = start
@@ -109,7 +109,7 @@ export function navParams(input: { dir?: string; from?: string; to: string }) {
const k = key(input.dir, input.to) const k = key(input.dir, input.to)
const pendingId = pending.get(k) const pendingId = pending.get(k)
if (pendingId) pending.delete(k) if (pendingId) pending.delete(k)
const id = pendingId ?? uid() const id = pendingId ?? uuid()
const start = now() const start = now()
const nav = ensure(id, { ...input, id, start, trigger: pendingId ? "key" : "route" }) const nav = ensure(id, { ...input, id, start, trigger: pendingId ? "key" : "route" })
+3 -4
View File
@@ -1,4 +1,4 @@
import { usePlatform } from "@/context/platform" import { Platform, usePlatform } from "@/context/platform"
import { makePersisted, type AsyncStorage, type SyncStorage } from "@solid-primitives/storage" import { makePersisted, type AsyncStorage, type SyncStorage } from "@solid-primitives/storage"
import { checksum } from "@opencode-ai/util/encode" import { checksum } from "@opencode-ai/util/encode"
import { createResource, type Accessor } from "solid-js" import { createResource, type Accessor } from "solid-js"
@@ -318,9 +318,8 @@ export const Persist = {
}, },
} }
export function removePersisted(target: { storage?: string; key: string }) { export function removePersisted(target: { storage?: string; key: string }, platform?: Platform) {
const platform = usePlatform() const isDesktop = platform?.platform === "desktop" && !!platform.storage
const isDesktop = platform.platform === "desktop" && !!platform.storage
if (isDesktop) { if (isDesktop) {
return platform.storage?.(target.storage)?.removeItem(target.key) return platform.storage?.(target.storage)?.removeItem(target.key)
+78
View File
@@ -0,0 +1,78 @@
import { afterEach, describe, expect, test } from "bun:test"
import { uuid } from "./uuid"
const cryptoDescriptor = Object.getOwnPropertyDescriptor(globalThis, "crypto")
const secureDescriptor = Object.getOwnPropertyDescriptor(globalThis, "isSecureContext")
const randomDescriptor = Object.getOwnPropertyDescriptor(Math, "random")
const setCrypto = (value: Partial<Crypto>) => {
Object.defineProperty(globalThis, "crypto", {
configurable: true,
value: value as Crypto,
})
}
const setSecure = (value: boolean) => {
Object.defineProperty(globalThis, "isSecureContext", {
configurable: true,
value,
})
}
const setRandom = (value: () => number) => {
Object.defineProperty(Math, "random", {
configurable: true,
value,
})
}
afterEach(() => {
if (cryptoDescriptor) {
Object.defineProperty(globalThis, "crypto", cryptoDescriptor)
}
if (secureDescriptor) {
Object.defineProperty(globalThis, "isSecureContext", secureDescriptor)
}
if (!secureDescriptor) {
delete (globalThis as { isSecureContext?: boolean }).isSecureContext
}
if (randomDescriptor) {
Object.defineProperty(Math, "random", randomDescriptor)
}
})
describe("uuid", () => {
test("uses randomUUID in secure contexts", () => {
setCrypto({ randomUUID: () => "00000000-0000-0000-0000-000000000000" })
setSecure(true)
expect(uuid()).toBe("00000000-0000-0000-0000-000000000000")
})
test("falls back in insecure contexts", () => {
setCrypto({ randomUUID: () => "00000000-0000-0000-0000-000000000000" })
setSecure(false)
setRandom(() => 0.5)
expect(uuid()).toBe("8")
})
test("falls back when randomUUID throws", () => {
setCrypto({
randomUUID: () => {
throw new DOMException("Failed", "OperationError")
},
})
setSecure(true)
setRandom(() => 0.5)
expect(uuid()).toBe("8")
})
test("falls back when randomUUID is unavailable", () => {
setCrypto({})
setSecure(true)
setRandom(() => 0.5)
expect(uuid()).toBe("8")
})
})
+12
View File
@@ -0,0 +1,12 @@
const fallback = () => Math.random().toString(16).slice(2)
export function uuid() {
const c = globalThis.crypto
if (!c || typeof c.randomUUID !== "function") return fallback()
if (typeof globalThis.isSecureContext === "boolean" && !globalThis.isSecureContext) return fallback()
try {
return c.randomUUID()
} catch {
return fallback()
}
}
+2 -2
View File
@@ -85,8 +85,8 @@
"@kilocode/sdk": "workspace:*", "@kilocode/sdk": "workspace:*",
"@opencode-ai/util": "workspace:*", "@opencode-ai/util": "workspace:*",
"@openrouter/ai-sdk-provider": "1.5.4", "@openrouter/ai-sdk-provider": "1.5.4",
"@opentui/core": "0.1.77", "@opentui/core": "0.1.79",
"@opentui/solid": "0.1.77", "@opentui/solid": "0.1.79",
"@parcel/watcher": "2.5.1", "@parcel/watcher": "2.5.1",
"@pierre/diffs": "catalog:", "@pierre/diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2", "@solid-primitives/event-bus": "1.1.2",
+12 -1
View File
@@ -3,6 +3,7 @@ import { Clipboard } from "@tui/util/clipboard"
import { TextAttributes } from "@opentui/core" import { TextAttributes } from "@opentui/core"
import { RouteProvider, useRoute } from "@tui/context/route" import { RouteProvider, useRoute } from "@tui/context/route"
import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal, onMount, batch, Show, on } from "solid-js" import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal, onMount, batch, Show, on } from "solid-js"
import { win32DisableProcessedInput, win32FlushInputBuffer, win32InstallCtrlCGuard } from "./win32"
import { Installation } from "@/installation" import { Installation } from "@/installation"
import { Flag } from "@/flag/flag" import { Flag } from "@/flag/flag"
import { DialogProvider, useDialog } from "@tui/ui/dialog" import { DialogProvider, useDialog } from "@tui/ui/dialog"
@@ -114,8 +115,17 @@ export function tui(input: {
}) { }) {
// promise to prevent immediate exit // promise to prevent immediate exit
return new Promise<void>(async (resolve) => { return new Promise<void>(async (resolve) => {
const unguard = win32InstallCtrlCGuard()
win32DisableProcessedInput()
const mode = await getTerminalBackgroundColor() const mode = await getTerminalBackgroundColor()
// Re-clear after getTerminalBackgroundColor() — setRawMode(false) restores
// the original console mode which re-enables ENABLE_PROCESSED_INPUT.
win32DisableProcessedInput()
const onExit = async () => { const onExit = async () => {
unguard?.()
await input.onExit?.() await input.onExit?.()
resolve() resolve()
} }
@@ -741,7 +751,8 @@ function ErrorComponent(props: {
const handleExit = async () => { const handleExit = async () => {
renderer.setTerminalTitle("") renderer.setTerminalTitle("")
renderer.destroy() renderer.destroy()
props.onExit() win32FlushInputBuffer()
await props.onExit()
} }
useKeyboard((evt) => { useKeyboard((evt) => {
+30 -22
View File
@@ -1,5 +1,6 @@
import { cmd } from "../cmd" import { cmd } from "../cmd"
import { tui } from "./app" import { tui } from "./app"
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
export const AttachCommand = cmd({ export const AttachCommand = cmd({
command: "attach <url>", command: "attach <url>",
@@ -26,27 +27,34 @@ export const AttachCommand = cmd({
describe: "basic auth password (defaults to KILO_SERVER_PASSWORD)", describe: "basic auth password (defaults to KILO_SERVER_PASSWORD)",
}), }),
handler: async (args) => { handler: async (args) => {
const directory = (() => { const unguard = win32InstallCtrlCGuard()
if (!args.dir) return undefined try {
try { win32DisableProcessedInput()
process.chdir(args.dir)
return process.cwd() const directory = (() => {
} catch { if (!args.dir) return undefined
// If the directory doesn't exist locally (remote attach), pass it through. try {
return args.dir process.chdir(args.dir)
} return process.cwd()
})() } catch {
const headers = (() => { // If the directory doesn't exist locally (remote attach), pass it through.
const password = args.password ?? process.env.KILO_SERVER_PASSWORD return args.dir
if (!password) return undefined }
const auth = `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}` })()
return { Authorization: auth } const headers = (() => {
})() const password = args.password ?? process.env.KILO_SERVER_PASSWORD
await tui({ if (!password) return undefined
url: args.url, const auth = `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}`
args: { sessionID: args.session }, return { Authorization: auth }
directory, })()
headers, await tui({
}) url: args.url,
args: { sessionID: args.session },
directory,
headers,
})
} finally {
unguard?.()
}
}, },
}) })
@@ -1,6 +1,7 @@
import { useRenderer } from "@opentui/solid" import { useRenderer } from "@opentui/solid"
import { createSimpleContext } from "./helper" import { createSimpleContext } from "./helper"
import { FormatError, FormatUnknownError } from "@/cli/error" import { FormatError, FormatUnknownError } from "@/cli/error"
import { win32FlushInputBuffer } from "../win32"
type Exit = ((reason?: unknown) => Promise<void>) & { type Exit = ((reason?: unknown) => Promise<void>) & {
message: { message: {
set: (value?: string) => () => void set: (value?: string) => () => void
@@ -32,6 +33,7 @@ export const { use: useExit, provider: ExitProvider } = createSimpleContext({
// Reset window title before destroying renderer // Reset window title before destroying renderer
renderer.setTerminalTitle("") renderer.setTerminalTitle("")
renderer.destroy() renderer.destroy()
win32FlushInputBuffer()
await input.onExit?.() await input.onExit?.()
if (reason) { if (reason) {
const formatted = FormatError(reason) ?? FormatUnknownError(reason) const formatted = FormatError(reason) ?? FormatUnknownError(reason)
@@ -151,6 +151,7 @@ export function Session() {
const [showDetails, setShowDetails] = kv.signal("tool_details_visibility", true) const [showDetails, setShowDetails] = kv.signal("tool_details_visibility", true)
const [showAssistantMetadata, setShowAssistantMetadata] = kv.signal("assistant_metadata_visibility", true) const [showAssistantMetadata, setShowAssistantMetadata] = kv.signal("assistant_metadata_visibility", true)
const [showScrollbar, setShowScrollbar] = kv.signal("scrollbar_visible", false) const [showScrollbar, setShowScrollbar] = kv.signal("scrollbar_visible", false)
const [showHeader, setShowHeader] = kv.signal("header_visible", true)
const [diffWrapMode] = kv.signal<"word" | "none">("diff_wrap_mode", "word") const [diffWrapMode] = kv.signal<"word" | "none">("diff_wrap_mode", "word")
const [animationsEnabled, setAnimationsEnabled] = kv.signal("animations_enabled", true) const [animationsEnabled, setAnimationsEnabled] = kv.signal("animations_enabled", true)
@@ -596,6 +597,15 @@ export function Session() {
dialog.clear() dialog.clear()
}, },
}, },
{
title: showHeader() ? "Hide header" : "Show header",
value: "session.toggle.header",
category: "Session",
onSelect: (dialog) => {
setShowHeader((prev) => !prev)
dialog.clear()
},
},
{ {
title: "Page up", title: "Page up",
value: "session.page.up", value: "session.page.up",
@@ -977,7 +987,7 @@ export function Session() {
<box flexDirection="row"> <box flexDirection="row">
<box flexGrow={1} paddingBottom={1} paddingTop={1} paddingLeft={2} paddingRight={2} gap={1}> <box flexGrow={1} paddingBottom={1} paddingTop={1} paddingLeft={2} paddingRight={2} gap={1}>
<Show when={session()}> <Show when={session()}>
<Show when={!sidebarVisible() || !wide()}> <Show when={showHeader() && (!sidebarVisible() || !wide())}>
<Header /> <Header />
</Show> </Show>
<scrollbox <scrollbox
+105 -92
View File
@@ -9,6 +9,7 @@ import { Log } from "@/util/log"
import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network" import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network"
import type { Event } from "@kilocode/sdk/v2" import type { Event } from "@kilocode/sdk/v2"
import type { EventSource } from "./context/sdk" import type { EventSource } from "./context/sdk"
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
declare global { declare global {
const KILO_WORKER_PATH: string // kilocode_change const KILO_WORKER_PATH: string // kilocode_change
@@ -77,99 +78,111 @@ export const TuiThreadCommand = cmd({
describe: "agent to use", describe: "agent to use",
}), }),
handler: async (args) => { handler: async (args) => {
if (args.fork && !args.continue && !args.session) { // Keep ENABLE_PROCESSED_INPUT cleared even if other code flips it.
UI.error("--fork requires --continue or --session") // (Important when running under `bun run` wrappers on Windows.)
process.exit(1) const unguard = win32InstallCtrlCGuard()
}
// Resolve relative paths against PWD to preserve behavior when using --cwd flag
const baseCwd = process.env.PWD ?? process.cwd()
const cwd = args.project ? path.resolve(baseCwd, args.project) : process.cwd()
const localWorker = new URL("./worker.ts", import.meta.url)
const distWorker = new URL("./cli/cmd/tui/worker.js", import.meta.url)
const workerPath = await iife(async () => {
if (typeof KILO_WORKER_PATH !== "undefined") return KILO_WORKER_PATH // kilocode_change
if (await Bun.file(distWorker).exists()) return distWorker
return localWorker
})
try { try {
process.chdir(cwd) // Must be the very first thing — disables CTRL_C_EVENT before any Worker
} catch (e) { // spawn or async work so the OS cannot kill the process group.
UI.error("Failed to change directory to " + cwd) win32DisableProcessedInput()
return
if (args.fork && !args.continue && !args.session) {
UI.error("--fork requires --continue or --session")
process.exitCode = 1
return
}
// Resolve relative paths against PWD to preserve behavior when using --cwd flag
const baseCwd = process.env.PWD ?? process.cwd()
const cwd = args.project ? path.resolve(baseCwd, args.project) : process.cwd()
const localWorker = new URL("./worker.ts", import.meta.url)
const distWorker = new URL("./cli/cmd/tui/worker.js", import.meta.url)
const workerPath = await iife(async () => {
if (typeof KILO_WORKER_PATH !== "undefined") return KILO_WORKER_PATH
if (await Bun.file(distWorker).exists()) return distWorker
return localWorker
})
try {
process.chdir(cwd)
} catch (e) {
UI.error("Failed to change directory to " + cwd)
return
}
const worker = new Worker(workerPath, {
env: Object.fromEntries(
Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined),
),
})
worker.onerror = (e) => {
Log.Default.error(e)
}
const client = Rpc.client<typeof rpc>(worker)
process.on("uncaughtException", (e) => {
Log.Default.error(e)
})
process.on("unhandledRejection", (e) => {
Log.Default.error(e)
})
process.on("SIGUSR2", async () => {
await client.call("reload", undefined)
})
const prompt = await iife(async () => {
const piped = !process.stdin.isTTY ? await Bun.stdin.text() : undefined
if (!args.prompt) return piped
return piped ? piped + "\n" + args.prompt : args.prompt
})
// Check if server should be started (port or hostname explicitly set in CLI or config)
const networkOpts = await resolveNetworkOptions(args)
const shouldStartServer =
process.argv.includes("--port") ||
process.argv.includes("--hostname") ||
process.argv.includes("--mdns") ||
networkOpts.mdns ||
networkOpts.port !== 0 ||
networkOpts.hostname !== "127.0.0.1"
let url: string
let customFetch: typeof fetch | undefined
let events: EventSource | undefined
if (shouldStartServer) {
// Start HTTP server for external access
const server = await client.call("server", networkOpts)
url = server.url
} else {
// Use direct RPC communication (no HTTP)
url = "http://opencode.internal"
customFetch = createWorkerFetch(client)
events = createEventSource(client)
}
const tuiPromise = tui({
url,
fetch: customFetch,
events,
args: {
continue: args.continue,
sessionID: args.session,
agent: args.agent,
model: args.model,
prompt,
fork: args.fork,
},
onExit: async () => {
await client.call("shutdown", undefined)
},
})
setTimeout(() => {
client.call("checkUpgrade", { directory: cwd }).catch(() => {})
}, 1000)
await tuiPromise
} finally {
unguard?.()
} }
const worker = new Worker(workerPath, {
env: Object.fromEntries(
Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined),
),
})
worker.onerror = (e) => {
Log.Default.error(e)
}
const client = Rpc.client<typeof rpc>(worker)
process.on("uncaughtException", (e) => {
Log.Default.error(e)
})
process.on("unhandledRejection", (e) => {
Log.Default.error(e)
})
process.on("SIGUSR2", async () => {
await client.call("reload", undefined)
})
const prompt = await iife(async () => {
const piped = !process.stdin.isTTY ? await Bun.stdin.text() : undefined
if (!args.prompt) return piped
return piped ? piped + "\n" + args.prompt : args.prompt
})
// Check if server should be started (port or hostname explicitly set in CLI or config)
const networkOpts = await resolveNetworkOptions(args)
const shouldStartServer =
process.argv.includes("--port") ||
process.argv.includes("--hostname") ||
process.argv.includes("--mdns") ||
networkOpts.mdns ||
networkOpts.port !== 0 ||
networkOpts.hostname !== "127.0.0.1"
let url: string
let customFetch: typeof fetch | undefined
let events: EventSource | undefined
if (shouldStartServer) {
// Start HTTP server for external access
const server = await client.call("server", networkOpts)
url = server.url
} else {
// Use direct RPC communication (no HTTP)
url = "http://opencode.internal"
customFetch = createWorkerFetch(client)
events = createEventSource(client)
}
const tuiPromise = tui({
url,
fetch: customFetch,
events,
args: {
continue: args.continue,
sessionID: args.session,
agent: args.agent,
model: args.model,
prompt,
fork: args.fork,
},
onExit: async () => {
await client.call("shutdown", undefined)
},
})
setTimeout(() => {
client.call("checkUpgrade", { directory: cwd }).catch(() => {})
}, 1000)
await tuiPromise
}, },
}) })
+129
View File
@@ -0,0 +1,129 @@
import { dlopen, ptr } from "bun:ffi"
const STD_INPUT_HANDLE = -10
const ENABLE_PROCESSED_INPUT = 0x0001
const kernel = () =>
dlopen("kernel32.dll", {
GetStdHandle: { args: ["i32"], returns: "ptr" },
GetConsoleMode: { args: ["ptr", "ptr"], returns: "i32" },
SetConsoleMode: { args: ["ptr", "u32"], returns: "i32" },
FlushConsoleInputBuffer: { args: ["ptr"], returns: "i32" },
})
let k32: ReturnType<typeof kernel> | undefined
function load() {
if (process.platform !== "win32") return false
try {
k32 ??= kernel()
return true
} catch {
return false
}
}
/**
* Clear ENABLE_PROCESSED_INPUT on the console stdin handle.
*/
export function win32DisableProcessedInput() {
if (process.platform !== "win32") return
if (!process.stdin.isTTY) return
if (!load()) return
const handle = k32!.symbols.GetStdHandle(STD_INPUT_HANDLE)
const buf = new Uint32Array(1)
if (k32!.symbols.GetConsoleMode(handle, ptr(buf)) === 0) return
const mode = buf[0]!
if ((mode & ENABLE_PROCESSED_INPUT) === 0) return
k32!.symbols.SetConsoleMode(handle, mode & ~ENABLE_PROCESSED_INPUT)
}
/**
* Discard any queued console input (mouse events, key presses, etc.).
*/
export function win32FlushInputBuffer() {
if (process.platform !== "win32") return
if (!process.stdin.isTTY) return
if (!load()) return
const handle = k32!.symbols.GetStdHandle(STD_INPUT_HANDLE)
k32!.symbols.FlushConsoleInputBuffer(handle)
}
let unhook: (() => void) | undefined
/**
* Keep ENABLE_PROCESSED_INPUT disabled.
*
* On Windows, Ctrl+C becomes a CTRL_C_EVENT (instead of stdin input) when
* ENABLE_PROCESSED_INPUT is set. Various runtimes can re-apply console modes
* (sometimes on a later tick), and the flag is console-global, not per-process.
*
* We combine:
* - A `setRawMode(...)` hook to re-clear after known raw-mode toggles.
* - A low-frequency poll as a backstop for native/external mode changes.
*/
export function win32InstallCtrlCGuard() {
if (process.platform !== "win32") return
if (!process.stdin.isTTY) return
if (!load()) return
if (unhook) return unhook
const stdin = process.stdin as any
const original = stdin.setRawMode
const handle = k32!.symbols.GetStdHandle(STD_INPUT_HANDLE)
const buf = new Uint32Array(1)
if (k32!.symbols.GetConsoleMode(handle, ptr(buf)) === 0) return
const initial = buf[0]!
const enforce = () => {
if (k32!.symbols.GetConsoleMode(handle, ptr(buf)) === 0) return
const mode = buf[0]!
if ((mode & ENABLE_PROCESSED_INPUT) === 0) return
k32!.symbols.SetConsoleMode(handle, mode & ~ENABLE_PROCESSED_INPUT)
}
// Some runtimes can re-apply console modes on the next tick; enforce twice.
const later = () => {
enforce()
setImmediate(enforce)
}
let wrapped: ((mode: boolean) => unknown) | undefined
if (typeof original === "function") {
wrapped = (mode: boolean) => {
const result = original.call(stdin, mode)
later()
return result
}
stdin.setRawMode = wrapped
}
// Ensure it's cleared immediately too (covers any earlier mode changes).
later()
const interval = setInterval(enforce, 100)
interval.unref()
let done = false
unhook = () => {
if (done) return
done = true
clearInterval(interval)
if (wrapped && stdin.setRawMode === wrapped) {
stdin.setRawMode = original
}
k32!.symbols.SetConsoleMode(handle, initial)
unhook = undefined
}
return unhook
}
+21 -18
View File
@@ -571,25 +571,28 @@ export namespace MCP {
const clientsSnapshot = await clients() const clientsSnapshot = await clients()
const defaultTimeout = cfg.experimental?.mcp_timeout const defaultTimeout = cfg.experimental?.mcp_timeout
for (const [clientName, client] of Object.entries(clientsSnapshot)) { const connectedClients = Object.entries(clientsSnapshot).filter(
// Only include tools from connected MCPs (skip disabled ones) ([clientName]) => s.status[clientName]?.status === "connected",
if (s.status[clientName]?.status !== "connected") { )
continue
}
const toolsResult = await client.listTools().catch((e) => { const toolsResults = await Promise.all(
log.error("failed to get tools", { clientName, error: e.message }) connectedClients.map(async ([clientName, client]) => {
const failedStatus = { const toolsResult = await client.listTools().catch((e) => {
status: "failed" as const, log.error("failed to get tools", { clientName, error: e.message })
error: e instanceof Error ? e.message : String(e), const failedStatus = {
} status: "failed" as const,
s.status[clientName] = failedStatus error: e instanceof Error ? e.message : String(e),
delete s.clients[clientName] }
return undefined s.status[clientName] = failedStatus
}) delete s.clients[clientName]
if (!toolsResult) { return undefined
continue })
} return { clientName, client, toolsResult }
}),
)
for (const { clientName, client, toolsResult } of toolsResults) {
if (!toolsResult) continue
const mcpConfig = config[clientName] const mcpConfig = config[clientName]
const entry = isMcpConfigured(mcpConfig) ? mcpConfig : undefined const entry = isMcpConfigured(mcpConfig) ? mcpConfig : undefined
const timeout = entry?.timeout ?? defaultTimeout const timeout = entry?.timeout ?? defaultTimeout
+1 -1
View File
@@ -82,7 +82,7 @@ export namespace ModelsDev {
status: z.enum(["alpha", "beta", "deprecated"]).optional(), status: z.enum(["alpha", "beta", "deprecated"]).optional(),
options: z.record(z.string(), z.any()), options: z.record(z.string(), z.any()),
headers: z.record(z.string(), z.string()).optional(), headers: z.record(z.string(), z.string()).optional(),
provider: z.object({ npm: z.string() }).optional(), provider: z.object({ npm: z.string(), api: z.string() }).optional(),
variants: z.record(z.string(), z.record(z.string(), z.any())).optional(), variants: z.record(z.string(), z.record(z.string(), z.any())).optional(),
}) })
export type Model = z.infer<typeof Model> export type Model = z.infer<typeof Model>
+2 -2
View File
@@ -648,7 +648,7 @@ export namespace Provider {
family: model.family, family: model.family,
api: { api: {
id: model.id, id: model.id,
url: provider.api!, url: model.provider?.api ?? provider.api!,
npm: model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible", npm: model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible",
}, },
status: model.status ?? "active", status: model.status ?? "active",
@@ -800,7 +800,7 @@ export namespace Provider {
existingModel?.api.npm ?? existingModel?.api.npm ??
modelsDev[providerID]?.npm ?? modelsDev[providerID]?.npm ??
"@ai-sdk/openai-compatible", "@ai-sdk/openai-compatible",
url: provider?.api ?? existingModel?.api.url ?? modelsDev[providerID]?.api, url: model.provider?.api ?? provider?.api ?? existingModel?.api.url ?? modelsDev[providerID]?.api,
}, },
status: model.status ?? existingModel?.status ?? "active", status: model.status ?? existingModel?.status ?? "active",
name, name,
@@ -539,6 +539,8 @@ export namespace ProviderTransform {
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/xai // https://v5.ai-sdk.dev/providers/ai-sdk-providers/xai
case "@ai-sdk/deepinfra": case "@ai-sdk/deepinfra":
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/deepinfra // https://v5.ai-sdk.dev/providers/ai-sdk-providers/deepinfra
case "venice-ai-sdk-provider":
// https://docs.venice.ai/overview/guides/reasoning-models#reasoning-effort
case "@ai-sdk/openai-compatible": case "@ai-sdk/openai-compatible":
return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }])) return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
+3 -1
View File
@@ -41,7 +41,9 @@ export namespace SessionCompaction {
const reserved = const reserved =
config.compaction?.reserved ?? Math.min(COMPACTION_BUFFER, ProviderTransform.maxOutputTokens(input.model)) config.compaction?.reserved ?? Math.min(COMPACTION_BUFFER, ProviderTransform.maxOutputTokens(input.model))
const usable = input.model.limit.input ? input.model.limit.input - reserved : context - reserved const usable = input.model.limit.input
? input.model.limit.input - reserved
: context - ProviderTransform.maxOutputTokens(input.model)
return count >= usable return count >= usable
} }
+2
View File
@@ -44,6 +44,7 @@ export namespace LLM {
small?: boolean small?: boolean
tools: Record<string, Tool> tools: Record<string, Tool>
retries?: number retries?: number
toolChoice?: "auto" | "required" | "none"
} }
export type StreamOutput = StreamTextResult<ToolSet, unknown> export type StreamOutput = StreamTextResult<ToolSet, unknown>
@@ -221,6 +222,7 @@ export namespace LLM {
providerOptions: ProviderTransform.providerOptions(input.model, params.options), providerOptions: ProviderTransform.providerOptions(input.model, params.options),
activeTools: Object.keys(tools).filter((x) => x !== "invalid"), activeTools: Object.keys(tools).filter((x) => x !== "invalid"),
tools, tools,
toolChoice: input.toolChoice,
maxOutputTokens, maxOutputTokens,
abortSignal: input.abort, abortSignal: input.abort,
headers: { headers: {
@@ -15,6 +15,13 @@ import type { Provider } from "@/provider/provider"
export namespace MessageV2 { export namespace MessageV2 {
export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({})) export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
export const AbortedError = NamedError.create("MessageAbortedError", z.object({ message: z.string() })) export const AbortedError = NamedError.create("MessageAbortedError", z.object({ message: z.string() }))
export const StructuredOutputError = NamedError.create(
"StructuredOutputError",
z.object({
message: z.string(),
retries: z.number(),
}),
)
export const AuthError = NamedError.create( export const AuthError = NamedError.create(
"ProviderAuthError", "ProviderAuthError",
z.object({ z.object({
@@ -39,6 +46,29 @@ export namespace MessageV2 {
z.object({ message: z.string(), responseBody: z.string().optional() }), z.object({ message: z.string(), responseBody: z.string().optional() }),
) )
export const OutputFormatText = z
.object({
type: z.literal("text"),
})
.meta({
ref: "OutputFormatText",
})
export const OutputFormatJsonSchema = z
.object({
type: z.literal("json_schema"),
schema: z.record(z.string(), z.any()).meta({ ref: "JSONSchema" }),
retryCount: z.number().int().min(0).default(2),
})
.meta({
ref: "OutputFormatJsonSchema",
})
export const Format = z.discriminatedUnion("type", [OutputFormatText, OutputFormatJsonSchema]).meta({
ref: "OutputFormat",
})
export type OutputFormat = z.infer<typeof Format>
const PartBase = z.object({ const PartBase = z.object({
id: z.string(), id: z.string(),
sessionID: z.string(), sessionID: z.string(),
@@ -313,6 +343,7 @@ export namespace MessageV2 {
time: z.object({ time: z.object({
created: z.number(), created: z.number(),
}), }),
format: Format.optional(),
summary: z summary: z
.object({ .object({
title: z.string().optional(), title: z.string().optional(),
@@ -365,6 +396,7 @@ export namespace MessageV2 {
NamedError.Unknown.Schema, NamedError.Unknown.Schema,
OutputLengthError.Schema, OutputLengthError.Schema,
AbortedError.Schema, AbortedError.Schema,
StructuredOutputError.Schema,
ContextOverflowError.Schema, ContextOverflowError.Schema,
APIError.Schema, APIError.Schema,
]) ])
@@ -393,6 +425,7 @@ export namespace MessageV2 {
write: z.number(), write: z.number(),
}), }),
}), }),
structured: z.any().optional(),
variant: z.string().optional(), variant: z.string().optional(),
finish: z.string().optional(), finish: z.string().optional(),
}).meta({ }).meta({
+95 -4
View File
@@ -50,6 +50,16 @@ import { Truncate } from "@/tool/truncation"
// @ts-ignore // @ts-ignore
globalThis.AI_SDK_LOG_WARNINGS = false globalThis.AI_SDK_LOG_WARNINGS = false
const STRUCTURED_OUTPUT_DESCRIPTION = `Use this tool to return your final response in the requested structured format.
IMPORTANT:
- You MUST call this tool exactly once at the end of your response
- The input must be valid JSON matching the required schema
- Complete all necessary research and tool calls BEFORE calling this tool
- This tool provides your final answer - no further actions are taken after calling it`
const STRUCTURED_OUTPUT_SYSTEM_PROMPT = `IMPORTANT: The user has requested structured output. You MUST use the StructuredOutput tool to provide your final response. Do NOT respond with plain text - you MUST call the StructuredOutput tool with your answer formatted according to the schema.`
export namespace SessionPrompt { export namespace SessionPrompt {
const log = Log.create({ service: "session.prompt" }) const log = Log.create({ service: "session.prompt" })
@@ -96,6 +106,7 @@ export namespace SessionPrompt {
.describe( .describe(
"@deprecated tools and permissions have been merged, you can set permissions on the session itself now", "@deprecated tools and permissions have been merged, you can set permissions on the session itself now",
), ),
format: MessageV2.Format.optional(),
system: z.string().optional(), system: z.string().optional(),
variant: z.string().optional(), variant: z.string().optional(),
parts: z.array( parts: z.array(
@@ -285,6 +296,11 @@ export namespace SessionPrompt {
}) })
// kilocode_change end // kilocode_change end
// Structured output state
// Note: On session resumption, state is reset but outputFormat is preserved
// on the user message and will be retrieved from lastUser below
let structuredOutput: unknown | undefined
let step = 0 let step = 0
const session = await Session.get(sessionID) const session = await Session.get(sessionID)
while (true) { while (true) {
@@ -603,6 +619,16 @@ export namespace SessionPrompt {
messages: msgs, messages: msgs,
}) })
// Inject StructuredOutput tool if JSON schema mode enabled
if (lastUser.format?.type === "json_schema") {
tools["StructuredOutput"] = createStructuredOutputTool({
schema: lastUser.format.schema,
onSuccess(output) {
structuredOutput = output
},
})
}
if (step === 1) { if (step === 1) {
SessionSummary.summarize({ SessionSummary.summarize({
sessionID: sessionID, sessionID: sessionID,
@@ -633,12 +659,19 @@ export namespace SessionPrompt {
await Plugin.trigger("experimental.chat.messages.transform", {}, { messages: sessionMessages }) await Plugin.trigger("experimental.chat.messages.transform", {}, { messages: sessionMessages })
// Build system prompt, adding structured output instruction if needed
const system = [...(await SystemPrompt.environment(model)), ...(await InstructionPrompt.system())]
const format = lastUser.format ?? { type: "text" }
if (format.type === "json_schema") {
system.push(STRUCTURED_OUTPUT_SYSTEM_PROMPT)
}
const result = await processor.process({ const result = await processor.process({
user: lastUser, user: lastUser,
agent, agent,
abort, abort,
sessionID, sessionID,
system: [...(await SystemPrompt.environment(model)), ...(await InstructionPrompt.system())], system,
messages: [ messages: [
...MessageV2.toModelMessages(sessionMessages, model), ...MessageV2.toModelMessages(sessionMessages, model),
...(isLastStep ...(isLastStep
@@ -652,7 +685,33 @@ export namespace SessionPrompt {
], ],
tools, tools,
model, model,
toolChoice: format.type === "json_schema" ? "required" : undefined,
}) })
// If structured output was captured, save it and exit immediately
// This takes priority because the StructuredOutput tool was called successfully
if (structuredOutput !== undefined) {
processor.message.structured = structuredOutput
processor.message.finish = processor.message.finish ?? "stop"
await Session.updateMessage(processor.message)
break
}
// Check if model finished (finish reason is not "tool-calls" or "unknown")
const modelFinished = processor.message.finish && !["tool-calls", "unknown"].includes(processor.message.finish)
if (modelFinished && !processor.message.error) {
if (format.type === "json_schema") {
// Model stopped without calling StructuredOutput tool
processor.message.error = new MessageV2.StructuredOutputError({
message: "Model did not produce structured output",
retries: 0,
}).toObject()
await Session.updateMessage(processor.message)
break
}
}
// kilocode_change start // kilocode_change start
if (result === "stop") { if (result === "stop") {
if (abort.aborted || processor.message.error?.name === "MessageAbortedError") closeReason = "interrupted" if (abort.aborted || processor.message.error?.name === "MessageAbortedError") closeReason = "interrupted"
@@ -692,7 +751,8 @@ export namespace SessionPrompt {
return Provider.defaultModel() return Provider.defaultModel()
} }
async function resolveTools(input: { /** @internal Exported for testing */
export async function resolveTools(input: {
agent: Agent.Info agent: Agent.Info
model: Provider.Model model: Provider.Model
session: Session.Info session: Session.Info
@@ -872,6 +932,36 @@ export namespace SessionPrompt {
return tools return tools
} }
/** @internal Exported for testing */
export function createStructuredOutputTool(input: {
schema: Record<string, any>
onSuccess: (output: unknown) => void
}): AITool {
// Remove $schema property if present (not needed for tool input)
const { $schema, ...toolSchema } = input.schema
return tool({
id: "StructuredOutput" as any,
description: STRUCTURED_OUTPUT_DESCRIPTION,
inputSchema: jsonSchema(toolSchema as any),
async execute(args) {
// AI SDK validates args against inputSchema before calling execute()
input.onSuccess(args)
return {
output: "Structured output captured successfully.",
title: "Structured Output",
metadata: { valid: true },
}
},
toModelOutput(result) {
return {
type: "text",
value: result.output,
}
},
})
}
async function createUserMessage(input: PromptInput) { async function createUserMessage(input: PromptInput) {
const agent = await Agent.get(input.agent ?? (await Agent.defaultAgent())) const agent = await Agent.get(input.agent ?? (await Agent.defaultAgent()))
@@ -893,6 +983,7 @@ export namespace SessionPrompt {
agent: agent.name, agent: agent.name,
model, model,
system: input.system, system: input.system,
format: input.format,
variant, variant,
} }
using _ = defer(() => InstructionPrompt.clear(info.id)) using _ = defer(() => InstructionPrompt.clear(info.id))
@@ -1045,9 +1136,9 @@ export namespace SessionPrompt {
} }
} }
} }
offset = Math.max(start - 1, 0) offset = Math.max(start, 1)
if (end) { if (end) {
limit = end - offset limit = end - (offset - 1)
} }
} }
const args = { filePath: filepath, offset, limit } const args = { filePath: filepath, offset, limit }
+2 -2
View File
@@ -2,9 +2,9 @@ Performs exact string replacements in files.
Usage: Usage:
- You must use your `Read` tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file. - You must use your `Read` tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file.
- When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: spaces + line number + tab. Everything after that tab is the actual file content to match. Never include any part of the line number prefix in the oldString or newString. - When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: line number + colon + space (e.g., `1: `). Everything after that space is the actual file content to match. Never include any part of the line number prefix in the oldString or newString.
- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required. - ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked. - Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.
- The edit will FAIL if `oldString` is not found in the file with an error "oldString not found in content". - The edit will FAIL if `oldString` is not found in the file with an error "oldString not found in content".
- The edit will FAIL if `oldString` is found multiple times in the file with an error "oldString found multiple times and requires more code context to uniquely identify the intended match". Either provide a larger string with more surrounding context to make it unique or use `replaceAll` to change every instance of `oldString`. - The edit will FAIL if `oldString` is found multiple times in the file with an error "Found multiple matches for oldString. Provide more surrounding lines in oldString to identify the correct match." Either provide a larger string with more surrounding context to make it unique or use `replaceAll` to change every instance of `oldString`.
- Use `replaceAll` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance. - Use `replaceAll` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.
+63 -13
View File
@@ -17,19 +17,26 @@ const MAX_BYTES = 50 * 1024
export const ReadTool = Tool.define("read", { export const ReadTool = Tool.define("read", {
description: DESCRIPTION, description: DESCRIPTION,
parameters: z.object({ parameters: z.object({
filePath: z.string().describe("The path to the file to read"), filePath: z.string().describe("The absolute path to the file or directory to read"),
offset: z.coerce.number().describe("The line number to start reading from (0-based)").optional(), offset: z.coerce.number().describe("The line number to start reading from (1-indexed)").optional(),
limit: z.coerce.number().describe("The number of lines to read (defaults to 2000)").optional(), limit: z.coerce.number().describe("The maximum number of lines to read (defaults to 2000)").optional(),
}), }),
async execute(params, ctx) { async execute(params, ctx) {
if (params.offset !== undefined && params.offset < 1) {
throw new Error("offset must be greater than or equal to 1")
}
let filepath = params.filePath let filepath = params.filePath
if (!path.isAbsolute(filepath)) { if (!path.isAbsolute(filepath)) {
filepath = path.resolve(Instance.directory, filepath) filepath = path.resolve(Instance.directory, filepath)
} }
const title = path.relative(Instance.worktree, filepath) const title = path.relative(Instance.worktree, filepath)
const file = Bun.file(filepath)
const stat = await file.stat().catch(() => undefined)
await assertExternalDirectory(ctx, filepath, { await assertExternalDirectory(ctx, filepath, {
bypass: Boolean(ctx.extra?.["bypassCwdCheck"]), bypass: Boolean(ctx.extra?.["bypassCwdCheck"]),
kind: stat?.isDirectory() ? "directory" : "file",
}) })
await ctx.ask({ await ctx.ask({
@@ -39,8 +46,7 @@ export const ReadTool = Tool.define("read", {
metadata: {}, metadata: {},
}) })
const file = Bun.file(filepath) if (!stat) {
if (!(await file.exists())) {
const dir = path.dirname(filepath) const dir = path.dirname(filepath)
const base = path.basename(filepath) const base = path.basename(filepath)
@@ -60,6 +66,48 @@ export const ReadTool = Tool.define("read", {
throw new Error(`File not found: ${filepath}`) throw new Error(`File not found: ${filepath}`)
} }
if (stat.isDirectory()) {
const dirents = await fs.promises.readdir(filepath, { withFileTypes: true })
const entries = await Promise.all(
dirents.map(async (dirent) => {
if (dirent.isDirectory()) return dirent.name + "/"
if (dirent.isSymbolicLink()) {
const target = await fs.promises.stat(path.join(filepath, dirent.name)).catch(() => undefined)
if (target?.isDirectory()) return dirent.name + "/"
}
return dirent.name
}),
)
entries.sort((a, b) => a.localeCompare(b))
const limit = params.limit ?? DEFAULT_READ_LIMIT
const offset = params.offset ?? 1
const start = offset - 1
const sliced = entries.slice(start, start + limit)
const truncated = start + sliced.length < entries.length
const output = [
`<path>${filepath}</path>`,
`<type>directory</type>`,
`<entries>`,
sliced.join("\n"),
truncated
? `\n(Showing ${sliced.length} of ${entries.length} entries. Use 'offset' parameter to read beyond entry ${offset + sliced.length})`
: `\n(${entries.length} entries)`,
`</entries>`,
].join("\n")
return {
title,
output,
metadata: {
preview: sliced.slice(0, 20).join("\n"),
truncated,
loaded: [] as string[],
},
}
}
const instructions = await InstructionPrompt.resolve(ctx.messages, filepath, ctx.messageID) const instructions = await InstructionPrompt.resolve(ctx.messages, filepath, ctx.messageID)
// Exclude SVG (XML-based) and vnd.fastbidsheet (.fbs extension, commonly FlatBuffers schema files) // Exclude SVG (XML-based) and vnd.fastbidsheet (.fbs extension, commonly FlatBuffers schema files)
@@ -75,7 +123,7 @@ export const ReadTool = Tool.define("read", {
metadata: { metadata: {
preview: msg, preview: msg,
truncated: false, truncated: false,
...(instructions.length > 0 && { loaded: instructions.map((i) => i.filepath) }), loaded: instructions.map((i) => i.filepath),
}, },
attachments: [ attachments: [
{ {
@@ -94,13 +142,15 @@ export const ReadTool = Tool.define("read", {
if (isBinary) throw new Error(`Cannot read binary file: ${filepath}`) if (isBinary) throw new Error(`Cannot read binary file: ${filepath}`)
const limit = params.limit ?? DEFAULT_READ_LIMIT const limit = params.limit ?? DEFAULT_READ_LIMIT
const offset = params.offset || 0 const offset = params.offset ?? 1
const start = offset - 1
const lines = await file.text().then((text) => text.split("\n")) const lines = await file.text().then((text) => text.split("\n"))
if (start >= lines.length) throw new Error(`Offset ${offset} is out of range for this file (${lines.length} lines)`)
const raw: string[] = [] const raw: string[] = []
let bytes = 0 let bytes = 0
let truncatedByBytes = false let truncatedByBytes = false
for (let i = offset; i < Math.min(lines.length, offset + limit); i++) { for (let i = start; i < Math.min(lines.length, start + limit); i++) {
const line = lines[i].length > MAX_LINE_LENGTH ? lines[i].substring(0, MAX_LINE_LENGTH) + "..." : lines[i] const line = lines[i].length > MAX_LINE_LENGTH ? lines[i].substring(0, MAX_LINE_LENGTH) + "..." : lines[i]
const size = Buffer.byteLength(line, "utf-8") + (raw.length > 0 ? 1 : 0) const size = Buffer.byteLength(line, "utf-8") + (raw.length > 0 ? 1 : 0)
if (bytes + size > MAX_BYTES) { if (bytes + size > MAX_BYTES) {
@@ -112,15 +162,15 @@ export const ReadTool = Tool.define("read", {
} }
const content = raw.map((line, index) => { const content = raw.map((line, index) => {
return `${(index + offset + 1).toString().padStart(5, "0")}| ${line}` return `${index + offset}: ${line}`
}) })
const preview = raw.slice(0, 20).join("\n") const preview = raw.slice(0, 20).join("\n")
let output = "<file>\n" let output = [`<path>${filepath}</path>`, `<type>file</type>`, "<content>"].join("\n")
output += content.join("\n") output += content.join("\n")
const totalLines = lines.length const totalLines = lines.length
const lastReadLine = offset + raw.length const lastReadLine = offset + raw.length - 1
const hasMoreLines = totalLines > lastReadLine const hasMoreLines = totalLines > lastReadLine
const truncated = hasMoreLines || truncatedByBytes const truncated = hasMoreLines || truncatedByBytes
@@ -131,7 +181,7 @@ export const ReadTool = Tool.define("read", {
} else { } else {
output += `\n\n(End of file - total ${totalLines} lines)` output += `\n\n(End of file - total ${totalLines} lines)`
} }
output += "\n</file>" output += "\n</content>"
// just warms the lsp client // just warms the lsp client
LSP.touchFile(filepath, false) LSP.touchFile(filepath, false)
@@ -147,7 +197,7 @@ export const ReadTool = Tool.define("read", {
metadata: { metadata: {
preview, preview,
truncated, truncated,
...(instructions.length > 0 && { loaded: instructions.map((i) => i.filepath) }), loaded: instructions.map((i) => i.filepath),
}, },
} }
}, },
+12 -10
View File
@@ -1,12 +1,14 @@
Reads a file from the local filesystem. You can access any file directly by using this tool. Read a file or directory from the local filesystem. If the path does not exist, an error is returned.
Assume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.
Usage: Usage:
- The filePath parameter must be an absolute path, not a relative path - The filePath parameter should be an absolute path.
- By default, it reads up to 2000 lines starting from the beginning of the file - By default, this tool returns up to 2000 lines from the start of the file.
- You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters - The offset parameter is the line number to start from (1-indexed).
- Any lines longer than 2000 characters will be truncated - To read later sections, call this tool again with a larger offset.
- Results are returned using cat -n format, with line numbers starting at 1 - Use the grep tool to find specific content in large files or files with long lines.
- You have the capability to call multiple tools in a single response. It is always better to speculatively read multiple files as a batch that are potentially useful. - If you are unsure of the correct file path, use the glob tool to look up filenames by glob pattern.
- If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents. - Contents are returned with each line prefixed by its line number as `<line>: <content>`. For example, if a file has contents "foo\n", you will receive "1: foo\n". For directories, entries are returned one per line (without line numbers) with a trailing `/` for subdirectories.
- You can read image files using this tool. - Any line longer than 2000 characters is truncated.
- Call this tool in parallel when you know there are multiple files you want to read.
- Avoid tiny repeated slices (30 line chunks). If you need more context, read a larger window.
- This tool can read image files and PDFs and return them as file attachments.
@@ -0,0 +1,233 @@
import { describe, expect, test } from "bun:test"
import path from "path"
import { Session } from "../../src/session"
import { SessionPrompt } from "../../src/session/prompt"
import { Log } from "../../src/util/log"
import { Instance } from "../../src/project/instance"
import { MessageV2 } from "../../src/session/message-v2"
const projectRoot = path.join(__dirname, "../..")
Log.init({ print: false })
// Skip tests if no API key is available
const hasApiKey = !!process.env.ANTHROPIC_API_KEY
// Helper to run test within Instance context
async function withInstance<T>(fn: () => Promise<T>): Promise<T> {
return Instance.provide({
directory: projectRoot,
fn,
})
}
describe("StructuredOutput Integration", () => {
test.skipIf(!hasApiKey)(
"produces structured output with simple schema",
async () => {
await withInstance(async () => {
const session = await Session.create({ title: "Structured Output Test" })
const result = await SessionPrompt.prompt({
sessionID: session.id,
parts: [
{
type: "text",
text: "What is 2 + 2? Provide a simple answer.",
},
],
format: {
type: "json_schema",
schema: {
type: "object",
properties: {
answer: { type: "number", description: "The numerical answer" },
explanation: { type: "string", description: "Brief explanation" },
},
required: ["answer"],
},
retryCount: 0,
},
})
// Verify structured output was captured (only on assistant messages)
expect(result.info.role).toBe("assistant")
if (result.info.role === "assistant") {
expect(result.info.structured).toBeDefined()
expect(typeof result.info.structured).toBe("object")
const output = result.info.structured as any
expect(output.answer).toBe(4)
// Verify no error was set
expect(result.info.error).toBeUndefined()
}
// Clean up
// Note: Not removing session to avoid race with background SessionSummary.summarize
})
},
60000,
)
test.skipIf(!hasApiKey)(
"produces structured output with nested objects",
async () => {
await withInstance(async () => {
const session = await Session.create({ title: "Nested Schema Test" })
const result = await SessionPrompt.prompt({
sessionID: session.id,
parts: [
{
type: "text",
text: "Tell me about Anthropic company in a structured format.",
},
],
format: {
type: "json_schema",
schema: {
type: "object",
properties: {
company: {
type: "object",
properties: {
name: { type: "string" },
founded: { type: "number" },
},
required: ["name", "founded"],
},
products: {
type: "array",
items: { type: "string" },
},
},
required: ["company"],
},
retryCount: 0,
},
})
// Verify structured output was captured (only on assistant messages)
expect(result.info.role).toBe("assistant")
if (result.info.role === "assistant") {
expect(result.info.structured).toBeDefined()
const output = result.info.structured as any
expect(output.company).toBeDefined()
expect(output.company.name).toBe("Anthropic")
expect(typeof output.company.founded).toBe("number")
if (output.products) {
expect(Array.isArray(output.products)).toBe(true)
}
// Verify no error was set
expect(result.info.error).toBeUndefined()
}
// Clean up
// Note: Not removing session to avoid race with background SessionSummary.summarize
})
},
60000,
)
test.skipIf(!hasApiKey)(
"works with text outputFormat (default)",
async () => {
await withInstance(async () => {
const session = await Session.create({ title: "Text Output Test" })
const result = await SessionPrompt.prompt({
sessionID: session.id,
parts: [
{
type: "text",
text: "Say hello.",
},
],
format: {
type: "text",
},
})
// Verify no structured output (text mode) and no error
expect(result.info.role).toBe("assistant")
if (result.info.role === "assistant") {
expect(result.info.structured).toBeUndefined()
expect(result.info.error).toBeUndefined()
}
// Verify we got a response with parts
expect(result.parts.length).toBeGreaterThan(0)
// Clean up
// Note: Not removing session to avoid race with background SessionSummary.summarize
})
},
60000,
)
test.skipIf(!hasApiKey)(
"stores outputFormat on user message",
async () => {
await withInstance(async () => {
const session = await Session.create({ title: "OutputFormat Storage Test" })
await SessionPrompt.prompt({
sessionID: session.id,
parts: [
{
type: "text",
text: "What is 1 + 1?",
},
],
format: {
type: "json_schema",
schema: {
type: "object",
properties: {
result: { type: "number" },
},
required: ["result"],
},
retryCount: 3,
},
})
// Get all messages from session
const messages = await Session.messages({ sessionID: session.id })
const userMessage = messages.find((m) => m.info.role === "user")
// Verify outputFormat was stored on user message
expect(userMessage).toBeDefined()
if (userMessage?.info.role === "user") {
expect(userMessage.info.format).toBeDefined()
expect(userMessage.info.format?.type).toBe("json_schema")
if (userMessage.info.format?.type === "json_schema") {
expect(userMessage.info.format.retryCount).toBe(3)
}
}
// Clean up
// Note: Not removing session to avoid race with background SessionSummary.summarize
})
},
60000,
)
test("unit test: StructuredOutputError is properly structured", () => {
const error = new MessageV2.StructuredOutputError({
message: "Failed to produce valid structured output after 3 attempts",
retries: 3,
})
expect(error.name).toBe("StructuredOutputError")
expect(error.data.message).toContain("3 attempts")
expect(error.data.retries).toBe(3)
const obj = error.toObject()
expect(obj.name).toBe("StructuredOutputError")
expect(obj.data.retries).toBe(3)
})
})
@@ -0,0 +1,385 @@
import { describe, expect, test } from "bun:test"
import { MessageV2 } from "../../src/session/message-v2"
import { SessionPrompt } from "../../src/session/prompt"
describe("structured-output.OutputFormat", () => {
test("parses text format", () => {
const result = MessageV2.Format.safeParse({ type: "text" })
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.type).toBe("text")
}
})
test("parses json_schema format with defaults", () => {
const result = MessageV2.Format.safeParse({
type: "json_schema",
schema: { type: "object", properties: { name: { type: "string" } } },
})
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.type).toBe("json_schema")
if (result.data.type === "json_schema") {
expect(result.data.retryCount).toBe(2) // default value
}
}
})
test("parses json_schema format with custom retryCount", () => {
const result = MessageV2.Format.safeParse({
type: "json_schema",
schema: { type: "object" },
retryCount: 5,
})
expect(result.success).toBe(true)
if (result.success && result.data.type === "json_schema") {
expect(result.data.retryCount).toBe(5)
}
})
test("rejects invalid type", () => {
const result = MessageV2.Format.safeParse({ type: "invalid" })
expect(result.success).toBe(false)
})
test("rejects json_schema without schema", () => {
const result = MessageV2.Format.safeParse({ type: "json_schema" })
expect(result.success).toBe(false)
})
test("rejects negative retryCount", () => {
const result = MessageV2.Format.safeParse({
type: "json_schema",
schema: { type: "object" },
retryCount: -1,
})
expect(result.success).toBe(false)
})
})
describe("structured-output.StructuredOutputError", () => {
test("creates error with message and retries", () => {
const error = new MessageV2.StructuredOutputError({
message: "Failed to validate",
retries: 3,
})
expect(error.name).toBe("StructuredOutputError")
expect(error.data.message).toBe("Failed to validate")
expect(error.data.retries).toBe(3)
})
test("converts to object correctly", () => {
const error = new MessageV2.StructuredOutputError({
message: "Test error",
retries: 2,
})
const obj = error.toObject()
expect(obj.name).toBe("StructuredOutputError")
expect(obj.data.message).toBe("Test error")
expect(obj.data.retries).toBe(2)
})
test("isInstance correctly identifies error", () => {
const error = new MessageV2.StructuredOutputError({
message: "Test",
retries: 1,
})
expect(MessageV2.StructuredOutputError.isInstance(error)).toBe(true)
expect(MessageV2.StructuredOutputError.isInstance({ name: "other" })).toBe(false)
})
})
describe("structured-output.UserMessage", () => {
test("user message accepts outputFormat", () => {
const result = MessageV2.User.safeParse({
id: "test-id",
sessionID: "test-session",
role: "user",
time: { created: Date.now() },
agent: "default",
model: { providerID: "anthropic", modelID: "claude-3" },
outputFormat: {
type: "json_schema",
schema: { type: "object" },
},
})
expect(result.success).toBe(true)
})
test("user message works without outputFormat (optional)", () => {
const result = MessageV2.User.safeParse({
id: "test-id",
sessionID: "test-session",
role: "user",
time: { created: Date.now() },
agent: "default",
model: { providerID: "anthropic", modelID: "claude-3" },
})
expect(result.success).toBe(true)
})
})
describe("structured-output.AssistantMessage", () => {
const baseAssistantMessage = {
id: "test-id",
sessionID: "test-session",
role: "assistant" as const,
parentID: "parent-id",
modelID: "claude-3",
providerID: "anthropic",
mode: "default",
agent: "default",
path: { cwd: "/test", root: "/test" },
cost: 0.001,
tokens: { input: 100, output: 50, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: Date.now() },
}
test("assistant message accepts structured", () => {
const result = MessageV2.Assistant.safeParse({
...baseAssistantMessage,
structured: { company: "Anthropic", founded: 2021 },
})
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.structured).toEqual({ company: "Anthropic", founded: 2021 })
}
})
test("assistant message works without structured_output (optional)", () => {
const result = MessageV2.Assistant.safeParse(baseAssistantMessage)
expect(result.success).toBe(true)
})
})
describe("structured-output.createStructuredOutputTool", () => {
test("creates tool with correct id", () => {
const tool = SessionPrompt.createStructuredOutputTool({
schema: { type: "object", properties: { name: { type: "string" } } },
onSuccess: () => {},
})
// AI SDK tool type doesn't expose id, but we set it internally
expect((tool as any).id).toBe("StructuredOutput")
})
test("creates tool with description", () => {
const tool = SessionPrompt.createStructuredOutputTool({
schema: { type: "object" },
onSuccess: () => {},
})
expect(tool.description).toContain("structured format")
})
test("creates tool with schema as inputSchema", () => {
const schema = {
type: "object",
properties: {
company: { type: "string" },
founded: { type: "number" },
},
required: ["company"],
}
const tool = SessionPrompt.createStructuredOutputTool({
schema,
onSuccess: () => {},
})
// AI SDK wraps schema in { jsonSchema: {...} }
expect(tool.inputSchema).toBeDefined()
const inputSchema = tool.inputSchema as any
expect(inputSchema.jsonSchema?.properties?.company).toBeDefined()
expect(inputSchema.jsonSchema?.properties?.founded).toBeDefined()
})
test("strips $schema property from inputSchema", () => {
const schema = {
$schema: "http://json-schema.org/draft-07/schema#",
type: "object",
properties: { name: { type: "string" } },
}
const tool = SessionPrompt.createStructuredOutputTool({
schema,
onSuccess: () => {},
})
// AI SDK wraps schema in { jsonSchema: {...} }
const inputSchema = tool.inputSchema as any
expect(inputSchema.jsonSchema?.$schema).toBeUndefined()
})
test("execute calls onSuccess with valid args", async () => {
let capturedOutput: unknown
const tool = SessionPrompt.createStructuredOutputTool({
schema: { type: "object", properties: { name: { type: "string" } } },
onSuccess: (output) => {
capturedOutput = output
},
})
expect(tool.execute).toBeDefined()
const testArgs = { name: "Test Company" }
const result = await tool.execute!(testArgs, {
toolCallId: "test-call-id",
messages: [],
abortSignal: undefined as any,
})
expect(capturedOutput).toEqual(testArgs)
expect(result.output).toBe("Structured output captured successfully.")
expect(result.metadata.valid).toBe(true)
})
test("AI SDK validates schema before execute - missing required field", async () => {
// Note: The AI SDK validates the input against the schema BEFORE calling execute()
// So invalid inputs never reach the tool's execute function
// This test documents the expected schema behavior
const tool = SessionPrompt.createStructuredOutputTool({
schema: {
type: "object",
properties: {
name: { type: "string" },
age: { type: "number" },
},
required: ["name", "age"],
},
onSuccess: () => {},
})
// The schema requires both 'name' and 'age'
expect(tool.inputSchema).toBeDefined()
const inputSchema = tool.inputSchema as any
expect(inputSchema.jsonSchema?.required).toContain("name")
expect(inputSchema.jsonSchema?.required).toContain("age")
})
test("AI SDK validates schema types before execute - wrong type", async () => {
// Note: The AI SDK validates the input against the schema BEFORE calling execute()
// So invalid inputs never reach the tool's execute function
// This test documents the expected schema behavior
const tool = SessionPrompt.createStructuredOutputTool({
schema: {
type: "object",
properties: {
count: { type: "number" },
},
required: ["count"],
},
onSuccess: () => {},
})
// The schema defines 'count' as a number
expect(tool.inputSchema).toBeDefined()
const inputSchema = tool.inputSchema as any
expect(inputSchema.jsonSchema?.properties?.count?.type).toBe("number")
})
test("execute handles nested objects", async () => {
let capturedOutput: unknown
const tool = SessionPrompt.createStructuredOutputTool({
schema: {
type: "object",
properties: {
user: {
type: "object",
properties: {
name: { type: "string" },
email: { type: "string" },
},
required: ["name"],
},
},
required: ["user"],
},
onSuccess: (output) => {
capturedOutput = output
},
})
// Valid nested object - AI SDK validates before calling execute()
const validResult = await tool.execute!(
{ user: { name: "John", email: "john@test.com" } },
{
toolCallId: "test-call-id",
messages: [],
abortSignal: undefined as any,
},
)
expect(capturedOutput).toEqual({ user: { name: "John", email: "john@test.com" } })
expect(validResult.metadata.valid).toBe(true)
// Verify schema has correct nested structure
const inputSchema = tool.inputSchema as any
expect(inputSchema.jsonSchema?.properties?.user?.type).toBe("object")
expect(inputSchema.jsonSchema?.properties?.user?.properties?.name?.type).toBe("string")
expect(inputSchema.jsonSchema?.properties?.user?.required).toContain("name")
})
test("execute handles arrays", async () => {
let capturedOutput: unknown
const tool = SessionPrompt.createStructuredOutputTool({
schema: {
type: "object",
properties: {
tags: {
type: "array",
items: { type: "string" },
},
},
required: ["tags"],
},
onSuccess: (output) => {
capturedOutput = output
},
})
// Valid array - AI SDK validates before calling execute()
const validResult = await tool.execute!(
{ tags: ["a", "b", "c"] },
{
toolCallId: "test-call-id",
messages: [],
abortSignal: undefined as any,
},
)
expect(capturedOutput).toEqual({ tags: ["a", "b", "c"] })
expect(validResult.metadata.valid).toBe(true)
// Verify schema has correct array structure
const inputSchema = tool.inputSchema as any
expect(inputSchema.jsonSchema?.properties?.tags?.type).toBe("array")
expect(inputSchema.jsonSchema?.properties?.tags?.items?.type).toBe("string")
})
test("toModelOutput returns text value", () => {
const tool = SessionPrompt.createStructuredOutputTool({
schema: { type: "object" },
onSuccess: () => {},
})
expect(tool.toModelOutput).toBeDefined()
const modelOutput = tool.toModelOutput!({
output: "Test output",
title: "Test",
metadata: { valid: true },
})
expect(modelOutput.type).toBe("text")
expect(modelOutput.value).toBe("Test output")
})
// Note: Retry behavior is handled by the AI SDK and the prompt loop, not the tool itself
// The tool simply calls onSuccess when execute() is called with valid args
// See prompt.ts loop() for actual retry logic
})
+64 -1
View File
@@ -78,6 +78,32 @@ describe("tool.read external_directory permission", () => {
}) })
}) })
test("asks for directory-scoped external_directory permission when reading external directory", async () => {
await using outerTmp = await tmpdir({
init: async (dir) => {
await Bun.write(path.join(dir, "external", "a.txt"), "a")
},
})
await using tmp = await tmpdir({ git: true })
await Instance.provide({
directory: tmp.path,
fn: async () => {
const read = await ReadTool.init()
const requests: Array<Omit<PermissionNext.Request, "id" | "sessionID" | "tool">> = []
const testCtx = {
...ctx,
ask: async (req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) => {
requests.push(req)
},
}
await read.execute({ filePath: path.join(outerTmp.path, "external") }, testCtx)
const extDirReq = requests.find((r) => r.permission === "external_directory")
expect(extDirReq).toBeDefined()
expect(extDirReq!.patterns).toContain(path.join(outerTmp.path, "external", "*"))
},
})
})
test("asks for external_directory permission when reading relative path outside project", async () => { test("asks for external_directory permission when reading relative path outside project", async () => {
await using tmp = await tmpdir({ git: true }) await using tmp = await tmpdir({ git: true })
await Instance.provide({ await Instance.provide({
@@ -234,7 +260,7 @@ describe("tool.read truncation", () => {
test("respects offset parameter", async () => { test("respects offset parameter", async () => {
await using tmp = await tmpdir({ await using tmp = await tmpdir({
init: async (dir) => { init: async (dir) => {
const lines = Array.from({ length: 20 }, (_, i) => `line${i}`).join("\n") const lines = Array.from({ length: 20 }, (_, i) => `line${i + 1}`).join("\n")
await Bun.write(path.join(dir, "offset.txt"), lines) await Bun.write(path.join(dir, "offset.txt"), lines)
}, },
}) })
@@ -251,6 +277,43 @@ describe("tool.read truncation", () => {
}) })
}) })
test("throws when offset is beyond end of file", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
const lines = Array.from({ length: 3 }, (_, i) => `line${i + 1}`).join("\n")
await Bun.write(path.join(dir, "short.txt"), lines)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const read = await ReadTool.init()
await expect(
read.execute({ filePath: path.join(tmp.path, "short.txt"), offset: 4, limit: 5 }, ctx),
).rejects.toThrow("Offset 4 is out of range for this file (3 lines)")
},
})
})
test("does not mark final directory page as truncated", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Promise.all(
Array.from({ length: 10 }, (_, i) => Bun.write(path.join(dir, "dir", `file-${i + 1}.txt`), `line${i}`)),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const read = await ReadTool.init()
const result = await read.execute({ filePath: path.join(tmp.path, "dir"), offset: 6, limit: 5 }, ctx)
expect(result.metadata.truncated).toBe(false)
expect(result.output).not.toContain("Showing 5 of 10 entries")
},
})
})
test("truncates long lines", async () => { test("truncates long lines", async () => {
await using tmp = await tmpdir({ await using tmp = await tmpdir({
init: async (dir) => { init: async (dir) => {
+5
View File
@@ -65,6 +65,7 @@ import type {
McpLocalConfig, McpLocalConfig,
McpRemoteConfig, McpRemoteConfig,
McpStatusResponses, McpStatusResponses,
OutputFormat,
Part as Part2, Part as Part2,
PartDeleteErrors, PartDeleteErrors,
PartDeleteResponses, PartDeleteResponses,
@@ -1481,6 +1482,7 @@ export class Session extends HeyApiClient {
tools?: { tools?: {
[key: string]: boolean [key: string]: boolean
} }
format?: OutputFormat
system?: string system?: string
variant?: string variant?: string
parts?: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput> parts?: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
@@ -1499,6 +1501,7 @@ export class Session extends HeyApiClient {
{ in: "body", key: "agent" }, { in: "body", key: "agent" },
{ in: "body", key: "noReply" }, { in: "body", key: "noReply" },
{ in: "body", key: "tools" }, { in: "body", key: "tools" },
{ in: "body", key: "format" },
{ in: "body", key: "system" }, { in: "body", key: "system" },
{ in: "body", key: "variant" }, { in: "body", key: "variant" },
{ in: "body", key: "parts" }, { in: "body", key: "parts" },
@@ -1569,6 +1572,7 @@ export class Session extends HeyApiClient {
tools?: { tools?: {
[key: string]: boolean [key: string]: boolean
} }
format?: OutputFormat
system?: string system?: string
variant?: string variant?: string
parts?: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput> parts?: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
@@ -1587,6 +1591,7 @@ export class Session extends HeyApiClient {
{ in: "body", key: "agent" }, { in: "body", key: "agent" },
{ in: "body", key: "noReply" }, { in: "body", key: "noReply" },
{ in: "body", key: "tools" }, { in: "body", key: "tools" },
{ in: "body", key: "format" },
{ in: "body", key: "system" }, { in: "body", key: "system" },
{ in: "body", key: "variant" }, { in: "body", key: "variant" },
{ in: "body", key: "parts" }, { in: "body", key: "parts" },
+32
View File
@@ -90,6 +90,22 @@ export type EventFileEdited = {
} }
} }
export type OutputFormatText = {
type: "text"
}
export type JsonSchema = {
[key: string]: unknown
}
export type OutputFormatJsonSchema = {
type: "json_schema"
schema: JsonSchema
retryCount?: number
}
export type OutputFormat = OutputFormatText | OutputFormatJsonSchema
export type FileDiff = { export type FileDiff = {
file: string file: string
before: string before: string
@@ -106,6 +122,7 @@ export type UserMessage = {
time: { time: {
created: number created: number
} }
format?: OutputFormat
summary?: { summary?: {
title?: string title?: string
body?: string body?: string
@@ -152,6 +169,14 @@ export type MessageAbortedError = {
} }
} }
export type StructuredOutputError = {
name: "StructuredOutputError"
data: {
message: string
retries: number
}
}
export type ContextOverflowError = { export type ContextOverflowError = {
name: "ContextOverflowError" name: "ContextOverflowError"
data: { data: {
@@ -189,6 +214,7 @@ export type AssistantMessage = {
| UnknownError | UnknownError
| MessageOutputLengthError | MessageOutputLengthError
| MessageAbortedError | MessageAbortedError
| StructuredOutputError
| ContextOverflowError | ContextOverflowError
| ApiError | ApiError
parentID: string parentID: string
@@ -212,6 +238,7 @@ export type AssistantMessage = {
write: number write: number
} }
} }
structured?: unknown
variant?: string variant?: string
finish?: string finish?: string
} }
@@ -845,6 +872,7 @@ export type EventSessionError = {
| UnknownError | UnknownError
| MessageOutputLengthError | MessageOutputLengthError
| MessageAbortedError | MessageAbortedError
| StructuredOutputError
| ContextOverflowError | ContextOverflowError
| ApiError | ApiError
} }
@@ -1546,6 +1574,7 @@ export type ProviderConfig = {
} }
provider?: { provider?: {
npm: string npm: string
api: string
} }
/** /**
* Variant-specific configuration * Variant-specific configuration
@@ -3430,6 +3459,7 @@ export type SessionPromptData = {
tools?: { tools?: {
[key: string]: boolean [key: string]: boolean
} }
format?: OutputFormat
system?: string system?: string
variant?: string variant?: string
parts: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput> parts: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
@@ -3617,6 +3647,7 @@ export type SessionPromptAsyncData = {
tools?: { tools?: {
[key: string]: boolean [key: string]: boolean
} }
format?: OutputFormat
system?: string system?: string
variant?: string variant?: string
parts: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput> parts: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
@@ -4066,6 +4097,7 @@ export type ProviderListResponses = {
} }
provider?: { provider?: {
npm: string npm: string
api: string
} }
variants?: { variants?: {
[key: string]: { [key: string]: {
+92 -2
View File
@@ -2420,6 +2420,9 @@
"type": "boolean" "type": "boolean"
} }
}, },
"format": {
"$ref": "#/components/schemas/OutputFormat"
},
"system": { "system": {
"type": "string" "type": "string"
}, },
@@ -2796,6 +2799,9 @@
"type": "boolean" "type": "boolean"
} }
}, },
"format": {
"$ref": "#/components/schemas/OutputFormat"
},
"system": { "system": {
"type": "string" "type": "string"
}, },
@@ -3790,9 +3796,12 @@
"properties": { "properties": {
"npm": { "npm": {
"type": "string" "type": "string"
},
"api": {
"type": "string"
} }
}, },
"required": ["npm"] "required": ["npm", "api"]
}, },
"variants": { "variants": {
"type": "object", "type": "object",
@@ -6385,6 +6394,52 @@
}, },
"required": ["type", "properties"] "required": ["type", "properties"]
}, },
"OutputFormatText": {
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "text"
}
},
"required": ["type"]
},
"JSONSchema": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"OutputFormatJsonSchema": {
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "json_schema"
},
"schema": {
"$ref": "#/components/schemas/JSONSchema"
},
"retryCount": {
"default": 2,
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}
},
"required": ["type", "schema"]
},
"OutputFormat": {
"anyOf": [
{
"$ref": "#/components/schemas/OutputFormatText"
},
{
"$ref": "#/components/schemas/OutputFormatJsonSchema"
}
]
},
"FileDiff": { "FileDiff": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -6432,6 +6487,9 @@
}, },
"required": ["created"] "required": ["created"]
}, },
"format": {
"$ref": "#/components/schemas/OutputFormat"
},
"summary": { "summary": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -6557,6 +6615,28 @@
}, },
"required": ["name", "data"] "required": ["name", "data"]
}, },
"StructuredOutputError": {
"type": "object",
"properties": {
"name": {
"type": "string",
"const": "StructuredOutputError"
},
"data": {
"type": "object",
"properties": {
"message": {
"type": "string"
},
"retries": {
"type": "number"
}
},
"required": ["message", "retries"]
}
},
"required": ["name", "data"]
},
"ContextOverflowError": { "ContextOverflowError": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -6664,6 +6744,9 @@
{ {
"$ref": "#/components/schemas/MessageAbortedError" "$ref": "#/components/schemas/MessageAbortedError"
}, },
{
"$ref": "#/components/schemas/StructuredOutputError"
},
{ {
"$ref": "#/components/schemas/ContextOverflowError" "$ref": "#/components/schemas/ContextOverflowError"
}, },
@@ -6735,6 +6818,7 @@
}, },
"required": ["input", "output", "reasoning", "cache"] "required": ["input", "output", "reasoning", "cache"]
}, },
"structured": {},
"variant": { "variant": {
"type": "string" "type": "string"
}, },
@@ -8444,6 +8528,9 @@
{ {
"$ref": "#/components/schemas/MessageAbortedError" "$ref": "#/components/schemas/MessageAbortedError"
}, },
{
"$ref": "#/components/schemas/StructuredOutputError"
},
{ {
"$ref": "#/components/schemas/ContextOverflowError" "$ref": "#/components/schemas/ContextOverflowError"
}, },
@@ -9686,9 +9773,12 @@
"properties": { "properties": {
"npm": { "npm": {
"type": "string" "type": "string"
},
"api": {
"type": "string"
} }
}, },
"required": ["npm"] "required": ["npm", "api"]
}, },
"variants": { "variants": {
"description": "Variant-specific configuration", "description": "Variant-specific configuration",
+3 -2
View File
@@ -1,20 +1,21 @@
export const dict = { export const dict = {
"ui.sessionReview.title": "تغييرات الجلسة", "ui.sessionReview.title": "تغييرات الجلسة",
"ui.sessionReview.title.lastTurn": "تغييرات آخر دور", "ui.sessionReview.title.lastTurn": "تغييرات آخر دور",
"ui.sessionReview.diffStyle.unified": "موجد", "ui.sessionReview.diffStyle.unified": "موحد",
"ui.sessionReview.diffStyle.split": "منقسم", "ui.sessionReview.diffStyle.split": "منقسم",
"ui.sessionReview.expandAll": "توسيع الكل", "ui.sessionReview.expandAll": "توسيع الكل",
"ui.sessionReview.collapseAll": "طي الكل", "ui.sessionReview.collapseAll": "طي الكل",
"ui.sessionReview.change.added": "مضاف", "ui.sessionReview.change.added": "مضاف",
"ui.sessionReview.change.removed": "محذوف", "ui.sessionReview.change.removed": "محذوف",
"ui.sessionReview.change.modified": "معدل", "ui.sessionReview.change.modified": "معدل",
"ui.lineComment.label.prefix": "تعليق على ", "ui.lineComment.label.prefix": "تعليق على ",
"ui.lineComment.label.suffix": "", "ui.lineComment.label.suffix": "",
"ui.lineComment.editorLabel.prefix": "جارٍ التعليق على ", "ui.lineComment.editorLabel.prefix": "جارٍ التعليق على ",
"ui.lineComment.editorLabel.suffix": "", "ui.lineComment.editorLabel.suffix": "",
"ui.lineComment.placeholder": "أضف تعليقًا", "ui.lineComment.placeholder": "أضف تعليقًا",
"ui.lineComment.submit": "تعليق", "ui.lineComment.submit": "تعليق",
"ui.sessionTurn.steps.show": "إظهار الخطوات", "ui.sessionTurn.steps.show": "إظهار الخطوات",
"ui.sessionTurn.steps.hide": "إخفاء الخطوات", "ui.sessionTurn.steps.hide": "إخفاء الخطوات",
"ui.sessionTurn.summary.response": "استجابة", "ui.sessionTurn.summary.response": "استجابة",
+3 -2
View File
@@ -5,16 +5,17 @@ export const dict = {
"ui.sessionReview.diffStyle.split": "Dividido", "ui.sessionReview.diffStyle.split": "Dividido",
"ui.sessionReview.expandAll": "Expandir tudo", "ui.sessionReview.expandAll": "Expandir tudo",
"ui.sessionReview.collapseAll": "Recolher tudo", "ui.sessionReview.collapseAll": "Recolher tudo",
"ui.sessionReview.change.added": "Adicionado", "ui.sessionReview.change.added": "Adicionado",
"ui.sessionReview.change.removed": "Removido", "ui.sessionReview.change.removed": "Removido",
"ui.sessionReview.change.modified": "Modificado", "ui.sessionReview.change.modified": "Modificado",
"ui.lineComment.label.prefix": "Comentar em ", "ui.lineComment.label.prefix": "Comentar em ",
"ui.lineComment.label.suffix": "", "ui.lineComment.label.suffix": "",
"ui.lineComment.editorLabel.prefix": "Comentando em ", "ui.lineComment.editorLabel.prefix": "Comentando em ",
"ui.lineComment.editorLabel.suffix": "", "ui.lineComment.editorLabel.suffix": "",
"ui.lineComment.placeholder": "Adicionar comentário", "ui.lineComment.placeholder": "Adicionar comentário",
"ui.lineComment.submit": "Comentar", "ui.lineComment.submit": "Comentar",
"ui.sessionTurn.steps.show": "Mostrar passos", "ui.sessionTurn.steps.show": "Mostrar passos",
"ui.sessionTurn.steps.hide": "Ocultar passos", "ui.sessionTurn.steps.hide": "Ocultar passos",
"ui.sessionTurn.summary.response": "Resposta", "ui.sessionTurn.summary.response": "Resposta",
@@ -94,7 +95,7 @@ export const dict = {
"ui.patch.action.deleted": "Excluído", "ui.patch.action.deleted": "Excluído",
"ui.patch.action.created": "Criado", "ui.patch.action.created": "Criado",
"ui.patch.action.moved": "Movido", "ui.patch.action.moved": "Movido",
"ui.patch.action.patched": "Aplicado patch", "ui.patch.action.patched": "Patch aplicado",
"ui.question.subtitle.answered": "{{count}} respondidas", "ui.question.subtitle.answered": "{{count}} respondidas",
"ui.question.answer.none": "(sem resposta)", "ui.question.answer.none": "(sem resposta)",
+3 -3
View File
@@ -63,8 +63,8 @@ export const dict = {
"ui.tool.webfetch": "Webhentning", "ui.tool.webfetch": "Webhentning",
"ui.tool.shell": "Shell", "ui.tool.shell": "Shell",
"ui.tool.patch": "Patch", "ui.tool.patch": "Patch",
"ui.tool.todos": "To-dos", "ui.tool.todos": "Opgaver",
"ui.tool.todos.read": "Læs to-dos", "ui.tool.todos.read": "Læs opgaver",
"ui.tool.questions": "Spørgsmål", "ui.tool.questions": "Spørgsmål",
"ui.tool.agent": "{{type}} Agent", "ui.tool.agent": "{{type}} Agent",
@@ -94,7 +94,7 @@ export const dict = {
"ui.patch.action.deleted": "Slettet", "ui.patch.action.deleted": "Slettet",
"ui.patch.action.created": "Oprettet", "ui.patch.action.created": "Oprettet",
"ui.patch.action.moved": "Flyttet", "ui.patch.action.moved": "Flyttet",
"ui.patch.action.patched": "Patched", "ui.patch.action.patched": "Patchet",
"ui.question.subtitle.answered": "{{count}} besvaret", "ui.question.subtitle.answered": "{{count}} besvaret",
"ui.question.answer.none": "(intet svar)", "ui.question.answer.none": "(intet svar)",
+2 -1
View File
@@ -5,16 +5,17 @@ export const dict = {
"ui.sessionReview.diffStyle.split": "Dividido", "ui.sessionReview.diffStyle.split": "Dividido",
"ui.sessionReview.expandAll": "Expandir todo", "ui.sessionReview.expandAll": "Expandir todo",
"ui.sessionReview.collapseAll": "Colapsar todo", "ui.sessionReview.collapseAll": "Colapsar todo",
"ui.sessionReview.change.added": "Añadido", "ui.sessionReview.change.added": "Añadido",
"ui.sessionReview.change.removed": "Eliminado", "ui.sessionReview.change.removed": "Eliminado",
"ui.sessionReview.change.modified": "Modificado", "ui.sessionReview.change.modified": "Modificado",
"ui.lineComment.label.prefix": "Comentar en ", "ui.lineComment.label.prefix": "Comentar en ",
"ui.lineComment.label.suffix": "", "ui.lineComment.label.suffix": "",
"ui.lineComment.editorLabel.prefix": "Comentando en ", "ui.lineComment.editorLabel.prefix": "Comentando en ",
"ui.lineComment.editorLabel.suffix": "", "ui.lineComment.editorLabel.suffix": "",
"ui.lineComment.placeholder": "Añadir comentario", "ui.lineComment.placeholder": "Añadir comentario",
"ui.lineComment.submit": "Comentar", "ui.lineComment.submit": "Comentar",
"ui.sessionTurn.steps.show": "Mostrar pasos", "ui.sessionTurn.steps.show": "Mostrar pasos",
"ui.sessionTurn.steps.hide": "Ocultar pasos", "ui.sessionTurn.steps.hide": "Ocultar pasos",
"ui.sessionTurn.summary.response": "Respuesta", "ui.sessionTurn.summary.response": "Respuesta",
+2 -1
View File
@@ -5,16 +5,17 @@ export const dict = {
"ui.sessionReview.diffStyle.split": "Divisé", "ui.sessionReview.diffStyle.split": "Divisé",
"ui.sessionReview.expandAll": "Tout développer", "ui.sessionReview.expandAll": "Tout développer",
"ui.sessionReview.collapseAll": "Tout réduire", "ui.sessionReview.collapseAll": "Tout réduire",
"ui.sessionReview.change.added": "Ajouté", "ui.sessionReview.change.added": "Ajouté",
"ui.sessionReview.change.removed": "Supprimé", "ui.sessionReview.change.removed": "Supprimé",
"ui.sessionReview.change.modified": "Modifié", "ui.sessionReview.change.modified": "Modifié",
"ui.lineComment.label.prefix": "Commenter sur ", "ui.lineComment.label.prefix": "Commenter sur ",
"ui.lineComment.label.suffix": "", "ui.lineComment.label.suffix": "",
"ui.lineComment.editorLabel.prefix": "Commentaire sur ", "ui.lineComment.editorLabel.prefix": "Commentaire sur ",
"ui.lineComment.editorLabel.suffix": "", "ui.lineComment.editorLabel.suffix": "",
"ui.lineComment.placeholder": "Ajouter un commentaire", "ui.lineComment.placeholder": "Ajouter un commentaire",
"ui.lineComment.submit": "Commenter", "ui.lineComment.submit": "Commenter",
"ui.sessionTurn.steps.show": "Afficher les étapes", "ui.sessionTurn.steps.show": "Afficher les étapes",
"ui.sessionTurn.steps.hide": "Masquer les étapes", "ui.sessionTurn.steps.hide": "Masquer les étapes",
"ui.sessionTurn.summary.response": "Réponse", "ui.sessionTurn.summary.response": "Réponse",
+2 -1
View File
@@ -5,16 +5,17 @@ export const dict = {
"ui.sessionReview.diffStyle.split": "분할 보기", "ui.sessionReview.diffStyle.split": "분할 보기",
"ui.sessionReview.expandAll": "모두 펼치기", "ui.sessionReview.expandAll": "모두 펼치기",
"ui.sessionReview.collapseAll": "모두 접기", "ui.sessionReview.collapseAll": "모두 접기",
"ui.sessionReview.change.added": "추가됨", "ui.sessionReview.change.added": "추가됨",
"ui.sessionReview.change.removed": "삭제됨", "ui.sessionReview.change.removed": "삭제됨",
"ui.sessionReview.change.modified": "수정됨", "ui.sessionReview.change.modified": "수정됨",
"ui.lineComment.label.prefix": "", "ui.lineComment.label.prefix": "",
"ui.lineComment.label.suffix": "에 댓글 달기", "ui.lineComment.label.suffix": "에 댓글 달기",
"ui.lineComment.editorLabel.prefix": "", "ui.lineComment.editorLabel.prefix": "",
"ui.lineComment.editorLabel.suffix": "에 댓글 작성 중", "ui.lineComment.editorLabel.suffix": "에 댓글 작성 중",
"ui.lineComment.placeholder": "댓글 추가", "ui.lineComment.placeholder": "댓글 추가",
"ui.lineComment.submit": "댓글", "ui.lineComment.submit": "댓글",
"ui.sessionTurn.steps.show": "단계 표시", "ui.sessionTurn.steps.show": "단계 표시",
"ui.sessionTurn.steps.hide": "단계 숨기기", "ui.sessionTurn.steps.hide": "단계 숨기기",
"ui.sessionTurn.summary.response": "응답", "ui.sessionTurn.summary.response": "응답",
+2 -1
View File
@@ -8,16 +8,17 @@ export const dict: Record<Keys, string> = {
"ui.sessionReview.diffStyle.split": "Delt", "ui.sessionReview.diffStyle.split": "Delt",
"ui.sessionReview.expandAll": "Utvid alle", "ui.sessionReview.expandAll": "Utvid alle",
"ui.sessionReview.collapseAll": "Fold sammen alle", "ui.sessionReview.collapseAll": "Fold sammen alle",
"ui.sessionReview.change.added": "Lagt til", "ui.sessionReview.change.added": "Lagt til",
"ui.sessionReview.change.removed": "Fjernet", "ui.sessionReview.change.removed": "Fjernet",
"ui.sessionReview.change.modified": "Endret", "ui.sessionReview.change.modified": "Endret",
"ui.lineComment.label.prefix": "Kommenter på ", "ui.lineComment.label.prefix": "Kommenter på ",
"ui.lineComment.label.suffix": "", "ui.lineComment.label.suffix": "",
"ui.lineComment.editorLabel.prefix": "Kommenterer på ", "ui.lineComment.editorLabel.prefix": "Kommenterer på ",
"ui.lineComment.editorLabel.suffix": "", "ui.lineComment.editorLabel.suffix": "",
"ui.lineComment.placeholder": "Legg til kommentar", "ui.lineComment.placeholder": "Legg til kommentar",
"ui.lineComment.submit": "Kommenter", "ui.lineComment.submit": "Kommenter",
"ui.sessionTurn.steps.show": "Vis trinn", "ui.sessionTurn.steps.show": "Vis trinn",
"ui.sessionTurn.steps.hide": "Skjul trinn", "ui.sessionTurn.steps.hide": "Skjul trinn",
"ui.sessionTurn.summary.response": "Svar", "ui.sessionTurn.summary.response": "Svar",
+1
View File
@@ -57,6 +57,7 @@ export const dict = {
"ui.imagePreview.alt": "ตัวอย่างรูปภาพ", "ui.imagePreview.alt": "ตัวอย่างรูปภาพ",
"ui.tool.read": "อ่าน", "ui.tool.read": "อ่าน",
"ui.tool.loaded": "โหลดแล้ว",
"ui.tool.list": "รายการ", "ui.tool.list": "รายการ",
"ui.tool.glob": "Glob", "ui.tool.glob": "Glob",
"ui.tool.grep": "Grep", "ui.tool.grep": "Grep",
+3 -2
View File
@@ -9,16 +9,17 @@ export const dict = {
"ui.sessionReview.diffStyle.split": "拆分", "ui.sessionReview.diffStyle.split": "拆分",
"ui.sessionReview.expandAll": "全部展开", "ui.sessionReview.expandAll": "全部展开",
"ui.sessionReview.collapseAll": "全部收起", "ui.sessionReview.collapseAll": "全部收起",
"ui.sessionReview.change.added": "已添加", "ui.sessionReview.change.added": "已添加",
"ui.sessionReview.change.removed": "已移除", "ui.sessionReview.change.removed": "已移除",
"ui.sessionReview.change.modified": "已修改", "ui.sessionReview.change.modified": "已修改",
"ui.lineComment.label.prefix": "评论 ", "ui.lineComment.label.prefix": "评论 ",
"ui.lineComment.label.suffix": "", "ui.lineComment.label.suffix": "",
"ui.lineComment.editorLabel.prefix": "正在评论 ", "ui.lineComment.editorLabel.prefix": "正在评论 ",
"ui.lineComment.editorLabel.suffix": "", "ui.lineComment.editorLabel.suffix": "",
"ui.lineComment.placeholder": "添加评论", "ui.lineComment.placeholder": "添加评论",
"ui.lineComment.submit": "评论", "ui.lineComment.submit": "评论",
"ui.sessionTurn.steps.show": "显示步骤", "ui.sessionTurn.steps.show": "显示步骤",
"ui.sessionTurn.steps.hide": "隐藏步骤", "ui.sessionTurn.steps.hide": "隐藏步骤",
"ui.sessionTurn.summary.response": "回复", "ui.sessionTurn.summary.response": "回复",
@@ -92,7 +93,7 @@ export const dict = {
"ui.message.expand": "展开消息", "ui.message.expand": "展开消息",
"ui.message.collapse": "收起消息", "ui.message.collapse": "收起消息",
"ui.message.copy": "复制", "ui.message.copy": "复制",
"ui.message.copied": "已复制", "ui.message.copied": "已复制",
"ui.message.attachment.alt": "附件", "ui.message.attachment.alt": "附件",
"ui.patch.action.deleted": "已删除", "ui.patch.action.deleted": "已删除",
+3 -2
View File
@@ -9,16 +9,17 @@ export const dict = {
"ui.sessionReview.diffStyle.split": "拆分", "ui.sessionReview.diffStyle.split": "拆分",
"ui.sessionReview.expandAll": "全部展開", "ui.sessionReview.expandAll": "全部展開",
"ui.sessionReview.collapseAll": "全部收合", "ui.sessionReview.collapseAll": "全部收合",
"ui.sessionReview.change.added": "已新增", "ui.sessionReview.change.added": "已新增",
"ui.sessionReview.change.removed": "已移除", "ui.sessionReview.change.removed": "已移除",
"ui.sessionReview.change.modified": "已修改", "ui.sessionReview.change.modified": "已修改",
"ui.lineComment.label.prefix": "評論 ", "ui.lineComment.label.prefix": "評論 ",
"ui.lineComment.label.suffix": "", "ui.lineComment.label.suffix": "",
"ui.lineComment.editorLabel.prefix": "正在評論 ", "ui.lineComment.editorLabel.prefix": "正在評論 ",
"ui.lineComment.editorLabel.suffix": "", "ui.lineComment.editorLabel.suffix": "",
"ui.lineComment.placeholder": "新增評論", "ui.lineComment.placeholder": "新增評論",
"ui.lineComment.submit": "評論", "ui.lineComment.submit": "評論",
"ui.sessionTurn.steps.show": "顯示步驟", "ui.sessionTurn.steps.show": "顯示步驟",
"ui.sessionTurn.steps.hide": "隱藏步驟", "ui.sessionTurn.steps.hide": "隱藏步驟",
"ui.sessionTurn.summary.response": "回覆", "ui.sessionTurn.summary.response": "回覆",
@@ -92,7 +93,7 @@ export const dict = {
"ui.message.expand": "展開訊息", "ui.message.expand": "展開訊息",
"ui.message.collapse": "收合訊息", "ui.message.collapse": "收合訊息",
"ui.message.copy": "複製", "ui.message.copy": "複製",
"ui.message.copied": "已複製", "ui.message.copied": "已複製",
"ui.message.attachment.alt": "附件", "ui.message.attachment.alt": "附件",
"ui.patch.action.deleted": "已刪除", "ui.patch.action.deleted": "已刪除",