Resolve merge conflicts
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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}} متاحة الآن للاستخدام.",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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}} พร้อมใช้งานแล้ว",
|
||||||
|
|||||||
@@ -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}} 模型了。",
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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" })
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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")
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 }]))
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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,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.
|
||||||
|
|||||||
@@ -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),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
|
})
|
||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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" },
|
||||||
|
|||||||
@@ -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]: {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "استجابة",
|
||||||
|
|||||||
@@ -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)",
|
||||||
|
|||||||
@@ -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)",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "응답",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "已删除",
|
||||||
|
|||||||
@@ -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": "已刪除",
|
||||||
|
|||||||
Reference in New Issue
Block a user