mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-01 15:32:11 +08:00
Merge branch 'main' into fix-cli-subprocess-timeouts
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": minor
|
||||
---
|
||||
|
||||
Add a `notify_user` tool that lets an agent send a push notification to the user's phone (Kilo mobile app) for explicitly requested pings and significant mid-run milestones. The tool sends a single `agent_notification` item over the session's existing authenticated ingest channel with a bounded readiness wait, returns a friendly failure when the session is not connected to Kilo cloud, and never prompts for permission. Delivery may still be suppressed server-side by the user's notification preference, per-session rate limits, or active presence in the session.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Prevent Enter from activating the first result in searchable lists when no row is highlighted.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Restore stream idle timeouts to opt-in provider configuration instead of aborting quiet model streams by default.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Remote CLI sessions no longer appear frozen on mobile when the connection to the session relay stalls; they now recover on their own instead of staying read-only until the CLI is restarted. Token acquisition and connection attempts are bounded by deadlines with a single fenced retry owner, and heartbeat session gathers are bounded so one stuck gather can no longer silently kill every future heartbeat.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Fix compaction failure against strict OpenAI-compatible providers during context compaction. The compaction path no longer leaks `maxOutputTokens` into provider options, which was rejected by strict upstreams with "Unsupported parameter(s)".
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Fix: inject `$schema` into config files using jsonc-parser, avoiding write-on-read for comment-first JSONC and preventing unnecessary file rewrites on every load
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Fix a fatal startup crash ("attempt to write a readonly database") when the local database or its WAL sidecar files lost write permission. Kilo now repairs the permissions automatically when it safely can, and otherwise reports the exact file to fix instead of an opaque error.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": minor
|
||||
"@kilocode/kilo-ui": minor
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Support deleting queued chat messages from the VS Code chat before they run.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Keep Plan and Architect mode source edits denied when agent-specific permissions request edit approval.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Improve JetBrains diff previews by hiding hunk headers and adding full-path tooltips to clickable file links.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Render edit tool results with a clickable file target and a highlighted, simplified diff view.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Open edit tool file links directly when multiple files share the same name.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Render multi-file apply_patch edits as a "Patch" with a file-count tag and one section per file, each showing a clickable filename link and its own changes badge aligned with the diff.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Smooth out chat scrolling in large JetBrains sessions by only refreshing hover state for the message under the pointer.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Improve chat scrolling performance in large JetBrains sessions.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Size edit and shell preview popovers to their content with a wider maximum width.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Emit session queue state so remote clients can show queued messages.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": minor
|
||||
---
|
||||
|
||||
`kilo remote` instances now advertise themselves on the relay heartbeat. Each heartbeat carries the host's hostname, the project directory name, and the CLI build version, and each session entry advertises the platform it was created on. The cloud relay learns about a freshly-connected instance immediately (no 10s wait for the first timer tick), and the advertisement is race-safe across the explicit `kilo remote` command and bootstrap auto-enable (`KILO_REMOTE=1` / `remote_control` config). Legacy CLIs that send neither field remain wire-compatible.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": minor
|
||||
---
|
||||
|
||||
Support file attachments in remote CLI sessions.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": minor
|
||||
---
|
||||
|
||||
Run asynchronous Cloud Agent tasks with repository, model, mode, and organization defaults through `kilo cloud`. Add `--stream` to `kilo cloud start` to print admission output and then stream WebSocket events as JSONL until completion or inactivity ends the stream.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"kilo-code": minor
|
||||
"@kilocode/cli": minor
|
||||
---
|
||||
|
||||
Reference past chats inline with `@` in the prompt. Typing `@` now surfaces a "Past chats" option that opens a searchable picker of previous sessions (scoped to the current workspace/worktree, searched like the Agent Manager session search); selecting one attaches that session's transcript as context so the model can build on a prior conversation. Clicking the mention opens that session. Available in the CLI TUI and the VS Code extension.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Add a searchable open-tabs switcher to the sidebar tab bar.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Show the request ID when a model response ends without a finish reason.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": minor
|
||||
"@kilocode/sdk": minor
|
||||
---
|
||||
|
||||
Show tokens-per-second text-generation throughput (TG) on each assistant message and in the usage sidebar, computed from step duration and tokens. The toggle "Show Token Throughput" in Display settings controls both surfaces. PP (prompt-processing) support lands in a follow-up once the upstream llama.cpp metadata wiring ships.
|
||||
@@ -0,0 +1,137 @@
|
||||
// kilocode_change - new file
|
||||
|
||||
/**
|
||||
* Collects PRs merged to the source repos since the watermark, applies a
|
||||
* deterministic pre-filter, and writes docs-sync-out/digest.json for the LLM
|
||||
* triage pass.
|
||||
*
|
||||
* Pre-filter drops (triage never sees these):
|
||||
* - PRs labeled auto-docs (this bot's own rolling PRs)
|
||||
* - chore/test/ci/build/docs/style/refactor/revert conventional titles
|
||||
* - PRs touching only docs/non-product paths
|
||||
*
|
||||
* Bot-authored PRs are kept: release/dependency bots ship user-facing
|
||||
* changes too, and the label + docs-only guards above prevent loops.
|
||||
*/
|
||||
|
||||
import fs from "node:fs"
|
||||
import { api, appendOutput, appendSummary, listPrFiles, searchIssues } from "./lib.mjs"
|
||||
|
||||
const SOURCE_REPOS = ["Kilo-Org/cloud", "Kilo-Org/kilocode"]
|
||||
const OUT_DIR = "docs-sync-out"
|
||||
const BODY_LIMIT = 2000
|
||||
const SLIM_BODY_LIMIT = 300
|
||||
const PATCH_LIMIT = 8000
|
||||
const FILE_LIMIT = 30
|
||||
const DROP_TITLE = /^(chore|test|ci|build|docs|style|refactor|revert)(\(.+\))?!?:/i
|
||||
const DOCS_ONLY_PATH = /^(packages\/kilo-docs\/|\.github\/docs-sync\/|docs-sync-out\/|docs\/|[^/]+\.md$)/
|
||||
|
||||
function argSince() {
|
||||
const i = process.argv.indexOf("--since")
|
||||
const v = i >= 0 ? process.argv[i + 1] : null
|
||||
if (!v || Number.isNaN(new Date(v).getTime())) {
|
||||
throw new Error("usage: collect.mjs --since <ISO date>")
|
||||
}
|
||||
return new Date(v)
|
||||
}
|
||||
|
||||
async function mergedPrs(fullRepo, since) {
|
||||
const query = `repo:${fullRepo} is:pr is:merged merged:>=${since.toISOString()}`
|
||||
return searchIssues(query)
|
||||
}
|
||||
|
||||
const since = argSince()
|
||||
console.log(`collecting PRs merged since ${since.toISOString()}`)
|
||||
|
||||
const digest = []
|
||||
const dropped = { label: 0, title: 0, docs_only: 0, fetch_error: 0 }
|
||||
|
||||
for (const fullRepo of SOURCE_REPOS) {
|
||||
const prs = await mergedPrs(fullRepo, since)
|
||||
console.log(`${fullRepo}: ${prs.length} merged PRs in window`)
|
||||
|
||||
for (const item of prs) {
|
||||
const author = item.user?.login ?? ""
|
||||
if ((item.labels ?? []).some((l) => l.name === "auto-docs")) {
|
||||
dropped.label++
|
||||
continue
|
||||
}
|
||||
if (DROP_TITLE.test(item.title ?? "")) {
|
||||
dropped.title++
|
||||
continue
|
||||
}
|
||||
|
||||
const number = item.number
|
||||
let pr
|
||||
let files
|
||||
try {
|
||||
pr = await api(`/repos/${fullRepo}/pulls/${number}`)
|
||||
files = await listPrFiles(fullRepo, number)
|
||||
} catch (err) {
|
||||
// Isolate per-PR failures: one dead PR must not abort the whole run.
|
||||
console.warn(`::warning::skipping ${fullRepo}#${number}: ${err.message}`)
|
||||
dropped.fetch_error++
|
||||
continue
|
||||
}
|
||||
// listPrFiles caps at 300 files; a truncated list can't support the
|
||||
// docs-only classification, so keep such PRs and record the true total.
|
||||
const truncated = files.length >= 300
|
||||
if (!truncated && files.length > 0 && files.every((f) => DOCS_ONLY_PATH.test(f.filename))) {
|
||||
dropped.docs_only++
|
||||
continue
|
||||
}
|
||||
|
||||
let patch = ""
|
||||
for (const f of files) {
|
||||
if (!f.patch) continue
|
||||
const chunk = `--- ${f.filename}\n${f.patch}\n`
|
||||
if (patch.length + chunk.length > PATCH_LIMIT) {
|
||||
patch += "\n... (diff truncated) ...\n"
|
||||
break
|
||||
}
|
||||
patch += chunk
|
||||
}
|
||||
|
||||
digest.push({
|
||||
repo: fullRepo,
|
||||
number,
|
||||
title: pr.title,
|
||||
url: pr.html_url,
|
||||
author,
|
||||
merged_at: pr.merged_at,
|
||||
labels: (pr.labels ?? []).map((l) => l.name),
|
||||
body: (pr.body ?? "").slice(0, BODY_LIMIT),
|
||||
files: files.slice(0, FILE_LIMIT).map((f) => `${f.status} ${f.filename} (+${f.additions}/-${f.deletions})`),
|
||||
files_total: pr.changed_files ?? files.length,
|
||||
patch_excerpt: patch,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
digest.sort((a, b) => new Date(a.merged_at) - new Date(b.merged_at))
|
||||
|
||||
fs.mkdirSync(OUT_DIR, { recursive: true })
|
||||
// Full digest (bodies + patch excerpts) is filtered down to docs-worthy PRs
|
||||
// for the edit pass; the slim digest keeps the triage pass context small.
|
||||
fs.writeFileSync(`${OUT_DIR}/digest-full.json`, JSON.stringify(digest, null, 2))
|
||||
const slim = digest.map(({ patch_excerpt, body, ...rest }) => ({
|
||||
...rest,
|
||||
body: body.slice(0, SLIM_BODY_LIMIT),
|
||||
}))
|
||||
fs.writeFileSync(`${OUT_DIR}/digest.json`, JSON.stringify(slim, null, 2))
|
||||
|
||||
console.log(`kept ${digest.length} PRs, dropped:`, dropped)
|
||||
appendOutput("count", digest.length)
|
||||
appendOutput("digest", `${OUT_DIR}/digest.json`)
|
||||
|
||||
appendSummary(
|
||||
[
|
||||
"### docs-sync collect",
|
||||
"",
|
||||
`- window: since \`${since.toISOString()}\``,
|
||||
`- kept: **${digest.length}** PRs`,
|
||||
`- dropped: ${dropped.label} auto-docs, ${dropped.title} title filter, ${dropped.docs_only} docs-only, ${dropped.fetch_error} fetch errors`,
|
||||
"",
|
||||
...digest.map((d) => `- [${d.repo}#${d.number}](${d.url}) ${d.title}`),
|
||||
].join("\n"),
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
You are the Kilo Code documentation bot. You update the public product documentation in `packages/kilo-docs` (a Markdoc/Next.js site served at kilo.ai/docs) so it reflects recently merged PRs. You are handling one batch of PRs; the batch files and your output file are named at the end of these instructions.
|
||||
|
||||
Before writing anything:
|
||||
|
||||
1. Read `packages/kilo-docs/AGENTS.md` and `packages/kilo-docs/STYLE_GUIDE.md` and follow them exactly: Markdoc custom tags, the `/docs` prefix in image paths, navigation files under `lib/nav/`, redirect rules, and the generated-screenshot policy.
|
||||
2. Read the attached batch files: the full-details file (PR title, body, file list, `patch_excerpt` diffs) and the triage file (docs-worthiness verdicts, target sections, priorities).
|
||||
|
||||
For each PR in the batch, in priority order:
|
||||
|
||||
- Find the most relevant existing docs page(s) and make minimal, precise updates in the style of the surrounding content.
|
||||
- Create a new page only when no existing page fits; then add it to the matching nav file in `packages/kilo-docs/lib/nav/`.
|
||||
- Document only behavior that is actually present in the merged diff. If the PR body or diff shows the feature is behind a flag or otherwise not user-visible yet, skip it and record why.
|
||||
- If a PR turns out not to need documentation, skip it and record why. Trust evidence over the triage verdict.
|
||||
|
||||
Hard rules:
|
||||
|
||||
- Only create or modify files under `packages/kilo-docs/`. Never touch code, tests, config, images, or anything outside that directory.
|
||||
- Never remove or rename pages. Never document unreleased behavior. Never copy internal PR discussion into the docs; write user-facing documentation.
|
||||
- Do not run git commands and do not commit anything; automation handles git.
|
||||
- Keep the change small and precise. Do not rewrite sections that are already accurate.
|
||||
|
||||
When finished, write the summary JSON file named in the batch specifics below: a JSON array with exactly one entry per batch PR, consumed by automation (this file is never committed). Use `action` values like `updated <path>`, `created <path>`, or `skipped`. Example:
|
||||
|
||||
[{"pr": 123, "url": "https://github.com/Kilo-Org/kilocode/pull/123", "action": "updated pages/code-with-ai/platforms/cli.md", "reason": "documented --variant flag"}, {"pr": 124, "url": "https://github.com/Kilo-Org/kilocode/pull/124", "action": "skipped", "reason": "feature behind unreleased flag"}]
|
||||
@@ -0,0 +1,122 @@
|
||||
// kilocode_change - new file
|
||||
|
||||
/**
|
||||
* Runs the LLM edit pass over docs-sync-out/worthy.json in batches.
|
||||
*
|
||||
* Batching bounds each `kilo run` context (a replay window can yield dozens
|
||||
* of docs-worthy PRs with large diffs). Each batch gets its own CLI session
|
||||
* and writes its own summary file; results are merged into
|
||||
* docs-sync-out/edit-summary.json. A batch that fails is skipped with a
|
||||
* warning — its PRs show up in the rolling PR body as skipped, so nothing
|
||||
* fails silently.
|
||||
*
|
||||
* Env: EDIT_MODEL (provider/model), KILO_API_KEY (set by workflow; read natively by the kilo provider).
|
||||
*/
|
||||
|
||||
import { execFileSync } from "node:child_process"
|
||||
import fs from "node:fs"
|
||||
import path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
|
||||
const BATCH_SIZE = 5
|
||||
const ATTEMPTS = 2
|
||||
const OUT_DIR = "docs-sync-out"
|
||||
export const SUMMARY_FILE = ".docs-sync-summary.json"
|
||||
|
||||
const HERE = path.dirname(fileURLToPath(import.meta.url))
|
||||
const basePrompt = fs.readFileSync(path.join(HERE, "edit-prompt.md"), "utf8")
|
||||
const model = process.env.EDIT_MODEL
|
||||
if (!model) throw new Error("EDIT_MODEL is required")
|
||||
|
||||
const worthy = JSON.parse(fs.readFileSync(`${OUT_DIR}/worthy.json`, "utf8"))
|
||||
const triage = JSON.parse(fs.readFileSync(`${OUT_DIR}/triage.json`, "utf8"))
|
||||
const priority = new Map(triage.map((e) => [e.url, e]))
|
||||
const ordered = [...worthy].sort((a, b) => {
|
||||
const rank = { high: 0, medium: 1, low: 2 }
|
||||
return (rank[priority.get(a.url)?.priority] ?? 1) - (rank[priority.get(b.url)?.priority] ?? 1)
|
||||
})
|
||||
|
||||
function editBatch(batch, index) {
|
||||
const batchFile = `${OUT_DIR}/edit-batch-${index}.json`
|
||||
const triageFile = `${OUT_DIR}/edit-batch-triage-${index}.json`
|
||||
const summaryFile = `${OUT_DIR}/edit-summary-${index}.json`
|
||||
fs.writeFileSync(batchFile, JSON.stringify(batch, null, 2))
|
||||
fs.writeFileSync(
|
||||
triageFile,
|
||||
JSON.stringify(
|
||||
batch.map((d) => priority.get(d.url)).filter(Boolean),
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
|
||||
const prompt = `${basePrompt}
|
||||
|
||||
Batch specifics for this run: the PRs to handle are in the attached ${batchFile} (full details) and ${triageFile} (triage verdicts). Handle ONLY the PRs in these batch files. When finished, write your per-PR results in the summary JSON format described above to the file \`${summaryFile}\` (path relative to the repository root).`
|
||||
|
||||
for (let attempt = 1; attempt <= ATTEMPTS; attempt++) {
|
||||
try {
|
||||
// Message positional first: --file is multi-value and would otherwise
|
||||
// consume a trailing message as a file path ("File not found").
|
||||
execFileSync(
|
||||
"kilo",
|
||||
["run", prompt, "-m", model, "--variant", "high", "--dir", process.cwd(), "-f", batchFile, "-f", triageFile],
|
||||
// stdout streams live to the Actions log; stderr is piped so failure
|
||||
// warnings can include the tail of the actual CLI error.
|
||||
{ encoding: "utf8", maxBuffer: 32 * 1024 * 1024, timeout: 25 * 60 * 1000, stdio: ["ignore", "inherit", "pipe"] },
|
||||
)
|
||||
if (fs.existsSync(summaryFile)) return true
|
||||
// Tolerate the agent dropping the docs-sync-out/ prefix.
|
||||
const alt = path.basename(summaryFile)
|
||||
if (fs.existsSync(alt)) {
|
||||
fs.renameSync(alt, summaryFile)
|
||||
return true
|
||||
}
|
||||
console.warn(`batch ${index} attempt ${attempt}: summary file ${summaryFile} not produced`)
|
||||
} catch (err) {
|
||||
const stderr = String(err.stderr ?? "").trim().split("\n").slice(-5).join("\n")
|
||||
console.warn(`batch ${index} attempt ${attempt}: kilo run failed: ${stderr || err.message}`)
|
||||
}
|
||||
}
|
||||
console.warn(`::warning::edit batch ${index} failed after ${ATTEMPTS} attempts; ${batch.length} PRs skipped`)
|
||||
return false
|
||||
}
|
||||
|
||||
const batches = []
|
||||
for (let i = 0; i < ordered.length; i += BATCH_SIZE) {
|
||||
batches.push(ordered.slice(i, i + BATCH_SIZE))
|
||||
}
|
||||
console.log(`editing docs for ${ordered.length} PRs in ${batches.length} batches of up to ${BATCH_SIZE}`)
|
||||
|
||||
for (let i = 0; i < batches.length; i++) {
|
||||
editBatch(batches[i], i)
|
||||
}
|
||||
|
||||
// Merge batch summaries. Coverage: every worthy PR gets an entry so the PR
|
||||
// body accounts for it; failed batches show up as skipped.
|
||||
const merged = []
|
||||
const seen = new Set()
|
||||
for (let i = 0; i < batches.length; i++) {
|
||||
const file = `${OUT_DIR}/edit-summary-${i}.json`
|
||||
let entries = []
|
||||
try {
|
||||
entries = JSON.parse(fs.readFileSync(file, "utf8"))
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
for (const e of entries) {
|
||||
const url = String(e?.url ?? "")
|
||||
if (!url.startsWith("http") || seen.has(url)) continue
|
||||
seen.add(url)
|
||||
merged.push({ pr: Number(e.pr) || 0, url, action: String(e.action ?? "skipped"), reason: String(e.reason ?? "") })
|
||||
}
|
||||
}
|
||||
for (const d of ordered) {
|
||||
if (seen.has(d.url)) continue
|
||||
merged.push({ pr: d.number, url: d.url, action: "skipped", reason: "edit pass failed or timed out for this PR" })
|
||||
}
|
||||
|
||||
// upsert-pr.mjs consumes the merged summary from the repo root; the file is
|
||||
// removed there before committing so it never lands in the docs PR.
|
||||
fs.writeFileSync(SUMMARY_FILE, JSON.stringify(merged, null, 2))
|
||||
console.log(`edit pass complete: ${merged.filter((e) => e.action !== "skipped").length} changed, ${merged.filter((e) => e.action === "skipped").length} skipped`)
|
||||
@@ -0,0 +1,76 @@
|
||||
// kilocode_change - new file
|
||||
|
||||
/**
|
||||
* Extracts and validates the triage JSON array from raw LLM stdout.
|
||||
* Usage: extract-json.mjs <raw-input-file> <output-file>
|
||||
* Exit 0 on success, 1 on any failure. Also exports parseTriageEntries for
|
||||
* the chunked triage runner.
|
||||
*/
|
||||
|
||||
import fs from "node:fs"
|
||||
import { pathToFileURL } from "node:url"
|
||||
|
||||
/** Returns validated triage entries, or null when extraction fails. */
|
||||
export function parseTriageEntries(raw) {
|
||||
// `kilo run` prints the assistant message twice (streaming render + final
|
||||
// summary), so stdout can hold the same array back-to-back. Try each "["
|
||||
// from the right and return the first slice that parses — i.e. the last
|
||||
// (most recent) valid array in the output.
|
||||
const end = raw.lastIndexOf("]")
|
||||
if (end < 0) return null
|
||||
|
||||
const starts = []
|
||||
for (let i = 0; i <= end; i++) {
|
||||
if (raw[i] === "[") starts.push(i)
|
||||
}
|
||||
|
||||
for (let s = starts.length - 1; s >= 0; s--) {
|
||||
let parsed
|
||||
try {
|
||||
parsed = JSON.parse(raw.slice(starts[s], end + 1))
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
if (!Array.isArray(parsed)) continue
|
||||
const entries = validate(parsed)
|
||||
if (entries) return entries
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function validate(parsed) {
|
||||
const entries = []
|
||||
for (const e of parsed) {
|
||||
const pr = Number(e?.pr)
|
||||
const url = String(e?.url ?? "")
|
||||
if (!Number.isInteger(pr) || !url.startsWith("http")) continue
|
||||
entries.push({
|
||||
pr,
|
||||
url,
|
||||
docs_worthy: e.docs_worthy === true,
|
||||
reason: String(e.reason ?? ""),
|
||||
target_sections: Array.isArray(e.target_sections) ? e.target_sections.map(String) : [],
|
||||
priority: ["high", "medium", "low"].includes(e.priority) ? e.priority : "medium",
|
||||
})
|
||||
}
|
||||
return entries.length > 0 ? entries : null
|
||||
}
|
||||
|
||||
function main() {
|
||||
const [, , inputPath, outputPath] = process.argv
|
||||
if (!inputPath || !outputPath) {
|
||||
console.error("usage: extract-json.mjs <raw-input-file> <output-file>")
|
||||
process.exit(1)
|
||||
}
|
||||
const entries = parseTriageEntries(fs.readFileSync(inputPath, "utf8"))
|
||||
if (!entries) {
|
||||
console.error("no valid triage JSON array found in input")
|
||||
process.exit(1)
|
||||
}
|
||||
fs.writeFileSync(outputPath, JSON.stringify(entries, null, 2))
|
||||
console.log(`extracted ${entries.length} triage entries (${entries.filter((e) => e.docs_worthy).length} docs-worthy)`)
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
main()
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// kilocode_change - new file
|
||||
|
||||
/**
|
||||
* Filters the full digest down to PRs the triage pass marked docs-worthy.
|
||||
* Usage: filter-worthy.mjs <digest-full.json> <triage.json> <output.json>
|
||||
* The edit pass consumes the output so its context stays small.
|
||||
*/
|
||||
|
||||
import fs from "node:fs"
|
||||
|
||||
const [, , digestPath, triagePath, outputPath] = process.argv
|
||||
if (!digestPath || !triagePath || !outputPath) {
|
||||
console.error("usage: filter-worthy.mjs <digest-full.json> <triage.json> <output.json>")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const digest = JSON.parse(fs.readFileSync(digestPath, "utf8"))
|
||||
const triage = JSON.parse(fs.readFileSync(triagePath, "utf8"))
|
||||
|
||||
const worthy = new Set(triage.filter((e) => e.docs_worthy).map((e) => e.url))
|
||||
const out = digest.filter((d) => worthy.has(d.url))
|
||||
|
||||
fs.writeFileSync(outputPath, JSON.stringify(out, null, 2))
|
||||
console.log(`${out.length} of ${digest.length} digest entries are docs-worthy`)
|
||||
@@ -0,0 +1,113 @@
|
||||
// kilocode_change - new file
|
||||
|
||||
/**
|
||||
* Shared helpers for the docs-sync bot scripts. Dependency-free (Node 20+
|
||||
* global fetch) so the workflow does not rely on runner images shipping the
|
||||
* gh CLI.
|
||||
*/
|
||||
|
||||
import fs from "node:fs"
|
||||
|
||||
const API = "https://api.github.com"
|
||||
const MAX_RETRIES = 3
|
||||
|
||||
export function token() {
|
||||
const t = process.env.GH_TOKEN || process.env.GITHUB_TOKEN
|
||||
if (!t) throw new Error("GH_TOKEN (or GITHUB_TOKEN) is required")
|
||||
return t
|
||||
}
|
||||
|
||||
export function repo() {
|
||||
const r = process.env.GITHUB_REPOSITORY
|
||||
if (!r) throw new Error("GITHUB_REPOSITORY is required")
|
||||
return r
|
||||
}
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
||||
|
||||
export async function api(path, { method = "GET", body } = {}) {
|
||||
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
||||
let res
|
||||
try {
|
||||
res = await fetch(`${API}${path}`, {
|
||||
method,
|
||||
headers: {
|
||||
authorization: `Bearer ${token()}`,
|
||||
accept: "application/vnd.github+json",
|
||||
"x-github-api-version": "2022-11-28",
|
||||
"user-agent": "kilo-docs-sync-bot",
|
||||
},
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
})
|
||||
} catch (err) {
|
||||
if (attempt < MAX_RETRIES) {
|
||||
console.warn(`network error (${err.message}), retrying in ${5 * attempt}s`)
|
||||
await sleep(5000 * attempt)
|
||||
continue
|
||||
}
|
||||
throw err
|
||||
}
|
||||
|
||||
if (res.status === 403) {
|
||||
const text = await res.text()
|
||||
if (text.includes("rate limit") && attempt < MAX_RETRIES) {
|
||||
const retryAfter = Number(res.headers.get("retry-after")) || 30
|
||||
console.warn(`rate limited, retrying in ${retryAfter}s`)
|
||||
await sleep(retryAfter * 1000)
|
||||
continue
|
||||
}
|
||||
const err = new Error(`${method} ${path} -> 403: ${text}`)
|
||||
err.status = 403
|
||||
throw err
|
||||
}
|
||||
|
||||
if (res.status >= 500 && attempt < MAX_RETRIES) {
|
||||
console.warn(`${method} ${path} -> ${res.status}, retrying in ${5 * attempt}s`)
|
||||
await sleep(5000 * attempt)
|
||||
continue
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text()
|
||||
const err = new Error(`${method} ${path} -> ${res.status}: ${text}`)
|
||||
err.status = res.status
|
||||
throw err
|
||||
}
|
||||
|
||||
if (res.status === 204) return null
|
||||
return res.json()
|
||||
}
|
||||
throw new Error(`${method} ${path}: exhausted retries`)
|
||||
}
|
||||
|
||||
/** Paginated search/issues. Caps at `maxPages` * 100 results. */
|
||||
export async function searchIssues(query, { maxPages = 5 } = {}) {
|
||||
const items = []
|
||||
for (let page = 1; page <= maxPages; page++) {
|
||||
const data = await api(`/search/issues?q=${encodeURIComponent(query)}&per_page=100&page=${page}`)
|
||||
items.push(...(data.items ?? []))
|
||||
if ((data.items ?? []).length < 100) break
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
export async function listPrFiles(fullRepo, number, { maxPages = 3 } = {}) {
|
||||
const files = []
|
||||
for (let page = 1; page <= maxPages; page++) {
|
||||
const batch = await api(`/repos/${fullRepo}/pulls/${number}/files?per_page=100&page=${page}`)
|
||||
files.push(...batch)
|
||||
if (batch.length < 100) break
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
export function appendOutput(name, value) {
|
||||
const out = process.env.GITHUB_OUTPUT
|
||||
if (out) fs.appendFileSync(out, `${name}=${value}\n`)
|
||||
console.log(`output ${name}=${value}`)
|
||||
}
|
||||
|
||||
export function appendSummary(markdown) {
|
||||
const summary = process.env.GITHUB_STEP_SUMMARY
|
||||
if (summary) fs.appendFileSync(summary, markdown + "\n")
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// kilocode_change - new file
|
||||
|
||||
/**
|
||||
* Prepares the rolling docs-sync branch before the edit pass:
|
||||
* - an open auto-docs PR exists -> check out its head branch and merge
|
||||
* origin/main (preserves any human commits on the branch)
|
||||
* - otherwise -> fresh branch from origin/main (bot force-pushes later)
|
||||
*
|
||||
* Outputs: branch, mode (update|fresh), pr_number (empty when fresh).
|
||||
*/
|
||||
|
||||
import { execFileSync } from "node:child_process"
|
||||
import { api, appendOutput, repo, searchIssues } from "./lib.mjs"
|
||||
|
||||
export const DEFAULT_BRANCH = "docs/auto-sync"
|
||||
|
||||
const git = (args) => execFileSync("git", args, { stdio: ["ignore", "pipe", "inherit"] }).toString().trim()
|
||||
|
||||
const prs = await searchIssues(`repo:${repo()} is:pr is:open label:auto-docs sort:created-desc`, { maxPages: 1 })
|
||||
|
||||
let mode = "fresh"
|
||||
let prNumber = ""
|
||||
let branch = DEFAULT_BRANCH
|
||||
|
||||
if (prs.length > 0) {
|
||||
const pr = await api(`/repos/${repo()}/pulls/${prs[0].number}`)
|
||||
branch = pr.head?.ref ?? DEFAULT_BRANCH
|
||||
prNumber = String(pr.number)
|
||||
git(["fetch", "origin", "main", branch])
|
||||
git(["checkout", branch])
|
||||
try {
|
||||
git(["merge", "origin/main", "--no-edit"])
|
||||
mode = "update"
|
||||
} catch {
|
||||
console.warn(`merge of origin/main into ${branch} conflicted.`)
|
||||
console.warn("Leaving the conflicted branch untouched so human commits are preserved; continuing on a fresh dated branch.")
|
||||
git(["merge", "--abort"])
|
||||
branch = `${DEFAULT_BRANCH}-${new Date().toISOString().slice(0, 10)}`
|
||||
try {
|
||||
git(["fetch", "origin", `+refs/heads/${branch}:refs/remotes/origin/${branch}`])
|
||||
} catch {
|
||||
console.log(`dated branch ${branch} does not exist on origin yet; will create it on push`)
|
||||
}
|
||||
git(["checkout", "-B", branch, "origin/main"])
|
||||
mode = "conflict"
|
||||
}
|
||||
} else {
|
||||
// Keep the remote-tracking ref current so the later --force-with-lease
|
||||
// push (stale branch left over from a merged/closed PR) is safe.
|
||||
try {
|
||||
git(["fetch", "origin", `+refs/heads/${branch}:refs/remotes/origin/${branch}`])
|
||||
} catch {
|
||||
console.log(`branch ${branch} does not exist on origin yet; will create it on push`)
|
||||
}
|
||||
git(["checkout", "-B", branch, "origin/main"])
|
||||
}
|
||||
|
||||
appendOutput("branch", branch)
|
||||
appendOutput("mode", mode)
|
||||
appendOutput("pr_number", prNumber)
|
||||
console.log(`branch ${branch} ready (mode=${mode}, pr=${prNumber || "none"})`)
|
||||
@@ -0,0 +1,19 @@
|
||||
You are the triage pass of an automated documentation pipeline for Kilo Code. Kilo Code is an open-source agentic engineering platform: VS Code extension, JetBrains plugin, CLI, and the kilo.ai cloud platform (teams, KiloClaw, gateway, code reviews).
|
||||
|
||||
The attached `digest.json` file contains PRs recently merged to Kilo-Org/cloud and Kilo-Org/kilocode. Your only job is to decide which of them require changes to the public product documentation at kilo.ai/docs.
|
||||
|
||||
A PR is docs-worthy ONLY if a user of Kilo Code would need to learn something new or change how they use the product after this PR ships. Examples: new commands, flags, settings, UI workflows, providers, pricing/limits changes, breaking behavior changes, or fixes that change documented behavior.
|
||||
|
||||
A PR is NOT docs-worthy when it is: an internal refactor, infrastructure or CI work, a feature-flag scaffold that is not yet user-visible, test or dependency work, a bug fix that merely restores already-documented behavior, or a change only visible to contributors or self-hosters.
|
||||
|
||||
Rules:
|
||||
|
||||
- Include every input PR exactly once, identified by its `number` and `url`. Never invent PRs.
|
||||
- When unsure, set `docs_worthy` to false and explain the doubt in `reason`.
|
||||
- `target_sections` is only filled for docs-worthy PRs. Use rough docs areas, e.g. `getting-started`, `code-with-ai/platforms/cli`, `code-with-ai/platforms/vscode`, `code-with-ai/agents`, `ai-providers`, `teams`, `enterprise`, `automate`.
|
||||
- `reason` is one short sentence, written for the human who reviews the final docs PR.
|
||||
- `priority` reflects user impact: high = most users affected, medium = notable subset, low = edge case.
|
||||
|
||||
Respond with a STRICT JSON array and nothing else: no prose, no markdown fences, no comments. Schema:
|
||||
|
||||
[{"pr": 123, "url": "https://github.com/Kilo-Org/kilocode/pull/123", "docs_worthy": true, "reason": "Adds --variant flag to kilo run", "target_sections": ["code-with-ai/platforms/cli"], "priority": "high"}]
|
||||
@@ -0,0 +1,111 @@
|
||||
// kilocode_change - new file
|
||||
|
||||
/**
|
||||
* Runs the LLM triage pass over docs-sync-out/digest.json in chunks.
|
||||
*
|
||||
* A daily window holds ~30-50 PRs; a replay can hold several hundred. A
|
||||
* single triage call over that volume truncates its JSON output, so the
|
||||
* digest is split into chunks of CHUNK_SIZE and each chunk is triaged with
|
||||
* its own `kilo run` call. A chunk that fails twice is degraded to
|
||||
* "unclassified" entries (docs_worthy=false) instead of failing the run —
|
||||
* the PR body then shows those PRs as skipped, visible to reviewers.
|
||||
*
|
||||
* Env: TRIAGE_MODEL (provider/model), KILO_API_KEY (gateway auth, set by the workflow;
|
||||
* the kilo provider reads it natively). Reads the prompt from triage-prompt.md next to this script.
|
||||
*/
|
||||
|
||||
import { execFileSync } from "node:child_process"
|
||||
import fs from "node:fs"
|
||||
import path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { parseTriageEntries } from "./extract-json.mjs"
|
||||
|
||||
const CHUNK_SIZE = 25
|
||||
const ATTEMPTS = 2
|
||||
const OUT_DIR = "docs-sync-out"
|
||||
|
||||
const HERE = path.dirname(fileURLToPath(import.meta.url))
|
||||
const prompt = fs.readFileSync(path.join(HERE, "triage-prompt.md"), "utf8")
|
||||
const model = process.env.TRIAGE_MODEL
|
||||
if (!model) throw new Error("TRIAGE_MODEL is required")
|
||||
|
||||
const digest = JSON.parse(fs.readFileSync(`${OUT_DIR}/digest.json`, "utf8"))
|
||||
|
||||
function triageChunk(chunk, index) {
|
||||
const chunkFile = `${OUT_DIR}/triage-chunk-${index}.json`
|
||||
fs.writeFileSync(chunkFile, JSON.stringify(chunk, null, 2))
|
||||
|
||||
for (let attempt = 1; attempt <= ATTEMPTS; attempt++) {
|
||||
let raw
|
||||
try {
|
||||
// Message positional first: --file is multi-value and would otherwise
|
||||
// consume a trailing message as a file path ("File not found").
|
||||
raw = execFileSync(
|
||||
"kilo",
|
||||
["run", prompt, "-m", model, "--dir", process.cwd(), "-f", chunkFile],
|
||||
{ encoding: "utf8", maxBuffer: 32 * 1024 * 1024, timeout: 10 * 60 * 1000, stdio: ["ignore", "pipe", "pipe"] },
|
||||
)
|
||||
} catch (err) {
|
||||
const stderr = String(err.stderr ?? "").trim().split("\n").slice(-5).join("\n")
|
||||
console.warn(`chunk ${index} attempt ${attempt}: kilo run failed: ${stderr || err.message}`)
|
||||
continue
|
||||
}
|
||||
fs.writeFileSync(`${OUT_DIR}/triage-raw-${index}.txt`, raw)
|
||||
const entries = parseTriageEntries(raw)
|
||||
if (entries) {
|
||||
// An entry for a PR outside this chunk must not win the shared dedupe
|
||||
// against the chunk that actually owns it — drop foreign entries.
|
||||
const allowed = new Set(chunk.map((d) => d.url))
|
||||
const owned = entries.filter((e) => allowed.has(e.url))
|
||||
if (owned.length !== entries.length) {
|
||||
console.warn(`chunk ${index}: dropped ${entries.length - owned.length} entries for PRs outside the chunk`)
|
||||
}
|
||||
if (owned.length > 0) return owned
|
||||
}
|
||||
console.warn(`chunk ${index} attempt ${attempt}: no valid JSON in output`)
|
||||
}
|
||||
|
||||
console.warn(`::warning::chunk ${index} failed triage after ${ATTEMPTS} attempts; marking ${chunk.length} PRs unclassified`)
|
||||
return chunk.map((d) => ({
|
||||
pr: d.number,
|
||||
url: d.url,
|
||||
docs_worthy: false,
|
||||
reason: "triage failed to classify this PR",
|
||||
target_sections: [],
|
||||
priority: "medium",
|
||||
}))
|
||||
}
|
||||
|
||||
const chunks = []
|
||||
for (let i = 0; i < digest.length; i += CHUNK_SIZE) {
|
||||
chunks.push(digest.slice(i, i + CHUNK_SIZE))
|
||||
}
|
||||
console.log(`triaging ${digest.length} PRs in ${chunks.length} chunks of up to ${CHUNK_SIZE}`)
|
||||
|
||||
const merged = []
|
||||
const seen = new Set()
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
for (const e of triageChunk(chunks[i], i)) {
|
||||
if (seen.has(e.url)) continue
|
||||
seen.add(e.url)
|
||||
merged.push(e)
|
||||
}
|
||||
}
|
||||
|
||||
// Coverage: every digest PR gets a triage entry so the PR body's skipped
|
||||
// table is complete. Unclassified defaults to not-docs-worthy (conservative).
|
||||
for (const d of digest) {
|
||||
if (seen.has(d.url)) continue
|
||||
merged.push({
|
||||
pr: d.number,
|
||||
url: d.url,
|
||||
docs_worthy: false,
|
||||
reason: "not classified by triage",
|
||||
target_sections: [],
|
||||
priority: "medium",
|
||||
})
|
||||
}
|
||||
|
||||
fs.writeFileSync(`${OUT_DIR}/triage.json`, JSON.stringify(merged, null, 2))
|
||||
const worthy = merged.filter((e) => e.docs_worthy).length
|
||||
console.log(`triage complete: ${merged.length} entries, ${worthy} docs-worthy`)
|
||||
@@ -0,0 +1,241 @@
|
||||
// kilocode_change - new file
|
||||
|
||||
/**
|
||||
* Commits the agent's packages/kilo-docs changes, pushes the rolling branch,
|
||||
* and creates or updates the rolling auto-docs PR.
|
||||
*
|
||||
* No-op when the agent produced no docs changes. PRs become drafts when the
|
||||
* diff exceeds the file cap or verification failed. The PR body carries
|
||||
* marker-delimited sections so later runs can append rows, plus a
|
||||
* machine-readable processed-through watermark.
|
||||
*/
|
||||
|
||||
import { execFileSync } from "node:child_process"
|
||||
import fs from "node:fs"
|
||||
import { pathToFileURL } from "node:url"
|
||||
|
||||
const BRANCH = process.env.BRANCH || "docs/auto-sync"
|
||||
const FILE_CAP = 15
|
||||
const ROW_CAP = 150
|
||||
const SUMMARY_FILE = ".docs-sync-summary.json"
|
||||
const DOCS_PATH = "packages/kilo-docs"
|
||||
|
||||
const git = (args) => execFileSync("git", args, { stdio: ["ignore", "pipe", "inherit"] }).toString().trim()
|
||||
|
||||
// Agent-generated strings land in the PR body next to machine-read markers.
|
||||
// Strip HTML-comment sequences so a crafted/adversarial value cannot forge
|
||||
// section boundaries or the processed-through watermark.
|
||||
function clean(value) {
|
||||
return String(value ?? "").replaceAll("<!--", "").replaceAll("-->", "")
|
||||
}
|
||||
|
||||
function shortRef(url) {
|
||||
return clean(url).replace("https://github.com/", "").replace("/pull/", "#")
|
||||
}
|
||||
|
||||
function changeRow(e) {
|
||||
return `| ${clean(e.action).replaceAll("|", "\\|")} | [${shortRef(e.url)}](${clean(e.url)}) |`
|
||||
}
|
||||
|
||||
function skippedRow(e) {
|
||||
const reason = clean(e.reason).replaceAll("|", "\\|").replaceAll("\n", " ")
|
||||
return `| [${shortRef(e.url)}](${clean(e.url)}) | ${reason} |`
|
||||
}
|
||||
|
||||
export function extractSectionRows(body, name) {
|
||||
const m = String(body ?? "").match(
|
||||
new RegExp(`<!--\\s*docs-sync:${name}:start\\s*-->([\\s\\S]*?)<!--\\s*docs-sync:${name}:end\\s*-->`),
|
||||
)
|
||||
if (!m) return []
|
||||
return m[1]
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
.filter((l) => l.startsWith("|") && !l.startsWith("| ---") && !/^\|\s*Docs change/.test(l) && !/^\|\s*PR\s*\|/.test(l))
|
||||
}
|
||||
|
||||
function section(name, header, rows) {
|
||||
const body = rows.length > 0 ? [header, "| --- | --- |", ...rows].join("\n") : "_None._"
|
||||
return `<!-- docs-sync:${name}:start -->\n${body}\n<!-- docs-sync:${name}:end -->`
|
||||
}
|
||||
|
||||
export function renderBody({ date, since, through, changesRows, skippedRows, verified, draftReasons, note }) {
|
||||
return `## Automated docs sync — ${date}
|
||||
|
||||
This PR keeps kilo.ai/docs in sync with features merged to [Kilo-Org/cloud](https://github.com/Kilo-Org/cloud) and [Kilo-Org/kilocode](https://github.com/Kilo-Org/kilocode). Every change below links to the merged PR it documents.
|
||||
|
||||
- Window: \`${since}\` → \`${through}\`
|
||||
- Verification (docs build + tests): **${verified ? "passing" : "FAILING — needs a human look"}**
|
||||
${note ? `- ${note}\n` : ""}${draftReasons.length > 0 ? `- Draft because: ${draftReasons.join("; ")}\n` : ""}
|
||||
### Changes
|
||||
|
||||
${section("changes", "| Docs change | Source |", changesRows)}
|
||||
|
||||
### Considered, no docs change needed
|
||||
|
||||
${section("skipped", "| PR | Reason |", skippedRows)}
|
||||
|
||||
---
|
||||
|
||||
(bot) Generated by the docs-sync workflow. Humans review and merge; while this PR stays open, the next daily run appends new changes here. Branch: \`${BRANCH}\`.
|
||||
<!-- docs-sync: processed-through ${through} -->
|
||||
`
|
||||
}
|
||||
|
||||
function mergeRows(oldRows, newRows) {
|
||||
const seen = new Set()
|
||||
const out = []
|
||||
for (const row of [...oldRows, ...newRows]) {
|
||||
if (seen.has(row)) continue
|
||||
seen.add(row)
|
||||
out.push(row)
|
||||
}
|
||||
return out.slice(-ROW_CAP)
|
||||
}
|
||||
|
||||
function readJson(path, fallback) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(path, "utf8"))
|
||||
} catch {
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { api, appendOutput, appendSummary, repo } = await import("./lib.mjs")
|
||||
|
||||
const through = process.env.PROCESSED_THROUGH ?? new Date().toISOString()
|
||||
const since = process.env.SINCE ?? "unknown"
|
||||
const mode = ["update", "conflict"].includes(process.env.PREP_MODE) ? process.env.PREP_MODE : "fresh"
|
||||
const existingPr = process.env.PR_NUMBER || ""
|
||||
const verified = process.env.VERIFIED === "true"
|
||||
const date = through.slice(0, 10)
|
||||
|
||||
// The agent's run summary is consumed here and never committed.
|
||||
const agentSummary = readJson(SUMMARY_FILE, [])
|
||||
fs.rmSync(SUMMARY_FILE, { force: true })
|
||||
const triage = readJson("docs-sync-out/triage.json", [])
|
||||
|
||||
if (git(["status", "--porcelain", "--", DOCS_PATH]) === "") {
|
||||
console.log("no packages/kilo-docs changes produced; nothing to commit")
|
||||
appendSummary("### docs-sync: no docs changes\n\nThe agent found nothing worth documenting in this window.")
|
||||
return
|
||||
}
|
||||
|
||||
git(["config", "user.name", "github-actions[bot]"])
|
||||
git(["config", "user.email", "41898282+github-actions[bot]@users.noreply.github.com"])
|
||||
git(["add", DOCS_PATH])
|
||||
git(["commit", "-m", `docs: sync with merged PRs (${date})`])
|
||||
|
||||
// The draft cap bounds the cumulative PR diff, not just this run's commit.
|
||||
const changedFiles = git(["diff", "--name-only", "origin/main...HEAD", "--", DOCS_PATH])
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
const draftReasons = []
|
||||
if (changedFiles.length > FILE_CAP) draftReasons.push(`diff exceeds ${FILE_CAP} files (${changedFiles.length})`)
|
||||
if (!verified) draftReasons.push("docs build/tests not passing")
|
||||
// Content gate: legitimate bot edits are docs pages and nav files. Anything
|
||||
// else in the docs package (build config, components, tests) executes
|
||||
// during the verify build, so force human review before merge.
|
||||
const nonContent = changedFiles.filter(
|
||||
(f) => !f.startsWith("packages/kilo-docs/pages/") && !f.startsWith("packages/kilo-docs/lib/nav/"),
|
||||
)
|
||||
if (nonContent.length > 0) {
|
||||
// File paths are agent-chosen; sanitize before they land in the PR body.
|
||||
const listed = nonContent
|
||||
.slice(0, 5)
|
||||
.map((f) => clean(f).replaceAll("|", "\\|"))
|
||||
.join(", ")
|
||||
draftReasons.push(`touches non-content files outside pages/ and lib/nav/: ${listed}`)
|
||||
}
|
||||
const draft = draftReasons.length > 0
|
||||
|
||||
git(mode === "update" ? ["push", "origin", `HEAD:${BRANCH}`] : ["push", "--force-with-lease", "origin", `HEAD:${BRANCH}`])
|
||||
|
||||
const changesNew = agentSummary.filter((e) => e.action !== "skipped").map(changeRow)
|
||||
const skippedNew = [
|
||||
...triage.filter((e) => e.docs_worthy === false),
|
||||
...agentSummary.filter((e) => e.action === "skipped"),
|
||||
].map(skippedRow)
|
||||
|
||||
let oldChanges = []
|
||||
let oldSkipped = []
|
||||
if (mode === "update" && existingPr) {
|
||||
const pr = await api(`/repos/${repo()}/pulls/${existingPr}`)
|
||||
oldChanges = extractSectionRows(pr.body, "changes")
|
||||
oldSkipped = extractSectionRows(pr.body, "skipped")
|
||||
}
|
||||
|
||||
const body = renderBody({
|
||||
date,
|
||||
since,
|
||||
through,
|
||||
changesRows: mergeRows(oldChanges, changesNew),
|
||||
skippedRows: mergeRows(oldSkipped, skippedNew),
|
||||
verified,
|
||||
draftReasons,
|
||||
note:
|
||||
mode === "conflict" && existingPr
|
||||
? `Continues from #${existingPr}, whose branch conflicted with \`main\` (its commits are preserved there).`
|
||||
: "",
|
||||
})
|
||||
|
||||
try {
|
||||
await api(`/repos/${repo()}/labels`, {
|
||||
method: "POST",
|
||||
body: { name: "auto-docs", color: "1d76db", description: "Automated docs-sync PRs" },
|
||||
})
|
||||
} catch (err) {
|
||||
if (err.status !== 422) throw err // 422 = label already exists
|
||||
}
|
||||
|
||||
let prNumber
|
||||
let prUrl
|
||||
if (mode === "update" && existingPr) {
|
||||
const pr = await api(`/repos/${repo()}/pulls/${existingPr}`, {
|
||||
method: "PATCH",
|
||||
body: { title: `docs: auto-sync with merged PRs (through ${date})`, body },
|
||||
})
|
||||
prNumber = pr.number
|
||||
prUrl = pr.html_url
|
||||
await api(`/repos/${repo()}/issues/${prNumber}/comments`, {
|
||||
method: "POST",
|
||||
body: {
|
||||
body: `(bot) Appended changes processed through \`${through}\`. Verification: **${verified ? "passing" : "failing"}**.${draft ? ` Draft because: ${draftReasons.join("; ")}.` : ""}`,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
const pr = await api(`/repos/${repo()}/pulls`, {
|
||||
method: "POST",
|
||||
body: {
|
||||
title: `docs: auto-sync with merged PRs (through ${date})`,
|
||||
head: BRANCH,
|
||||
base: "main",
|
||||
body,
|
||||
draft,
|
||||
},
|
||||
})
|
||||
prNumber = pr.number
|
||||
prUrl = pr.html_url
|
||||
await api(`/repos/${repo()}/issues/${prNumber}/labels`, { method: "POST", body: { labels: ["auto-docs"] } })
|
||||
if (mode === "conflict" && existingPr) {
|
||||
await api(`/repos/${repo()}/issues/${existingPr}/comments`, {
|
||||
method: "POST",
|
||||
body: {
|
||||
body: `(bot) This branch conflicted with \`main\`, so the sync continues in ${prUrl}. Commits on this branch are preserved — please close this PR after the new one is reviewed.`,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
appendOutput("pr_url", prUrl)
|
||||
appendSummary(`### docs-sync PR\n\n- ${prUrl}\n- changed files: ${changedFiles.length}\n- draft: ${draft}\n`)
|
||||
console.log(`PR ${prNumber}: ${prUrl} (draft=${draft}, files=${changedFiles.length})`)
|
||||
}
|
||||
|
||||
const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href
|
||||
if (isMain) {
|
||||
main().catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// kilocode_change - new file
|
||||
|
||||
/**
|
||||
* Resolves the docs-sync watermark: the timestamp of the newest source PR the
|
||||
* bot has already processed. Derived from the bot's own PRs (marker in the PR
|
||||
* body), so there is no external state to keep consistent.
|
||||
*
|
||||
* Priority: workflow_dispatch input `since` > latest open bot PR marker >
|
||||
* last merged bot PR marker > 72h ago. Hard cap: never look back more than
|
||||
* 14 days.
|
||||
*/
|
||||
|
||||
import { appendOutput, appendSummary, repo, searchIssues } from "./lib.mjs"
|
||||
|
||||
const FALLBACK_HOURS = 72
|
||||
const CAP_DAYS = 14
|
||||
const MARKER = /<!--\s*docs-sync:\s*processed-through\s+(\S+?)\s*-->/
|
||||
|
||||
function extractMarker(body) {
|
||||
const m = (body ?? "").match(MARKER)
|
||||
if (!m) return null
|
||||
const d = new Date(m[1])
|
||||
return Number.isNaN(d.getTime()) ? null : d
|
||||
}
|
||||
|
||||
async function findWatermark() {
|
||||
const r = repo()
|
||||
for (const state of ["open", "merged"]) {
|
||||
const query = `repo:${r} is:pr label:auto-docs sort:created-desc ${state === "open" ? "is:open" : "is:merged"}`
|
||||
const prs = await searchIssues(query, { maxPages: 1 })
|
||||
for (const pr of prs) {
|
||||
// Only trust markers on PRs authored by the bot itself: bodies are
|
||||
// editable and the label can be applied by anyone with triage access.
|
||||
if (pr.user?.login !== "github-actions[bot]") continue
|
||||
const marker = extractMarker(pr.body)
|
||||
if (marker) {
|
||||
console.log(`watermark from ${state} PR #${pr.number}: ${marker.toISOString()}`)
|
||||
return marker
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
let since
|
||||
|
||||
const input = (process.env.INPUT_SINCE ?? "").trim()
|
||||
if (input) {
|
||||
since = new Date(input)
|
||||
if (Number.isNaN(since.getTime())) {
|
||||
throw new Error(`Invalid INPUT_SINCE: ${input}`)
|
||||
}
|
||||
console.log(`watermark from dispatch input: ${since.toISOString()}`)
|
||||
} else {
|
||||
since =
|
||||
(await findWatermark()) ?? new Date(now.getTime() - FALLBACK_HOURS * 3600 * 1000)
|
||||
}
|
||||
|
||||
// A forged, edited, or malformed marker in the future would silently match
|
||||
// nothing in the merged:>= search; clamp it loudly.
|
||||
if (since > now) {
|
||||
console.warn(`watermark ${since.toISOString()} is in the future, clamping to now`)
|
||||
since = now
|
||||
}
|
||||
|
||||
const cap = new Date(now.getTime() - CAP_DAYS * 24 * 3600 * 1000)
|
||||
if (since < cap) {
|
||||
console.log(`watermark ${since.toISOString()} older than ${CAP_DAYS}d cap, clamping`)
|
||||
since = cap
|
||||
}
|
||||
|
||||
appendOutput("since", since.toISOString())
|
||||
appendOutput("now", now.toISOString())
|
||||
appendSummary(`### docs-sync watermark\n\n- since: \`${since.toISOString()}\`\n- now: \`${now.toISOString()}\`\n`)
|
||||
@@ -0,0 +1,166 @@
|
||||
# kilocode_change - new file
|
||||
name: docs-sync
|
||||
|
||||
# Daily bot: collects PRs merged to Kilo-Org/cloud and Kilo-Org/kilocode,
|
||||
# triages them for docs relevance, runs Kilo CLI headless to update
|
||||
# packages/kilo-docs, and maintains one rolling PR for human review.
|
||||
#
|
||||
# Security posture: scheduled/manual only, checks out main, never executes
|
||||
# code from PR branches. State is derived from the bot's own PRs (watermark
|
||||
# marker in the PR body), so missed or failed runs self-heal on the next run.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 7 * * *" # 07:00 UTC daily
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
since:
|
||||
description: "Override watermark (ISO date, e.g. 2026-07-20). Default: last processed-through marker, 72h fallback, 14d cap."
|
||||
required: false
|
||||
type: string
|
||||
dry_run:
|
||||
description: "Collect + triage only, no edits, no PR"
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: write # push the rolling branch, create the auto-docs label
|
||||
pull-requests: write # create/update the rolling PR
|
||||
issues: write # comment on the rolling PR
|
||||
|
||||
concurrency:
|
||||
group: docs-sync
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
TRIAGE_MODEL: ${{ vars.DOCS_SYNC_TRIAGE_MODEL || 'kilo/moonshotai/kimi-k3' }}
|
||||
EDIT_MODEL: ${{ vars.DOCS_SYNC_EDIT_MODEL || 'kilo/moonshotai/kimi-k3' }}
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
if: github.repository == 'Kilo-Org/kilocode'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0 # prepare-branch merges main into the rolling branch
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24"
|
||||
package-manager-cache: false
|
||||
|
||||
- name: Install Kilo CLI
|
||||
run: |
|
||||
npm install -g @kilocode/cli
|
||||
kilo --version
|
||||
|
||||
- name: Resolve watermark
|
||||
id: wm
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
INPUT_SINCE: ${{ inputs.since }}
|
||||
run: node .github/docs-sync/watermark.mjs
|
||||
|
||||
- name: Collect merged PRs
|
||||
id: collect
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: node .github/docs-sync/collect.mjs --since "${{ steps.wm.outputs.since }}"
|
||||
|
||||
- name: Triage merged PRs (LLM, chunked)
|
||||
id: triage
|
||||
if: steps.collect.outputs.count != '0'
|
||||
env:
|
||||
KILO_API_KEY: ${{ secrets.KILO_API_KEY }}
|
||||
run: node .github/docs-sync/triage.mjs
|
||||
|
||||
- name: Filter docs-worthy PRs
|
||||
id: worthy
|
||||
if: steps.collect.outputs.count != '0'
|
||||
run: |
|
||||
node .github/docs-sync/filter-worthy.mjs \
|
||||
docs-sync-out/digest-full.json docs-sync-out/triage.json docs-sync-out/worthy.json
|
||||
count=$(node -p "require('./docs-sync-out/worthy.json').length")
|
||||
echo "count=$count" >> "$GITHUB_OUTPUT"
|
||||
if [ "$count" = "0" ]; then
|
||||
echo "No docs-worthy PRs in this window; skipping edit/verify/PR."
|
||||
fi
|
||||
|
||||
- name: Setup Bun
|
||||
if: (steps.worthy.outputs.count || '0') != '0' && inputs.dry_run != true
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Prepare rolling branch
|
||||
id: prep
|
||||
if: (steps.worthy.outputs.count || '0') != '0' && inputs.dry_run != true
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: node .github/docs-sync/prepare-branch.mjs
|
||||
|
||||
- name: Update docs (Kilo CLI, batched)
|
||||
if: (steps.worthy.outputs.count || '0') != '0' && inputs.dry_run != true
|
||||
env:
|
||||
KILO_API_KEY: ${{ secrets.KILO_API_KEY }}
|
||||
run: node .github/docs-sync/edit.mjs
|
||||
|
||||
- name: Verify docs build and tests
|
||||
id: verify
|
||||
if: (steps.worthy.outputs.count || '0') != '0' && inputs.dry_run != true
|
||||
continue-on-error: true
|
||||
env:
|
||||
NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.POSTHOG_API_KEY }}
|
||||
run: |
|
||||
set -o pipefail
|
||||
{ bun run --filter @kilocode/kilo-docs build && bun run --filter @kilocode/kilo-docs test; } 2>&1 | tee docs-sync-out/verify.log
|
||||
|
||||
- name: Fix verify failures (one pass)
|
||||
id: fix
|
||||
if: steps.verify.outcome == 'failure'
|
||||
continue-on-error: true
|
||||
env:
|
||||
KILO_API_KEY: ${{ secrets.KILO_API_KEY }}
|
||||
NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.POSTHOG_API_KEY }}
|
||||
run: |
|
||||
set -o pipefail
|
||||
kilo run "The docs build or tests failed. Read the attached docs-sync-out/verify.log and fix the packages/kilo-docs changes so they pass. Do not revert doc edits; fix them. Do not modify anything outside packages/kilo-docs." \
|
||||
-m "$EDIT_MODEL" --dir "$GITHUB_WORKSPACE" -f docs-sync-out/verify.log \
|
||||
| tee -a docs-sync-out/edit-log.txt
|
||||
{ bun run --filter @kilocode/kilo-docs build && bun run --filter @kilocode/kilo-docs test; } 2>&1 | tee docs-sync-out/verify2.log
|
||||
|
||||
- name: Re-verify status
|
||||
id: verified
|
||||
if: (steps.worthy.outputs.count || '0') != '0' && inputs.dry_run != true
|
||||
env:
|
||||
VERIFY_OUTCOME: ${{ steps.verify.outcome }}
|
||||
FIX_OUTCOME: ${{ steps.fix.outcome }}
|
||||
run: |
|
||||
if [ "$VERIFY_OUTCOME" = "success" ] || [ "$FIX_OUTCOME" = "success" ]; then
|
||||
echo "ok=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "ok=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Upsert rolling PR
|
||||
if: (steps.worthy.outputs.count || '0') != '0' && inputs.dry_run != true
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PROCESSED_THROUGH: ${{ steps.wm.outputs.now }}
|
||||
SINCE: ${{ steps.wm.outputs.since }}
|
||||
BRANCH: ${{ steps.prep.outputs.branch }}
|
||||
PREP_MODE: ${{ steps.prep.outputs.mode }}
|
||||
PR_NUMBER: ${{ steps.prep.outputs.pr_number }}
|
||||
VERIFIED: ${{ steps.verified.outputs.ok }}
|
||||
run: node .github/docs-sync/upsert-pr.mjs
|
||||
|
||||
- name: Upload run artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: docs-sync-out
|
||||
path: docs-sync-out/
|
||||
retention-days: 14
|
||||
if-no-files-found: ignore
|
||||
@@ -32,7 +32,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opencode-ai/core",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -127,7 +127,7 @@
|
||||
},
|
||||
"packages/effect-drizzle-sqlite": {
|
||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"dependencies": {
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -141,7 +141,7 @@
|
||||
},
|
||||
"packages/effect-sqlite-node": {
|
||||
"name": "@opencode-ai/effect-sqlite-node",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"dependencies": {
|
||||
"effect": "catalog:",
|
||||
},
|
||||
@@ -153,7 +153,7 @@
|
||||
},
|
||||
"packages/http-recorder": {
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "4.0.0-beta.74",
|
||||
"@effect/platform-node-shared": "4.0.0-beta.74",
|
||||
@@ -174,7 +174,7 @@
|
||||
},
|
||||
"packages/kilo-console": {
|
||||
"name": "@kilocode/kilo-console",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"dependencies": {
|
||||
"@kilocode/kilo-indexing": "workspace:*",
|
||||
"@kilocode/kilo-web-ui": "workspace:*",
|
||||
@@ -197,7 +197,7 @@
|
||||
},
|
||||
"packages/kilo-docs": {
|
||||
"name": "@kilocode/kilo-docs",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"dependencies": {
|
||||
"@docsearch/css": "^4",
|
||||
"@docsearch/js": "^4",
|
||||
@@ -227,7 +227,7 @@
|
||||
},
|
||||
"packages/kilo-gateway": {
|
||||
"name": "@kilocode/kilo-gateway",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"dependencies": {
|
||||
"@ai-sdk/alibaba": "1.0.17",
|
||||
"@ai-sdk/anthropic": "3.0.71",
|
||||
@@ -263,7 +263,7 @@
|
||||
},
|
||||
"packages/kilo-i18n": {
|
||||
"name": "@kilocode/kilo-i18n",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"devDependencies": {
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
@@ -273,7 +273,7 @@
|
||||
},
|
||||
"packages/kilo-indexing": {
|
||||
"name": "@kilocode/kilo-indexing",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-bedrock-runtime": "3.1005.0",
|
||||
"@aws-sdk/credential-provider-ini": "3.972.31",
|
||||
@@ -305,11 +305,11 @@
|
||||
},
|
||||
"packages/kilo-jetbrains": {
|
||||
"name": "@kilocode/kilo-jetbrains",
|
||||
"version": "7.4.13",
|
||||
"version": "7.4.15",
|
||||
},
|
||||
"packages/kilo-memory": {
|
||||
"name": "@kilocode/kilo-memory",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"dependencies": {
|
||||
"effect": "catalog:",
|
||||
"zod": "catalog:",
|
||||
@@ -323,7 +323,7 @@
|
||||
},
|
||||
"packages/kilo-sandbox": {
|
||||
"name": "@kilocode/sandbox",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sandbox-runtime": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -338,7 +338,7 @@
|
||||
},
|
||||
"packages/kilo-telemetry": {
|
||||
"name": "@kilocode/kilo-telemetry",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"dependencies": {
|
||||
"@kilocode/kilo-gateway": "workspace:*",
|
||||
"posthog-node": "4.4.0",
|
||||
@@ -352,7 +352,7 @@
|
||||
},
|
||||
"packages/kilo-ui": {
|
||||
"name": "@kilocode/kilo-ui",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"dependencies": {
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"@kobalte/core": "0.13.11",
|
||||
@@ -389,7 +389,7 @@
|
||||
},
|
||||
"packages/kilo-vscode": {
|
||||
"name": "kilo-code",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.39.0",
|
||||
"@kilocode/kilo-gateway": "workspace:*",
|
||||
@@ -458,7 +458,7 @@
|
||||
},
|
||||
"packages/kilo-web-ui": {
|
||||
"name": "@kilocode/kilo-web-ui",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"dependencies": {
|
||||
"@kilocode/kilo-ui": "workspace:*",
|
||||
"@kobalte/core": "catalog:",
|
||||
@@ -475,7 +475,7 @@
|
||||
},
|
||||
"packages/llm": {
|
||||
"name": "@opencode-ai/llm",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"dependencies": {
|
||||
"@smithy/eventstream-codec": "4.2.14",
|
||||
"@smithy/util-utf8": "4.2.2",
|
||||
@@ -493,7 +493,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "@kilocode/cli",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"bin": {
|
||||
"kilo": "./bin/kilo",
|
||||
"kilocode": "./bin/kilo",
|
||||
@@ -660,7 +660,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@kilocode/plugin",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"dependencies": {
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
@@ -688,7 +688,7 @@
|
||||
},
|
||||
"packages/plugin-atomic-chat": {
|
||||
"name": "@kilocode/plugin-atomic-chat",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"dependencies": {
|
||||
"@kilocode/plugin": "workspace:*",
|
||||
},
|
||||
@@ -702,7 +702,7 @@
|
||||
},
|
||||
"packages/script": {
|
||||
"name": "@opencode-ai/script",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"dependencies": {
|
||||
"semver": "^7.6.3",
|
||||
},
|
||||
@@ -713,7 +713,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@kilocode/sdk",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"dependencies": {
|
||||
"cross-spawn": "catalog:",
|
||||
},
|
||||
@@ -728,7 +728,7 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@opencode-ai/server",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"dependencies": {
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"drizzle-orm": "catalog:",
|
||||
@@ -742,7 +742,7 @@
|
||||
},
|
||||
"packages/storybook": {
|
||||
"name": "@opencode-ai/storybook",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"devDependencies": {
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@solidjs/meta": "catalog:",
|
||||
@@ -765,7 +765,7 @@
|
||||
},
|
||||
"packages/tui": {
|
||||
"name": "@opencode-ai/tui",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"dependencies": {
|
||||
"@kilocode/plugin": "workspace:*",
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
@@ -792,7 +792,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"dependencies": {
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"@kobalte/core": "catalog:",
|
||||
|
||||
+1
-1
@@ -165,6 +165,6 @@
|
||||
"pacote@21.5.1": "patches/pacote@21.5.1.patch",
|
||||
"mammoth@1.12.0": "patches/mammoth@1.12.0.patch"
|
||||
},
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"peerDependencies": {}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"name": "@opencode-ai/core",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -253,6 +253,8 @@ export const StepFinishPart = Schema.Struct({
|
||||
modelID: ModelV2.ID,
|
||||
}),
|
||||
),
|
||||
generationID: Schema.optional(Schema.String), // kilocode_change
|
||||
vercelID: Schema.optional(Schema.String), // kilocode_change
|
||||
metrics: Schema.optional(
|
||||
Schema.Struct({
|
||||
prompt: Schema.optional(Schema.Finite),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"name": "@opencode-ai/effect-sqlite-node",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "kilo"
|
||||
name = "Kilo"
|
||||
description = "The open source coding agent."
|
||||
version = "7.4.15"
|
||||
version = "7.4.16"
|
||||
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.4.15/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.16/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.4.15/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.16/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.15/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.16/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.4.15/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.16/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.4.15/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.16/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"description": "Record and replay Effect HTTP client traffic with deterministic cassettes",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kilocode/kilo-console",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kilocode/kilo-docs",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --webpack --port 3002",
|
||||
|
||||
@@ -62,5 +62,5 @@ Then set your default model:
|
||||
## Tips and Notes
|
||||
|
||||
- **Performance:** Fireworks AI is optimized for speed and offers excellent performance for both chat and completion tasks.
|
||||
- **Pricing:** Refer to the [Fireworks AI Pricing](https://fireworks.ai/pricing) page for current pricing information.
|
||||
- **Pricing:** Refer to the [Fireworks AI Pricing](https://docs.fireworks.ai/serverless/pricing) page for current pricing information.
|
||||
- **Rate Limits:** Fireworks AI has usage-based rate limits. Monitor your usage in the dashboard and consider upgrading your plan if needed.
|
||||
|
||||
@@ -11,6 +11,10 @@ description: "Configure automatic approval settings for Kilo Code operations"
|
||||
|
||||
Auto-approve settings speed up your workflow by eliminating repetitive confirmation prompts, but they significantly increase security risks. The VS Code extension and CLI share the same permission model; choose the tab that matches how you configure Kilo Code.
|
||||
|
||||
{% callout type="note" %}
|
||||
**Editing project config while a session is running:** Kilo caches project-level `kilo.jsonc` / `kilo.json` (in `.kilo/`) when it first loads a workspace, and does not re-read it on every prompt. If you add, change, or remove a project permission rule while the backend is already running, reload the VS Code window (or start a fresh CLI session) for the change to take effect. Until then, Kilo keeps using the previously loaded rules — so an auto-approved call may still cite a project rule you just edited. Global config (`~/.config/kilo/`) is reloaded automatically.
|
||||
{% /callout %}
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="VSCode" %}
|
||||
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9ccf10c46b0e7bb882eee890c27396821b16b4ffef995da648b9bc47768360cb
|
||||
size 22759
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-gateway",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Unified Kilo Gateway package for OpenCode - authentication, provider, and API integration",
|
||||
|
||||
@@ -32,13 +32,9 @@ export async function fetchKiloPassState(token: string): Promise<KiloPassState |
|
||||
const response = await fetch(`${KILO_API_BASE}/api/trpc/kiloPass.getState?${params}`, {
|
||||
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", ...buildKiloHeaders() },
|
||||
})
|
||||
if (!response.ok) {
|
||||
console.warn(`Failed to fetch Kilo Pass: ${response.status}`)
|
||||
return null
|
||||
}
|
||||
if (!response.ok) return null
|
||||
return parseKiloPassState(await response.json())
|
||||
} catch (err) {
|
||||
console.warn("Error fetching Kilo Pass:", err)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { parseKiloPassState } from "../../src/api/kilo-pass"
|
||||
import { describe, expect, mock, spyOn, test } from "bun:test"
|
||||
import { fetchKiloPassState, parseKiloPassState } from "../../src/api/kilo-pass"
|
||||
|
||||
describe("parseKiloPassState", () => {
|
||||
test("parses batched tRPC subscription data", () => {
|
||||
@@ -60,4 +60,32 @@ describe("parseKiloPassState", () => {
|
||||
test("returns null without period amounts", () => {
|
||||
expect(parseKiloPassState({ status: "none" })).toBeNull()
|
||||
})
|
||||
|
||||
test("silently ignores transport failures", async () => {
|
||||
const prev = global.fetch
|
||||
const warn = spyOn(console, "warn").mockImplementation(() => undefined)
|
||||
global.fetch = mock(() => Promise.reject(new DOMException("The operation timed out.", "TimeoutError")))
|
||||
|
||||
try {
|
||||
await expect(fetchKiloPassState("token")).resolves.toBeNull()
|
||||
expect(warn).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
warn.mockRestore()
|
||||
global.fetch = prev
|
||||
}
|
||||
})
|
||||
|
||||
test("silently ignores unsuccessful responses", async () => {
|
||||
const prev = global.fetch
|
||||
const warn = spyOn(console, "warn").mockImplementation(() => undefined)
|
||||
global.fetch = mock(() => Promise.resolve(new Response(null, { status: 503 })))
|
||||
|
||||
try {
|
||||
await expect(fetchKiloPassState("token")).resolves.toBeNull()
|
||||
expect(warn).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
warn.mockRestore()
|
||||
global.fetch = prev
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-i18n",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Kilo-specific i18n translations and overrides",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-indexing",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Standalone indexing engine and host helpers for Kilo Code",
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# Changelog
|
||||
|
||||
## 7.4.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#12491](https://github.com/Kilo-Org/kilocode/pull/12491) [`2b13e7d`](https://github.com/Kilo-Org/kilocode/commit/2b13e7da2a6a776baeb2d797cd5aaeb07a526c0b) - Improve JetBrains diff previews by hiding hunk headers and adding full-path tooltips to clickable file links.
|
||||
|
||||
- [#12491](https://github.com/Kilo-Org/kilocode/pull/12491) [`5c526f1`](https://github.com/Kilo-Org/kilocode/commit/5c526f140b78b13608ad3855532f5215c0b29675) - Render edit tool results with a clickable file target and a highlighted, simplified diff view.
|
||||
|
||||
- [#12491](https://github.com/Kilo-Org/kilocode/pull/12491) [`73942c3`](https://github.com/Kilo-Org/kilocode/commit/73942c3f262dda53030d748e6c08f84db2384253) - Open edit tool file links directly when multiple files share the same name.
|
||||
|
||||
- [#12491](https://github.com/Kilo-Org/kilocode/pull/12491) [`dd31044`](https://github.com/Kilo-Org/kilocode/commit/dd3104400840e1b4641097bf892e25dfccfd592d) - Render multi-file apply_patch edits as a "Patch" with a file-count tag and one section per file, each showing a clickable filename link and its own changes badge aligned with the diff.
|
||||
|
||||
- [#12491](https://github.com/Kilo-Org/kilocode/pull/12491) [`79e606e`](https://github.com/Kilo-Org/kilocode/commit/79e606ebcbb15d20b5fde29d614f07270b1c0b3d) - Smooth out chat scrolling in large JetBrains sessions by only refreshing hover state for the message under the pointer.
|
||||
|
||||
- [#12491](https://github.com/Kilo-Org/kilocode/pull/12491) [`95ae0e0`](https://github.com/Kilo-Org/kilocode/commit/95ae0e0b3b066ec5ab60c36b7bcffb973a942872) - Improve chat scrolling performance in large JetBrains sessions.
|
||||
|
||||
- [#12491](https://github.com/Kilo-Org/kilocode/pull/12491) [`b2a3a8d`](https://github.com/Kilo-Org/kilocode/commit/b2a3a8dc10d5f579396e1bd76e16a0eef696bede) - Size edit and shell preview popovers to their content with a wider maximum width.
|
||||
|
||||
## 7.5.0
|
||||
|
||||
### Minor Changes
|
||||
@@ -84,6 +102,21 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [7.0.10] - 2026-07-24
|
||||
|
||||
### Added
|
||||
|
||||
- Render edit, write, and apply-patch tool results as expandable diff previews with clickable file links, change counts, syntax-highlighted diffs, and clearer multi-file patch sections.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improve session performance for large transcripts.
|
||||
- Fix Kilo Core failures caused by strict OpenAI-compatible compaction requests, unexpected provider finish reasons, read-only database files at startup, AWS profile credentials, and config files being rewritten just by reading them.
|
||||
|
||||
### Changed
|
||||
|
||||
- Update the JetBrains CLI pin from Kilo Core 7.4.13 to 7.4.15.
|
||||
|
||||
## [7.0.9] - 2026-07-21
|
||||
|
||||
### Added
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
kotlin.stdlib.default.dependency=false
|
||||
kilo.jetbrains.version=7.0.9
|
||||
kilo.jetbrains.version=7.0.10
|
||||
# When true (default) the JetBrains plugin uses the pinned CLI release from package.json.
|
||||
# Set to false ONLY for local dev: generate the client from local source + bundle the local binary.
|
||||
# false is NOT releasable -- production builds fail unless this is true.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-memory",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Project memory storage, indexing, recall, and command helpers for Kilo Code",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/sandbox",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-telemetry",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Telemetry for Kilo CLI - PostHog analytics integration",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kilocode/kilo-ui",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
|
||||
@@ -494,3 +494,25 @@ html[data-theme="kilo-vscode"] [data-component="tool-part-wrapper"][data-part-ty
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* "why was this allowed" line inside a tool's expanded body */
|
||||
[data-slot="tool-approval-line"] {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 4px;
|
||||
padding: 4px 0 6px;
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
line-height: var(--line-height-normal);
|
||||
color: var(--text-weak);
|
||||
|
||||
[data-slot="tool-approval-decision"] {
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--text-strong);
|
||||
}
|
||||
|
||||
[data-slot="tool-approval-rule"] {
|
||||
font-family: var(--font-family-mono);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Show } from "solid-js"
|
||||
import { BasicTool as Base, GenericTool } from "@opencode-ai/ui/basic-tool"
|
||||
import type { BasicToolProps as BaseProps, TriggerTitle } from "@opencode-ai/ui/basic-tool"
|
||||
import { toolOpenKey, readToolOpen, writeToolOpen } from "./tool-open-state"
|
||||
import { useToolApproval, ToolApprovalLine } from "./tool-approval"
|
||||
|
||||
export { GenericTool }
|
||||
export type { TriggerTitle }
|
||||
@@ -20,16 +22,24 @@ export function initialOpen(props: OpenProps) {
|
||||
export function BasicTool(props: BasicToolProps) {
|
||||
const key = () => toolOpenKey(props)
|
||||
const initial = () => initialOpen(props)
|
||||
const approval = useToolApproval()
|
||||
const change = (open: boolean) => {
|
||||
writeToolOpen(key(), open)
|
||||
props.onOpenChange?.(open)
|
||||
}
|
||||
if (!("children" in props)) {
|
||||
// The "why was this allowed" line lives in the expanded body, above any tool-specific details.
|
||||
const details = () => (
|
||||
<div data-slot="basic-tool-details">
|
||||
<Show when={approval()}>{(value) => <ToolApprovalLine display={value()} />}</Show>
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
if (!("children" in props) && !approval()) {
|
||||
return <Base {...props} defaultOpen={initial()} retainDetails={props.defer} onOpenChange={change} />
|
||||
}
|
||||
return (
|
||||
<Base {...props} defaultOpen={initial()} retainDetails={props.defer} onOpenChange={change}>
|
||||
<div data-slot="basic-tool-details">{props.children}</div>
|
||||
<Base {...props} defaultOpen={initial()} retainDetails={props.defer} onOpenChange={change} hasDetails>
|
||||
{details()}
|
||||
</Base>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
/* Kilo Message Part overrides */
|
||||
|
||||
/* Message action rows (copy, fork, revert, feedback, delete queued)
|
||||
share one compact icon-button size. */
|
||||
[data-component="text-part"] [data-slot="text-part-copy-wrapper"][data-is-turn-copy],
|
||||
[data-component="text-part"] [data-slot="assistant-copy-wrapper"],
|
||||
[data-component="user-message"] [data-slot="user-message-copy-wrapper"],
|
||||
[data-component="user-message"] [data-slot="user-message-queued-indicator"] {
|
||||
[data-component="icon-button"] {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="text-part"] {
|
||||
margin-top: 8px;
|
||||
|
||||
@@ -13,11 +25,6 @@
|
||||
|
||||
[data-slot="text-part-copy-wrapper"][data-is-turn-copy] {
|
||||
display: flex;
|
||||
|
||||
[data-component="icon-button"] {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="assistant-copy-wrapper"] {
|
||||
@@ -27,11 +34,6 @@
|
||||
gap: 2px;
|
||||
margin-top: 2px;
|
||||
|
||||
[data-component="icon-button"] {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
/* Thumbs up/down: fill the outline on hover to preview the rated state. */
|
||||
[data-component="icon-button"][data-icon="thumbs-up"]:hover [data-slot="icon-svg"] path,
|
||||
[data-component="icon-button"][data-icon="thumbs-down"]:hover [data-slot="icon-svg"] path {
|
||||
@@ -513,11 +515,6 @@ html[data-theme="kilo-vscode"] [data-component="todos"] {
|
||||
[data-slot="user-message-meta-wrap"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-component="icon-button"] {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,8 @@ import { checksum } from "@opencode-ai/core/util/encode"
|
||||
import { Tooltip } from "./tooltip"
|
||||
import { IconButton } from "./icon-button"
|
||||
import { TextShimmer } from "@opencode-ai/ui/text-shimmer"
|
||||
import { ToolApprovalProvider, resolveToolApproval } from "./tool-approval"
|
||||
export { ToolApprovalProvider, resolveToolApproval } from "./tool-approval"
|
||||
import { GrowBox } from "./grow-box"
|
||||
import { COLLAPSIBLE_SPRING } from "./motion"
|
||||
import { busy, createThrottledValue, useToolFade, useContextToolPending } from "./tool-utils"
|
||||
@@ -820,18 +822,20 @@ export function UserMessageDisplay(props: {
|
||||
|
||||
const Delete = () => (
|
||||
<Show when={props.onDelete}>
|
||||
<IconButton
|
||||
data-slot="user-message-delete"
|
||||
icon="close-small"
|
||||
size="normal"
|
||||
variant="ghost"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
props.onDelete?.()
|
||||
}}
|
||||
aria-label={i18n.t("ui.message.deleteQueued")}
|
||||
/>
|
||||
<Tooltip value={i18n.t("ui.message.deleteQueued")} placement="right" gutter={4}>
|
||||
<IconButton
|
||||
data-slot="user-message-delete"
|
||||
icon="close-small"
|
||||
size="normal"
|
||||
variant="ghost"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
props.onDelete?.()
|
||||
}}
|
||||
aria-label={i18n.t("ui.message.deleteQueued")}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
)
|
||||
|
||||
@@ -1307,26 +1311,28 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
|
||||
}}
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<Dynamic
|
||||
component={render()}
|
||||
input={input()}
|
||||
tool={part.tool}
|
||||
partID={part.id}
|
||||
callID={part.callID}
|
||||
metadata={meta()}
|
||||
partMetadata={top()}
|
||||
// @ts-expect-error
|
||||
output={part.state.output}
|
||||
status={part.state.status}
|
||||
// @ts-expect-error
|
||||
attachments={part.state.attachments}
|
||||
hideDetails={props.hideDetails}
|
||||
defaultOpen={props.defaultOpen}
|
||||
forceOpen={props.forceOpen}
|
||||
forceOpenFile={props.forceOpenFile}
|
||||
animate
|
||||
reveal={props.animate}
|
||||
/>
|
||||
<ToolApprovalProvider value={() => resolveToolApproval(meta(), i18n.t as (k: string, p?: Record<string, string | number | boolean>) => string)}>
|
||||
<Dynamic
|
||||
component={render()}
|
||||
input={input()}
|
||||
tool={part.tool}
|
||||
partID={part.id}
|
||||
callID={part.callID}
|
||||
metadata={meta()}
|
||||
partMetadata={top()}
|
||||
// @ts-expect-error
|
||||
output={part.state.output}
|
||||
status={part.state.status}
|
||||
// @ts-expect-error
|
||||
attachments={part.state.attachments}
|
||||
hideDetails={props.hideDetails}
|
||||
defaultOpen={props.defaultOpen}
|
||||
forceOpen={props.forceOpen}
|
||||
forceOpenFile={props.forceOpenFile}
|
||||
animate
|
||||
reveal={props.animate}
|
||||
/>
|
||||
</ToolApprovalProvider>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { createContext, useContext, Show, type Accessor, type ParentProps } from "solid-js"
|
||||
|
||||
/**
|
||||
* Explains why a tool call was auto-approved, inside the expanded tool row.
|
||||
*
|
||||
* The backend records this on the tool part's `state.metadata.approval`. The display strings are
|
||||
* resolved by the caller (which owns the localized `t`) and carried on the context, so this stays
|
||||
* free of any i18n key coupling.
|
||||
*/
|
||||
export type ToolApproval = {
|
||||
source: "agent" | "global" | "project" | "yolo" | "session" | "manual" | "default"
|
||||
agent?: string
|
||||
rule?: { permission: string; pattern: string; action: string }
|
||||
}
|
||||
|
||||
/** Pre-resolved, localized text plus the raw approval, supplied by the caller. */
|
||||
export type ToolApprovalDisplay = {
|
||||
approval: ToolApproval
|
||||
decision: string
|
||||
source?: string
|
||||
rule?: string
|
||||
}
|
||||
|
||||
const SOURCE_KEYS = ["agent", "global", "project", "yolo", "session", "manual", "default"] as const
|
||||
|
||||
const Context = createContext<Accessor<ToolApprovalDisplay | undefined>>(() => undefined)
|
||||
|
||||
/** Provide the resolved approval to the tool row below. */
|
||||
export function ToolApprovalProvider(props: ParentProps<{ value: Accessor<ToolApprovalDisplay | undefined> }>) {
|
||||
return <Context.Provider value={props.value}>{props.children}</Context.Provider>
|
||||
}
|
||||
|
||||
export function useToolApproval() {
|
||||
return useContext(Context)
|
||||
}
|
||||
|
||||
/** Read the raw approval payload off a tool part's metadata, if present. */
|
||||
export function toolApprovalFrom(metadata: Record<string, unknown> | undefined): ToolApproval | undefined {
|
||||
const value = metadata?.approval
|
||||
if (!value || typeof value !== "object") return undefined
|
||||
const approval = value as ToolApproval
|
||||
return SOURCE_KEYS.includes(approval.source) ? approval : undefined
|
||||
}
|
||||
|
||||
type Translate = (key: string, params?: Record<string, string | number | boolean>) => string
|
||||
|
||||
/** Resolve an approval read off metadata into localized display text via the caller's `t`. */
|
||||
export function resolveToolApproval(
|
||||
metadata: Record<string, unknown> | undefined,
|
||||
t: Translate,
|
||||
): ToolApprovalDisplay | undefined {
|
||||
const approval = toolApprovalFrom(metadata)
|
||||
if (!approval) return undefined
|
||||
const sourceText = () => {
|
||||
if (approval.source === "agent")
|
||||
return approval.agent
|
||||
? t("ui.approval.source.agent", { agent: approval.agent })
|
||||
: t("ui.approval.source.agent.default")
|
||||
if (approval.source === "manual") return undefined
|
||||
return t(`ui.approval.source.${approval.source}`)
|
||||
}
|
||||
return {
|
||||
approval,
|
||||
decision: approval.source === "manual" ? t("ui.approval.manual") : t("ui.approval.auto"),
|
||||
source: sourceText(),
|
||||
rule: approval.rule
|
||||
? t("ui.approval.rule", { permission: approval.rule.permission, pattern: approval.rule.pattern })
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
/** The single "why was this allowed" line shown inside a tool row's expanded body. */
|
||||
export function ToolApprovalLine(props: { display: ToolApprovalDisplay }) {
|
||||
const manual = () => props.display.approval.source === "manual"
|
||||
return (
|
||||
<div data-slot="tool-approval-line" data-source={props.display.approval.source}>
|
||||
<span data-slot="tool-approval-decision">{props.display.decision}</span>
|
||||
<Show when={!manual()}>
|
||||
<Show when={props.display.source}>
|
||||
{(text) => <span data-slot="tool-approval-source">{text()}</span>}
|
||||
</Show>
|
||||
<Show when={props.display.rule}>{(text) => <span data-slot="tool-approval-rule">{text()}</span>}</Show>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,40 @@
|
||||
# kilo-code
|
||||
|
||||
## 7.4.16
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#12370](https://github.com/Kilo-Org/kilocode/pull/12370) [`b367105`](https://github.com/Kilo-Org/kilocode/commit/b367105c8d648c8e05b62c2d27a28a95a4772f61) Thanks [@hdcodedev](https://github.com/hdcodedev)! - Support deleting queued chat messages from the VS Code chat before they run.
|
||||
|
||||
- [#12297](https://github.com/Kilo-Org/kilocode/pull/12297) [`bcff5cb`](https://github.com/Kilo-Org/kilocode/commit/bcff5cb3608f9fc6a6a441405cf50694f6bf3efa) - Emit session queue state so remote clients can show queued messages.
|
||||
|
||||
- [#12456](https://github.com/Kilo-Org/kilocode/pull/12456) [`3d648d7`](https://github.com/Kilo-Org/kilocode/commit/3d648d7fcdc186f86b2c63ab842e70acb1f0aee2) - Reference past chats inline with `@` in the prompt. Typing `@` now surfaces a "Past chats" option that opens a searchable picker of previous sessions (scoped to the current workspace/worktree, searched like the Agent Manager session search); selecting one attaches that session's transcript as context so the model can build on a prior conversation. Clicking the mention opens that session. Available in the CLI TUI and the VS Code extension.
|
||||
|
||||
- [#12462](https://github.com/Kilo-Org/kilocode/pull/12462) [`8eeaa54`](https://github.com/Kilo-Org/kilocode/commit/8eeaa546aeec9c06d513248b42546ec779ab2178) Thanks [@hdcodedev](https://github.com/hdcodedev)! - Add a searchable open-tabs switcher to the sidebar tab bar.
|
||||
|
||||
- [#12494](https://github.com/Kilo-Org/kilocode/pull/12494) [`85dbf44`](https://github.com/Kilo-Org/kilocode/commit/85dbf443af727524a90c1838eeecd37d5011bcaa) Thanks [@bagatao-anaconda](https://github.com/bagatao-anaconda)! - Show why a tool call was auto-approved. Expanding a tool call now explains whether it ran automatically or after your approval, and which rule allowed it — from your agent, the project config, your global config, or auto-approve (YOLO) mode.
|
||||
|
||||
- [#12509](https://github.com/Kilo-Org/kilocode/pull/12509) [`99c04c7`](https://github.com/Kilo-Org/kilocode/commit/99c04c7163efb8cafa6e35f052b743c6dcf96f12) - Filter `/sessions` history to sessions in the current Agent Manager worktree.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#12486](https://github.com/Kilo-Org/kilocode/pull/12486) [`d0e8a86`](https://github.com/Kilo-Org/kilocode/commit/d0e8a86bfce001821441e8ac8d7398bfba6c93f6) Thanks [@hdcodedev](https://github.com/hdcodedev)! - Prevent Enter from activating the first result in searchable lists when no row is highlighted.
|
||||
|
||||
- [#12511](https://github.com/Kilo-Org/kilocode/pull/12511) [`9e1b54d`](https://github.com/Kilo-Org/kilocode/commit/9e1b54d8754e670dd149a8536ed25b16e223fe2e) - Open plan implementation sessions immediately and submit recovered plan choices without requiring a second click.
|
||||
|
||||
- [#12496](https://github.com/Kilo-Org/kilocode/pull/12496) [`2fcb137`](https://github.com/Kilo-Org/kilocode/commit/2fcb137ebcbf9101ca655804d0a61af2f222bbc5) - Preserve unexpected provider finish reasons and show the request and Gateway generation IDs when a response ends unexpectedly.
|
||||
|
||||
- [#12488](https://github.com/Kilo-Org/kilocode/pull/12488) [`c25f041`](https://github.com/Kilo-Org/kilocode/commit/c25f041eb3922defc4dadb9ad7b2f8c8edb74fbd) - Show the request ID when a model response ends without a finish reason.
|
||||
|
||||
- Updated dependencies [[`b367105`](https://github.com/Kilo-Org/kilocode/commit/b367105c8d648c8e05b62c2d27a28a95a4772f61), [`c1f057a`](https://github.com/Kilo-Org/kilocode/commit/c1f057ad5a2021cd57e003cad7d45e5e6b0b4cba), [`2fcb137`](https://github.com/Kilo-Org/kilocode/commit/2fcb137ebcbf9101ca655804d0a61af2f222bbc5), [`f715e2f`](https://github.com/Kilo-Org/kilocode/commit/f715e2f5fa4db5abe5c734e1c360e8da3367f3e5), [`dcc0d64`](https://github.com/Kilo-Org/kilocode/commit/dcc0d64a3249bdd3aa27d564759253126ff9a5fe)]:
|
||||
- @kilocode/kilo-ui@7.5.0
|
||||
- @kilocode/sdk@7.5.0
|
||||
- @kilocode/kilo-gateway@7.4.16
|
||||
- @kilocode/plugin@7.4.16
|
||||
- @opencode-ai/ui@7.4.16
|
||||
- @opencode-ai/core@7.4.16
|
||||
- @kilocode/kilo-indexing@7.4.16
|
||||
|
||||
## 7.4.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "kilo-code",
|
||||
"displayName": "Kilo Code: AI Coding Agent, Copilot, and Autocomplete",
|
||||
"description": "Open Source AI coding agent that generates code from natural language, automates tasks, and runs terminal commands. Features inline autocomplete, browser automation, automated refactoring, and custom modes for planning, coding, and debugging. Supports 500+ AI models including Claude (Anthropic), Gemini, Grok, GPT, Codex and GLM.",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"icon": "assets/icons/logo-outline-black.png",
|
||||
"galleryBanner": {
|
||||
"color": "#FFFFFF",
|
||||
|
||||
@@ -768,7 +768,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
}
|
||||
|
||||
/** Register a session created externally and notify the webview. */
|
||||
public registerSession(session: Session): void {
|
||||
public registerSession(session: Session, activate = false): void {
|
||||
this.stopCurrentSessionProcesses(session.id)
|
||||
this.setCurrentSession(session)
|
||||
this.contextSessionID = session.id
|
||||
@@ -776,6 +776,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
this.postMessage({
|
||||
type: "sessionCreated",
|
||||
session: this.sessionToWebview(session),
|
||||
...(activate ? { activate: true } : {}),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4479,7 +4480,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
this.pendingFollowup = null
|
||||
this.trackDirectory(session.id, session.directory)
|
||||
for (const cb of this.followupListeners) cb(session, session.directory)
|
||||
this.registerSession(session)
|
||||
this.registerSession(session, true)
|
||||
void this.handleLoadMessages(session.id)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ interface QuestionRecovery {
|
||||
readonly complete: boolean
|
||||
}
|
||||
|
||||
type QuestionRoute = { kind: "retry"; dir: string } | { kind: "stale" } | { kind: "failed" }
|
||||
|
||||
function isNotFoundError(error: unknown): boolean {
|
||||
const record = (value: unknown) =>
|
||||
value && typeof value === "object" ? (value as Record<string, unknown>) : undefined
|
||||
@@ -41,18 +43,18 @@ function isNotFoundError(error: unknown): boolean {
|
||||
)
|
||||
}
|
||||
|
||||
function stale(ctx: QuestionContext, requestID: string): void {
|
||||
async function recover(ctx: QuestionContext, requestID: string): Promise<QuestionRoute> {
|
||||
const result = await fetchAndSendPendingQuestions(ctx, requestID)
|
||||
if (!result) return { kind: "failed" }
|
||||
if (result.seen.has(requestID)) {
|
||||
const dir = ctx.getQuestionDirectory(requestID)
|
||||
return dir ? { kind: "retry", dir } : { kind: "failed" }
|
||||
}
|
||||
// Absence only proves staleness when every directory was scanned.
|
||||
if (!result.complete) return { kind: "failed" }
|
||||
ctx.clearQuestionDirectory(requestID)
|
||||
ctx.postMessage({ type: "questionResolved", requestID })
|
||||
void fetchAndSendPendingQuestions(ctx)
|
||||
}
|
||||
|
||||
async function recover(ctx: QuestionContext, requestID: string): Promise<boolean> {
|
||||
const result = await fetchAndSendPendingQuestions(ctx)
|
||||
if (!result?.complete || result.seen.has(requestID)) return false
|
||||
ctx.clearQuestionDirectory(requestID)
|
||||
ctx.postMessage({ type: "questionResolved", requestID })
|
||||
return true
|
||||
return { kind: "stale" }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,7 +63,10 @@ async function recover(ctx: QuestionContext, requestID: string): Promise<boolean
|
||||
* called after child-session sync and after SSE reconnects so missed
|
||||
* question.asked events don't leave the server blocked indefinitely.
|
||||
*/
|
||||
export async function fetchAndSendPendingQuestions(ctx: QuestionContext): Promise<QuestionRecovery | undefined> {
|
||||
export async function fetchAndSendPendingQuestions(
|
||||
ctx: QuestionContext,
|
||||
omit?: string,
|
||||
): Promise<QuestionRecovery | undefined> {
|
||||
if (!ctx.client) return
|
||||
try {
|
||||
for (;;) {
|
||||
@@ -94,6 +99,9 @@ export async function fetchAndSendPendingQuestions(ctx: QuestionContext): Promis
|
||||
if (ctx.getQuestionRevision() !== revision) continue
|
||||
for (const item of pending) {
|
||||
ctx.recordQuestionDirectory(item.question.id, item.dir)
|
||||
// The omitted request is mid-reply; its card is still visible, so
|
||||
// reposting it would only churn the webview.
|
||||
if (item.question.id === omit) continue
|
||||
ctx.postMessage({
|
||||
type: "questionRequest",
|
||||
question: {
|
||||
@@ -126,19 +134,26 @@ export async function handleQuestionReply(
|
||||
}
|
||||
|
||||
const sid = sessionID ?? ctx.currentSessionId
|
||||
const origin = ctx.getQuestionDirectory(requestID)
|
||||
const dir = origin ?? ctx.getWorkspaceDirectory(sid)
|
||||
const dir = ctx.getQuestionDirectory(requestID) ?? ctx.getWorkspaceDirectory(sid)
|
||||
|
||||
try {
|
||||
await ctx.client.question.reply({ requestID, answers, directory: dir }, { throwOnError: true })
|
||||
ctx.clearQuestionDirectory(requestID)
|
||||
return true
|
||||
} catch (error) {
|
||||
if (isNotFoundError(error) && origin) {
|
||||
stale(ctx, requestID)
|
||||
return false
|
||||
const route = isNotFoundError(error) ? await recover(ctx, requestID) : undefined
|
||||
if (route?.kind === "stale") return false
|
||||
if (route?.kind === "retry" && route.dir !== dir) {
|
||||
try {
|
||||
await ctx.client.question.reply({ requestID, answers, directory: route.dir }, { throwOnError: true })
|
||||
ctx.clearQuestionDirectory(requestID)
|
||||
return true
|
||||
} catch (retry) {
|
||||
console.error("[Kilo New] KiloProvider: Failed to reply to recovered question:", retry)
|
||||
ctx.postMessage({ type: "questionError", requestID })
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (isNotFoundError(error) && (await recover(ctx, requestID))) return false
|
||||
console.error("[Kilo New] KiloProvider: Failed to reply to question:", error)
|
||||
ctx.postMessage({ type: "questionError", requestID })
|
||||
return false
|
||||
@@ -157,19 +172,26 @@ export async function handleQuestionReject(
|
||||
}
|
||||
|
||||
const sid = sessionID ?? ctx.currentSessionId
|
||||
const origin = ctx.getQuestionDirectory(requestID)
|
||||
const dir = origin ?? ctx.getWorkspaceDirectory(sid)
|
||||
const dir = ctx.getQuestionDirectory(requestID) ?? ctx.getWorkspaceDirectory(sid)
|
||||
|
||||
try {
|
||||
await ctx.client.question.reject({ requestID, directory: dir }, { throwOnError: true })
|
||||
ctx.clearQuestionDirectory(requestID)
|
||||
return true
|
||||
} catch (error) {
|
||||
if (isNotFoundError(error) && origin) {
|
||||
stale(ctx, requestID)
|
||||
return false
|
||||
const route = isNotFoundError(error) ? await recover(ctx, requestID) : undefined
|
||||
if (route?.kind === "stale") return false
|
||||
if (route?.kind === "retry" && route.dir !== dir) {
|
||||
try {
|
||||
await ctx.client.question.reject({ requestID, directory: route.dir }, { throwOnError: true })
|
||||
ctx.clearQuestionDirectory(requestID)
|
||||
return true
|
||||
} catch (retry) {
|
||||
console.error("[Kilo New] KiloProvider: Failed to reject recovered question:", retry)
|
||||
ctx.postMessage({ type: "questionError", requestID })
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (isNotFoundError(error) && (await recover(ctx, requestID))) return false
|
||||
console.error("[Kilo New] KiloProvider: Failed to reject question:", error)
|
||||
ctx.postMessage({ type: "questionError", requestID })
|
||||
return false
|
||||
|
||||
@@ -99,4 +99,29 @@ test.describe("history session accessibility", () => {
|
||||
await expect(local).toHaveAttribute("aria-selected", "true")
|
||||
await expect(page.getByRole("tabpanel", { name: "Local" })).toBeVisible()
|
||||
})
|
||||
|
||||
test("filters sessions to the current worktree and includes it in keyboard navigation", async ({ page }) => {
|
||||
await story(page, "history-sessionlist--worktree-sources")
|
||||
|
||||
const local = page.getByRole("tab", { name: "Local" })
|
||||
const worktree = page.getByRole("tab", { name: "Worktree" })
|
||||
await local.focus()
|
||||
await page.keyboard.press("End")
|
||||
await expect(worktree).toBeFocused()
|
||||
await page.keyboard.press("Enter")
|
||||
|
||||
await expect(worktree).toHaveAttribute("aria-selected", "true")
|
||||
await expect(page.getByRole("tabpanel", { name: "Worktree" })).toBeVisible()
|
||||
const rows = page.locator('[data-slot="list-item"]')
|
||||
await expect(rows.filter({ hasText: "Refactor authentication module" })).toBeVisible()
|
||||
await expect(rows.filter({ hasText: "Fix TypeScript errors in webview" })).toBeVisible()
|
||||
await expect(rows.filter({ hasText: "Add screenshot test coverage" })).toHaveCount(0)
|
||||
|
||||
await rows.filter({ hasText: "Refactor authentication module" }).click()
|
||||
await expect(page.locator('[data-slot="selected-session"]')).toHaveText("s1")
|
||||
|
||||
await worktree.focus()
|
||||
await page.keyboard.press("ArrowRight")
|
||||
await expect(local).toBeFocused()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"type": "module",
|
||||
"version": "7.4.15",
|
||||
"version": "7.4.16",
|
||||
"dependencies": {},
|
||||
"devDependencies": {},
|
||||
"peerDependencies": {}
|
||||
|
||||
@@ -140,7 +140,7 @@ describe("KiloProvider follow-up sessions", () => {
|
||||
revert: null,
|
||||
summary: null,
|
||||
},
|
||||
draftID: undefined,
|
||||
activate: true,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
restoreTabs,
|
||||
restoreTrackedTabs,
|
||||
showTabStrip,
|
||||
tabsForCreatedSession,
|
||||
trackedSessionInventory,
|
||||
type LocalTabState,
|
||||
} from "../../webview-ui/src/utils/local-tabs"
|
||||
@@ -61,6 +62,25 @@ const tracked = () =>
|
||||
)
|
||||
|
||||
describe("local session tabs", () => {
|
||||
it("opens explicitly activated sessions in the foreground", () => {
|
||||
expect(tabsForCreatedSession(state(["s1"], "s1"), "s2", undefined, true)).toEqual({
|
||||
ids: ["s1", "s2"],
|
||||
active: "s2",
|
||||
})
|
||||
})
|
||||
|
||||
it("promotes a matching pending draft into the created session", () => {
|
||||
expect(tabsForCreatedSession(state([pending()], pending()), "s1", pending(), undefined)).toEqual({
|
||||
ids: ["s1"],
|
||||
active: "s1",
|
||||
})
|
||||
})
|
||||
|
||||
it("ignores created sessions without activation or a pending draft", () => {
|
||||
expect(tabsForCreatedSession(state(["s1"], "s1"), "s2", undefined, undefined)).toBeUndefined()
|
||||
expect(tabsForCreatedSession(state(["s1"], "s1"), "s2", "sidebar-pending:gone", undefined)).toBeUndefined()
|
||||
})
|
||||
|
||||
it("hides the tab strip when only one tab remains", () => {
|
||||
expect(showTabStrip([pending()])).toBe(false)
|
||||
expect(showTabStrip([pending(), "sidebar-pending:2"])).toBe(true)
|
||||
|
||||
@@ -29,7 +29,7 @@ function ctx(
|
||||
dirs?: Map<string, string>
|
||||
extra?: string[]
|
||||
pending?: Record<string, QuestionRequest[]>
|
||||
errors?: { list?: Record<string, unknown>; reply?: unknown; reject?: unknown }
|
||||
errors?: { list?: Record<string, unknown>; reply?: unknown | unknown[]; reject?: unknown | unknown[] }
|
||||
changeOnList?: string
|
||||
removeOnList?: string
|
||||
} = {},
|
||||
@@ -42,7 +42,11 @@ function ctx(
|
||||
const dirs = opts.dirs ?? new Map<string, string>()
|
||||
let revision = 0
|
||||
let changed = false
|
||||
let reply = 0
|
||||
let reject = 0
|
||||
const removed = new Set<string>()
|
||||
const failure = (value: unknown | unknown[] | undefined, index: number) =>
|
||||
Array.isArray(value) ? value[index] : value
|
||||
const client = {
|
||||
question: {
|
||||
list: async (args: { directory?: string }) => {
|
||||
@@ -63,12 +67,14 @@ function ctx(
|
||||
},
|
||||
reply: async (args: unknown) => {
|
||||
replies.push(args)
|
||||
if (opts.errors?.reply) throw opts.errors.reply
|
||||
const error = failure(opts.errors?.reply, reply++)
|
||||
if (error) throw error
|
||||
return { data: true }
|
||||
},
|
||||
reject: async (args: unknown) => {
|
||||
rejects.push(args)
|
||||
if (opts.errors?.reject) throw opts.errors.reject
|
||||
const error = failure(opts.errors?.reject, reject++)
|
||||
if (error) throw error
|
||||
return { data: true }
|
||||
},
|
||||
},
|
||||
@@ -167,26 +173,96 @@ describe("question handlers", () => {
|
||||
expect(messages).toContainEqual({ type: "questionResolved", requestID: "req-stale" })
|
||||
})
|
||||
|
||||
it("keeps fallback-directory 404s retryable while recovering the request route", async () => {
|
||||
it("retries a reply through the recovered request directory", async () => {
|
||||
const error = new Error("Question request not found", {
|
||||
cause: { status: 404, body: { name: "NotFoundError" } },
|
||||
})
|
||||
const dir = "/workspace/.kilo/worktrees/origin"
|
||||
const { fake, messages, questionDirs } = ctx({
|
||||
const { fake, messages, replies, questionDirs } = ctx({
|
||||
tracked: ["ses-root"],
|
||||
extra: [dir],
|
||||
pending: { [dir]: [pending("req-misrouted", "ses-root")] },
|
||||
errors: { reply: error },
|
||||
errors: { reply: [error] },
|
||||
})
|
||||
questionDirs.set("req-misrouted", "/workspace/.kilo/worktrees/stale")
|
||||
|
||||
const ok = await handleQuestionReply(fake, "req-misrouted", [["Continue"]], "ses-root")
|
||||
|
||||
expect(ok).toBe(true)
|
||||
expect(replies).toEqual([
|
||||
{
|
||||
requestID: "req-misrouted",
|
||||
answers: [["Continue"]],
|
||||
directory: "/workspace/.kilo/worktrees/stale",
|
||||
},
|
||||
{
|
||||
requestID: "req-misrouted",
|
||||
answers: [["Continue"]],
|
||||
directory: dir,
|
||||
},
|
||||
])
|
||||
expect(messages).not.toContainEqual({ type: "questionResolved", requestID: "req-misrouted" })
|
||||
expect(messages).not.toContainEqual({ type: "questionError", requestID: "req-misrouted" })
|
||||
expect(messages).not.toContainEqual({
|
||||
type: "questionRequest",
|
||||
question: pending("req-misrouted", "ses-root"),
|
||||
})
|
||||
expect(questionDirs.has("req-misrouted")).toBe(false)
|
||||
})
|
||||
|
||||
it("retries even when an unrelated directory fails to list", async () => {
|
||||
const error = new Error("Question request not found", {
|
||||
cause: { status: 404, body: { name: "NotFoundError" } },
|
||||
})
|
||||
const dir = "/workspace/.kilo/worktrees/origin"
|
||||
const failing = "/workspace/.kilo/worktrees/failing"
|
||||
const { fake, messages, replies, questionDirs } = ctx({
|
||||
tracked: ["ses-root"],
|
||||
extra: [dir, failing],
|
||||
pending: { [dir]: [pending("req-misrouted", "ses-root")] },
|
||||
errors: { list: { [failing]: new Error("temporary failure") }, reply: [error] },
|
||||
})
|
||||
questionDirs.set("req-misrouted", "/workspace/.kilo/worktrees/stale")
|
||||
const spy = spyOn(console, "error").mockImplementation(() => {})
|
||||
|
||||
const ok = await handleQuestionReply(fake, "req-misrouted", [["Continue"]], "ses-root")
|
||||
spy.mockRestore()
|
||||
|
||||
expect(ok).toBe(false)
|
||||
expect(messages).not.toContainEqual({ type: "questionResolved", requestID: "req-misrouted" })
|
||||
expect(messages).toContainEqual({ type: "questionError", requestID: "req-misrouted" })
|
||||
expect(questionDirs.get("req-misrouted")).toBe(dir)
|
||||
expect(ok).toBe(true)
|
||||
expect(replies.map((args) => (args as { directory: string }).directory)).toEqual([
|
||||
"/workspace/.kilo/worktrees/stale",
|
||||
dir,
|
||||
])
|
||||
expect(messages).not.toContainEqual({ type: "questionError", requestID: "req-misrouted" })
|
||||
expect(questionDirs.has("req-misrouted")).toBe(false)
|
||||
})
|
||||
|
||||
it("retries a reject through the recovered request directory", async () => {
|
||||
const error = new Error("Question request not found", {
|
||||
cause: { status: 404, body: { name: "NotFoundError" } },
|
||||
})
|
||||
const dir = "/workspace/.kilo/worktrees/origin"
|
||||
const { fake, messages, rejects, questionDirs } = ctx({
|
||||
tracked: ["ses-root"],
|
||||
extra: [dir],
|
||||
pending: { [dir]: [pending("req-misrouted", "ses-root")] },
|
||||
errors: { reject: [error] },
|
||||
})
|
||||
questionDirs.set("req-misrouted", "/workspace/.kilo/worktrees/stale")
|
||||
|
||||
const ok = await handleQuestionReject(fake, "req-misrouted", "ses-root")
|
||||
|
||||
expect(ok).toBe(true)
|
||||
expect(rejects).toEqual([
|
||||
{ requestID: "req-misrouted", directory: "/workspace/.kilo/worktrees/stale" },
|
||||
{ requestID: "req-misrouted", directory: dir },
|
||||
])
|
||||
expect(messages).not.toContainEqual({ type: "questionError", requestID: "req-misrouted" })
|
||||
expect(messages).not.toContainEqual({
|
||||
type: "questionRequest",
|
||||
question: pending("req-misrouted", "ses-root"),
|
||||
})
|
||||
expect(questionDirs.has("req-misrouted")).toBe(false)
|
||||
})
|
||||
|
||||
it("removes a fallback question when recovery confirms it is stale", async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import { terminal } from "../../webview-ui/src/context/session-outcome"
|
||||
import type { Message, TodoItem } from "../../webview-ui/src/types/messages"
|
||||
import type { Message, Part, TodoItem } from "../../webview-ui/src/types/messages"
|
||||
|
||||
function message(finish?: string, error?: Message["error"]): Message {
|
||||
return {
|
||||
@@ -70,6 +70,49 @@ describe("terminal", () => {
|
||||
expect(terminal({ reason: "completed", messages: [message("other")], todos: [] })?.kind).toBe("unexpected")
|
||||
})
|
||||
|
||||
it("includes both request ids for unexpected provider finishes", () => {
|
||||
const parts: Part[] = [
|
||||
{
|
||||
id: "p1",
|
||||
sessionID: "s1",
|
||||
messageID: "m1",
|
||||
type: "step-finish",
|
||||
reason: "other",
|
||||
generationID: "gen_test",
|
||||
vercelID: "fra1::other",
|
||||
},
|
||||
]
|
||||
expect(
|
||||
terminal({
|
||||
reason: "completed",
|
||||
messages: [message("other")],
|
||||
todos: [],
|
||||
parts: () => parts,
|
||||
}),
|
||||
).toMatchObject({
|
||||
kind: "unexpected",
|
||||
finish: "other",
|
||||
vercelID: "fra1::other",
|
||||
generationID: "gen_test",
|
||||
})
|
||||
})
|
||||
|
||||
it("does not expose generation ids for other terminal outcomes", () => {
|
||||
const parts: Part[] = [
|
||||
{
|
||||
id: "p1",
|
||||
sessionID: "s1",
|
||||
messageID: "m1",
|
||||
type: "step-finish",
|
||||
reason: "unknown",
|
||||
generationID: "gen_test",
|
||||
},
|
||||
]
|
||||
expect(
|
||||
terminal({ reason: "completed", messages: [message("unknown")], todos: [], parts: () => parts }),
|
||||
).not.toHaveProperty("generationID")
|
||||
})
|
||||
|
||||
it("surfaces interruption and failures without a rendered error", () => {
|
||||
expect(terminal({ reason: "interrupted", messages: [message("stop")], todos: [todo("pending")] })).toEqual({
|
||||
kind: "interrupted",
|
||||
|
||||
@@ -761,6 +761,16 @@ const AgentManagerContent: Component = () => {
|
||||
return sessionsForWorktree(sel)
|
||||
})
|
||||
|
||||
const activeWorktreeSessionIds = createMemo<ReadonlySet<string> | undefined>(() => {
|
||||
const sel = selection()
|
||||
if (!sel || sel === LOCAL) return undefined
|
||||
return new Set(
|
||||
managedSessions()
|
||||
.filter((item) => item.worktreeId === sel)
|
||||
.map((item) => item.id),
|
||||
)
|
||||
})
|
||||
|
||||
const activeTabs = createMemo((): SessionInfo[] => {
|
||||
const sel = selection()
|
||||
if (sel === LOCAL) return localSessions()
|
||||
@@ -2898,6 +2908,7 @@ const AgentManagerContent: Component = () => {
|
||||
openLocally(id)
|
||||
}}
|
||||
onBack={() => setHistory(false)}
|
||||
worktreeSessionIds={activeWorktreeSessionIds}
|
||||
/>
|
||||
</Show>
|
||||
<Show when={!contextEmpty() && !history()}>
|
||||
|
||||
@@ -9,7 +9,13 @@
|
||||
|
||||
import { Component, For, Show, createMemo, type JSX } from "solid-js"
|
||||
import { Dynamic } from "solid-js/web"
|
||||
import { Part, PART_MAPPING, ToolRegistry } from "@kilocode/kilo-ui/message-part"
|
||||
import {
|
||||
Part,
|
||||
PART_MAPPING,
|
||||
ToolRegistry,
|
||||
ToolApprovalProvider,
|
||||
resolveToolApproval,
|
||||
} from "@kilocode/kilo-ui/message-part"
|
||||
import type { MessageFeedbackControls } from "@kilocode/kilo-ui/message-part"
|
||||
import type {
|
||||
AssistantMessage as SDKAssistantMessage,
|
||||
@@ -119,22 +125,25 @@ type ToolStateProps = {
|
||||
function TodoToolCard(props: { part: ToolPart; forceOpen?: boolean }) {
|
||||
const render = ToolRegistry.render(props.part.tool)
|
||||
const state = () => props.part.state as ToolStateProps
|
||||
const language = useLanguage()
|
||||
return (
|
||||
<Show when={render}>
|
||||
{(renderFn) => (
|
||||
<Dynamic
|
||||
component={renderFn()}
|
||||
input={state()?.input ?? {}}
|
||||
metadata={state()?.metadata ?? {}}
|
||||
tool={props.part.tool}
|
||||
partID={props.part.id}
|
||||
callID={props.part.callID}
|
||||
output={state()?.output}
|
||||
status={state()?.status}
|
||||
defaultOpen
|
||||
forceOpen={props.forceOpen}
|
||||
reveal={false}
|
||||
/>
|
||||
<ToolApprovalProvider value={() => resolveToolApproval(state()?.metadata, language.t)}>
|
||||
<Dynamic
|
||||
component={renderFn()}
|
||||
input={state()?.input ?? {}}
|
||||
metadata={state()?.metadata ?? {}}
|
||||
tool={props.part.tool}
|
||||
partID={props.part.id}
|
||||
callID={props.part.callID}
|
||||
output={state()?.output}
|
||||
status={state()?.status}
|
||||
defaultOpen
|
||||
forceOpen={props.forceOpen}
|
||||
reveal={false}
|
||||
/>
|
||||
</ToolApprovalProvider>
|
||||
)}
|
||||
</Show>
|
||||
)
|
||||
@@ -143,24 +152,27 @@ function TodoToolCard(props: { part: ToolPart; forceOpen?: boolean }) {
|
||||
function BashToolCard(props: { part: ToolPart; defaultOpen: boolean; forceOpen?: boolean }) {
|
||||
const render = ToolRegistry.render(props.part.tool)
|
||||
const state = () => props.part.state as ToolStateProps
|
||||
const language = useLanguage()
|
||||
return (
|
||||
<Show when={render}>
|
||||
{(card) => (
|
||||
<Dynamic
|
||||
component={card() as unknown as Component<Record<string, unknown>>}
|
||||
input={state()?.input ?? {}}
|
||||
metadata={state()?.metadata ?? {}}
|
||||
partMetadata={props.part.metadata ?? {}}
|
||||
tool={props.part.tool}
|
||||
partID={props.part.id}
|
||||
callID={props.part.callID}
|
||||
output={state()?.output}
|
||||
status={state()?.status}
|
||||
defaultOpen={props.defaultOpen}
|
||||
forceOpen={props.forceOpen}
|
||||
animate
|
||||
reveal={state()?.status === "pending" || state()?.status === "running"}
|
||||
/>
|
||||
<ToolApprovalProvider value={() => resolveToolApproval(state()?.metadata, language.t)}>
|
||||
<Dynamic
|
||||
component={card() as unknown as Component<Record<string, unknown>>}
|
||||
input={state()?.input ?? {}}
|
||||
metadata={state()?.metadata ?? {}}
|
||||
partMetadata={props.part.metadata ?? {}}
|
||||
tool={props.part.tool}
|
||||
partID={props.part.id}
|
||||
callID={props.part.callID}
|
||||
output={state()?.output}
|
||||
status={state()?.status}
|
||||
defaultOpen={props.defaultOpen}
|
||||
forceOpen={props.forceOpen}
|
||||
animate
|
||||
reveal={state()?.status === "pending" || state()?.status === "running"}
|
||||
/>
|
||||
</ToolApprovalProvider>
|
||||
)}
|
||||
</Show>
|
||||
)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* HistoryView component
|
||||
* Unified panel for local and cloud session history.
|
||||
* Contains a tab bar ("Local" | "Cloud") and an always-visible "Import session" button.
|
||||
* Unified panel for local, cloud, and optional worktree session history.
|
||||
* Contains a source tab bar and an always-visible "Import session" button.
|
||||
*/
|
||||
|
||||
import { Component, createEffect, createSignal, onCleanup } from "solid-js"
|
||||
import { Component, Show, createEffect, createSignal, onCleanup, type Accessor } from "solid-js"
|
||||
import { Button } from "@kilocode/kilo-ui/button"
|
||||
import { useDialog } from "@kilocode/kilo-ui/context/dialog"
|
||||
import { useLanguage } from "../../context/language"
|
||||
@@ -17,21 +17,34 @@ import CloudSessionList from "./CloudSessionList"
|
||||
interface HistoryViewProps {
|
||||
onSelectSession: (id: string) => void
|
||||
onBack?: () => void
|
||||
worktreeSessionIds?: Accessor<ReadonlySet<string> | undefined>
|
||||
}
|
||||
|
||||
type Source = "local" | "cloud" | "worktree"
|
||||
|
||||
const EMPTY_SESSION_IDS = new Set<string>()
|
||||
|
||||
const HistoryView: Component<HistoryViewProps> = (props) => {
|
||||
const language = useLanguage()
|
||||
const dialog = useDialog()
|
||||
const session = useSession()
|
||||
const tabs = useLocalTabs()
|
||||
const [tab, setTab] = createSignal<"local" | "cloud">("local")
|
||||
const [tab, setTab] = createSignal<Source>("local")
|
||||
let local: HTMLButtonElement | undefined
|
||||
let cloud: HTMLButtonElement | undefined
|
||||
let worktree: HTMLButtonElement | undefined
|
||||
let localPanel: HTMLDivElement | undefined
|
||||
let cloudPanel: HTMLDivElement | undefined
|
||||
let worktreePanel: HTMLDivElement | undefined
|
||||
|
||||
const worktreeIds = () => props.worktreeSessionIds?.()
|
||||
|
||||
createEffect(() => {
|
||||
const panel = tab() === "local" ? localPanel : cloudPanel
|
||||
if (tab() === "worktree" && !worktreeIds()) setTab("local")
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const panel = tab() === "local" ? localPanel : tab() === "cloud" ? cloudPanel : worktreePanel
|
||||
|
||||
const frame = requestAnimationFrame(() => {
|
||||
panel
|
||||
@@ -60,17 +73,20 @@ const HistoryView: Component<HistoryViewProps> = (props) => {
|
||||
props.onBack?.()
|
||||
}
|
||||
|
||||
function move(event: KeyboardEvent, current: "local" | "cloud") {
|
||||
const next =
|
||||
function move(event: KeyboardEvent, current: Source) {
|
||||
const sources: Source[] = worktreeIds() ? ["local", "cloud", "worktree"] : ["local", "cloud"]
|
||||
const index = sources.indexOf(current)
|
||||
const source =
|
||||
event.key === "Home"
|
||||
? local
|
||||
? sources[0]
|
||||
: event.key === "End"
|
||||
? cloud
|
||||
: event.key === "ArrowLeft" || event.key === "ArrowRight"
|
||||
? current === "local"
|
||||
? cloud
|
||||
: local
|
||||
: undefined
|
||||
? sources.at(-1)
|
||||
: event.key === "ArrowLeft"
|
||||
? sources[(index - 1 + sources.length) % sources.length]
|
||||
: event.key === "ArrowRight"
|
||||
? sources[(index + 1) % sources.length]
|
||||
: undefined
|
||||
const next = source === "local" ? local : source === "cloud" ? cloud : source === "worktree" ? worktree : undefined
|
||||
if (!next) return
|
||||
event.preventDefault()
|
||||
next.focus()
|
||||
@@ -113,6 +129,23 @@ const HistoryView: Component<HistoryViewProps> = (props) => {
|
||||
>
|
||||
{language.t("session.tab.cloud")}
|
||||
</button>
|
||||
<Show when={worktreeIds()}>
|
||||
<button
|
||||
ref={worktree}
|
||||
id="history-tab-worktree"
|
||||
class="history-tab-btn"
|
||||
classList={{ "history-tab-btn--active": tab() === "worktree" }}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={tab() === "worktree"}
|
||||
aria-controls="history-panel-worktree"
|
||||
tabIndex={tab() === "worktree" ? 0 : -1}
|
||||
onClick={() => setTab("worktree")}
|
||||
onKeyDown={(event) => move(event, "worktree")}
|
||||
>
|
||||
{language.t("session.tab.worktree")}
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
<Button variant="secondary" size="small" onClick={openImport} class="history-import-btn">
|
||||
{language.t("session.cloud.import")}
|
||||
@@ -139,6 +172,23 @@ const HistoryView: Component<HistoryViewProps> = (props) => {
|
||||
>
|
||||
{tab() === "cloud" && <CloudSessionList onSelectSession={selectCloudSession} />}
|
||||
</div>
|
||||
<Show when={worktreeIds()}>
|
||||
<div
|
||||
class="history-view-content"
|
||||
ref={worktreePanel}
|
||||
id="history-panel-worktree"
|
||||
role="tabpanel"
|
||||
aria-labelledby="history-tab-worktree"
|
||||
hidden={tab() !== "worktree"}
|
||||
>
|
||||
{tab() === "worktree" && (
|
||||
<SessionList
|
||||
onSelectSession={props.onSelectSession}
|
||||
sessionIds={() => worktreeIds() ?? EMPTY_SESSION_IDS}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* Header/back button are owned by the parent HistoryView.
|
||||
*/
|
||||
|
||||
import { Component, Show, createSignal, onMount, type JSX } from "solid-js"
|
||||
import { Component, Show, createMemo, createSignal, onMount, type Accessor, type JSX } from "solid-js"
|
||||
import { List } from "@kilocode/kilo-ui/list"
|
||||
import { ContextMenu } from "@kilocode/kilo-ui/context-menu"
|
||||
import { Dialog } from "@kilocode/kilo-ui/dialog"
|
||||
@@ -38,6 +38,7 @@ function dateGroupKey(iso: string): (typeof DATE_GROUP_KEYS)[number] {
|
||||
|
||||
interface SessionListProps {
|
||||
onSelectSession: (id: string) => void
|
||||
sessionIds?: Accessor<ReadonlySet<string>>
|
||||
}
|
||||
|
||||
const SessionList: Component<SessionListProps> = (props) => {
|
||||
@@ -50,6 +51,12 @@ const SessionList: Component<SessionListProps> = (props) => {
|
||||
const [notice, setNotice] = createSignal("")
|
||||
let seq = 0
|
||||
|
||||
const items = createMemo(() => {
|
||||
const ids = props.sessionIds?.()
|
||||
if (!ids) return session.sessions()
|
||||
return session.sessions().filter((item) => ids.has(item.id))
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
console.log("[Kilo New] SessionList mounted, loading sessions")
|
||||
session.loadSessions()
|
||||
@@ -57,7 +64,7 @@ const SessionList: Component<SessionListProps> = (props) => {
|
||||
|
||||
const currentSession = (): SessionInfo | undefined => {
|
||||
const id = session.currentSessionID()
|
||||
return session.sessions().find((s) => s.id === id)
|
||||
return items().find((s) => s.id === id)
|
||||
}
|
||||
|
||||
function startRename(s: SessionInfo) {
|
||||
@@ -189,7 +196,7 @@ const SessionList: Component<SessionListProps> = (props) => {
|
||||
return (
|
||||
<div class="session-list">
|
||||
<List<SessionInfo>
|
||||
items={session.sessions()}
|
||||
items={items()}
|
||||
key={(s) => s.id}
|
||||
filterKeys={["title"]}
|
||||
current={currentSession()}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Card } from "@kilocode/kilo-ui/card"
|
||||
import { Card, CardDescription } from "@kilocode/kilo-ui/card"
|
||||
import { type Component, Show, createMemo } from "solid-js"
|
||||
import { useSession } from "../../context/session"
|
||||
import { terminal, type TerminalState } from "../../context/session-outcome"
|
||||
@@ -12,6 +12,7 @@ export const TurnOutcome: Component = () => {
|
||||
reason: session.closeReason(),
|
||||
messages: session.visibleMessages(),
|
||||
todos: session.todos(),
|
||||
parts: session.getParts,
|
||||
hidden: session.isErrorHidden,
|
||||
}),
|
||||
)
|
||||
@@ -36,7 +37,10 @@ export const TurnOutcome: Component = () => {
|
||||
>
|
||||
<Card variant={value().tone === "critical" ? "error" : "warning"}>
|
||||
<div>{label(value())}</div>
|
||||
<Show when={value().kind === "unknown" && value().vercelID}>{(id) => <code>Request ID: {id()}</code>}</Show>
|
||||
<Show when={value().vercelID}>{(id) => <code>Request ID: {id()}</code>}</Show>
|
||||
<Show when={value().generationID}>
|
||||
{(id) => <CardDescription>{language.t("session.outcome.generationId", { id: id() })}</CardDescription>}
|
||||
</Show>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -21,10 +21,9 @@ import {
|
||||
insertSessionTabAfter,
|
||||
isPendingTab,
|
||||
openSessionTab,
|
||||
pendingTabForCreated,
|
||||
reconcileTabs,
|
||||
replacePendingTab,
|
||||
restoreTabs,
|
||||
tabsForCreatedSession,
|
||||
type LocalTabState,
|
||||
} from "../utils/local-tabs"
|
||||
import {
|
||||
@@ -186,10 +185,8 @@ export const LocalTabsProvider: ParentComponent = (props) => {
|
||||
}
|
||||
if (message.type === "sessionCreated") {
|
||||
if (message.draftID && promotePendingDraftDiscard(message.draftID, message.session.id)) return
|
||||
const draft = pendingTabForCreated(ids(), message.draftID)
|
||||
if (!draft) return
|
||||
const before = active()
|
||||
const next = replacePendingTab(current(), draft, message.session.id)
|
||||
const next = tabsForCreatedSession(current(), message.session.id, message.draftID, message.activate)
|
||||
if (!next) return
|
||||
fresh.add(message.session.id)
|
||||
apply(next)
|
||||
focus(next.active)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Message, SessionCloseReason, TodoItem } from "../types/messages"
|
||||
import type { Message, Part, SessionCloseReason, TodoItem } from "../types/messages"
|
||||
|
||||
type TerminalKind = "incomplete" | "limit" | "unknown" | "filtered" | "unexpected" | "interrupted" | "error"
|
||||
type TerminalTone = "warning" | "critical"
|
||||
@@ -8,6 +8,7 @@ export interface TerminalState {
|
||||
tone: TerminalTone
|
||||
finish?: string
|
||||
vercelID?: string
|
||||
generationID?: string
|
||||
remaining: number
|
||||
}
|
||||
|
||||
@@ -15,6 +16,7 @@ interface Input {
|
||||
reason?: SessionCloseReason
|
||||
messages: Message[]
|
||||
todos: TodoItem[]
|
||||
parts?: (messageID: string) => Part[]
|
||||
hidden?: (id: string) => boolean
|
||||
}
|
||||
|
||||
@@ -26,10 +28,22 @@ function vercelID(message: Message | undefined) {
|
||||
)?.[1]
|
||||
}
|
||||
|
||||
function identifiers(
|
||||
message: Message | undefined,
|
||||
parts: Input["parts"],
|
||||
): Pick<TerminalState, "vercelID" | "generationID"> {
|
||||
if (message?.role !== "assistant" || message.finish !== "other") return {}
|
||||
const part = (parts?.(message.id) ?? []).findLast(
|
||||
(item): item is Extract<Part, { type: "step-finish" }> => item.type === "step-finish" && item.reason === "other",
|
||||
)
|
||||
return { vercelID: part?.vercelID ?? vercelID(message), generationID: part?.generationID }
|
||||
}
|
||||
|
||||
export function terminal(input: Input): TerminalState | undefined {
|
||||
if (!input.reason) return undefined
|
||||
const last = input.messages[input.messages.length - 1]
|
||||
const finish = last?.role === "assistant" ? last.finish : undefined
|
||||
const ids = identifiers(last, input.parts)
|
||||
const remaining = input.todos.filter((item) => item.status !== "completed" && item.status !== "cancelled").length
|
||||
|
||||
if (input.reason === "interrupted") return { kind: "interrupted", tone: "warning", finish, remaining }
|
||||
@@ -40,6 +54,8 @@ export function terminal(input: Input): TerminalState | undefined {
|
||||
if (finish === "length") return { kind: "limit", tone: "warning", finish, remaining }
|
||||
if (finish === "unknown") return { kind: "unknown", tone: "warning", finish, remaining, vercelID: vercelID(last) }
|
||||
if (finish === "content-filter") return { kind: "filtered", tone: "warning", finish, remaining }
|
||||
if (finish === "other") return { kind: "unexpected", tone: "warning", finish, remaining }
|
||||
if (finish === "other") {
|
||||
return { kind: "unexpected", tone: "warning", finish, ...ids, remaining }
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
+12
@@ -641,6 +641,16 @@ export const dict = {
|
||||
"ui.permission.toolLabel.codeSearch": "بحث الكود",
|
||||
"ui.permission.toggleWrap": "تبديل التفاف الكلمات",
|
||||
"ui.permission.copyCommand": "نسخ",
|
||||
"ui.approval.auto": "تمت الموافقة تلقائيًا",
|
||||
"ui.approval.manual": "تمت الموافقة من قبلك",
|
||||
"ui.approval.rule": "طابق قاعدة `{{permission}}` `{{pattern}}`",
|
||||
"ui.approval.source.agent": "بواسطة وكيل {{agent}}",
|
||||
"ui.approval.source.agent.default": "بواسطة الوكيل",
|
||||
"ui.approval.source.global": "بواسطة الإعداد العام",
|
||||
"ui.approval.source.project": "بواسطة إعداد المشروع",
|
||||
"ui.approval.source.yolo": "بواسطة وضع الموافقة التلقائية (YOLO)",
|
||||
"ui.approval.source.session": "بواسطة قاعدة موافقة تلقائية للجلسة",
|
||||
"ui.approval.source.default": "افتراضيًا",
|
||||
"notification.question.title": "سؤال",
|
||||
"notification.question.description": "{{sessionTitle}} في {{projectName}} لديه سؤال",
|
||||
"notification.action.goToSession": "انتقل إلى الجلسة",
|
||||
@@ -1109,6 +1119,7 @@ export const dict = {
|
||||
"session.tabs.switcher.busy": "جارٍ العمل",
|
||||
"session.tab.local": "محلي",
|
||||
"session.tab.cloud": "السحابة",
|
||||
"session.tab.worktree": "شجرة العمل",
|
||||
"session.cloud.repoOnly": "هذا المستودع فقط",
|
||||
"session.cloud.import": "استيراد من السحابة",
|
||||
"feedback.button": "التغذية الراجعة والدعم",
|
||||
@@ -1282,6 +1293,7 @@ export const dict = {
|
||||
"session.outcome.unknown": "انتهى الدور بدون سبب إنهاء من النموذج",
|
||||
"session.outcome.filtered": "أوقف المزود هذه الاستجابة بسبب عامل تصفية المحتوى.",
|
||||
"session.outcome.unexpected": "انتهت الاستجابة بشكل غير متوقع وقد تكون غير مكتملة.",
|
||||
"session.outcome.generationId": "معرّف التوليد: {{id}}",
|
||||
"session.outcome.interrupted": "تمت مقاطعة الدور",
|
||||
"session.outcome.error": "فشل الدور",
|
||||
"session.outcome.finish": "سبب الإنهاء: {{reason}}",
|
||||
|
||||
+12
@@ -652,6 +652,16 @@ export const dict = {
|
||||
"ui.permission.toolLabel.codeSearch": "Pesquisa de Código",
|
||||
"ui.permission.toggleWrap": "Alternar quebra de linha",
|
||||
"ui.permission.copyCommand": "Copiar",
|
||||
"ui.approval.auto": "Aprovado automaticamente",
|
||||
"ui.approval.manual": "Aprovado por você",
|
||||
"ui.approval.rule": "correspondeu à regra `{{permission}}` `{{pattern}}`",
|
||||
"ui.approval.source.agent": "pelo agente {{agent}}",
|
||||
"ui.approval.source.agent.default": "pelo agente",
|
||||
"ui.approval.source.global": "pela configuração global",
|
||||
"ui.approval.source.project": "pela configuração do projeto",
|
||||
"ui.approval.source.yolo": "pelo modo de aprovação automática (YOLO)",
|
||||
"ui.approval.source.session": "por uma regra de aprovação automática da sessão",
|
||||
"ui.approval.source.default": "por padrão",
|
||||
"notification.question.title": "Pergunta",
|
||||
"notification.question.description": "{{sessionTitle}} em {{projectName}} tem uma pergunta",
|
||||
"notification.action.goToSession": "Ir para sessão",
|
||||
@@ -1130,6 +1140,7 @@ export const dict = {
|
||||
"session.tabs.switcher.busy": "Trabalhando",
|
||||
"session.tab.local": "Local",
|
||||
"session.tab.cloud": "Nuvem",
|
||||
"session.tab.worktree": "Árvore de trabalho",
|
||||
"session.cloud.repoOnly": "Apenas este repositório",
|
||||
"session.cloud.import": "Importar da nuvem",
|
||||
"feedback.button": "Feedback e suporte",
|
||||
@@ -1308,6 +1319,7 @@ export const dict = {
|
||||
"session.outcome.unknown": "Turno encerrado sem um motivo de conclusão do modelo",
|
||||
"session.outcome.filtered": "O provedor interrompeu esta resposta devido a um filtro de conteúdo.",
|
||||
"session.outcome.unexpected": "A resposta terminou inesperadamente e pode estar incompleta.",
|
||||
"session.outcome.generationId": "ID da geração: {{id}}",
|
||||
"session.outcome.interrupted": "Turno interrompido",
|
||||
"session.outcome.error": "Turno falhou",
|
||||
"session.outcome.finish": "Motivo da conclusão: {{reason}}",
|
||||
|
||||
+12
@@ -653,6 +653,16 @@ export const dict = {
|
||||
"ui.permission.toolLabel.codeSearch": "Pretraga koda",
|
||||
"ui.permission.toggleWrap": "Prebaci prelamanje teksta",
|
||||
"ui.permission.copyCommand": "Kopiraj",
|
||||
"ui.approval.auto": "Automatski odobreno",
|
||||
"ui.approval.manual": "Odobrili ste vi",
|
||||
"ui.approval.rule": "odgovara pravilu `{{permission}}` `{{pattern}}`",
|
||||
"ui.approval.source.agent": "od strane agenta {{agent}}",
|
||||
"ui.approval.source.agent.default": "od strane agenta",
|
||||
"ui.approval.source.global": "globalnom konfiguracijom",
|
||||
"ui.approval.source.project": "konfiguracijom projekta",
|
||||
"ui.approval.source.yolo": "režimom automatskog odobravanja (YOLO)",
|
||||
"ui.approval.source.session": "pravilom automatskog odobravanja sesije",
|
||||
"ui.approval.source.default": "podrazumevano",
|
||||
"notification.question.title": "Pitanje",
|
||||
"notification.question.description": "{{sessionTitle}} u {{projectName}} ima pitanje",
|
||||
"notification.action.goToSession": "Idi na sesiju",
|
||||
@@ -1177,6 +1187,7 @@ export const dict = {
|
||||
"session.tabs.switcher.busy": "Radi",
|
||||
"session.tab.local": "Lokalno",
|
||||
"session.tab.cloud": "Oblak",
|
||||
"session.tab.worktree": "Radno stablo",
|
||||
"session.cloud.repoOnly": "Samo ovaj repozitorij",
|
||||
"session.cloud.import": "Uvezi iz oblaka",
|
||||
"feedback.button": "Povratne informacije i podrška",
|
||||
@@ -1311,6 +1322,7 @@ export const dict = {
|
||||
"session.outcome.unknown": "Potez završen bez razloga završetka modela",
|
||||
"session.outcome.filtered": "Provajder je zaustavio ovaj odgovor zbog filtera sadržaja.",
|
||||
"session.outcome.unexpected": "Odgovor je neočekivano završen i može biti nepotpun.",
|
||||
"session.outcome.generationId": "ID generiranja: {{id}}",
|
||||
"session.outcome.interrupted": "Potez prekinut",
|
||||
"session.outcome.error": "Potez nije uspio",
|
||||
"session.outcome.finish": "Razlog završetka: {{reason}}",
|
||||
|
||||
+12
@@ -650,6 +650,16 @@ export const dict = {
|
||||
"ui.permission.toolLabel.codeSearch": "Kodesøgning",
|
||||
"ui.permission.toggleWrap": "Skift tekstombrydning",
|
||||
"ui.permission.copyCommand": "Kopiér",
|
||||
"ui.approval.auto": "Automatisk godkendt",
|
||||
"ui.approval.manual": "Godkendt af dig",
|
||||
"ui.approval.rule": "matchede reglen `{{permission}}` `{{pattern}}`",
|
||||
"ui.approval.source.agent": "af {{agent}}-agenten",
|
||||
"ui.approval.source.agent.default": "af agenten",
|
||||
"ui.approval.source.global": "af din globale konfiguration",
|
||||
"ui.approval.source.project": "af projektkonfigurationen",
|
||||
"ui.approval.source.yolo": "af automatisk godkendelse (YOLO)",
|
||||
"ui.approval.source.session": "af en session-autogodkendelsesregel",
|
||||
"ui.approval.source.default": "som standard",
|
||||
"notification.question.title": "Spørgsmål",
|
||||
"notification.question.description": "{{sessionTitle}} i {{projectName}} har et spørgsmål",
|
||||
"notification.action.goToSession": "Gå til session",
|
||||
@@ -1169,6 +1179,7 @@ export const dict = {
|
||||
"session.tabs.switcher.busy": "Arbejder",
|
||||
"session.tab.local": "Lokal",
|
||||
"session.tab.cloud": "Sky",
|
||||
"session.tab.worktree": "Arbejdstræ",
|
||||
"session.cloud.repoOnly": "Kun dette repository",
|
||||
"session.cloud.import": "Importér fra skyen",
|
||||
"feedback.button": "Feedback & support",
|
||||
@@ -1304,6 +1315,7 @@ export const dict = {
|
||||
"session.outcome.unknown": "Tur afsluttet uden en model-afslutningsårsag",
|
||||
"session.outcome.filtered": "Udbyderen stoppede dette svar på grund af et indholdsfilter.",
|
||||
"session.outcome.unexpected": "Svaret sluttede uventet og kan være ufuldstændigt.",
|
||||
"session.outcome.generationId": "Genererings-id: {{id}}",
|
||||
"session.outcome.interrupted": "Tur afbrudt",
|
||||
"session.outcome.error": "Tur mislykkedes",
|
||||
"session.outcome.finish": "Afslutningsårsag: {{reason}}",
|
||||
|
||||
@@ -662,6 +662,16 @@ export const dict = {
|
||||
"ui.permission.toolLabel.codeSearch": "Code-Suche",
|
||||
"ui.permission.toggleWrap": "Zeilenumbruch umschalten",
|
||||
"ui.permission.copyCommand": "Kopieren",
|
||||
"ui.approval.auto": "Automatisch genehmigt",
|
||||
"ui.approval.manual": "Von dir genehmigt",
|
||||
"ui.approval.rule": "entsprach Regel `{{permission}}` `{{pattern}}`",
|
||||
"ui.approval.source.agent": "durch den {{agent}}-Agenten",
|
||||
"ui.approval.source.agent.default": "durch den Agenten",
|
||||
"ui.approval.source.global": "durch deine globale Konfiguration",
|
||||
"ui.approval.source.project": "durch die Projektkonfiguration",
|
||||
"ui.approval.source.yolo": "durch den Auto-Genehmigungsmodus (YOLO)",
|
||||
"ui.approval.source.session": "durch eine Sitzungs-Auto-Genehmigungsregel",
|
||||
"ui.approval.source.default": "standardmäßig",
|
||||
"notification.question.title": "Frage",
|
||||
"notification.question.description": "{{sessionTitle}} in {{projectName}} hat eine Frage",
|
||||
"notification.action.goToSession": "Zur Sitzung gehen",
|
||||
@@ -1189,6 +1199,7 @@ export const dict = {
|
||||
"session.tabs.switcher.busy": "In Arbeit",
|
||||
"session.tab.local": "Lokal",
|
||||
"session.tab.cloud": "Cloud",
|
||||
"session.tab.worktree": "Arbeitsbaum",
|
||||
"session.cloud.repoOnly": "Nur dieses Repository",
|
||||
"session.cloud.import": "Aus der Cloud importieren",
|
||||
"feedback.button": "Feedback & Support",
|
||||
@@ -1325,6 +1336,7 @@ export const dict = {
|
||||
"session.outcome.unknown": "Zug ohne Modell-Abschlussgrund beendet",
|
||||
"session.outcome.filtered": "Der Anbieter hat diese Antwort aufgrund eines Inhaltsfilters gestoppt.",
|
||||
"session.outcome.unexpected": "Die Antwort wurde unerwartet beendet und ist möglicherweise unvollständig.",
|
||||
"session.outcome.generationId": "Generierungs-ID: {{id}}",
|
||||
"session.outcome.interrupted": "Zug unterbrochen",
|
||||
"session.outcome.error": "Zug fehlgeschlagen",
|
||||
"session.outcome.finish": "Abschlussgrund: {{reason}}",
|
||||
|
||||
@@ -649,6 +649,16 @@ export const dict = {
|
||||
"ui.permission.toolLabel.codeSearch": "Code Search",
|
||||
"ui.permission.toggleWrap": "Toggle word wrap",
|
||||
"ui.permission.copyCommand": "Copy",
|
||||
"ui.approval.auto": "Auto-approved",
|
||||
"ui.approval.manual": "Approved by you",
|
||||
"ui.approval.rule": "matched `{{permission}}` rule `{{pattern}}`",
|
||||
"ui.approval.source.agent": "by the {{agent}} agent",
|
||||
"ui.approval.source.agent.default": "by the agent",
|
||||
"ui.approval.source.global": "by your global config",
|
||||
"ui.approval.source.project": "by the project config",
|
||||
"ui.approval.source.yolo": "by auto-approve (YOLO) mode",
|
||||
"ui.approval.source.session": "by a session auto-approve rule",
|
||||
"ui.approval.source.default": "by default",
|
||||
"notification.question.title": "Question",
|
||||
"notification.question.description": "{{sessionTitle}} in {{projectName}} has a question",
|
||||
"notification.action.goToSession": "Go to session",
|
||||
@@ -1084,6 +1094,7 @@ export const dict = {
|
||||
"session.tabs.switcher.busy": "Working",
|
||||
"session.tab.local": "Local",
|
||||
"session.tab.cloud": "Cloud",
|
||||
"session.tab.worktree": "Worktree",
|
||||
"session.cloud.repoOnly": "Only this repository",
|
||||
"session.cloud.import": "Import session",
|
||||
"feedback.button": "Feedback & Support",
|
||||
@@ -1254,6 +1265,7 @@ export const dict = {
|
||||
"session.outcome.unknown": "Response ended without a finish reason and may be incomplete.",
|
||||
"session.outcome.filtered": "The provider stopped this response because of a content filter.",
|
||||
"session.outcome.unexpected": "Response ended unexpectedly and may be incomplete.",
|
||||
"session.outcome.generationId": "Generation ID: {{id}}",
|
||||
"session.outcome.interrupted": "Turn interrupted.",
|
||||
"session.outcome.error": "Turn failed.",
|
||||
"session.outcome.finish": "Technical finish reason: {{reason}}",
|
||||
|
||||
+12
@@ -656,6 +656,16 @@ export const dict = {
|
||||
"ui.permission.toolLabel.codeSearch": "Búsqueda de Código",
|
||||
"ui.permission.toggleWrap": "Alternar ajuste de línea",
|
||||
"ui.permission.copyCommand": "Copiar",
|
||||
"ui.approval.auto": "Aprobado automáticamente",
|
||||
"ui.approval.manual": "Aprobado por ti",
|
||||
"ui.approval.rule": "coincidió con la regla `{{permission}}` `{{pattern}}`",
|
||||
"ui.approval.source.agent": "por el agente {{agent}}",
|
||||
"ui.approval.source.agent.default": "por el agente",
|
||||
"ui.approval.source.global": "por tu configuración global",
|
||||
"ui.approval.source.project": "por la configuración del proyecto",
|
||||
"ui.approval.source.yolo": "por el modo de aprobación automática (YOLO)",
|
||||
"ui.approval.source.session": "por una regla de aprobación automática de sesión",
|
||||
"ui.approval.source.default": "de forma predeterminada",
|
||||
"notification.question.title": "Pregunta",
|
||||
"notification.question.description": "{{sessionTitle}} en {{projectName}} tiene una pregunta",
|
||||
"notification.action.goToSession": "Ir a sesión",
|
||||
@@ -1181,6 +1191,7 @@ export const dict = {
|
||||
"session.tabs.switcher.busy": "Trabajando",
|
||||
"session.tab.local": "Local",
|
||||
"session.tab.cloud": "Nube",
|
||||
"session.tab.worktree": "Árbol de trabajo",
|
||||
"session.cloud.repoOnly": "Solo este repositorio",
|
||||
"session.cloud.import": "Importar desde la nube",
|
||||
"feedback.button": "Comentarios y soporte",
|
||||
@@ -1318,6 +1329,7 @@ export const dict = {
|
||||
"session.outcome.unknown": "Turno finalizado sin motivo de finalización del modelo",
|
||||
"session.outcome.filtered": "El proveedor detuvo esta respuesta debido a un filtro de contenido.",
|
||||
"session.outcome.unexpected": "La respuesta terminó inesperadamente y puede estar incompleta.",
|
||||
"session.outcome.generationId": "ID de generación: {{id}}",
|
||||
"session.outcome.interrupted": "Turno interrumpido",
|
||||
"session.outcome.error": "Turno fallido",
|
||||
"session.outcome.finish": "Motivo de finalización: {{reason}}",
|
||||
|
||||
+12
@@ -660,6 +660,16 @@ export const dict = {
|
||||
"ui.permission.toolLabel.codeSearch": "Recherche de code",
|
||||
"ui.permission.toggleWrap": "Basculer le retour à la ligne",
|
||||
"ui.permission.copyCommand": "Copier",
|
||||
"ui.approval.auto": "Approuvé automatiquement",
|
||||
"ui.approval.manual": "Approuvé par vous",
|
||||
"ui.approval.rule": "a correspondu à la règle `{{permission}}` `{{pattern}}`",
|
||||
"ui.approval.source.agent": "par l'agent {{agent}}",
|
||||
"ui.approval.source.agent.default": "par l'agent",
|
||||
"ui.approval.source.global": "par votre configuration globale",
|
||||
"ui.approval.source.project": "par la configuration du projet",
|
||||
"ui.approval.source.yolo": "par le mode d'approbation automatique (YOLO)",
|
||||
"ui.approval.source.session": "par une règle d'approbation automatique de session",
|
||||
"ui.approval.source.default": "par défaut",
|
||||
"notification.question.title": "Question",
|
||||
"notification.question.description": "{{sessionTitle}} dans {{projectName}} a une question",
|
||||
"notification.action.goToSession": "Aller à la session",
|
||||
@@ -1188,6 +1198,7 @@ export const dict = {
|
||||
"session.tabs.switcher.busy": "En cours",
|
||||
"session.tab.local": "Local",
|
||||
"session.tab.cloud": "Cloud",
|
||||
"session.tab.worktree": "Arbre de travail",
|
||||
"session.cloud.repoOnly": "Uniquement ce dépôt",
|
||||
"session.cloud.import": "Importer depuis le cloud",
|
||||
"feedback.button": "Commentaires & support",
|
||||
@@ -1329,6 +1340,7 @@ export const dict = {
|
||||
"session.outcome.unknown": "Tour terminé sans motif de fin du modèle",
|
||||
"session.outcome.filtered": "Le fournisseur a arrêté cette réponse en raison d'un filtre de contenu.",
|
||||
"session.outcome.unexpected": "La réponse s'est terminée de manière inattendue et peut être incomplète.",
|
||||
"session.outcome.generationId": "ID de génération : {{id}}",
|
||||
"session.outcome.interrupted": "Tour interrompu",
|
||||
"session.outcome.error": "Échec du tour",
|
||||
"session.outcome.finish": "Motif de fin : {{reason}}",
|
||||
|
||||
+12
@@ -528,6 +528,16 @@ export const dict = {
|
||||
"ui.permission.toolLabel.codeSearch": "Ricerca codice",
|
||||
"ui.permission.toggleWrap": "Attiva/disattiva ritorno a capo",
|
||||
"ui.permission.copyCommand": "Copia",
|
||||
"ui.approval.auto": "Approvato automaticamente",
|
||||
"ui.approval.manual": "Approvato da te",
|
||||
"ui.approval.rule": "ha corrisposto alla regola `{{permission}}` `{{pattern}}`",
|
||||
"ui.approval.source.agent": "dall'agente {{agent}}",
|
||||
"ui.approval.source.agent.default": "dall'agente",
|
||||
"ui.approval.source.global": "dalla configurazione globale",
|
||||
"ui.approval.source.project": "dalla configurazione del progetto",
|
||||
"ui.approval.source.yolo": "dalla modalità di approvazione automatica (YOLO)",
|
||||
"ui.approval.source.session": "da una regola di approvazione automatica della sessione",
|
||||
"ui.approval.source.default": "per impostazione predefinita",
|
||||
"notification.question.title": "Domanda",
|
||||
"notification.question.description": "{{sessionTitle}} in {{projectName}} ha una domanda",
|
||||
"notification.action.goToSession": "Vai alla sessione",
|
||||
@@ -935,6 +945,7 @@ export const dict = {
|
||||
"session.tabs.switcher.busy": "In corso",
|
||||
"session.tab.local": "Locale",
|
||||
"session.tab.cloud": "Cloud",
|
||||
"session.tab.worktree": "Albero di lavoro",
|
||||
"session.cloud.repoOnly": "Solo questa repository",
|
||||
"session.cloud.import": "Importa sessione",
|
||||
"feedback.button": "Feedback e supporto",
|
||||
@@ -1692,6 +1703,7 @@ export const dict = {
|
||||
"session.outcome.unknown": "La risposta è terminata senza un motivo di fine e potrebbe essere incompleta.",
|
||||
"session.outcome.filtered": "Il provider ha interrotto questa risposta a causa di un filtro contenuti.",
|
||||
"session.outcome.unexpected": "La risposta è terminata inaspettatamente e potrebbe essere incompleta.",
|
||||
"session.outcome.generationId": "ID della generazione: {{id}}",
|
||||
"session.outcome.interrupted": "Turno interrotto.",
|
||||
"session.outcome.error": "Turno fallito.",
|
||||
"session.outcome.finish": "Motivo tecnico di fine: {{reason}}",
|
||||
|
||||
+12
@@ -648,6 +648,16 @@ export const dict = {
|
||||
"ui.permission.toolLabel.codeSearch": "コード検索",
|
||||
"ui.permission.toggleWrap": "折り返しを切り替え",
|
||||
"ui.permission.copyCommand": "コピー",
|
||||
"ui.approval.auto": "自動承認されました",
|
||||
"ui.approval.manual": "あなたが承認しました",
|
||||
"ui.approval.rule": "ルール `{{permission}}` `{{pattern}}` に一致",
|
||||
"ui.approval.source.agent": "{{agent}} エージェントによって",
|
||||
"ui.approval.source.agent.default": "エージェントによって",
|
||||
"ui.approval.source.global": "グローバル設定によって",
|
||||
"ui.approval.source.project": "プロジェクト設定によって",
|
||||
"ui.approval.source.yolo": "自動承認(YOLO)モードによって",
|
||||
"ui.approval.source.session": "セッションの自動承認ルールによって",
|
||||
"ui.approval.source.default": "デフォルトで",
|
||||
"notification.question.title": "質問",
|
||||
"notification.question.description": "{{projectName}} の {{sessionTitle}} から質問があります",
|
||||
"notification.action.goToSession": "セッションへ移動",
|
||||
@@ -1164,6 +1174,7 @@ export const dict = {
|
||||
"session.tabs.switcher.busy": "作業中",
|
||||
"session.tab.local": "ローカル",
|
||||
"session.tab.cloud": "クラウド",
|
||||
"session.tab.worktree": "ワークツリー",
|
||||
"session.cloud.repoOnly": "このリポジトリのみ",
|
||||
"session.cloud.import": "クラウドからインポート",
|
||||
"feedback.button": "フィードバック & サポート",
|
||||
@@ -1299,6 +1310,7 @@ export const dict = {
|
||||
"session.outcome.unknown": "モデルの終了理由なしでターンが終了しました",
|
||||
"session.outcome.filtered": "コンテンツフィルターにより、プロバイダーがこの応答を停止しました。",
|
||||
"session.outcome.unexpected": "応答が予期せず終了したため、不完全である可能性があります。",
|
||||
"session.outcome.generationId": "生成 ID: {{id}}",
|
||||
"session.outcome.interrupted": "ターンが中断されました",
|
||||
"session.outcome.error": "ターンが失敗しました",
|
||||
"session.outcome.finish": "終了理由: {{reason}}",
|
||||
|
||||
+12
@@ -648,6 +648,16 @@ export const dict = {
|
||||
"ui.permission.toolLabel.codeSearch": "코드 검색",
|
||||
"ui.permission.toggleWrap": "줄 바꿈 전환",
|
||||
"ui.permission.copyCommand": "복사",
|
||||
"ui.approval.auto": "자동 승인됨",
|
||||
"ui.approval.manual": "사용자가 승인함",
|
||||
"ui.approval.rule": "`{{permission}}` 규칙 `{{pattern}}`과(와) 일치",
|
||||
"ui.approval.source.agent": "{{agent}} 에이전트에 의해",
|
||||
"ui.approval.source.agent.default": "에이전트에 의해",
|
||||
"ui.approval.source.global": "전역 설정에 의해",
|
||||
"ui.approval.source.project": "프로젝트 설정에 의해",
|
||||
"ui.approval.source.yolo": "자동 승인(YOLO) 모드에 의해",
|
||||
"ui.approval.source.session": "세션 자동 승인 규칙에 의해",
|
||||
"ui.approval.source.default": "기본값으로",
|
||||
"notification.question.title": "질문",
|
||||
"notification.question.description": "{{projectName}}의 {{sessionTitle}}에서 질문이 있습니다",
|
||||
"notification.action.goToSession": "세션으로 이동",
|
||||
@@ -1118,6 +1128,7 @@ export const dict = {
|
||||
"session.tabs.switcher.busy": "작업 중",
|
||||
"session.tab.local": "로컬",
|
||||
"session.tab.cloud": "클라우드",
|
||||
"session.tab.worktree": "작업 트리",
|
||||
"session.cloud.repoOnly": "이 저장소만",
|
||||
"session.cloud.import": "클라우드에서 가져오기",
|
||||
"feedback.button": "피드백 & 지원",
|
||||
@@ -1295,6 +1306,7 @@ export const dict = {
|
||||
"session.outcome.unknown": "모델의 종료 이유 없이 턴이 종료되었습니다",
|
||||
"session.outcome.filtered": "콘텐츠 필터로 인해 제공자가 이 응답을 중단했습니다.",
|
||||
"session.outcome.unexpected": "응답이 예기치 않게 종료되었으며 불완전할 수 있습니다.",
|
||||
"session.outcome.generationId": "생성 ID: {{id}}",
|
||||
"session.outcome.interrupted": "턴이 중단되었습니다",
|
||||
"session.outcome.error": "턴이 실패했습니다",
|
||||
"session.outcome.finish": "종료 이유: {{reason}}",
|
||||
|
||||
+12
@@ -655,6 +655,16 @@ export const dict = {
|
||||
"ui.permission.toolLabel.codeSearch": "Code Zoeken",
|
||||
"ui.permission.toggleWrap": "Regelterugloop in-/uitschakelen",
|
||||
"ui.permission.copyCommand": "Kopiëren",
|
||||
"ui.approval.auto": "Automatisch goedgekeurd",
|
||||
"ui.approval.manual": "Door jou goedgekeurd",
|
||||
"ui.approval.rule": "kwam overeen met regel `{{permission}}` `{{pattern}}`",
|
||||
"ui.approval.source.agent": "door de {{agent}}-agent",
|
||||
"ui.approval.source.agent.default": "door de agent",
|
||||
"ui.approval.source.global": "door je globale configuratie",
|
||||
"ui.approval.source.project": "door de projectconfiguratie",
|
||||
"ui.approval.source.yolo": "door de automatische goedkeuringsmodus (YOLO)",
|
||||
"ui.approval.source.session": "door een sessie-automatische-goedkeuringsregel",
|
||||
"ui.approval.source.default": "standaard",
|
||||
"notification.question.title": "Vraag",
|
||||
"notification.question.description": "{{sessionTitle}} in {{projectName}} heeft een vraag",
|
||||
"notification.action.goToSession": "Ga naar sessie",
|
||||
@@ -1124,6 +1134,7 @@ export const dict = {
|
||||
"session.tabs.switcher.busy": "Bezig",
|
||||
"session.tab.local": "Lokaal",
|
||||
"session.tab.cloud": "Cloud",
|
||||
"session.tab.worktree": "Werkboom",
|
||||
"session.cloud.repoOnly": "Alleen deze repository",
|
||||
"session.cloud.import": "Importeer uit de cloud",
|
||||
"feedback.button": "Feedback & Ondersteuning",
|
||||
@@ -1299,6 +1310,7 @@ export const dict = {
|
||||
"session.outcome.unknown": "Beurt beëindigd zonder voltooiingsreden van het model",
|
||||
"session.outcome.filtered": "De provider heeft deze reactie gestopt vanwege een contentfilter.",
|
||||
"session.outcome.unexpected": "Reactie is onverwacht beëindigd en is mogelijk onvolledig.",
|
||||
"session.outcome.generationId": "Generatie-ID: {{id}}",
|
||||
"session.outcome.interrupted": "Beurt onderbroken",
|
||||
"session.outcome.error": "Beurt mislukt",
|
||||
"session.outcome.finish": "Voltooiingsreden: {{reason}}",
|
||||
|
||||
+12
@@ -655,6 +655,16 @@ export const dict = {
|
||||
"ui.permission.toolLabel.codeSearch": "Kodesøk",
|
||||
"ui.permission.toggleWrap": "Veksle tekstbryting",
|
||||
"ui.permission.copyCommand": "Kopier",
|
||||
"ui.approval.auto": "Automatisk godkjent",
|
||||
"ui.approval.manual": "Godkjent av deg",
|
||||
"ui.approval.rule": "samsvarte med regelen `{{permission}}` `{{pattern}}`",
|
||||
"ui.approval.source.agent": "av {{agent}}-agenten",
|
||||
"ui.approval.source.agent.default": "av agenten",
|
||||
"ui.approval.source.global": "av den globale konfigurasjonen",
|
||||
"ui.approval.source.project": "av prosjektkonfigurasjonen",
|
||||
"ui.approval.source.yolo": "av automatisk godkjenning (YOLO)",
|
||||
"ui.approval.source.session": "av en økt-autogodkjenningsregel",
|
||||
"ui.approval.source.default": "som standard",
|
||||
"notification.question.title": "Spørsmål",
|
||||
"notification.question.description": "{{sessionTitle}} i {{projectName}} har et spørsmål",
|
||||
"notification.action.goToSession": "Gå til sesjon",
|
||||
@@ -1130,6 +1140,7 @@ export const dict = {
|
||||
"session.tabs.switcher.busy": "Jobber",
|
||||
"session.tab.local": "Lokal",
|
||||
"session.tab.cloud": "Sky",
|
||||
"session.tab.worktree": "Arbeidstre",
|
||||
"session.cloud.repoOnly": "Kun dette repositoriet",
|
||||
"session.cloud.import": "Importer fra skyen",
|
||||
"feedback.button": "Tilbakemelding & støtte",
|
||||
@@ -1264,6 +1275,7 @@ export const dict = {
|
||||
"session.outcome.unknown": "Runden ble avsluttet uten avslutningsårsak fra modellen",
|
||||
"session.outcome.filtered": "Leverandøren stoppet denne responsen på grunn av et innholdsfilter.",
|
||||
"session.outcome.unexpected": "Responsen ble avsluttet uventet og kan være ufullstendig.",
|
||||
"session.outcome.generationId": "Genererings-ID: {{id}}",
|
||||
"session.outcome.interrupted": "Runde avbrutt",
|
||||
"session.outcome.error": "Runden feilet",
|
||||
"session.outcome.finish": "Avslutningsårsak: {{reason}}",
|
||||
|
||||
+12
@@ -652,6 +652,16 @@ export const dict = {
|
||||
"ui.permission.toolLabel.codeSearch": "Wyszukiwanie kodu",
|
||||
"ui.permission.toggleWrap": "Przełącz zawijanie tekstu",
|
||||
"ui.permission.copyCommand": "Kopiuj",
|
||||
"ui.approval.auto": "Zatwierdzono automatycznie",
|
||||
"ui.approval.manual": "Zatwierdzone przez Ciebie",
|
||||
"ui.approval.rule": "dopasowano regułę `{{permission}}` `{{pattern}}`",
|
||||
"ui.approval.source.agent": "przez agenta {{agent}}",
|
||||
"ui.approval.source.agent.default": "przez agenta",
|
||||
"ui.approval.source.global": "przez konfigurację globalną",
|
||||
"ui.approval.source.project": "przez konfigurację projektu",
|
||||
"ui.approval.source.yolo": "przez tryb automatycznego zatwierdzania (YOLO)",
|
||||
"ui.approval.source.session": "przez regułę automatycznego zatwierdzania sesji",
|
||||
"ui.approval.source.default": "domyślnie",
|
||||
"notification.question.title": "Pytanie",
|
||||
"notification.question.description": "{{sessionTitle}} w {{projectName}} ma pytanie",
|
||||
"notification.action.goToSession": "Przejdź do sesji",
|
||||
@@ -1128,6 +1138,7 @@ export const dict = {
|
||||
"session.tabs.switcher.busy": "Pracuje",
|
||||
"session.tab.local": "Lokalny",
|
||||
"session.tab.cloud": "Chmura",
|
||||
"session.tab.worktree": "Drzewo robocze",
|
||||
"session.cloud.repoOnly": "Tylko to repozytorium",
|
||||
"session.cloud.import": "Importuj z chmury",
|
||||
"feedback.button": "Opinie i wsparcie",
|
||||
@@ -1263,6 +1274,7 @@ export const dict = {
|
||||
"session.outcome.unknown": "Tura zakończona bez powodu zakończenia od modelu",
|
||||
"session.outcome.filtered": "Dostawca zatrzymał tę odpowiedź z powodu filtru treści.",
|
||||
"session.outcome.unexpected": "Odpowiedź zakończyła się nieoczekiwanie i może być niekompletna.",
|
||||
"session.outcome.generationId": "ID generowania: {{id}}",
|
||||
"session.outcome.interrupted": "Tura przerwana",
|
||||
"session.outcome.error": "Tura nie powiodła się",
|
||||
"session.outcome.finish": "Powód zakończenia: {{reason}}",
|
||||
|
||||
+12
@@ -652,6 +652,16 @@ export const dict = {
|
||||
"ui.permission.toolLabel.codeSearch": "Поиск кода",
|
||||
"ui.permission.toggleWrap": "Переключить перенос строк",
|
||||
"ui.permission.copyCommand": "Копировать",
|
||||
"ui.approval.auto": "Одобрено автоматически",
|
||||
"ui.approval.manual": "Одобрено вами",
|
||||
"ui.approval.rule": "совпало с правилом `{{permission}}` `{{pattern}}`",
|
||||
"ui.approval.source.agent": "агентом {{agent}}",
|
||||
"ui.approval.source.agent.default": "агентом",
|
||||
"ui.approval.source.global": "вашей глобальной конфигурацией",
|
||||
"ui.approval.source.project": "конфигурацией проекта",
|
||||
"ui.approval.source.yolo": "режимом автоодобрения (YOLO)",
|
||||
"ui.approval.source.session": "правилом автоодобрения сессии",
|
||||
"ui.approval.source.default": "по умолчанию",
|
||||
"notification.question.title": "Вопрос",
|
||||
"notification.question.description": "У {{sessionTitle}} в {{projectName}} есть вопрос",
|
||||
"notification.action.goToSession": "Перейти к сессии",
|
||||
@@ -1174,6 +1184,7 @@ export const dict = {
|
||||
"session.tabs.switcher.busy": "В работе",
|
||||
"session.tab.local": "Локальный",
|
||||
"session.tab.cloud": "Облако",
|
||||
"session.tab.worktree": "Рабочее дерево",
|
||||
"session.cloud.repoOnly": "Только этот репозиторий",
|
||||
"session.cloud.import": "Импорт из облака",
|
||||
"feedback.button": "Отзывы и поддержка",
|
||||
@@ -1310,6 +1321,7 @@ export const dict = {
|
||||
"session.outcome.unknown": "Раунд завершен без указания причины моделью",
|
||||
"session.outcome.filtered": "Провайдер остановил этот ответ из-за фильтра контента.",
|
||||
"session.outcome.unexpected": "Ответ неожиданно завершился и может быть неполным.",
|
||||
"session.outcome.generationId": "Идентификатор генерации: {{id}}",
|
||||
"session.outcome.interrupted": "Раунд прерван",
|
||||
"session.outcome.error": "Раунд завершился с ошибкой",
|
||||
"session.outcome.finish": "Причина завершения: {{reason}}",
|
||||
|
||||
+12
@@ -646,6 +646,16 @@ export const dict = {
|
||||
"ui.permission.toolLabel.codeSearch": "ค้นหาโค้ด",
|
||||
"ui.permission.toggleWrap": "สลับการตัดคำ",
|
||||
"ui.permission.copyCommand": "คัดลอก",
|
||||
"ui.approval.auto": "อนุมัติอัตโนมัติ",
|
||||
"ui.approval.manual": "คุณเป็นผู้อนุมัติ",
|
||||
"ui.approval.rule": "ตรงกับกฎ `{{permission}}` `{{pattern}}`",
|
||||
"ui.approval.source.agent": "โดยเอเจนต์ {{agent}}",
|
||||
"ui.approval.source.agent.default": "โดยเอเจนต์",
|
||||
"ui.approval.source.global": "โดยการตั้งค่าส่วนกลาง",
|
||||
"ui.approval.source.project": "โดยการตั้งค่าโปรเจกต์",
|
||||
"ui.approval.source.yolo": "โดยโหมดอนุมัติอัตโนมัติ (YOLO)",
|
||||
"ui.approval.source.session": "โดยกฎอนุมัติอัตโนมัติของเซสชัน",
|
||||
"ui.approval.source.default": "ตามค่าเริ่มต้น",
|
||||
"notification.question.title": "คำถาม",
|
||||
"notification.question.description": "{{sessionTitle}} ใน {{projectName}} มีคำถาม",
|
||||
"notification.action.goToSession": "ไปที่เซสชัน",
|
||||
@@ -1158,6 +1168,7 @@ export const dict = {
|
||||
"session.tabs.switcher.busy": "กำลังทำงาน",
|
||||
"session.tab.local": "ในเครื่อง",
|
||||
"session.tab.cloud": "คลาวด์",
|
||||
"session.tab.worktree": "เวิร์กทรี Git",
|
||||
"session.cloud.repoOnly": "เฉพาะรีโพซิทอรีนี้",
|
||||
"session.cloud.import": "นำเข้าจากคลาวด์",
|
||||
"feedback.button": "ข้อเสนอแนะและการสนับสนุน",
|
||||
@@ -1291,6 +1302,7 @@ export const dict = {
|
||||
"session.outcome.unknown": "เทิร์นสิ้นสุดโดยไม่มีเหตุผลการเสร็จสิ้นของโมเดล",
|
||||
"session.outcome.filtered": "ผู้ให้บริการหยุดการตอบกลับนี้เนื่องจากตัวกรองเนื้อหา",
|
||||
"session.outcome.unexpected": "การตอบกลับสิ้นสุดลงโดยไม่คาดคิดและอาจไม่สมบูรณ์",
|
||||
"session.outcome.generationId": "รหัสการสร้าง: {{id}}",
|
||||
"session.outcome.interrupted": "เทิร์นถูกขัดจังหวะ",
|
||||
"session.outcome.error": "เทิร์นล้มเหลว",
|
||||
"session.outcome.finish": "เหตุผลการเสร็จสิ้น: {{reason}}",
|
||||
|
||||
+12
@@ -652,6 +652,16 @@ export const dict = {
|
||||
"ui.permission.toolLabel.codeSearch": "Kod Araması",
|
||||
"ui.permission.toggleWrap": "Sözcük kaydırmayı aç/kapat",
|
||||
"ui.permission.copyCommand": "Kopyala",
|
||||
"ui.approval.auto": "Otomatik onaylandı",
|
||||
"ui.approval.manual": "Sizin tarafınızdan onaylandı",
|
||||
"ui.approval.rule": "`{{permission}}` kuralı `{{pattern}}` ile eşleşti",
|
||||
"ui.approval.source.agent": "{{agent}} aracısı tarafından",
|
||||
"ui.approval.source.agent.default": "aracı tarafından",
|
||||
"ui.approval.source.global": "genel yapılandırmanız tarafından",
|
||||
"ui.approval.source.project": "proje yapılandırması tarafından",
|
||||
"ui.approval.source.yolo": "otomatik onay (YOLO) modu tarafından",
|
||||
"ui.approval.source.session": "bir oturum otomatik onay kuralı tarafından",
|
||||
"ui.approval.source.default": "varsayılan olarak",
|
||||
"notification.question.title": "Soru",
|
||||
"notification.question.description": "{{projectName}} içindeki {{sessionTitle}} bir soru soruyor",
|
||||
"notification.action.goToSession": "Oturuma git",
|
||||
@@ -1120,6 +1130,7 @@ export const dict = {
|
||||
"session.tabs.switcher.busy": "Çalışıyor",
|
||||
"session.tab.local": "Local",
|
||||
"session.tab.cloud": "Cloud",
|
||||
"session.tab.worktree": "Çalışma ağacı",
|
||||
"session.cloud.repoOnly": "Yalnızca bu depo",
|
||||
"session.cloud.import": "Buluttan içe aktar",
|
||||
"feedback.button": "Geri Bildirim ve Destek",
|
||||
@@ -1295,6 +1306,7 @@ export const dict = {
|
||||
"session.outcome.unknown": "Tur, model bitiş nedeni olmadan sona erdi",
|
||||
"session.outcome.filtered": "Sağlayıcı, bir içerik filtresi nedeniyle bu yanıtı durdurdu.",
|
||||
"session.outcome.unexpected": "Yanıt beklenmedik bir şekilde sona erdi ve eksik olabilir.",
|
||||
"session.outcome.generationId": "Oluşturma kimliği: {{id}}",
|
||||
"session.outcome.interrupted": "Tur kesintiye uğradı",
|
||||
"session.outcome.error": "Tur başarısız oldu",
|
||||
"session.outcome.finish": "Bitiş nedeni: {{reason}}",
|
||||
|
||||
+12
@@ -654,6 +654,16 @@ export const dict = {
|
||||
"ui.permission.toolLabel.codeSearch": "Пошук коду",
|
||||
"ui.permission.toggleWrap": "Перемкнути перенос рядків",
|
||||
"ui.permission.copyCommand": "Копіювати",
|
||||
"ui.approval.auto": "Схвалено автоматично",
|
||||
"ui.approval.manual": "Схвалено вами",
|
||||
"ui.approval.rule": "збіг із правилом `{{permission}}` `{{pattern}}`",
|
||||
"ui.approval.source.agent": "агентом {{agent}}",
|
||||
"ui.approval.source.agent.default": "агентом",
|
||||
"ui.approval.source.global": "вашою глобальною конфігурацією",
|
||||
"ui.approval.source.project": "конфігурацією проєкту",
|
||||
"ui.approval.source.yolo": "режимом автосхвалення (YOLO)",
|
||||
"ui.approval.source.session": "правилом автосхвалення сесії",
|
||||
"ui.approval.source.default": "за замовчуванням",
|
||||
"notification.question.title": "Питання",
|
||||
"notification.question.description": "{{sessionTitle}} у {{projectName}} задає питання",
|
||||
"notification.action.goToSession": "Перейти до сесії",
|
||||
@@ -1121,6 +1131,7 @@ export const dict = {
|
||||
"session.tabs.switcher.busy": "Працює",
|
||||
"session.tab.local": "Локальний",
|
||||
"session.tab.cloud": "Хмарний",
|
||||
"session.tab.worktree": "Робоче дерево",
|
||||
"session.cloud.repoOnly": "Лише цей репозиторій",
|
||||
"session.cloud.import": "Імпортувати з хмари",
|
||||
"feedback.button": "Зворотний зв'язок і підтримка",
|
||||
@@ -1292,6 +1303,7 @@ export const dict = {
|
||||
"session.outcome.unknown": "Хід завершився без причини завершення моделі",
|
||||
"session.outcome.filtered": "Провайдер зупинив цю відповідь через фільтр вмісту.",
|
||||
"session.outcome.unexpected": "Відповідь завершилася несподівано і може бути неповною.",
|
||||
"session.outcome.generationId": "Ідентифікатор генерації: {{id}}",
|
||||
"session.outcome.interrupted": "Хід перервано",
|
||||
"session.outcome.error": "Хід не вдався",
|
||||
"session.outcome.finish": "Причина завершення: {{reason}}",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user