feat: merge opencode v1.2.18

This commit is contained in:
Catriel Müller
2026-03-16 15:03:07 -03:00
parent 5aa1305159
commit 656db495d5
85 changed files with 1014 additions and 564 deletions
-1
View File
@@ -10,7 +10,6 @@ jobs:
github.event.pull_request.merged == true &&
startsWith(github.event.pull_request.title, 'feat:')
runs-on: ubuntu-latest
steps:
- name: Send webhook safely
run: |
+13 -11
View File
@@ -15,12 +15,13 @@
"@actions/artifact": "5.0.1",
"@tsconfig/bun": "catalog:",
"@types/mime-types": "3.0.1",
"@typescript/native-preview": "catalog:",
"glob": "13.0.5",
"husky": "9.1.7",
"prettier": "3.6.2",
"semver": "^7.6.0",
"sst": "3.18.10",
"turbo": "2.5.6",
"turbo": "2.8.13",
},
},
"packages/app": {
@@ -48,7 +49,7 @@
"@thisbeyond/solid-dnd": "0.7.5",
"diff": "catalog:",
"fuzzysort": "catalog:",
"ghostty-web": "0.4.0",
"ghostty-web": "github:anomalyco/ghostty-web#main",
"luxon": "catalog:",
"marked": "catalog:",
"marked-shiki": "catalog:",
@@ -110,7 +111,7 @@
},
"packages/desktop-electron": {
"name": "@opencode-ai/desktop-electron",
"version": "7.0.46",
"version": "7.0.47",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -494,6 +495,7 @@
"@solid-primitives/media": "2.3.3",
"@solid-primitives/resize-observer": "2.1.3",
"@solidjs/meta": "catalog:",
"@solidjs/router": "catalog:",
"@typescript/native-preview": "catalog:",
"dompurify": "3.3.1",
"fuzzysort": "catalog:",
@@ -2865,7 +2867,7 @@
"get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="],
"ghostty-web": ["ghostty-web@0.4.0", "", {}, "sha512-0puDBik2qapbD/QQBW9o5ZHfXnZBqZWx/ctBiVtKZ6ZLds4NYb+wZuw1cRLXZk9zYovIQ908z3rvFhexAvc5Hg=="],
"ghostty-web": ["ghostty-web@github:anomalyco/ghostty-web#4af877d", {}, "anomalyco-ghostty-web-4af877d", "sha512-fbEK8mtr7ar4ySsF+JUGjhaZrane7dKphanN+SxHt5XXI6yLMAh/Hpf6sNCOyyVa2UlGCd7YpXG/T2v2RUAX+A=="],
"gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="],
@@ -3993,19 +3995,19 @@
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
"turbo": ["turbo@2.5.6", "", { "optionalDependencies": { "turbo-darwin-64": "2.5.6", "turbo-darwin-arm64": "2.5.6", "turbo-linux-64": "2.5.6", "turbo-linux-arm64": "2.5.6", "turbo-windows-64": "2.5.6", "turbo-windows-arm64": "2.5.6" }, "bin": { "turbo": "bin/turbo" } }, "sha512-gxToHmi9oTBNB05UjUsrWf0OyN5ZXtD0apOarC1KIx232Vp3WimRNy3810QzeNSgyD5rsaIDXlxlbnOzlouo+w=="],
"turbo": ["turbo@2.8.13", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.13", "turbo-darwin-arm64": "2.8.13", "turbo-linux-64": "2.8.13", "turbo-linux-arm64": "2.8.13", "turbo-windows-64": "2.8.13", "turbo-windows-arm64": "2.8.13" }, "bin": { "turbo": "bin/turbo" } }, "sha512-nyM99hwFB9/DHaFyKEqatdayGjsMNYsQ/XBNO6MITc7roncZetKb97MpHxWf3uiU+LB9c9HUlU3Jp2Ixei2k1A=="],
"turbo-darwin-64": ["turbo-darwin-64@2.5.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-3C1xEdo4aFwMJAPvtlPqz1Sw/+cddWIOmsalHFMrsqqydcptwBfu26WW2cDm3u93bUzMbBJ8k3zNKFqxJ9ei2A=="],
"turbo-darwin-64": ["turbo-darwin-64@2.8.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-PmOvodQNiOj77+Zwoqku70vwVjKzL34RTNxxoARjp5RU5FOj/CGiC6vcDQhNtFPUOWSAaogHF5qIka9TBhX4XA=="],
"turbo-darwin-arm64": ["turbo-darwin-arm64@2.5.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LyiG+rD7JhMfYwLqB6k3LZQtYn8CQQUePbpA8mF/hMLPAekXdJo1g0bUPw8RZLwQXUIU/3BU7tXENvhSGz5DPA=="],
"turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kI+anKcLIM4L8h+NsM7mtAUpElkCOxv5LgiQVQR8BASyDFfc8Efj5kCk3cqxuxOvIqx0sLfCX7atrHQ2kwuNJQ=="],
"turbo-linux-64": ["turbo-linux-64@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-GOcUTT0xiT/pSnHL4YD6Yr3HreUhU8pUcGqcI2ksIF9b2/r/kRHwGFcsHgpG3+vtZF/kwsP0MV8FTlTObxsYIA=="],
"turbo-linux-64": ["turbo-linux-64@2.8.13", "", { "os": "linux", "cpu": "x64" }, "sha512-j29KnQhHyzdzgCykBFeBqUPS4Wj7lWMnZ8CHqytlYDap4Jy70l4RNG46pOL9+lGu6DepK2s1rE86zQfo0IOdPw=="],
"turbo-linux-arm64": ["turbo-linux-arm64@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-10Tm15bruJEA3m0V7iZcnQBpObGBcOgUcO+sY7/2vk1bweW34LMhkWi8svjV9iDF68+KJDThnYDlYE/bc7/zzQ=="],
"turbo-linux-arm64": ["turbo-linux-arm64@2.8.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-OEl1YocXGZDRDh28doOUn49QwNe82kXljO1HXApjU0LapkDiGpfl3jkAlPKxEkGDSYWc8MH5Ll8S16Rf5tEBYg=="],
"turbo-windows-64": ["turbo-windows-64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-FyRsVpgaj76It0ludwZsNN40ytHN+17E4PFJyeliBEbxrGTc5BexlXVpufB7XlAaoaZVxbS6KT8RofLfDRyEPg=="],
"turbo-windows-64": ["turbo-windows-64@2.8.13", "", { "os": "win32", "cpu": "x64" }, "sha512-717bVk1+Pn2Jody7OmWludhEirEe0okoj1NpRbSm5kVZz/yNN/jfjbxWC6ilimXMz7xoMT3IDfQFJsFR3PMANA=="],
"turbo-windows-arm64": ["turbo-windows-arm64@2.5.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-j/tWu8cMeQ7HPpKri6jvKtyXg9K1gRyhdK4tKrrchH8GNHscPX/F71zax58yYtLRWTiK04zNzPcUJuoS0+v/+Q=="],
"turbo-windows-arm64": ["turbo-windows-arm64@2.8.13", "", { "os": "win32", "cpu": "arm64" }, "sha512-R819HShLIT0Wj6zWVnIsYvSNtRNj1q9VIyaUz0P24SMcLCbQZIm1sV09F4SDbg+KCCumqD2lcaR2UViQ8SnUJA=="],
"turndown": ["turndown@7.2.0", "", { "dependencies": { "@mixmark-io/domino": "^2.2.0" } }, "sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A=="],
+2 -1
View File
@@ -69,12 +69,13 @@
"@actions/artifact": "5.0.1",
"@tsconfig/bun": "catalog:",
"@types/mime-types": "3.0.1",
"@typescript/native-preview": "catalog:",
"glob": "13.0.5",
"husky": "9.1.7",
"prettier": "3.6.2",
"semver": "^7.6.0",
"sst": "3.18.10",
"turbo": "2.5.6"
"turbo": "2.8.13"
},
"dependencies": {
"@aws-sdk/client-s3": "3.933.0",
@@ -101,3 +101,56 @@ test("cmd+f opens text viewer search while prompt is focused", async ({ page, go
await expect(findInput).toBeVisible()
await expect(findInput).toBeFocused()
})
test("cmd+f opens text viewer search while prompt is not focused", async ({ page, gotoSession }) => {
await gotoSession()
await page.locator(promptSelector).click()
await page.keyboard.type("/open")
const command = page.locator('[data-slash-id="file.open"]').first()
await expect(command).toBeVisible()
await page.keyboard.press("Enter")
const dialog = page
.getByRole("dialog")
.filter({ has: page.getByPlaceholder(/search files/i) })
.first()
await expect(dialog).toBeVisible()
const input = dialog.getByRole("textbox").first()
await input.fill("package.json")
const items = dialog.locator('[data-slot="list-item"][data-key^="file:"]')
let index = -1
await expect
.poll(
async () => {
const keys = await items.evaluateAll((nodes) => nodes.map((node) => node.getAttribute("data-key") ?? ""))
index = keys.findIndex((key) => /packages[\\/]+app[\\/]+package\.json$/i.test(key.replace(/^file:/, "")))
return index >= 0
},
{ timeout: 30_000 },
)
.toBe(true)
const item = items.nth(index)
await expect(item).toBeVisible()
await item.click()
await expect(dialog).toHaveCount(0)
const tab = page.getByRole("tab", { name: "package.json" })
await expect(tab).toBeVisible()
await tab.click()
const viewer = page.locator('[data-component="file"][data-mode="text"]').first()
await expect(viewer).toBeVisible()
await viewer.click()
await page.keyboard.press(`${modKey}+f`)
const findInput = page.getByPlaceholder("Find")
await expect(findInput).toBeVisible()
await expect(findInput).toBeFocused()
})
+1 -1
View File
@@ -57,7 +57,7 @@
"@thisbeyond/solid-dnd": "0.7.5",
"diff": "catalog:",
"fuzzysort": "catalog:",
"ghostty-web": "0.4.0",
"ghostty-web": "github:anomalyco/ghostty-web#main",
"luxon": "catalog:",
"marked": "catalog:",
"marked-shiki": "catalog:",
@@ -16,6 +16,7 @@ import { Identifier } from "@/utils/id"
import { Worktree as WorktreeState } from "@/utils/worktree"
import { buildRequestParts } from "./build-request-parts"
import { setCursorPosition } from "./editor-dom"
import { formatServerError } from "@/utils/server-errors"
type PendingPrompt = {
abort: AbortController
@@ -286,7 +287,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
.catch((err) => {
showToast({
title: language.t("prompt.toast.commandSendFailed.title"),
description: errorMessage(err),
description: formatServerError(err, language.t, language.t("common.requestFailed")),
})
restoreInput()
})
@@ -39,7 +39,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
const usd = createMemo(
() =>
new Intl.NumberFormat(language.locale(), {
new Intl.NumberFormat(language.intl(), {
style: "currency",
currency: "USD",
}),
@@ -77,7 +77,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
{(ctx) => (
<>
<div class="flex items-center gap-2">
<span class="text-text-invert-strong">{ctx().total.toLocaleString(language.locale())}</span>
<span class="text-text-invert-strong">{ctx().total.toLocaleString(language.intl())}</span>
<span class="text-text-invert-base">{language.t("context.usage.tokens")}</span>
</div>
<div class="flex items-center gap-2">
@@ -128,7 +128,7 @@ export function SessionContextTab() {
const usd = createMemo(
() =>
new Intl.NumberFormat(language.locale(), {
new Intl.NumberFormat(language.intl(), {
style: "currency",
currency: "USD",
}),
@@ -136,7 +136,7 @@ export function SessionContextTab() {
const metrics = createMemo(() => getSessionContextMetrics(messages(), sync.data.provider.all))
const ctx = createMemo(() => metrics().context)
const formatter = createMemo(() => createSessionContextFormatter(language.locale()))
const formatter = createMemo(() => createSessionContextFormatter(language.intl()))
const cost = createMemo(() => {
return usd().format(metrics().totalCost)
@@ -200,7 +200,7 @@ export function SessionContextTab() {
const stats = [
{ label: "context.stats.session", value: () => info()?.title ?? params.id ?? "—" },
{ label: "context.stats.messages", value: () => counts().all.toLocaleString(language.locale()) },
{ label: "context.stats.messages", value: () => counts().all.toLocaleString(language.intl()) },
{ label: "context.stats.provider", value: providerLabel },
{ label: "context.stats.model", value: modelLabel },
{ label: "context.stats.limit", value: () => formatter().number(ctx()?.limit) },
@@ -213,8 +213,8 @@ export function SessionContextTab() {
label: "context.stats.cacheTokens",
value: () => `${formatter().number(ctx()?.cacheRead)} / ${formatter().number(ctx()?.cacheWrite)}`,
},
{ label: "context.stats.userMessages", value: () => counts().user.toLocaleString(language.locale()) },
{ label: "context.stats.assistantMessages", value: () => counts().assistant.toLocaleString(language.locale()) },
{ label: "context.stats.userMessages", value: () => counts().user.toLocaleString(language.intl()) },
{ label: "context.stats.assistantMessages", value: () => counts().assistant.toLocaleString(language.intl()) },
{ label: "context.stats.totalCost", value: cost },
{ label: "context.stats.sessionCreated", value: () => formatter().time(info()?.time.created) },
{ label: "context.stats.lastActivity", value: () => formatter().time(ctx()?.message.time.created) },
@@ -307,7 +307,7 @@ export function SessionContextTab() {
<div class="flex items-center gap-1 text-11-regular text-text-weak">
<div class="size-2 rounded-sm" style={{ "background-color": BREAKDOWN_COLOR[segment.key] }} />
<div>{breakdownLabel(segment.key)}</div>
<div class="text-text-weaker">{segment.percent.toLocaleString(language.locale())}%</div>
<div class="text-text-weaker">{segment.percent.toLocaleString(language.intl())}%</div>
</div>
)}
</For>
@@ -51,26 +51,26 @@ export function NewSessionView(props: NewSessionViewProps) {
return (
<div class={ROOT_CLASS}>
<div class="text-20-medium text-text-weaker">{language.t("command.session.new")}</div>
<div class="flex justify-center items-center gap-3">
<Icon name="folder" size="small" />
<div class="text-12-medium text-text-weak select-text">
<div class="flex justify-center items-start gap-3 min-h-5">
<Icon name="folder" size="small" class="mt-0.5 shrink-0" />
<div class="text-12-medium text-text-weak select-text leading-5">
{getDirectory(projectRoot())}
<span class="text-text-strong">{getFilename(projectRoot())}</span>
</div>
</div>
<div class="flex justify-center items-center gap-1">
<Icon name="branch" size="small" />
<div class="text-12-medium text-text-weak select-text ml-2">{label(current())}</div>
<div class="flex justify-center items-start gap-3 min-h-5">
<Icon name="branch" size="small" class="mt-0.5 shrink-0" />
<div class="text-12-medium text-text-weak select-text leading-5">{label(current())}</div>
</div>
<Show when={sync.project}>
{(project) => (
<div class="flex justify-center items-center gap-3">
<Icon name="pencil-line" size="small" />
<div class="text-12-medium text-text-weak">
<div class="flex justify-center items-start gap-3 min-h-5">
<Icon name="pencil-line" size="small" class="mt-0.5 shrink-0" />
<div class="text-12-medium text-text-weak leading-5">
{language.t("session.new.lastModified")}&nbsp;
<span class="text-text-strong">
{DateTime.fromMillis(project().time.updated ?? project().time.created)
.setLocale(language.locale())
.setLocale(language.intl())
.toRelative()}
</span>
</div>
@@ -17,6 +17,7 @@ type ProviderItem = ReturnType<ReturnType<typeof useProviders>["connected"]>[num
const PROVIDER_NOTES = [
{ match: (id: string) => id === "opencode", key: "dialog.provider.opencode.note" },
{ match: (id: string) => id === "opencode-go", key: "dialog.provider.opencodeGo.tagline" },
{ match: (id: string) => id === "anthropic", key: "dialog.provider.anthropic.note" },
{ match: (id: string) => id.startsWith("github-copilot"), key: "dialog.provider.copilot.note" },
{ match: (id: string) => id === "openai", key: "dialog.provider.openai.note" },
@@ -181,21 +182,11 @@ export const SettingsProviders: Component = () => {
<div class="flex items-center gap-x-3">
<ProviderIcon id={item.id} class="size-5 shrink-0 icon-strong-base" />
<span class="text-14-medium text-text-strong">{item.name}</span>
<Show when={item.id === "opencode"}>
<span class="text-14-regular text-text-weak">
{language.t("dialog.provider.opencode.tagline")}
</span>
</Show>
<Show when={item.id === "opencode"}>
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
</Show>
<Show when={item.id === "opencode-go"}>
<>
<span class="text-14-regular text-text-weak">
{language.t("dialog.provider.opencodeGo.tagline")}
</span>
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
</>
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
</Show>
</div>
<Show when={note(item.id)}>
+3 -8
View File
@@ -228,10 +228,7 @@ function createGlobalSync() {
showToast({
variant: "error",
title: language.t("toast.session.listFailed.title", { project }),
description: formatServerError(err, {
unknown: language.t("error.chain.unknown"),
invalidConfiguration: language.t("error.server.invalidConfiguration"),
}),
description: formatServerError(err, language.t),
})
})
@@ -261,8 +258,7 @@ function createGlobalSync() {
setStore: child[1],
vcsCache: cache,
loadSessions,
unknownError: language.t("error.chain.unknown"),
invalidConfigurationError: language.t("error.server.invalidConfiguration"),
translate: language.t,
})
})()
@@ -331,8 +327,7 @@ function createGlobalSync() {
url: globalSDK.url,
}),
requestFailedTitle: language.t("common.requestFailed"),
unknownError: language.t("error.chain.unknown"),
invalidConfigurationError: language.t("error.server.invalidConfiguration"),
translate: language.t,
formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
setGlobalStore: setBootStore,
})
@@ -36,8 +36,7 @@ export async function bootstrapGlobal(input: {
connectErrorTitle: string
connectErrorDescription: string
requestFailedTitle: string
unknownError: string
invalidConfigurationError: string
translate: (key: string, vars?: Record<string, string | number>) => string
formatMoreCount: (count: number) => string
setGlobalStore: SetStoreFunction<GlobalStore>
}) {
@@ -91,10 +90,7 @@ export async function bootstrapGlobal(input: {
const results = await Promise.allSettled(tasks)
const errors = results.filter((r): r is PromiseRejectedResult => r.status === "rejected").map((r) => r.reason)
if (errors.length) {
const message = formatServerError(errors[0], {
unknown: input.unknownError,
invalidConfiguration: input.invalidConfigurationError,
})
const message = formatServerError(errors[0], input.translate)
const more = errors.length > 1 ? input.formatMoreCount(errors.length - 1) : ""
showToast({
variant: "error",
@@ -122,8 +118,7 @@ export async function bootstrapDirectory(input: {
setStore: SetStoreFunction<State>
vcsCache: VcsCache
loadSessions: (directory: string) => Promise<void> | void
unknownError: string
invalidConfigurationError: string
translate: (key: string, vars?: Record<string, string | number>) => string
}) {
if (input.store.status !== "complete") input.setStore("status", "loading")
@@ -145,10 +140,7 @@ export async function bootstrapDirectory(input: {
showToast({
variant: "error",
title: `Failed to reload ${project}`,
description: formatServerError(err, {
unknown: input.unknownError,
invalidConfiguration: input.invalidConfigurationError,
}),
description: formatServerError(err, input.translate),
})
input.setStore("status", "partial")
return
+22
View File
@@ -104,6 +104,26 @@ const LOCALES: readonly Locale[] = [
"tr",
]
const INTL: Record<Locale, string> = {
en: "en",
zh: "zh-Hans",
zht: "zh-Hant",
ko: "ko",
de: "de",
es: "es",
fr: "fr",
da: "da",
ja: "ja",
pl: "pl",
ru: "ru",
ar: "ar",
no: "nb-NO",
br: "pt-BR",
th: "th",
bs: "bs",
tr: "tr",
}
const LABEL_KEY: Record<Locale, keyof Dictionary> = {
en: "language.en",
zh: "language.zh",
@@ -217,6 +237,7 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
)
const locale = createMemo<Locale>(() => normalizeLocale(store.locale))
const intl = createMemo(() => INTL[locale()])
const dict = createMemo<Dictionary>(() => DICT[locale()])
@@ -233,6 +254,7 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
return {
ready,
locale,
intl,
locales: LOCALES,
label,
t,
+20 -10
View File
@@ -35,6 +35,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const agent = (() => {
const list = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent" && !x.hidden))
const models = useModels()
const [store, setStore] = createStore<{
current?: string
}>({
@@ -53,11 +55,17 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
setStore("current", undefined)
return
}
if (name && available.some((x) => x.name === name)) {
setStore("current", name)
return
}
setStore("current", available[0].name)
const match = name ? available.find((x) => x.name === name) : undefined
const value = match ?? available[0]
if (!value) return
setStore("current", value.name)
if (!value.model) return
setModel({
providerID: value.model.providerID,
modelID: value.model.modelID,
})
if (value.variant)
models.variant.set({ providerID: value.model.providerID, modelID: value.model.modelID }, value.variant)
},
move(direction: 1 | -1) {
const available = list()
@@ -71,11 +79,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const value = available[next]
if (!value) return
setStore("current", value.name)
if (value.model)
setModel({
providerID: value.model.providerID,
modelID: value.model.modelID,
})
if (!value.model) return
setModel({
providerID: value.model.providerID,
modelID: value.model.modelID,
})
if (value.variant)
models.variant.set({ providerID: value.model.providerID, modelID: value.model.modelID }, value.variant)
},
}
})()
-3
View File
@@ -551,9 +551,6 @@ export const dict = {
"settings.general.row.theme.description": "Personnaliser le thème d'Kilo.",
"settings.general.row.font.title": "Police",
"settings.general.row.font.description": "Personnaliser la police mono utilisée dans les blocs de code",
"settings.general.row.reasoningSummaries.title": "Afficher les résumés de raisonnement",
"settings.general.row.reasoningSummaries.description":
"Afficher les résumés de raisonnement du modèle dans la chronologie",
"settings.general.row.shellToolPartsExpanded.title": "Développer les parties de l'outil shell",
"settings.general.row.shellToolPartsExpanded.description":
"Afficher les parties de l'outil shell développées par défaut dans la chronologie",
-9
View File
@@ -113,7 +113,6 @@ export const dict = {
"dialog.model.empty": "ไม่พบผลลัพธ์โมเดล",
"dialog.model.manage": "จัดการโมเดล",
"dialog.model.manage.description": "ปรับแต่งโมเดลที่จะปรากฏในตัวเลือกโมเดล",
"dialog.model.manage.provider.toggle": "สลับโมเดล {{provider}} ทั้งหมด",
"dialog.model.unpaid.freeModels.title": "โมเดลฟรีที่จัดหาให้โดย Kilo",
"dialog.model.unpaid.addMore.title": "เพิ่มโมเดลเพิ่มเติมจากผู้ให้บริการยอดนิยม",
@@ -495,7 +494,6 @@ export const dict = {
"session.review.loadingChanges": "กำลังโหลดการเปลี่ยนแปลง...",
"session.review.empty": "ยังไม่มีการเปลี่ยนแปลงในเซสชันนี้",
"session.review.noChanges": "ไม่มีการเปลี่ยนแปลง",
"session.review.noVcs": "ไม่พบ git VCS ดังนั้นจะไม่สามารถตรวจจับการเปลี่ยนแปลงของเซสชันได้",
"session.files.selectToOpen": "เลือกไฟล์เพื่อเปิด",
"session.files.all": "ไฟล์ทั้งหมด",
@@ -512,11 +510,6 @@ export const dict = {
"session.todo.collapse": "ย่อ",
"session.todo.expand": "ขยาย",
"session.modeSwitch.switching": "กำลังสลับไปยังโหมด {{mode}}…",
"session.modeSwitch.waiting": "รอให้งานปัจจุบันเสร็จสมบูรณ์",
"session.modeSwitch.notAvailable": "ตัวแทนไม่พร้อมใช้งาน",
"session.modeSwitch.fallback": 'ไม่พบ "{{requested}}" กำลังใช้ "{{actual}}"',
"session.new.worktree.main": "สาขาหลัก",
"session.new.worktree.mainWithBranch": "สาขาหลัก ({{branch}})",
"session.new.worktree.create": "สร้าง worktree ใหม่",
@@ -613,8 +606,6 @@ export const dict = {
"settings.general.row.theme.description": "ปรับแต่งวิธีการที่ Kilo มีธีม",
"settings.general.row.font.title": "ฟอนต์",
"settings.general.row.font.description": "ปรับแต่งฟอนต์โมโนที่ใช้ในบล็อกโค้ด",
"settings.general.row.reasoningSummaries.title": "แสดงสรุปการให้เหตุผล",
"settings.general.row.reasoningSummaries.description": "แสดงสรุปการให้เหตุผลของโมเดลในไทม์ไลน์",
"settings.general.row.shellToolPartsExpanded.title": "ขยายส่วนเครื่องมือ shell",
"settings.general.row.shellToolPartsExpanded.description": "แสดงส่วนเครื่องมือ shell แบบขยายตามค่าเริ่มต้นในไทม์ไลน์",
-9
View File
@@ -117,7 +117,6 @@ export const dict = {
"dialog.model.empty": "找不到模型",
"dialog.model.manage": "管理模型",
"dialog.model.manage.description": "自訂模型選擇器中顯示的模型。",
"dialog.model.manage.provider.toggle": "切換所有 {{provider}} 模型",
"dialog.model.unpaid.freeModels.title": "Kilo 提供的免費模型",
"dialog.model.unpaid.addMore.title": "從熱門提供者新增更多模型",
@@ -493,7 +492,6 @@ export const dict = {
"session.review.loadingChanges": "正在載入變更...",
"session.review.empty": "此工作階段暫無變更",
"session.review.noChanges": "沒有變更",
"session.review.noVcs": "未偵測到 git VCS,因此無法偵測到工作階段變更",
"session.files.selectToOpen": "選取要開啟的檔案",
"session.files.all": "所有檔案",
"session.files.binaryContent": "二進位檔案(無法顯示內容)",
@@ -508,11 +506,6 @@ export const dict = {
"session.todo.collapse": "折疊",
"session.todo.expand": "展開",
"session.modeSwitch.switching": "正在切換到 {{mode}} 模式…",
"session.modeSwitch.waiting": "等待目前任務完成",
"session.modeSwitch.notAvailable": "Agent 不可用",
"session.modeSwitch.fallback": '找不到 "{{requested}}",使用 "{{actual}}"',
"session.new.worktree.main": "主分支",
"session.new.worktree.mainWithBranch": "主分支 ({{branch}})",
"session.new.worktree.create": "建立新的 worktree",
@@ -608,8 +601,6 @@ export const dict = {
"settings.general.row.theme.description": "自訂 Kilo 的主題。",
"settings.general.row.font.title": "字型",
"settings.general.row.font.description": "自訂程式碼區塊使用的等寬字型",
"settings.general.row.reasoningSummaries.title": "顯示推理摘要",
"settings.general.row.reasoningSummaries.description": "在時間軸中顯示模型推理摘要",
"settings.general.row.shellToolPartsExpanded.title": "展開 shell 工具區塊",
"settings.general.row.shellToolPartsExpanded.description": "在時間軸中預設展開 shell 工具區塊",
+35 -18
View File
@@ -44,6 +44,7 @@ import { playSound, soundSrc } from "@/utils/sound"
import { createAim } from "@/utils/aim"
import { setNavigate } from "@/utils/notification-click"
import { Worktree as WorktreeState } from "@/utils/worktree"
import { setSessionHandoff } from "@/pages/session/handoff"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
@@ -67,7 +68,12 @@ import {
sortedRootSessions,
workspaceKey,
} from "./layout/helpers"
import { collectOpenProjectDeepLinks, deepLinkEvent, drainPendingDeepLinks } from "./layout/deep-links"
import {
collectNewSessionDeepLinks,
collectOpenProjectDeepLinks,
deepLinkEvent,
drainPendingDeepLinks,
} from "./layout/deep-links"
import { createInlineEditorController } from "./layout/inline-editor"
import {
LocalWorkspace,
@@ -1182,9 +1188,20 @@ export default function Layout(props: ParentProps) {
const handleDeepLinks = (urls: string[]) => {
if (!server.isLocal()) return
for (const directory of collectOpenProjectDeepLinks(urls)) {
openProject(directory)
}
for (const link of collectNewSessionDeepLinks(urls)) {
openProject(link.directory, false)
const slug = base64Encode(link.directory)
if (link.prompt) {
setSessionHandoff(slug, { prompt: link.prompt })
}
const href = link.prompt ? `/${slug}/session?prompt=${encodeURIComponent(link.prompt)}` : `/${slug}/session`
navigateWithSidebarReset(href)
}
}
onMount(() => {
@@ -1834,7 +1851,7 @@ export default function Layout(props: ParentProps) {
}}
style={{ width: panelProps.mobile ? undefined : `${Math.max(layout.sidebar.width() - 64, 0)}px` }}
>
<Show when={panelProps.project}>
<Show when={panelProps.project} keyed>
{(p) => (
<>
<div class="shrink-0 px-2 py-1">
@@ -1843,7 +1860,7 @@ export default function Layout(props: ParentProps) {
<InlineEditor
id={`project:${projectId()}`}
value={projectName}
onSave={(next) => renameProject(p(), next)}
onSave={(next) => renameProject(p, next)}
class="text-14-medium text-text-strong truncate"
displayClass="text-14-medium text-text-strong truncate"
stopPropagation
@@ -1852,7 +1869,7 @@ export default function Layout(props: ParentProps) {
<Tooltip
placement="bottom"
gutter={2}
value={p().worktree}
value={p.worktree}
class="shrink-0"
contentStyle={{
"max-width": "640px",
@@ -1860,7 +1877,7 @@ export default function Layout(props: ParentProps) {
}}
>
<span class="text-12-regular text-text-base truncate select-text">
{p().worktree.replace(homedir(), "~")}
{p.worktree.replace(homedir(), "~")}
</span>
</Tooltip>
</div>
@@ -1871,7 +1888,7 @@ export default function Layout(props: ParentProps) {
icon="dot-grid"
variant="ghost"
data-action="project-menu"
data-project={base64Encode(p().worktree)}
data-project={base64Encode(p.worktree)}
class="shrink-0 size-6 rounded-md data-[expanded]:bg-surface-base-active"
classList={{
"opacity-0 group-hover/project:opacity-100 data-[expanded]:opacity-100": !panelProps.mobile,
@@ -1880,24 +1897,24 @@ export default function Layout(props: ParentProps) {
/>
<DropdownMenu.Portal mount={!panelProps.mobile ? state.nav : undefined}>
<DropdownMenu.Content class="mt-1">
<DropdownMenu.Item onSelect={() => showEditProjectDialog(p())}>
<DropdownMenu.Item onSelect={() => showEditProjectDialog(p)}>
<DropdownMenu.ItemLabel>{language.t("common.edit")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Item
data-action="project-workspaces-toggle"
data-project={base64Encode(p().worktree)}
disabled={p().vcs !== "git" && !layout.sidebar.workspaces(p().worktree)()}
onSelect={() => toggleProjectWorkspaces(p())}
data-project={base64Encode(p.worktree)}
disabled={p.vcs !== "git" && !layout.sidebar.workspaces(p.worktree)()}
onSelect={() => toggleProjectWorkspaces(p)}
>
<DropdownMenu.ItemLabel>
{layout.sidebar.workspaces(p().worktree)()
{layout.sidebar.workspaces(p.worktree)()
? language.t("sidebar.workspaces.disable")
: language.t("sidebar.workspaces.enable")}
</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Item
data-action="project-clear-notifications"
data-project={base64Encode(p().worktree)}
data-project={base64Encode(p.worktree)}
disabled={unseenCount() === 0}
onSelect={clearNotifications}
>
@@ -1908,8 +1925,8 @@ export default function Layout(props: ParentProps) {
<DropdownMenu.Separator />
<DropdownMenu.Item
data-action="project-close-menu"
data-project={base64Encode(p().worktree)}
onSelect={() => closeProject(p().worktree)}
data-project={base64Encode(p.worktree)}
onSelect={() => closeProject(p.worktree)}
>
<DropdownMenu.ItemLabel>{language.t("common.close")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
@@ -1934,7 +1951,7 @@ export default function Layout(props: ParentProps) {
size="large"
icon="plus-small"
class="w-full"
onClick={() => navigateWithSidebarReset(`/${base64Encode(p().worktree)}/session`)}
onClick={() => navigateWithSidebarReset(`/${base64Encode(p.worktree)}/session`)}
>
{language.t("command.session.new")}
</Button>
@@ -1943,7 +1960,7 @@ export default function Layout(props: ParentProps) {
<div class="flex-1 min-h-0">
<LocalWorkspace
ctx={workspaceSidebarCtx}
project={p()}
project={p}
sortNow={sortNow}
mobile={panelProps.mobile}
/>
@@ -1958,7 +1975,7 @@ export default function Layout(props: ParentProps) {
keybind={command.keybind("workspace.new")}
placement="top"
>
<Button size="large" icon="plus-small" class="w-full" onClick={() => createWorkspace(p())}>
<Button size="large" icon="plus-small" class="w-full" onClick={() => createWorkspace(p)}>
{language.t("workspace.new")}
</Button>
</TooltipKeybind>
@@ -1984,7 +2001,7 @@ export default function Layout(props: ParentProps) {
<SortableWorkspace
ctx={workspaceSidebarCtx}
directory={directory}
project={p()}
project={p}
sortNow={sortNow}
mobile={panelProps.mobile}
/>
+24 -8
View File
@@ -1,15 +1,17 @@
export const deepLinkEvent = "opencode:deep-link"
export const parseDeepLink = (input: string) => {
const parseUrl = (input: string) => {
if (!input.startsWith("opencode://")) return
if (typeof URL.canParse === "function" && !URL.canParse(input)) return
const url = (() => {
try {
return new URL(input)
} catch {
return undefined
}
})()
try {
return new URL(input)
} catch {
return
}
}
export const parseDeepLink = (input: string) => {
const url = parseUrl(input)
if (!url) return
if (url.hostname !== "open-project") return
const directory = url.searchParams.get("directory")
@@ -17,9 +19,23 @@ export const parseDeepLink = (input: string) => {
return directory
}
export const parseNewSessionDeepLink = (input: string) => {
const url = parseUrl(input)
if (!url) return
if (url.hostname !== "new-session") return
const directory = url.searchParams.get("directory")
if (!directory) return
const prompt = url.searchParams.get("prompt") || undefined
if (!prompt) return { directory }
return { directory, prompt }
}
export const collectOpenProjectDeepLinks = (urls: string[]) =>
urls.map(parseDeepLink).filter((directory): directory is string => !!directory)
export const collectNewSessionDeepLinks = (urls: string[]) =>
urls.map(parseNewSessionDeepLink).filter((link): link is { directory: string; prompt?: string } => !!link)
type OpenCodeWindow = Window & {
__KILO__?: {
deepLinks?: string[]
+31 -10
View File
@@ -1,15 +1,14 @@
import { describe, expect, test } from "bun:test"
import { type Session } from "@kilocode/sdk/v2/client"
import { collectOpenProjectDeepLinks, drainPendingDeepLinks, parseDeepLink } from "./deep-links"
import {
displayName,
errorMessage,
getDraggableId,
hasProjectPermissions,
latestRootSession,
syncWorkspaceOrder,
workspaceKey,
} from "./helpers"
collectNewSessionDeepLinks,
collectOpenProjectDeepLinks,
drainPendingDeepLinks,
parseDeepLink,
parseNewSessionDeepLink,
} from "./deep-links"
import { displayName, errorMessage, getDraggableId, syncWorkspaceOrder, workspaceKey } from "./helpers"
import { type Session } from "@kilocode/sdk/v2/client"
import { hasProjectPermissions, latestRootSession } from "./helpers"
const session = (input: Partial<Session> & Pick<Session, "id" | "directory">) =>
({
@@ -62,6 +61,28 @@ describe("layout deep links", () => {
expect(result).toEqual(["/a", "/c"])
})
test("parses new-session deep links with optional prompt", () => {
expect(parseNewSessionDeepLink("opencode://new-session?directory=/tmp/demo")).toEqual({ directory: "/tmp/demo" })
expect(parseNewSessionDeepLink("opencode://new-session?directory=/tmp/demo&prompt=hello%20world")).toEqual({
directory: "/tmp/demo",
prompt: "hello world",
})
})
test("ignores new-session deep links without directory", () => {
expect(parseNewSessionDeepLink("opencode://new-session")).toBeUndefined()
expect(parseNewSessionDeepLink("opencode://new-session?directory=")).toBeUndefined()
})
test("collects only valid new-session deep links", () => {
const result = collectNewSessionDeepLinks([
"opencode://new-session?directory=/a",
"opencode://open-project?directory=/b",
"opencode://new-session?directory=/c&prompt=ship%20it",
])
expect(result).toEqual([{ directory: "/a" }, { directory: "/c", prompt: "ship it" }])
})
test("drains global deep links once", () => {
const target = {
__KILO__: {
+17 -17
View File
@@ -1,10 +1,4 @@
import { A, useNavigate, useParams } from "@solidjs/router"
import { useGlobalSync } from "@/context/global-sync"
import { useLanguage } from "@/context/language"
import { useLayout, type LocalProject, getAvatarColors } from "@/context/layout"
import { useNotification } from "@/context/notification"
import { usePermission } from "@/context/permission"
import { base64Encode } from "@opencode-ai/util/encode"
import type { Message, Session, TextPart, UserMessage } from "@kilocode/sdk/v2/client"
import { Avatar } from "@opencode-ai/ui/avatar"
import { HoverCard } from "@opencode-ai/ui/hover-card"
import { Icon } from "@opencode-ai/ui/icon"
@@ -12,12 +6,18 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
import { MessageNav } from "@opencode-ai/ui/message-nav"
import { Spinner } from "@opencode-ai/ui/spinner"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { base64Encode } from "@opencode-ai/util/encode"
import { getFilename } from "@opencode-ai/util/path"
import { type Message, type Session, type TextPart, type UserMessage } from "@kilocode/sdk/v2/client"
import { For, Match, Show, Switch, createMemo, onCleanup, type Accessor, type JSX } from "solid-js"
import { A, useNavigate, useParams } from "@solidjs/router"
import { type Accessor, createMemo, For, type JSX, Match, onCleanup, Show, Switch } from "solid-js"
import { useGlobalSync } from "@/context/global-sync"
import { useLanguage } from "@/context/language"
import { getAvatarColors, type LocalProject, useLayout } from "@/context/layout"
import { useNotification } from "@/context/notification"
import { usePermission } from "@/context/permission"
import { agentColor } from "@/utils/agent"
import { hasProjectPermissions } from "./helpers"
import { sessionPermissionRequest } from "../session/composer/session-request-tree"
import { hasProjectPermissions } from "./helpers"
const KILO_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
@@ -229,7 +229,9 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
const hoverEnabled = createMemo(() => (props.popover ?? true) && hoverAllowed())
const isActive = createMemo(() => props.session.id === params.id)
const hoverPrefetch = { current: undefined as ReturnType<typeof setTimeout> | undefined }
const hoverPrefetch = {
current: undefined as ReturnType<typeof setTimeout> | undefined,
}
const cancelHoverPrefetch = () => {
if (hoverPrefetch.current === undefined) return
clearTimeout(hoverPrefetch.current)
@@ -298,17 +300,15 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
setHoverSession={props.setHoverSession}
messageLabel={messageLabel}
onMessageSelect={(message) => {
if (!isActive()) {
if (!isActive())
layout.pendingMessage.set(`${base64Encode(props.session.directory)}/${props.session.id}`, message.id)
navigate(`${props.slug}/session/${props.session.id}`)
return
}
window.history.replaceState(null, "", `#message-${message.id}`)
window.dispatchEvent(new HashChangeEvent("hashchange"))
navigate(`${props.slug}/session/${props.session.id}#message-${message.id}`)
}}
trigger={item}
/>
</Show>
<div
class={`absolute ${props.dense ? "top-0.5 right-0.5" : "top-1 right-1"} flex items-center gap-0.5 transition-opacity`}
classList={{
+40 -22
View File
@@ -1,3 +1,5 @@
import type { UserMessage } from "@kilocode/sdk/v2"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import {
onCleanup,
Show,
@@ -10,7 +12,6 @@ import {
on,
onMount,
untrack,
createSignal,
} from "solid-js"
import { createMediaQuery } from "@solid-primitives/media"
import { createResizeObserver } from "@solid-primitives/resize-observer"
@@ -21,31 +22,28 @@ import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { Select } from "@opencode-ai/ui/select"
import { createAutoScroll } from "@opencode-ai/ui/hooks"
import { Mark } from "@opencode-ai/ui/logo"
import { useSync } from "@/context/sync"
import { useLayout } from "@/context/layout"
import { checksum, base64Encode } from "@opencode-ai/util/encode"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useLanguage } from "@/context/language"
import { useNavigate, useParams } from "@solidjs/router"
import { UserMessage } from "@kilocode/sdk/v2"
import { useSDK } from "@/context/sdk"
import { usePrompt } from "@/context/prompt"
import { base64Encode, checksum } from "@opencode-ai/util/encode"
import { useNavigate, useParams, useSearchParams } from "@solidjs/router"
import { NewSessionView, SessionHeader } from "@/components/session"
import { useComments } from "@/context/comments"
import { Identifier } from "@/utils/id" // kilocode_change
import { showToast } from "@opencode-ai/ui/toast" // kilocode_change
import { SessionHeader, NewSessionView } from "@/components/session"
import { same } from "@/utils/same"
import { useLanguage } from "@/context/language"
import { useLayout } from "@/context/layout"
import { usePrompt } from "@/context/prompt"
import { useSDK } from "@/context/sdk"
import { useSync } from "@/context/sync"
import { createSessionComposerState, SessionComposerRegion } from "@/pages/session/composer"
import { createOpenReviewFile } from "@/pages/session/helpers"
import { createScrollSpy } from "@/pages/session/scroll-spy"
import { SessionReviewTab, type DiffStyle, type SessionReviewTabProps } from "@/pages/session/review-tab"
import { TerminalPanel } from "@/pages/session/terminal-panel"
import { MessageTimeline } from "@/pages/session/message-timeline"
import { useSessionCommands } from "@/pages/session/use-session-commands"
import { SessionComposerRegion, createSessionComposerState } from "@/pages/session/composer"
import { type DiffStyle, SessionReviewTab, type SessionReviewTabProps } from "@/pages/session/review-tab"
import { createScrollSpy } from "@/pages/session/scroll-spy"
import { SessionMobileTabs } from "@/pages/session/session-mobile-tabs"
import { SessionSidePanel } from "@/pages/session/session-side-panel"
import { TerminalPanel } from "@/pages/session/terminal-panel"
import { useSessionCommands } from "@/pages/session/use-session-commands"
import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll"
import { same } from "@/utils/same"
import { Identifier } from "@/utils/id" // kilocode_change
import { showToast } from "@opencode-ai/ui/toast" // kilocode_change
const emptyUserMessages: UserMessage[] = []
@@ -268,6 +266,19 @@ export default function Page() {
const sdk = useSDK()
const prompt = usePrompt()
const comments = useComments()
const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>()
createEffect(() => {
if (!untrack(() => prompt.ready())) return
prompt.ready()
untrack(() => {
if (params.id || !prompt.ready()) return
const text = searchParams.prompt
if (!text) return
prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length)
setSearchParams({ ...searchParams, prompt: undefined })
})
})
const [ui, setUi] = createStore({
pendingMessage: undefined as string | undefined,
@@ -507,7 +518,10 @@ export default function Page() {
() => {
const msg = lastUserMessage()
if (!msg) return
if (msg.agent) local.agent.set(msg.agent)
if (msg.agent) {
local.agent.set(msg.agent)
if (local.agent.current()?.model) return
}
if (msg.model) local.model.set(msg.model)
},
),
@@ -781,7 +795,11 @@ export default function Page() {
on(
sessionKey,
() => {
setTree({ reviewScroll: undefined, pendingDiff: undefined, activeDiff: undefined })
setTree({
reviewScroll: undefined,
pendingDiff: undefined,
activeDiff: undefined,
})
},
{ defer: true },
),
@@ -8,6 +8,8 @@ import type { QuestionAnswer, QuestionRequest } from "@kilocode/sdk/v2"
import { useLanguage } from "@/context/language"
import { useSDK } from "@/context/sdk"
const cache = new Map<string, { tab: number; answers: QuestionAnswer[]; custom: string[]; customOn: boolean[] }>()
// kilocode_change start - add onModeAction prop for mode-switching support
export const SessionQuestionDock: Component<{
request: QuestionRequest
@@ -21,16 +23,18 @@ export const SessionQuestionDock: Component<{
const questions = createMemo(() => props.request.questions)
const total = createMemo(() => questions().length)
const cached = cache.get(props.request.id)
const [store, setStore] = createStore({
tab: 0,
answers: [] as QuestionAnswer[],
custom: [] as string[],
customOn: [] as boolean[],
tab: cached?.tab ?? 0,
answers: cached?.answers ?? ([] as QuestionAnswer[]),
custom: cached?.custom ?? ([] as string[]),
customOn: cached?.customOn ?? ([] as boolean[]),
editing: false,
sending: false,
})
let root: HTMLDivElement | undefined
let replied = false
const question = createMemo(() => questions()[store.tab])
const options = createMemo(() => question()?.options ?? [])
@@ -113,6 +117,16 @@ export const SessionQuestionDock: Component<{
})
})
onCleanup(() => {
if (replied) return
cache.set(props.request.id, {
tab: store.tab,
answers: store.answers.map((a) => (a ? [...a] : [])),
custom: store.custom.map((s) => s ?? ""),
customOn: store.customOn.map((b) => b ?? false),
})
})
const fail = (err: unknown) => {
const message = err instanceof Error ? err.message : String(err)
showToast({ title: language.t("common.requestFailed"), description: message })
@@ -125,6 +139,8 @@ export const SessionQuestionDock: Component<{
setStore("sending", true)
try {
await sdk.client.question.reply({ requestID: props.request.id, answers })
replied = true
cache.delete(props.request.id)
} catch (err) {
fail(err)
} finally {
@@ -139,6 +155,8 @@ export const SessionQuestionDock: Component<{
setStore("sending", true)
try {
await sdk.client.question.reject({ requestID: props.request.id })
replied = true
cache.delete(props.request.id)
} catch (err) {
fail(err)
} finally {
@@ -234,7 +234,6 @@ export function FileTabContent(props: { tab: string }) {
if (typeof window === "undefined") return
const onKeyDown = (event: KeyboardEvent) => {
if (event.defaultPrevented) return
if (tabs().active() !== props.tab) return
if (!(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey) return
if (event.key.toLowerCase() !== "f") return
@@ -0,0 +1,6 @@
export const messageIdFromHash = (hash: string) => {
const value = hash.startsWith("#") ? hash.slice(1) : hash
const match = value.match(/^message-(.+)$/)
if (!match) return
return match[1]
}
@@ -611,7 +611,7 @@ export function MessageTimeline(props: {
</Show>
</Show>
</div>
<Show when={sessionID()}>
<Show when={sessionID()} keyed>
{(id) => (
<div class="shrink-0 flex items-center gap-3">
<SessionContextUsage placement="bottom" />
@@ -646,12 +646,12 @@ export function MessageTimeline(props: {
>
<DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Item onSelect={() => void archiveSession(id())}>
<DropdownMenu.Item onSelect={() => void archiveSession(id)}>
<DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item
onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id()} />)}
onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id} />)}
>
<DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
@@ -331,7 +331,9 @@ export function SessionSidePanel(props: {
const path = createMemo(() => file.pathFromTab(tab))
return (
<div data-component="tabs-drag-preview">
<Show when={path()}>{(p) => <FileVisual active path={p()} />}</Show>
<Show when={path()} keyed>
{(p) => <FileVisual active path={p} />}
</Show>
</div>
)
}}
@@ -191,8 +191,8 @@ export function TerminalPanel() {
<SortableProvider ids={ids()}>
<For each={ids()}>
{(id) => (
<Show when={byId().get(id)}>
{(pty) => <SortableTerminalTab terminal={pty()} onClose={close} />}
<Show when={byId().get(id)} keyed>
{(pty) => <SortableTerminalTab terminal={pty} onClose={close} />}
</Show>
)}
</For>
@@ -217,10 +217,10 @@ export function TerminalPanel() {
<div class="flex-1 min-h-0 relative">
<Show when={terminal.active()} keyed>
{(id) => (
<Show when={byId().get(id)}>
<Show when={byId().get(id)} keyed>
{(pty) => (
<div id={`terminal-wrapper-${id}`} class="absolute inset-0">
<Terminal pty={pty()} onCleanup={terminal.update} onConnectError={() => terminal.clone(id)} />
<Terminal pty={pty} onCleanup={terminal.update} onConnectError={() => terminal.clone(id)} />
</div>
)}
</Show>
@@ -229,14 +229,14 @@ export function TerminalPanel() {
</div>
</div>
<DragOverlay>
<Show when={store.activeDraggable}>
<Show when={store.activeDraggable} keyed>
{(draggedId) => (
<Show when={byId().get(draggedId())}>
<Show when={byId().get(draggedId)} keyed>
{(t) => (
<div class="relative p-1 h-10 flex items-center bg-background-stronger text-14-regular">
{terminalTabLabel({
title: t().title,
titleNumber: t().titleNumber,
title: t.title,
titleNumber: t.titleNumber,
t: language.t as (key: string, vars?: Record<string, string | number | boolean>) => string,
})}
</div>
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import { messageIdFromHash } from "./use-session-hash-scroll"
import { messageIdFromHash } from "./message-id-from-hash"
describe("messageIdFromHash", () => {
test("parses hash with leading #", () => {
@@ -1,12 +1,9 @@
import { createEffect, createMemo, onCleanup, onMount } from "solid-js"
import { UserMessage } from "@kilocode/sdk/v2"
import type { UserMessage } from "@kilocode/sdk/v2"
import { useLocation, useNavigate } from "@solidjs/router"
import { createEffect, createMemo, onMount } from "solid-js"
import { messageIdFromHash } from "./message-id-from-hash"
export const messageIdFromHash = (hash: string) => {
const value = hash.startsWith("#") ? hash.slice(1) : hash
const match = value.match(/^message-(.+)$/)
if (!match) return
return match[1]
}
export { messageIdFromHash } from "./message-id-from-hash"
export const useSessionHashScroll = (input: {
sessionKey: () => string
@@ -30,13 +27,18 @@ export const useSessionHashScroll = (input: {
const messageIndex = createMemo(() => new Map(visibleUserMessages().map((m, i) => [m.id, i])))
let pendingKey = ""
const location = useLocation()
const navigate = useNavigate()
const clearMessageHash = () => {
if (!window.location.hash) return
window.history.replaceState(null, "", window.location.href.replace(/#.*$/, ""))
if (!location.hash) return
navigate(location.pathname + location.search, { replace: true })
}
const updateHash = (id: string) => {
window.history.replaceState(null, "", `#${input.anchor(id)}`)
navigate(location.pathname + location.search + `#${input.anchor(id)}`, {
replace: true,
})
}
const scrollToElement = (el: HTMLElement, behavior: ScrollBehavior) => {
@@ -53,6 +55,7 @@ export const useSessionHashScroll = (input: {
}
const scrollToMessage = (message: UserMessage, behavior: ScrollBehavior = "smooth") => {
console.log({ message, behavior })
if (input.currentMessageId() !== message.id) input.setActiveMessage(message)
const index = messageIndex().get(message.id) ?? -1
@@ -100,7 +103,7 @@ export const useSessionHashScroll = (input: {
}
const applyHash = (behavior: ScrollBehavior) => {
const hash = window.location.hash.slice(1)
const hash = location.hash.slice(1)
if (!hash) {
input.autoScroll.forceScrollToBottom()
const el = input.scroller()
@@ -132,6 +135,7 @@ export const useSessionHashScroll = (input: {
}
createEffect(() => {
location.hash
if (!input.sessionID() || !input.messagesReady()) return
requestAnimationFrame(() => applyHash("auto"))
})
@@ -155,7 +159,7 @@ export const useSessionHashScroll = (input: {
}
}
if (!targetId) targetId = messageIdFromHash(window.location.hash)
if (!targetId) targetId = messageIdFromHash(location.hash)
if (!targetId) return
if (input.currentMessageId() === targetId) return
@@ -171,14 +175,6 @@ export const useSessionHashScroll = (input: {
if (typeof window !== "undefined" && "scrollRestoration" in window.history) {
window.history.scrollRestoration = "manual"
}
const handler = () => {
if (!input.sessionID() || !input.messagesReady()) return
requestAnimationFrame(() => applyHash("auto"))
}
window.addEventListener("hashchange", handler)
onCleanup(() => window.removeEventListener("hashchange", handler))
})
return {
+3 -2
View File
@@ -7,6 +7,7 @@ export const setNavigate = (fn: (href: string) => void) => {
export const handleNotificationClick = (href?: string) => {
window.focus()
if (!href) return
if (nav) nav(href)
else window.location.assign(href)
if (nav) return nav(href)
console.warn("notification-click: navigate function not set, falling back to window.location.assign")
window.location.assign(href)
}
+76 -14
View File
@@ -1,8 +1,37 @@
import { describe, expect, test } from "bun:test"
import type { ConfigInvalidError } from "./server-errors"
import { formatServerError, parseReabaleConfigInvalidError } from "./server-errors"
import type { ConfigInvalidError, ProviderModelNotFoundError } from "./server-errors"
import { formatServerError, parseReadableConfigInvalidError } from "./server-errors"
describe("parseReabaleConfigInvalidError", () => {
function fill(text: string, vars?: Record<string, string | number>) {
if (!vars) return text
return text.replace(/{{\s*(\w+)\s*}}/g, (_, key: string) => {
const value = vars[key]
if (value === undefined) return ""
return String(value)
})
}
function useLanguageMock() {
const dict: Record<string, string> = {
"error.chain.unknown": "Erro desconhecido",
"error.chain.configInvalid": "Arquivo de config em {{path}} invalido",
"error.chain.configInvalidWithMessage": "Arquivo de config em {{path}} invalido: {{message}}",
"error.chain.modelNotFound": "Modelo nao encontrado: {{provider}}/{{model}}",
"error.chain.didYouMean": "Voce quis dizer: {{suggestions}}",
"error.chain.checkConfig": "Revise provider/model no config",
}
return {
t(key: string, vars?: Record<string, string | number>) {
const text = dict[key]
if (!text) return key
return fill(text, vars)
},
}
}
const language = useLanguageMock()
describe("parseReadableConfigInvalidError", () => {
test("formats issues with file path", () => {
const error = {
name: "ConfigInvalidError",
@@ -15,10 +44,10 @@ describe("parseReabaleConfigInvalidError", () => {
},
} satisfies ConfigInvalidError
const result = parseReabaleConfigInvalidError(error)
const result = parseReadableConfigInvalidError(error, language.t)
expect(result).toBe(
["Invalid configuration", "opencode.config.ts", "settings.host: Required", "mode: Invalid"].join("\n"),
["Arquivo de config em opencode.config.ts invalido: settings.host: Required", "mode: Invalid"].join("\n"),
)
})
@@ -31,9 +60,9 @@ describe("parseReabaleConfigInvalidError", () => {
},
} satisfies ConfigInvalidError
const result = parseReabaleConfigInvalidError(error)
const result = parseReadableConfigInvalidError(error, language.t)
expect(result).toBe(["Invalid configuration", "Bad value"].join("\n"))
expect(result).toBe("Arquivo de config em config invalido: Bad value")
})
})
@@ -46,24 +75,57 @@ describe("formatServerError", () => {
},
} satisfies ConfigInvalidError
const result = formatServerError(error)
const result = formatServerError(error, language.t)
expect(result).toBe(["Invalid configuration", "Missing host"].join("\n"))
expect(result).toBe("Arquivo de config em config invalido: Missing host")
})
test("returns error messages", () => {
expect(formatServerError(new Error("Request failed with status 503"))).toBe("Request failed with status 503")
expect(formatServerError(new Error("Request failed with status 503"), language.t)).toBe(
"Request failed with status 503",
)
})
test("returns provided string errors", () => {
expect(formatServerError("Failed to connect to server")).toBe("Failed to connect to server")
expect(formatServerError("Failed to connect to server", language.t)).toBe("Failed to connect to server")
})
test("falls back to unknown", () => {
expect(formatServerError(0)).toBe("Unknown error")
test("uses translated unknown fallback", () => {
expect(formatServerError(0, language.t)).toBe("Erro desconhecido")
})
test("falls back for unknown error objects and names", () => {
expect(formatServerError({ name: "ServerTimeoutError", data: { seconds: 30 } })).toBe("Unknown error")
expect(formatServerError({ name: "ServerTimeoutError", data: { seconds: 30 } }, language.t)).toBe(
"Erro desconhecido",
)
})
test("formats provider model errors using provider/model", () => {
const error = {
name: "ProviderModelNotFoundError",
data: {
providerID: "openai",
modelID: "gpt-4.1",
},
} satisfies ProviderModelNotFoundError
expect(formatServerError(error, language.t)).toBe(
["Modelo nao encontrado: openai/gpt-4.1", "Revise provider/model no config"].join("\n"),
)
})
test("formats provider model suggestions", () => {
const error = {
name: "ProviderModelNotFoundError",
data: {
providerID: "x",
modelID: "y",
suggestions: ["x/y2", "x/y3"],
},
} satisfies ProviderModelNotFoundError
expect(formatServerError(error, language.t)).toBe(
["Modelo nao encontrado: x/y", "Voce quis dizer: x/y2, x/y3", "Revise provider/model no config"].join("\n"),
)
})
})
+55 -24
View File
@@ -7,28 +7,31 @@ export type ConfigInvalidError = {
}
}
type Label = {
unknown: string
invalidConfiguration: string
}
const fallback: Label = {
unknown: "Unknown error",
invalidConfiguration: "Invalid configuration",
}
function resolveLabel(labels: Partial<Label> | undefined): Label {
return {
unknown: labels?.unknown ?? fallback.unknown,
invalidConfiguration: labels?.invalidConfiguration ?? fallback.invalidConfiguration,
export type ProviderModelNotFoundError = {
name: "ProviderModelNotFoundError"
data: {
providerID: string
modelID: string
suggestions?: string[]
}
}
export function formatServerError(error: unknown, labels?: Partial<Label>) {
if (isConfigInvalidErrorLike(error)) return parseReabaleConfigInvalidError(error, labels)
type Translator = (key: string, vars?: Record<string, string | number>) => string
function tr(translator: Translator | undefined, key: string, text: string, vars?: Record<string, string | number>) {
if (!translator) return text
const out = translator(key, vars)
if (!out || out === key) return text
return out
}
export function formatServerError(error: unknown, translate?: Translator, fallback?: string) {
if (isConfigInvalidErrorLike(error)) return parseReadableConfigInvalidError(error, translate)
if (isProviderModelNotFoundErrorLike(error)) return parseReadableProviderModelNotFoundError(error, translate)
if (error instanceof Error && error.message) return error.message
if (typeof error === "string" && error) return error
return resolveLabel(labels).unknown
if (fallback) return fallback
return tr(translate, "error.chain.unknown", "Unknown error")
}
function isConfigInvalidErrorLike(error: unknown): error is ConfigInvalidError {
@@ -37,13 +40,41 @@ function isConfigInvalidErrorLike(error: unknown): error is ConfigInvalidError {
return o.name === "ConfigInvalidError" && typeof o.data === "object" && o.data !== null
}
export function parseReabaleConfigInvalidError(errorInput: ConfigInvalidError, labels?: Partial<Label>) {
const head = resolveLabel(labels).invalidConfiguration
const file = errorInput.data.path && errorInput.data.path !== "config" ? errorInput.data.path : ""
function isProviderModelNotFoundErrorLike(error: unknown): error is ProviderModelNotFoundError {
if (typeof error !== "object" || error === null) return false
const o = error as Record<string, unknown>
return o.name === "ProviderModelNotFoundError" && typeof o.data === "object" && o.data !== null
}
export function parseReadableConfigInvalidError(errorInput: ConfigInvalidError, translator?: Translator) {
const file = errorInput.data.path && errorInput.data.path !== "config" ? errorInput.data.path : "config"
const detail = errorInput.data.message?.trim() ?? ""
const issues = (errorInput.data.issues ?? []).map((issue) => {
return `${issue.path.join(".")}: ${issue.message}`
const issues = (errorInput.data.issues ?? [])
.map((issue) => {
const msg = issue.message.trim()
if (!issue.path.length) return msg
return `${issue.path.join(".")}: ${msg}`
})
.filter(Boolean)
const msg = issues.length ? issues.join("\n") : detail
if (!msg) return tr(translator, "error.chain.configInvalid", `Config file at ${file} is invalid`, { path: file })
return tr(translator, "error.chain.configInvalidWithMessage", `Config file at ${file} is invalid: ${msg}`, {
path: file,
message: msg,
})
if (issues.length) return [head, file, "", ...issues].filter(Boolean).join("\n")
return [head, file, detail].filter(Boolean).join("\n")
}
function parseReadableProviderModelNotFoundError(errorInput: ProviderModelNotFoundError, translator?: Translator) {
const p = errorInput.data.providerID.trim()
const m = errorInput.data.modelID.trim()
const list = (errorInput.data.suggestions ?? []).map((v) => v.trim()).filter(Boolean)
const body = tr(translator, "error.chain.modelNotFound", `Model not found: ${p}/${m}`, { provider: p, model: m })
const tail = tr(translator, "error.chain.checkConfig", "Check your config (opencode.json) provider/model names")
if (list.length) {
const suggestions = list.slice(0, 5).join(", ")
return [body, tr(translator, "error.chain.didYouMean", `Did you mean: ${suggestions}`, { suggestions }), tail].join(
"\n",
)
}
return [body, tail].join("\n")
}
+1 -2
View File
@@ -22,6 +22,5 @@
}
},
"include": ["src", "package.json"],
"exclude": ["dist", "ts-dist"],
"references": [{ "path": "../sdk/js" }]
"exclude": ["dist", "ts-dist"]
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop-electron",
"private": true,
"version": "7.0.46",
"version": "7.0.47",
"type": "module",
"license": "MIT",
"homepage": "https://opencode.ai",
@@ -12,19 +12,7 @@
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.automation.apple-events</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.personal-information.addressbook</key>
<true/>
<key>com.apple.security.personal-information.calendars</key>
<true/>
<key>com.apple.security.personal-information.location</key>
<true/>
<key>com.apple.security.personal-information.photos-library</key>
<true/>
</dict>
</plist>
+29 -15
View File
@@ -1,14 +1,18 @@
import { app, BrowserWindow, dialog } from "electron"
import type { Event } from "electron"
import pkg from "electron-updater"
import { randomUUID } from "node:crypto"
import { EventEmitter } from "node:events"
import { existsSync } from "node:fs"
import { createServer } from "node:net"
import { homedir } from "node:os"
import { join } from "node:path"
import { createServer } from "node:net"
import type { Event } from "electron"
import { app, type BrowserWindow, dialog } from "electron"
import pkg from "electron-updater"
const APP_NAMES: Record<string, string> = { dev: "OpenCode Dev", beta: "OpenCode Beta", prod: "OpenCode" }
const APP_NAMES: Record<string, string> = {
dev: "OpenCode Dev",
beta: "OpenCode Beta",
prod: "OpenCode",
}
const APP_IDS: Record<string, string> = {
dev: "ai.opencode.desktop.dev",
beta: "ai.opencode.desktop.beta",
@@ -18,7 +22,9 @@ app.setName(app.isPackaged ? APP_NAMES[CHANNEL] : "OpenCode Dev")
app.setPath("userData", join(app.getPath("appData"), app.isPackaged ? APP_IDS[CHANNEL] : "ai.opencode.desktop.dev"))
const { autoUpdater } = pkg
import type { InitStep, ServerReadyData, SqliteMigrationProgress, WslConfig } from "../preload/types"
import { checkAppExists, resolveAppPath, wslPath } from "./apps"
import type { CommandChild } from "./cli"
import { installCli, syncCli } from "./cli"
import { CHANNEL, UPDATER_ENABLED } from "./constants"
import { registerIpcHandlers, sendDeepLinks, sendMenuCommand, sendSqliteMigrationProgress } from "./ipc"
@@ -37,9 +43,6 @@ import {
} from "./server"
import { createLoadingWindow, createMainWindow, setDockIcon } from "./windows"
import type { InitStep, ServerReadyData, SqliteMigrationProgress, WslConfig } from "../preload/types"
import type { CommandChild } from "./cli"
type ServerConnection =
| { variant: "existing"; url: string }
| {
@@ -56,16 +59,19 @@ const initEmitter = new EventEmitter()
let initStep: InitStep = { phase: "server_waiting" }
let mainWindow: BrowserWindow | null = null
let loadingWindow: BrowserWindow | null = null
const loadingWindow: BrowserWindow | null = null
let sidecar: CommandChild | null = null
let loadingComplete = defer<void>()
const loadingComplete = defer<void>()
const pendingDeepLinks: string[] = []
const serverReady = defer<ServerReadyData>()
const logger = initLogging()
logger.log("app starting", { version: app.getVersion(), packaged: app.isPackaged })
logger.log("app starting", {
version: app.getVersion(),
packaged: app.isPackaged,
})
setupApp()
@@ -162,7 +168,10 @@ async function initialize() {
const loadingTask = (async () => {
logger.log("setting up server connection")
const serverConnection = await setupServerConnection()
logger.log("server connection ready", { variant: serverConnection.variant, url: serverConnection.url })
logger.log("server connection ready", {
variant: serverConnection.variant,
url: serverConnection.url,
})
const cliHealthCheck = (() => {
if (serverConnection.variant == "cli") {
@@ -175,7 +184,10 @@ async function initialize() {
if (progress.type === "Done") sqliteDone?.resolve()
})
await health.wait
serverReady.resolve({ url: serverConnection.url, password: serverConnection.password })
serverReady.resolve({
url: serverConnection.url,
password: serverConnection.password,
})
}
} else {
serverReady.resolve({ url: serverConnection.url, password: null })
@@ -370,8 +382,10 @@ async function checkUpdate() {
files: updateInfo?.files?.map((file) => file.url) ?? [],
})
const version = result?.updateInfo?.version
if (!version) {
logger.log("no update available", { reason: "provider returned no newer version" })
if (result?.isUpdateAvailable === false || !version) {
logger.log("no update available", {
reason: "provider returned no newer version",
})
return { updateAvailable: false }
}
logger.log("update available", { version })
+1 -1
View File
@@ -7,4 +7,4 @@
/// <reference path="../../sst-env.d.ts" />
import "sst"
export {}
export {}
@@ -12,19 +12,7 @@
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.automation.apple-events</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.personal-information.addressbook</key>
<true/>
<key>com.apple.security.personal-information.calendars</key>
<true/>
<key>com.apple.security.personal-information.location</key>
<true/>
<key>com.apple.security.personal-information.photos-library</key>
<true/>
</dict>
</plist>
+1 -1
View File
@@ -697,7 +697,7 @@ fn opencode_db_path() -> Result<PathBuf, &'static str> {
}
};
Ok(data_home.join("kilo").join("kilo.db")) // kilocode_change
Ok(data_home.join("opencode").join("kilo.db"))
}
// Creates a `once` listener for the specified event and returns a future that resolves
+6 -6
View File
@@ -1,7 +1,7 @@
id = "kilo"
name = "Kilo"
description = "The open source coding agent."
version = "7.0.47"
version = "1.2.18"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/Kilo-Org/kilocode"
@@ -11,26 +11,26 @@ name = "Kilo"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.47/opencode-darwin-arm64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.2.18/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.47/opencode-darwin-x64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.2.18/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.47/opencode-linux-arm64.tar.gz"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.2.18/opencode-linux-arm64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.47/opencode-linux-x64.tar.gz"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.2.18/opencode-linux-x64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.0.47/opencode-windows-x64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.2.18/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
+1 -3
View File
@@ -12,9 +12,7 @@ export const KiloClawNav: NavSection[] = [
{
href: "/kiloclaw/development-tools/github",
children: "Development Tools",
subLinks: [
{ href: "/kiloclaw/development-tools/github", children: "GitHub" },
],
subLinks: [{ href: "/kiloclaw/development-tools/github", children: "GitHub" }],
},
{ href: "/kiloclaw/version-pinning", children: "Version Pinning" },
{ href: "/kiloclaw/troubleshooting", children: "Troubleshooting" },
+7 -3
View File
@@ -32,9 +32,13 @@ module.exports = withMarkdoc(/* config: https://markdoc.io/docs/nextjs#options *
},
],
afterFiles: [
{ source: "/ingest/static/:path*", destination: "https://us-assets.i.posthog.com/static/:path*", basePath: false },
{ source: "/ingest/decide", destination: "https://us.i.posthog.com/decide", basePath: false },
{ source: "/ingest/:path*", destination: "https://us.i.posthog.com/:path*", basePath: false }, // catch-all must be last
{
source: "/ingest/static/:path*",
destination: "https://us-assets.i.posthog.com/static/:path*",
basePath: false,
},
{ source: "/ingest/decide", destination: "https://us.i.posthog.com/decide", basePath: false },
{ source: "/ingest/:path*", destination: "https://us.i.posthog.com/:path*", basePath: false }, // catch-all must be last
],
}
},
@@ -27,21 +27,21 @@ Once you have a GitHub account ready, continue to Step 2.
KiloClaw uses a [fine grained Personal Access Token](https://github.com/settings/tokens?type=beta) to authenticate as your bot. When creating the token, use these settings:
| Setting | Recommended Value |
| --- | --- |
| **Token name** | `kiloclaw-bot` (or any descriptive name) |
| **Expiration** | 90 days (set a reminder to rotate) |
| Setting | Recommended Value |
| --------------------- | ----------------------------------------- |
| **Token name** | `kiloclaw-bot` (or any descriptive name) |
| **Expiration** | 90 days (set a reminder to rotate) |
| **Repository access** | All repositories, or select specific ones |
Grant the following permissions:
| Permission | Access Level | Purpose |
| --- | --- | --- |
| **Contents** | Read & Write | Clone repos, push commits |
| **Pull requests** | Read & Write | Open and manage pull requests |
| **Issues** | Read & Write | Create and comment on issues |
| **Metadata** | Read only | List repositories and basic repo info |
| **Workflows** | Read & Write | Trigger and manage GitHub Actions workflows |
| Permission | Access Level | Purpose |
| ----------------- | ------------ | ------------------------------------------- |
| **Contents** | Read & Write | Clone repos, push commits |
| **Pull requests** | Read & Write | Open and manage pull requests |
| **Issues** | Read & Write | Create and comment on issues |
| **Metadata** | Read only | List repositories and basic repo info |
| **Workflows** | Read & Write | Trigger and manage GitHub Actions workflows |
### Step 3: Enter credentials in KiloClaw
+6 -6
View File
@@ -80,15 +80,13 @@
"@gitlab/opencode-gitlab-auth": "1.3.3",
"@hono/standard-validator": "0.1.5",
"@hono/zod-validator": "catalog:",
"@kilocode/kilo-gateway": "workspace:*",
"@kilocode/kilo-telemetry": "workspace:*",
"@kilocode/plugin": "workspace:*",
"@kilocode/sdk": "workspace:*",
"@modelcontextprotocol/sdk": "1.25.2",
"@octokit/graphql": "9.0.2",
"@octokit/rest": "catalog:",
"@openauthjs/openauth": "catalog:",
"@kilocode/plugin": "workspace:*",
"@opencode-ai/script": "workspace:*",
"@kilocode/sdk": "workspace:*",
"@opencode-ai/util": "workspace:*",
"@openrouter/ai-sdk-provider": "1.5.4",
"@opentui/core": "0.1.86",
@@ -122,7 +120,6 @@
"opentui-spinner": "0.0.6",
"partial-json": "0.1.7",
"remeda": "catalog:",
"simple-git": "3.31.1",
"solid-js": "catalog:",
"strip-ansi": "7.1.2",
"tree-sitter-bash": "0.25.0",
@@ -133,7 +130,10 @@
"xdg-basedir": "5.1.0",
"yargs": "18.0.0",
"zod": "catalog:",
"zod-to-json-schema": "3.24.5"
"zod-to-json-schema": "3.24.5",
"@kilocode/kilo-gateway": "workspace:*",
"@kilocode/kilo-telemetry": "workspace:*",
"simple-git": "3.31.1"
},
"overrides": {
"drizzle-orm": "1.0.0-beta.12-a5629fb"
+44 -69
View File
@@ -1,10 +1,10 @@
#!/usr/bin/env bun
import solidPlugin from "../node_modules/@opentui/solid/scripts/solid-plugin"
import path from "path"
import fs from "fs"
import { $ } from "bun"
import fs from "fs"
import path from "path"
import { fileURLToPath } from "url"
import solidPlugin from "../node_modules/@opentui/solid/scripts/solid-plugin"
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
@@ -12,9 +12,10 @@ const dir = path.resolve(__dirname, "..")
process.chdir(dir)
import pkg from "../package.json"
import { Script } from "@opencode-ai/script"
const modelsUrl = process.env.KILO_MODELS_URL || "https://models.dev" // kilocode_change
import pkg from "../package.json"
const modelsUrl = process.env.KILO_MODELS_URL || "https://models.dev"
// Fetch and generate models.dev snapshot
const modelsData = process.env.MODELS_DEV_API_JSON
? await Bun.file(process.env.MODELS_DEV_API_JSON).text()
@@ -25,6 +26,36 @@ await Bun.write(
)
console.log("Generated models-snapshot.ts")
// Load migrations from migration directories
const migrationDirs = (
await fs.promises.readdir(path.join(dir, "migration"), {
withFileTypes: true,
})
)
.filter((entry) => entry.isDirectory() && /^\d{4}\d{2}\d{2}\d{2}\d{2}\d{2}/.test(entry.name))
.map((entry) => entry.name)
.sort()
const migrations = await Promise.all(
migrationDirs.map(async (name) => {
const file = path.join(dir, "migration", name, "migration.sql")
const sql = await Bun.file(file).text()
const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(name)
const timestamp = match
? Date.UTC(
Number(match[1]),
Number(match[2]) - 1,
Number(match[3]),
Number(match[4]),
Number(match[5]),
Number(match[6]),
)
: 0
return { sql, timestamp }
}),
)
console.log(`Loaded ${migrations.length} migrations`)
const singleFlag = process.argv.includes("--single")
const baselineFlag = process.argv.includes("--baseline")
const skipInstall = process.argv.includes("--skip-install")
@@ -109,30 +140,6 @@ const targets = singleFlag
})
: allTargets
const migrationDirs = (await fs.promises.readdir(path.join(dir, "migration"), { withFileTypes: true }))
.filter((entry) => entry.isDirectory() && /^\d{4}\d{2}\d{2}\d{2}\d{2}\d{2}/.test(entry.name))
.map((entry) => entry.name)
.sort()
const migrationEntries = await Promise.all(
migrationDirs.map(async (name) => {
const sql = await Bun.file(path.join(dir, "migration", name, "migration.sql")).text()
const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(name)
const timestamp = match
? Date.UTC(
Number(match[1]),
Number(match[2]) - 1,
Number(match[3]),
Number(match[4]),
Number(match[5]),
Number(match[6]),
)
: 0
return { sql, timestamp }
}),
)
console.log(`Loaded ${migrationEntries.length} migrations`)
await $`rm -rf dist`
const binaries: Record<string, string> = {}
@@ -169,46 +176,24 @@ for (const item of targets) {
compile: {
autoloadBunfig: false,
autoloadDotenv: false,
//@ts-ignore (bun types aren't up to date)
autoloadTsconfig: true,
autoloadPackageJson: true,
target: name.replace(pkg.name, "bun") as any,
outfile: `dist/${name}/bin/kilo`,
execArgv: [`--user-agent=kilo/${Script.version}`, "--use-system-ca", "--"],
outfile: `dist/${name}/bin/opencode`,
execArgv: [`--user-agent=opencode/${Script.version}`, "--use-system-ca", "--"],
windows: {},
},
entrypoints: ["./src/index.ts", parserWorker, workerPath],
define: {
KILO_VERSION: `'${Script.version}'`, // kilocode_change
KILO_VERSION: `'${Script.version}'`,
KILO_MIGRATIONS: JSON.stringify(migrations),
OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + workerRelativePath,
KILO_WORKER_PATH: workerPath,
KILO_CHANNEL: `'${Script.channel}'`, // kilocode_change
KILO_CHANNEL: `'${Script.channel}'`,
KILO_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "",
KILO_MIGRATIONS: JSON.stringify(migrationEntries),
},
})
// kilocode_change start - fix Nix-specific ELF interpreter paths for Linux binaries
if (item.os === "linux") {
const interpreters: Record<string, string> = {
x64: "/lib64/ld-linux-x86-64.so.2",
arm64: "/lib/ld-linux-aarch64.so.1",
"x64-musl": "/lib/ld-musl-x86_64.so.1",
"arm64-musl": "/lib/ld-musl-aarch64.so.1",
}
const key = item.abi === "musl" ? `${item.arch}-musl` : item.arch
const interpreter = interpreters[key]
if (interpreter) {
try {
await $`patchelf --set-interpreter ${interpreter} dist/${name}/bin/kilo`
console.log(`patched interpreter for ${name} -> ${interpreter}`)
} catch {
console.warn(`patchelf not available, skipping interpreter fix for ${name}`)
}
}
}
// kilocode_change end
await $`rm -rf ./dist/${name}/bin/tui`
await Bun.file(`dist/${name}/package.json`).write(
JSON.stringify(
@@ -217,10 +202,6 @@ for (const item of targets) {
version: Script.version,
os: [item.os],
cpu: [item.arch],
repository: {
type: "git",
url: "https://github.com/Kilo-Org/kilocode",
},
},
null,
2,
@@ -230,20 +211,14 @@ for (const item of targets) {
}
if (Script.release) {
const archives: string[] = [] // kilocode_change
for (const key of Object.keys(binaries)) {
const archive = key.replace(pkg.name, "kilo") // kilocode_change
if (key.includes("linux")) {
const out = path.resolve("dist", `${archive}.tar.gz`) // kilocode_change
await $`tar -czf ${out} *`.cwd(`dist/${key}/bin`) // kilocode_change
archives.push(out) // kilocode_change
await $`tar -czf ../../${key}.tar.gz *`.cwd(`dist/${key}/bin`)
} else {
const out = path.resolve("dist", `${archive}.zip`) // kilocode_change
await $`zip -r ${out} *`.cwd(`dist/${key}/bin`) // kilocode_change
archives.push(out) // kilocode_change
await $`zip -r ../../${key}.zip *`.cwd(`dist/${key}/bin`)
}
}
await $`gh release upload v${Script.version} ${archives} --clobber` // kilocode_change
await $`gh release upload v${Script.version} ./dist/*.zip ./dist/*.tar.gz --clobber --repo ${process.env.GH_REPO}`
}
export { binaries }
+32 -44
View File
@@ -8,8 +8,7 @@ const dir = fileURLToPath(new URL("..", import.meta.url))
process.chdir(dir)
const binaries: Record<string, string> = {}
for (const filepath of new Bun.Glob("*/*/package.json").scanSync({ cwd: "./dist" })) {
// kilocode_change
for (const filepath of new Bun.Glob("*/package.json").scanSync({ cwd: "./dist" })) {
const pkg = await Bun.file(`./dist/${filepath}`).json()
binaries[pkg.name] = pkg.version
}
@@ -20,17 +19,13 @@ await $`mkdir -p ./dist/${pkg.name}`
await $`cp -r ./bin ./dist/${pkg.name}/bin`
await $`cp ./script/postinstall.mjs ./dist/${pkg.name}/postinstall.mjs`
await Bun.file(`./dist/${pkg.name}/LICENSE`).write(await Bun.file("../../LICENSE").text())
await Bun.file(`./dist/${pkg.name}/README.md`).write(await Bun.file("./README.md").text()) // kilocode_change
await Bun.file(`./dist/${pkg.name}/package.json`).write(
JSON.stringify(
{
name: pkg.name, // kilocode_change
name: pkg.name + "-ai",
bin: {
// kilocode_change start
kilo: `./bin/kilo`,
kilocode: `./bin/kilo`,
// kilocode_change end
[pkg.name]: `./bin/${pkg.name}`,
},
scripts: {
postinstall: "bun ./postinstall.mjs || node ./postinstall.mjs",
@@ -38,12 +33,6 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write(
version: version,
license: pkg.license,
optionalDependencies: binaries,
// kilocode_change start
repository: {
type: "git",
url: "https://github.com/Kilo-Org/kilocode",
},
// kilocode_change end
},
null,
2,
@@ -55,12 +44,12 @@ const tasks = Object.entries(binaries).map(async ([name]) => {
await $`chmod -R 755 .`.cwd(`./dist/${name}`)
}
await $`bun pm pack`.cwd(`./dist/${name}`)
await $`npm publish *.tgz --access public --tag ${Script.channel} --provenance`.cwd(`./dist/${name}`) // kilocode_change
await $`npm publish *.tgz --access public --tag ${Script.channel}`.cwd(`./dist/${name}`)
})
await Promise.all(tasks)
await $`cd ./dist/${pkg.name} && bun pm pack && npm publish *.tgz --access public --tag ${Script.channel} --provenance` // kilocode_change
await $`cd ./dist/${pkg.name} && bun pm pack && npm publish *.tgz --access public --tag ${Script.channel}`
const image = "ghcr.io/kilo-org/kilo" // kilocode_change
const image = "ghcr.io/Kilo-Org/kilocode"
const platforms = "linux/amd64,linux/arm64"
const tags = [`${image}:${version}`, `${image}:${Script.channel}`]
const tagFlags = tags.flatMap((t) => ["-t", t])
@@ -69,20 +58,19 @@ await $`docker buildx build --platform ${platforms} ${tagFlags} --push .`
// registries
if (!Script.preview) {
// Calculate SHA values
// kilocode_change start
const arm64Sha = await $`sha256sum ./dist/kilo-linux-arm64.tar.gz | cut -d' ' -f1`.text().then((x) => x.trim())
const x64Sha = await $`sha256sum ./dist/kilo-linux-x64.tar.gz | cut -d' ' -f1`.text().then((x) => x.trim())
const macX64Sha = await $`sha256sum ./dist/kilo-darwin-x64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
const macArm64Sha = await $`sha256sum ./dist/kilo-darwin-arm64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
// kilocode_change end
const arm64Sha = await $`sha256sum ./dist/opencode-linux-arm64.tar.gz | cut -d' ' -f1`.text().then((x) => x.trim())
const x64Sha = await $`sha256sum ./dist/opencode-linux-x64.tar.gz | cut -d' ' -f1`.text().then((x) => x.trim())
const macX64Sha = await $`sha256sum ./dist/opencode-darwin-x64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
const macArm64Sha = await $`sha256sum ./dist/opencode-darwin-arm64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
const [pkgver, _subver = ""] = Script.version.split(/(-.*)/, 2)
// arch
const binaryPkgbuild = [
"# Maintainer: kilo", // kilocode_change
"# Maintainer: dax",
"# Maintainer: adam",
"",
"pkgname='kilo-bin'",
"pkgname='opencode-bin'",
`pkgver=${pkgver}`,
`_subver=${_subver}`,
"options=('!debug' '!strip')",
@@ -91,23 +79,23 @@ if (!Script.preview) {
"url='https://github.com/Kilo-Org/kilocode'",
"arch=('aarch64' 'x86_64')",
"license=('MIT')",
"provides=('kilo')", // kilocode_change
"conflicts=('kilo')", // kilocode_change
"provides=('opencode')",
"conflicts=('opencode')",
"depends=('ripgrep')",
"",
`source_aarch64=("\${pkgname}_\${pkgver}_aarch64.tar.gz::https://github.com/Kilo-Org/kilocode/releases/download/v\${pkgver}\${_subver}/kilo-linux-arm64.tar.gz")`,
`source_aarch64=("\${pkgname}_\${pkgver}_aarch64.tar.gz::https://github.com/Kilo-Org/kilocode/releases/download/v\${pkgver}\${_subver}/opencode-linux-arm64.tar.gz")`,
`sha256sums_aarch64=('${arm64Sha}')`,
`source_x86_64=("\${pkgname}_\${pkgver}_x86_64.tar.gz::https://github.com/Kilo-Org/kilocode/releases/download/v\${pkgver}\${_subver}/kilo-linux-x64.tar.gz")`,
`source_x86_64=("\${pkgname}_\${pkgver}_x86_64.tar.gz::https://github.com/Kilo-Org/kilocode/releases/download/v\${pkgver}\${_subver}/opencode-linux-x64.tar.gz")`,
`sha256sums_x86_64=('${x64Sha}')`,
"",
"package() {",
' install -Dm755 ./kilo "${pkgdir}/usr/bin/kilo"',
' install -Dm755 ./opencode "${pkgdir}/usr/bin/opencode"',
"}",
"",
].join("\n")
for (const [pkg, pkgbuild] of [["kilo-bin", binaryPkgbuild]]) {
for (const [pkg, pkgbuild] of [["opencode-bin", binaryPkgbuild]]) {
for (let i = 0; i < 30; i++) {
try {
await $`rm -rf ./dist/aur-${pkg}`
@@ -131,45 +119,45 @@ if (!Script.preview) {
"# frozen_string_literal: true",
"",
"# This file was generated by GoReleaser. DO NOT EDIT.",
"class Kilo < Formula", // kilocode_change
"class Opencode < Formula",
` desc "The AI coding agent built for the terminal."`,
` homepage "https://kilo.ai"`, // kilocode_change
` homepage "https://github.com/Kilo-Org/kilocode"`,
` version "${Script.version.split("-")[0]}"`,
"",
` depends_on "ripgrep"`,
"",
" on_macos do",
" if Hardware::CPU.intel?",
` url "https://github.com/Kilo-Org/kilocode/releases/download/v${Script.version}/kilo-darwin-x64.zip"`,
` url "https://github.com/Kilo-Org/kilocode/releases/download/v${Script.version}/opencode-darwin-x64.zip"`,
` sha256 "${macX64Sha}"`,
"",
" def install",
' bin.install "kilo"',
' bin.install "opencode"',
" end",
" end",
" if Hardware::CPU.arm?",
` url "https://github.com/Kilo-Org/kilocode/releases/download/v${Script.version}/kilo-darwin-arm64.zip"`,
` url "https://github.com/Kilo-Org/kilocode/releases/download/v${Script.version}/opencode-darwin-arm64.zip"`,
` sha256 "${macArm64Sha}"`,
"",
" def install",
' bin.install "kilo"',
' bin.install "opencode"',
" end",
" end",
" end",
"",
" on_linux do",
" if Hardware::CPU.intel? and Hardware::CPU.is_64_bit?",
` url "https://github.com/Kilo-Org/kilocode/releases/download/v${Script.version}/kilo-linux-x64.tar.gz"`,
` url "https://github.com/Kilo-Org/kilocode/releases/download/v${Script.version}/opencode-linux-x64.tar.gz"`,
` sha256 "${x64Sha}"`,
" def install",
' bin.install "kilo"',
' bin.install "opencode"',
" end",
" end",
" if Hardware::CPU.arm? and Hardware::CPU.is_64_bit?",
` url "https://github.com/Kilo-Org/kilocode/releases/download/v${Script.version}/kilo-linux-arm64.tar.gz"`,
` url "https://github.com/Kilo-Org/kilocode/releases/download/v${Script.version}/opencode-linux-arm64.tar.gz"`,
` sha256 "${arm64Sha}"`,
" def install",
' bin.install "kilo"',
' bin.install "opencode"',
" end",
" end",
" end",
@@ -183,11 +171,11 @@ if (!Script.preview) {
console.error("GITHUB_TOKEN is required to update homebrew tap")
process.exit(1)
}
const tap = `https://x-access-token:${token}@github.com/Kilo-Org/homebrew-tap.git`
const tap = `https://x-access-token:${token}@github.com/anomalyco/homebrew-tap.git`
await $`rm -rf ./dist/homebrew-tap`
await $`git clone ${tap} ./dist/homebrew-tap`
await Bun.file("./dist/homebrew-tap/kilo.rb").write(homebrewFormula)
await $`cd ./dist/homebrew-tap && git add kilo.rb`
await Bun.file("./dist/homebrew-tap/opencode.rb").write(homebrewFormula)
await $`cd ./dist/homebrew-tap && git add opencode.rb`
await $`cd ./dist/homebrew-tap && git commit -m "Update to v${Script.version}"`
await $`cd ./dist/homebrew-tap && git push`
}
+81 -45
View File
@@ -20,10 +20,19 @@ type PluginAuth = NonNullable<Hooks["auth"]>
* Handle plugin-based authentication flow.
* Returns true if auth was handled, false if it should fall through to default handling.
*/
async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string): Promise<boolean> {
async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string, methodName?: string): Promise<boolean> {
let index = 0
if (plugin.auth.methods.length > 1) {
const method = await prompts.select({
if (methodName) {
const match = plugin.auth.methods.findIndex((x) => x.label.toLowerCase() === methodName.toLowerCase())
if (match === -1) {
prompts.log.error(
`Unknown method "${methodName}" for ${provider}. Available: ${plugin.auth.methods.map((x) => x.label).join(", ")}`,
)
process.exit(1)
}
index = match
} else if (plugin.auth.methods.length > 1) {
const selected = await prompts.select({
message: "Login method",
options: [
...plugin.auth.methods.map((x, index) => ({
@@ -32,8 +41,8 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string):
})),
],
})
if (prompts.isCancel(method)) throw new UI.CancelledError()
index = parseInt(method)
if (prompts.isCancel(selected)) throw new UI.CancelledError()
index = parseInt(selected)
}
const method = plugin.auth.methods[index]
@@ -259,10 +268,21 @@ export const AuthLoginCommand = cmd({
command: "login [url]",
describe: "log in to a provider",
builder: (yargs) =>
yargs.positional("url", {
describe: "kilo auth provider", // kilocode_change
type: "string",
}),
yargs
.positional("url", {
describe: "kilo auth provider", // kilocode_change
type: "string",
})
.option("provider", {
alias: ["p"],
describe: "provider id or name to log in to (skips provider selection)",
type: "string",
})
.option("method", {
alias: ["m"],
describe: "login method label (skips method selection)",
type: "string",
}),
async handler(args) {
await Instance.provide({
directory: process.cwd(),
@@ -333,61 +353,77 @@ export const AuthLoginCommand = cmd({
enabled,
providerNames: Object.fromEntries(Object.entries(config.provider ?? {}).map(([id, p]) => [id, p.name])),
})
let provider = await prompts.autocomplete({
message: "Select provider",
maxItems: 8,
options: [
...pipe(
providers,
values(),
sortBy(
(x) => priority[x.id] ?? 99,
(x) => x.name ?? x.id,
),
map((x) => ({
label: x.name,
value: x.id,
hint: {
kilo: "recommended", // kilocode_change
opencode: "recommended",
anthropic: "Claude Max or API key",
openai: "ChatGPT Plus/Pro or API key",
}[x.id],
})),
const options = [
...pipe(
providers,
values(),
sortBy(
(x) => priority[x.id] ?? 99,
(x) => x.name ?? x.id,
),
...pluginProviders.map((x) => ({
map((x) => ({
label: x.name,
value: x.id,
hint: "plugin",
hint: {
kilo: "recommended", // kilocode_change
opencode: "recommended",
anthropic: "Claude Max or API key",
openai: "ChatGPT Plus/Pro or API key",
}[x.id],
})),
{
value: "other",
label: "Other",
},
],
})
),
...pluginProviders.map((x) => ({
label: x.name,
value: x.id,
hint: "plugin",
})),
]
if (prompts.isCancel(provider)) throw new UI.CancelledError()
let provider: string
if (args.provider) {
const input = args.provider
const byID = options.find((x) => x.value === input)
const byName = options.find((x) => x.label.toLowerCase() === input.toLowerCase())
const match = byID ?? byName
if (!match) {
prompts.log.error(`Unknown provider "${input}"`)
process.exit(1)
}
provider = match.value
} else {
const selected = await prompts.autocomplete({
message: "Select provider",
maxItems: 8,
options: [
...options,
{
value: "other",
label: "Other",
},
],
})
if (prompts.isCancel(selected)) throw new UI.CancelledError()
provider = selected as string
}
const plugin = await Plugin.list().then((x) => x.findLast((x) => x.auth?.provider === provider))
if (plugin && plugin.auth) {
const handled = await handlePluginAuth({ auth: plugin.auth }, provider)
const handled = await handlePluginAuth({ auth: plugin.auth }, provider, args.method)
if (handled) return
}
if (provider === "other") {
provider = await prompts.text({
const custom = await prompts.text({
message: "Enter provider id",
validate: (x) => (x && x.match(/^[0-9a-z-]+$/) ? undefined : "a-z, 0-9 and hyphens only"),
})
if (prompts.isCancel(provider)) throw new UI.CancelledError()
provider = provider.replace(/^@ai-sdk\//, "")
if (prompts.isCancel(provider)) throw new UI.CancelledError()
if (prompts.isCancel(custom)) throw new UI.CancelledError()
provider = custom.replace(/^@ai-sdk\//, "")
// Check if a plugin provides auth for this custom provider
const customPlugin = await Plugin.list().then((x) => x.findLast((x) => x.auth?.provider === provider))
if (customPlugin && customPlugin.auth) {
const handled = await handlePluginAuth({ auth: customPlugin.auth }, provider)
const handled = await handlePluginAuth({ auth: customPlugin.auth }, provider, args.method)
if (handled) return
}
@@ -1641,11 +1641,14 @@ function InlineTool(props: {
spinner?: boolean
children: JSX.Element
part: ToolPart
onClick?: () => void
}) {
const [margin, setMargin] = createSignal(0)
const { theme } = useTheme()
const ctx = use()
const sync = useSync()
const renderer = useRenderer()
const [hover, setHover] = createSignal(false)
const permission = createMemo(() => {
const callID = sync.data.permission[ctx.sessionID]?.at(0)?.tool?.callID
@@ -1655,6 +1658,7 @@ function InlineTool(props: {
const fg = createMemo(() => {
if (permission()) return theme.warning
if (hover() && props.onClick) return theme.text
if (props.complete) return theme.textMuted
return theme.text
})
@@ -1672,6 +1676,12 @@ function InlineTool(props: {
<box
marginTop={margin()}
paddingLeft={3}
onMouseOver={() => props.onClick && setHover(true)}
onMouseOut={() => setHover(false)}
onMouseUp={() => {
if (renderer.getSelection()?.getSelectedText()) return
props.onClick?.()
}}
renderBefore={function () {
const el = this as BoxRenderable
const parent = el.parent
@@ -1894,8 +1904,10 @@ function Read(props: ToolProps<typeof ReadTool>) {
</InlineTool>
<For each={loaded()}>
{(filepath) => (
<box paddingLeft={5}>
<text fg={theme.textMuted}>⤷ Loaded {normalizePath(filepath)}</text>
<box paddingLeft={3}>
<text paddingLeft={3} fg={theme.textMuted}>
↳ Loaded {normalizePath(filepath)}
</text>
</box>
)}
</For>
@@ -2013,6 +2025,11 @@ function Task(props: ToolProps<typeof TaskTool>) {
complete={props.input.description}
pending="Delegating..."
part={props.part}
onClick={() => {
if (props.metadata.sessionId) {
navigate({ type: "session", sessionID: props.metadata.sessionId })
}
}}
>
{content()}
</InlineTool>
+1 -2
View File
@@ -355,7 +355,7 @@ export namespace Config {
}))
json.dependencies = {
...json.dependencies,
"@kilocode/plugin": targetVersion, // kilocode_change
"@kilocode/plugin": targetVersion,
}
await Filesystem.writeJson(pkg, json)
@@ -405,7 +405,6 @@ export namespace Config {
const parsed = await Filesystem.readJson<{ dependencies?: Record<string, string> }>(pkg).catch(() => null)
const dependencies = parsed?.dependencies ?? {}
// kilocode_change start
const depVersion = dependencies["@kilocode/plugin"]
if (!depVersion) return true
+5
View File
@@ -67,6 +67,11 @@ process.on("uncaughtException", (e) => {
})
})
// Ensure the process exits on terminal hangup (eg. closing the terminal tab).
// Without this, long-running commands like `serve` block on a never-resolving
// promise and survive as orphaned processes.
process.on("SIGHUP", () => process.exit())
let cli = yargs(hideBin(process.argv))
.parserConfiguration({ "populate--": true })
.scriptName("kilo") // kilocode_change
+3 -20
View File
@@ -4,11 +4,6 @@
"version": "7.0.47",
"type": "module",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/Kilo-Org/kilocode",
"directory": "packages/sdk/js"
},
"scripts": {
"typecheck": "tsgo --noEmit",
"build": "bun ./script/build.ts"
@@ -17,18 +12,9 @@
".": "./src/index.ts",
"./client": "./src/client.ts",
"./server": "./src/server.ts",
"./v2": {
"types": "./dist/v2/index.d.ts",
"default": "./src/v2/index.ts"
},
"./v2/client": {
"types": "./dist/v2/client.d.ts",
"default": "./src/v2/client.ts"
},
"./v2/gen/client": {
"types": "./dist/v2/gen/client/index.d.ts",
"default": "./src/v2/gen/client/index.ts"
},
"./v2": "./src/v2/index.ts",
"./v2/client": "./src/v2/client.ts",
"./v2/gen/client": "./src/v2/gen/client/index.ts",
"./v2/server": "./src/v2/server.ts"
},
"files": [
@@ -42,8 +28,5 @@
"@typescript/native-preview": "catalog:"
},
"dependencies": {},
"publishConfig": {
"directory": "dist"
},
"peerDependencies": {}
}
+1
View File
@@ -51,6 +51,7 @@
"@solid-primitives/media": "2.3.3",
"@solid-primitives/resize-observer": "2.1.3",
"@solidjs/meta": "catalog:",
"@solidjs/router": "catalog:",
"dompurify": "3.3.1",
"fuzzysort": "catalog:",
"katex": "0.16.27",
+37 -2
View File
@@ -208,6 +208,41 @@ export function BasicTool(props: BasicToolProps) {
)
}
export function GenericTool(props: { tool: string; status?: string; hideDetails?: boolean }) {
return <BasicTool icon="mcp" status={props.status} trigger={{ title: props.tool }} hideDetails={props.hideDetails} />
function label(input: Record<string, unknown> | undefined) {
const keys = ["description", "query", "url", "filePath", "path", "pattern", "name"]
return keys.map((key) => input?.[key]).find((value): value is string => typeof value === "string" && value.length > 0)
}
function args(input: Record<string, unknown> | undefined) {
if (!input) return []
const skip = new Set(["description", "query", "url", "filePath", "path", "pattern", "name"])
return Object.entries(input)
.filter(([key]) => !skip.has(key))
.flatMap(([key, value]) => {
if (typeof value === "string") return [`${key}=${value}`]
if (typeof value === "number") return [`${key}=${value}`]
if (typeof value === "boolean") return [`${key}=${value}`]
return []
})
.slice(0, 3)
}
export function GenericTool(props: {
tool: string
status?: string
hideDetails?: boolean
input?: Record<string, unknown>
}) {
return (
<BasicTool
icon="mcp"
status={props.status}
trigger={{
title: `Called \`${props.tool}\``,
subtitle: label(props.input),
args: args(props.input),
}}
hideDetails={props.hideDetails}
/>
)
}
+1
View File
@@ -1,4 +1,5 @@
[data-component="file-icon"] {
display: block;
flex-shrink: 0;
width: 16px;
height: 16px;
+7 -10
View File
@@ -1,10 +1,8 @@
import type { Component, JSX } from "solid-js"
import { createMemo, splitProps, Show } from "solid-js"
import { createMemo, createUniqueId, splitProps, Show } from "solid-js"
import sprite from "./file-icons/sprite.svg"
import type { IconName } from "./file-icons/types"
let filter = 0
export type FileIconProps = JSX.GSVGAttributes<SVGSVGElement> & {
node: { path: string; type: "file" | "directory" }
expanded?: boolean
@@ -14,7 +12,7 @@ export type FileIconProps = JSX.GSVGAttributes<SVGSVGElement> & {
export const FileIcon: Component<FileIconProps> = (props) => {
const [local, rest] = splitProps(props, ["node", "class", "classList", "expanded", "mono"])
const name = createMemo(() => chooseIconName(local.node.path, local.node.type, local.expanded || false))
const id = `file-icon-mono-${filter++}`
const id = `file-icon-mono-${createUniqueId()}`
return (
<svg
data-component="file-icon"
@@ -24,15 +22,14 @@ export const FileIcon: Component<FileIconProps> = (props) => {
[local.class ?? ""]: !!local.class,
}}
>
<Show when={local.mono}>
<Show when={local.mono} fallback={<use href={`${sprite}#${name()}`} />}>
<defs>
<filter id={id} color-interpolation-filters="sRGB">
<feFlood flood-color="currentColor" result="flood" />
<feComposite in="flood" in2="SourceAlpha" operator="in" />
</filter>
<mask id={id} mask-type="alpha">
<use href={`${sprite}#${name()}`} />
</mask>
</defs>
<rect width="100%" height="100%" fill="currentColor" mask={`url(#${id})`} />
</Show>
<use href={`${sprite}#${name()}`} filter={local.mono ? `url(#${id})` : undefined} />
</svg>
)
}
+9 -5
View File
@@ -332,11 +332,15 @@ export function Markdown(
const gen = ++highlightState.gen
const signal = { aborted: false }
highlightState.signal = signal
deferredHighlight(container, () => {
if (gen !== highlightState.gen) return
if (copyCleanup) copyCleanup()
copyCleanup = setupCodeCopy(container, labels)
}, signal)
deferredHighlight(
container,
() => {
if (gen !== highlightState.gen) return
if (copyCleanup) copyCleanup()
copyCleanup = setupCodeCopy(container, labels)
},
signal,
)
// kilocode_change end
})
+1 -1
View File
@@ -95,7 +95,7 @@
color: var(--text-base);
}
[data-slot="message-nav-tooltip"] {
.message-nav-tooltip {
z-index: 1000;
}
+15 -10
View File
@@ -1,7 +1,7 @@
import { UserMessage } from "@kilocode/sdk/v2"
import { ComponentProps, For, Match, Show, splitProps, Switch } from "solid-js"
import { DiffChanges } from "./diff-changes"
import { Tooltip } from "@kobalte/core/tooltip"
import { Tooltip } from "./tooltip"
import { useI18n } from "../context/i18n"
export function MessageNav(
@@ -70,15 +70,20 @@ export function MessageNav(
return (
<Switch>
<Match when={local.size === "compact"}>
<Tooltip openDelay={0} closeDelay={300} placement="right-start" gutter={-40} shift={-10} overlap>
<Tooltip.Trigger as="div">{content()}</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content data-slot="message-nav-tooltip">
<div data-slot="message-nav-tooltip-content">
<MessageNav {...props} size="normal" class="" />
</div>
</Tooltip.Content>
</Tooltip.Portal>
<Tooltip
openDelay={0}
placement="right-start"
gutter={-40}
shift={-10}
overlap
contentClass="message-nav-tooltip"
value={
<div data-slot="message-nav-tooltip-content">
<MessageNav {...props} size="normal" class="" />
</div>
}
>
{content()}
</Tooltip>
</Match>
<Match when={local.size === "normal"}>{content()}</Match>
@@ -609,6 +609,46 @@
justify-content: center;
}
[data-component="exa-tool-output"] {
width: 100%;
padding-top: 8px;
display: flex;
flex-direction: column;
}
[data-slot="basic-tool-tool-subtitle"].exa-tool-query {
display: block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
[data-slot="exa-tool-links"] {
display: flex;
flex-direction: column;
gap: 4px;
}
[data-slot="exa-tool-link"] {
display: block;
max-width: 100%;
color: var(--text-interactive-base);
text-decoration: underline;
text-underline-offset: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&:hover {
color: var(--text-interactive-base);
}
&:visited {
color: var(--text-interactive-base);
}
}
[data-component="todos"] {
padding: 10px 0 24px 0;
display: flex;
+105 -2
View File
@@ -76,6 +76,7 @@ import { TextShimmer } from "./text-shimmer"
import { AnimatedCountList } from "./tool-count-summary"
import { ToolStatusTitle } from "./tool-status-title"
import { animate } from "motion"
import { useLocation } from "@solidjs/router"
function ShellSubmessage(props: { text: string; animate?: boolean }) {
let widthRef: HTMLSpanElement | undefined
@@ -266,6 +267,18 @@ export function getToolInfo(tool: string, input: any = {}): ToolInfo {
title: i18n.t("ui.tool.webfetch"),
subtitle: input.url,
}
case "websearch":
return {
icon: "window-cursor",
title: i18n.t("ui.tool.websearch"),
subtitle: input.query,
}
case "codesearch":
return {
icon: "code",
title: i18n.t("ui.tool.codesearch"),
subtitle: input.query,
}
case "task":
return {
icon: "task",
@@ -326,6 +339,18 @@ export function getToolInfo(tool: string, input: any = {}): ToolInfo {
}
}
function urls(text: string | undefined) {
if (!text) return []
const seen = new Set<string>()
return [...text.matchAll(/https?:\/\/[^\s<>"'`)\]]+/g)]
.map((item) => item[0].replace(/[),.;:!?]+$/g, ""))
.filter((item) => {
if (seen.has(item)) return false
seen.add(item)
return true
})
}
const CONTEXT_GROUP_TOOLS = new Set(["read", "glob", "grep", "list"])
const HIDDEN_TOOLS = new Set(["todowrite", "todoread"])
@@ -621,6 +646,32 @@ function contextToolSummary(parts: ToolPart[]) {
return { read, search, list }
}
function ExaOutput(props: { output?: string }) {
const links = createMemo(() => urls(props.output))
return (
<Show when={links().length > 0}>
<div data-component="exa-tool-output">
<div data-slot="exa-tool-links">
<For each={links()}>
{(url) => (
<a
data-slot="exa-tool-link"
href={url}
target="_blank"
rel="noopener noreferrer"
onClick={(event) => event.stopPropagation()}
>
{url}
</a>
)}
</For>
</div>
</div>
</Show>
)
}
export function registerPartComponent(type: string, component: PartComponent) {
PART_MAPPING[type] = component
}
@@ -1563,11 +1614,64 @@ ToolRegistry.register({
},
})
ToolRegistry.register({
name: "websearch",
render(props) {
const i18n = useI18n()
const query = createMemo(() => {
const value = props.input.query
if (typeof value !== "string") return ""
return value
})
return (
<BasicTool
{...props}
icon="window-cursor"
trigger={{
title: i18n.t("ui.tool.websearch"),
subtitle: query(),
subtitleClass: "exa-tool-query",
}}
>
<ExaOutput output={props.output} />
</BasicTool>
)
},
})
ToolRegistry.register({
name: "codesearch",
render(props) {
const i18n = useI18n()
const query = createMemo(() => {
const value = props.input.query
if (typeof value !== "string") return ""
return value
})
return (
<BasicTool
{...props}
icon="code"
trigger={{
title: i18n.t("ui.tool.codesearch"),
subtitle: query(),
subtitleClass: "exa-tool-query",
}}
>
<ExaOutput output={props.output} />
</BasicTool>
)
},
})
ToolRegistry.register({
name: "task",
render(props) {
const data = useData()
const i18n = useI18n()
const location = useLocation()
const childSessionId = () => props.metadata.sessionId as string | undefined
const title = createMemo(() => i18n.t("ui.tool.agent", { type: props.input.subagent_type || props.tool }))
const description = createMemo(() => {
@@ -1584,8 +1688,7 @@ ToolRegistry.register({
const direct = data.sessionHref?.(sessionId)
if (direct) return direct
if (typeof window === "undefined") return
const path = window.location.pathname
const path = location.pathname
const idx = path.indexOf("/session")
if (idx === -1) return
return `${path.slice(0, idx)}/session/${sessionId}`
@@ -355,8 +355,6 @@ export const SessionReview = (props: SessionReviewProps) => {
if (typeof window === "undefined") return
const onKeyDown = (event: KeyboardEvent) => {
if (event.defaultPrevented) return
const mod = event.metaKey || event.ctrlKey
if (!mod) return
+2 -12
View File
@@ -171,10 +171,8 @@
[data-slot="session-turn-diff-path"] {
display: flex;
flex-grow: 1;
min-width: 0;
align-items: baseline;
overflow: hidden;
white-space: nowrap;
font-family: var(--font-family-sans);
font-size: var(--font-size-small);
@@ -182,24 +180,16 @@
}
[data-slot="session-turn-diff-directory"] {
flex: 1 1 auto;
color: var(--text-weak);
min-width: 0;
color: var(--text-base);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
direction: rtl;
unicode-bidi: plaintext;
text-align: left;
}
[data-slot="session-turn-diff-filename"] {
flex-shrink: 0;
max-width: 100%;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--text-strong);
font-weight: var(--font-weight-medium);
}
+5 -5
View File
@@ -241,26 +241,26 @@
[data-slot="tabs-trigger"] {
.tab-fileicon-color,
.tab-fileicon-mono {
transition: opacity 120ms ease;
pointer-events: none;
}
.tab-fileicon-color {
opacity: 0;
display: none;
}
.tab-fileicon-mono {
opacity: 1;
display: block;
color: currentColor;
}
&[data-selected],
&:hover {
.tab-fileicon-color {
opacity: 1;
display: block;
}
.tab-fileicon-mono {
opacity: 0;
display: none;
}
}
}
+1 -1
View File
@@ -47,7 +47,7 @@ export function Tooltip(props: TooltipProps) {
<Switch>
<Match when={local.inactive}>{local.children}</Match>
<Match when={true}>
<KobalteTooltip gutter={4} {...others} open={local.forceOpen || open()} onOpenChange={setOpen}>
<KobalteTooltip gutter={4} {...others} closeDelay={0} open={local.forceOpen || open()} onOpenChange={setOpen}>
<KobalteTooltip.Trigger as={"div"} data-component="tooltip-trigger" class={local.class}>
{local.children}
</KobalteTooltip.Trigger>
+25 -8
View File
@@ -1,6 +1,6 @@
import { marked } from "marked"
import markedKatex from "marked-katex-extension"
import markedShiki from "marked-shiki"
import katex from "katex"
import { bundledLanguages, type BundledLanguage } from "shiki"
import { parseFilePath } from "../file-path" // kilocode_change
@@ -534,9 +534,7 @@ export async function deferredHighlight(
onComplete?: () => void,
signal?: { aborted: boolean },
): Promise<void> {
const blocks = Array.from(
container.querySelectorAll("pre > code[data-lang]:not([data-highlighted])"),
)
const blocks = Array.from(container.querySelectorAll("pre > code[data-lang]:not([data-highlighted])"))
if (blocks.length === 0) {
onComplete?.()
return
@@ -591,10 +589,13 @@ export async function deferredHighlight(
resolve()
}
if (!highlighter.getLoadedLanguages().includes(language)) {
highlighter.loadLanguage(language as BundledLanguage).then(highlight).catch((err) => {
console.warn("Failed to load language for highlighting", language, err)
resolve()
})
highlighter
.loadLanguage(language as BundledLanguage)
.then(highlight)
.catch((err) => {
console.warn("Failed to load language for highlighting", language, err)
resolve()
})
return
}
highlight()
@@ -659,6 +660,22 @@ export const { use: useMarked, provider: MarkedProvider } = createSimpleContext(
throwOnError: false,
nonStandard: true,
}),
markedShiki({
async highlight(code, lang) {
const highlighter = await getSharedHighlighter({ themes: ["Kilo"], langs: [] })
if (!(lang in bundledLanguages)) {
lang = "text"
}
if (!highlighter.getLoadedLanguages().includes(lang)) {
await highlighter.loadLanguage(lang as BundledLanguage)
}
return highlighter.codeToHtml(code, {
lang: lang || "text",
theme: "Kilo",
tabindex: false,
})
},
}),
)
// kilocode_change end
+2
View File
@@ -94,6 +94,8 @@ export const dict = {
"ui.tool.glob": "Glob",
"ui.tool.grep": "Grep",
"ui.tool.webfetch": "جلب الويب",
"ui.tool.websearch": "بحث الويب",
"ui.tool.codesearch": "بحث الكود",
"ui.tool.shell": "Shell",
"ui.tool.patch": "تصحيح",
"ui.tool.todos": "المهام",
+2
View File
@@ -94,6 +94,8 @@ export const dict = {
"ui.tool.glob": "Glob",
"ui.tool.grep": "Grep",
"ui.tool.webfetch": "Buscar Web",
"ui.tool.websearch": "Pesquisa na Web",
"ui.tool.codesearch": "Pesquisa de Código",
"ui.tool.shell": "Shell",
"ui.tool.patch": "Patch",
"ui.tool.todos": "Tarefas",
+2
View File
@@ -98,6 +98,8 @@ export const dict = {
"ui.tool.glob": "Glob",
"ui.tool.grep": "Grep",
"ui.tool.webfetch": "Web preuzimanje",
"ui.tool.websearch": "Pretraga weba",
"ui.tool.codesearch": "Pretraga koda",
"ui.tool.shell": "Shell",
"ui.tool.patch": "Patch",
"ui.tool.todos": "Lista zadataka",
+2
View File
@@ -93,6 +93,8 @@ export const dict = {
"ui.tool.glob": "Glob",
"ui.tool.grep": "Grep",
"ui.tool.webfetch": "Webhentning",
"ui.tool.websearch": "Websøgning",
"ui.tool.codesearch": "Kodesøgning",
"ui.tool.shell": "Shell",
"ui.tool.patch": "Patch",
"ui.tool.todos": "Opgaver",
+2
View File
@@ -99,6 +99,8 @@ export const dict = {
"ui.tool.glob": "Glob",
"ui.tool.grep": "Grep",
"ui.tool.webfetch": "Webabruf",
"ui.tool.websearch": "Websuche",
"ui.tool.codesearch": "Codesuche",
"ui.tool.shell": "Shell",
"ui.tool.patch": "Patch",
"ui.tool.todos": "Aufgaben",
+2
View File
@@ -95,6 +95,8 @@ export const dict: Record<string, string> = {
"ui.tool.glob": "Glob",
"ui.tool.grep": "Grep",
"ui.tool.webfetch": "Webfetch",
"ui.tool.websearch": "Web Search",
"ui.tool.codesearch": "Code Search",
"ui.tool.shell": "Shell",
"ui.tool.patch": "Patch",
"ui.tool.todos": "To-dos",
+2
View File
@@ -94,6 +94,8 @@ export const dict = {
"ui.tool.glob": "Glob",
"ui.tool.grep": "Grep",
"ui.tool.webfetch": "Webfetch",
"ui.tool.websearch": "Búsqueda web",
"ui.tool.codesearch": "Búsqueda de código",
"ui.tool.shell": "Shell",
"ui.tool.patch": "Parche",
"ui.tool.todos": "Tareas",
+2
View File
@@ -94,6 +94,8 @@ export const dict = {
"ui.tool.glob": "Glob",
"ui.tool.grep": "Grep",
"ui.tool.webfetch": "Webfetch",
"ui.tool.websearch": "Recherche Web",
"ui.tool.codesearch": "Recherche de code",
"ui.tool.shell": "Shell",
"ui.tool.patch": "Patch",
"ui.tool.todos": "Tâches",
+2
View File
@@ -93,6 +93,8 @@ export const dict = {
"ui.tool.glob": "Glob",
"ui.tool.grep": "Grep",
"ui.tool.webfetch": "Webfetch",
"ui.tool.websearch": "Web検索",
"ui.tool.codesearch": "コード検索",
"ui.tool.shell": "Shell",
"ui.tool.patch": "Patch",
"ui.tool.todos": "Todo",
+2
View File
@@ -94,6 +94,8 @@ export const dict = {
"ui.tool.glob": "Glob",
"ui.tool.grep": "Grep",
"ui.tool.webfetch": "웹 가져오기",
"ui.tool.websearch": "웹 검색",
"ui.tool.codesearch": "코드 검색",
"ui.tool.shell": "셸",
"ui.tool.patch": "패치",
"ui.tool.todos": "할 일",
+2
View File
@@ -97,6 +97,8 @@ export const dict: Record<Keys, string> = {
"ui.tool.glob": "Glob",
"ui.tool.grep": "Grep",
"ui.tool.webfetch": "Webhenting",
"ui.tool.websearch": "Nettsøk",
"ui.tool.codesearch": "Kodesøk",
"ui.tool.shell": "Shell",
"ui.tool.patch": "Patch",
"ui.tool.todos": "Gjøremål",
+2
View File
@@ -93,6 +93,8 @@ export const dict = {
"ui.tool.glob": "Glob",
"ui.tool.grep": "Grep",
"ui.tool.webfetch": "Pobieranie sieciowe",
"ui.tool.websearch": "Wyszukiwanie w sieci",
"ui.tool.codesearch": "Wyszukiwanie kodu",
"ui.tool.shell": "Terminal",
"ui.tool.patch": "Patch",
"ui.tool.todos": "Zadania",
+2
View File
@@ -93,6 +93,8 @@ export const dict = {
"ui.tool.glob": "Glob",
"ui.tool.grep": "Grep",
"ui.tool.webfetch": "Webfetch",
"ui.tool.websearch": "Веб-поиск",
"ui.tool.codesearch": "Поиск кода",
"ui.tool.shell": "Оболочка",
"ui.tool.patch": "Патч",
"ui.tool.todos": "Задачи",
+2
View File
@@ -95,6 +95,8 @@ export const dict = {
"ui.tool.glob": "Glob",
"ui.tool.grep": "Grep",
"ui.tool.webfetch": "ดึงจากเว็บ",
"ui.tool.websearch": "ค้นหาเว็บ",
"ui.tool.codesearch": "ค้นหาโค้ด",
"ui.tool.shell": "เชลล์",
"ui.tool.patch": "แพตช์",
"ui.tool.todos": "รายการงาน",
+2
View File
@@ -100,6 +100,8 @@ export const dict = {
"ui.tool.glob": "Glob",
"ui.tool.grep": "Grep",
"ui.tool.webfetch": "Web getir",
"ui.tool.websearch": "Web Araması",
"ui.tool.codesearch": "Kod Araması",
"ui.tool.shell": "Kabuk",
"ui.tool.patch": "Yama",
"ui.tool.todos": "Görevler",
+2
View File
@@ -98,6 +98,8 @@ export const dict = {
"ui.tool.glob": "Glob",
"ui.tool.grep": "Grep",
"ui.tool.webfetch": "Webfetch",
"ui.tool.websearch": "网络搜索",
"ui.tool.codesearch": "代码搜索",
"ui.tool.shell": "Shell",
"ui.tool.patch": "补丁",
"ui.tool.todos": "待办",
+2
View File
@@ -98,6 +98,8 @@ export const dict = {
"ui.tool.glob": "Glob",
"ui.tool.grep": "Grep",
"ui.tool.webfetch": "Webfetch",
"ui.tool.websearch": "網頁搜尋",
"ui.tool.codesearch": "程式碼搜尋",
"ui.tool.shell": "Shell",
"ui.tool.patch": "修補",
"ui.tool.todos": "待辦",
-1
View File
@@ -74,7 +74,6 @@ export const defaultConfig: MergeConfig = {
"SECURITY.md",
"AGENTS.md",
// GitHub workflows - MANUAL REVIEW (can break CI/CD)
".github/workflows/publish-stable.yml",
".github/workflows/publish.yml",
".github/workflows/close-stale-prs.yml",
".github/pull_request_template.md",
+1 -1
View File
@@ -1,5 +1,5 @@
{
"$schema": "https://turborepo.com/schema.json",
"$schema": "https://v2-8-13.turborepo.dev/schema.json",
"globalEnv": ["CI", "KILO_DISABLE_SHARE"],
"globalPassThroughEnv": ["CI", "KILO_DISABLE_SHARE"],
"tasks": {