mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-30 17:14:40 +08:00
Merge branch 'main' into fix/cli-startup-performance
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Apply the Agent Manager base branch picker selection to the active diff immediately. Changing the base branch now refreshes the diff against the new base instead of keeping the previous comparison until the scope or session changed.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Add a scope selector and base branch picker to the Agent Manager diff review. The side panel and full-screen review now let you switch between Branch, Staged, Unstaged, and Session scopes for the selected worktree, and the Branch scope's base branch can be overridden from a picker next to it. Branch stays the default, so existing review behavior is unchanged.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Fix Agent Manager mode shortcuts in the New Worktree dialog so the selected mode and its matching model stay in sync.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Add an experimental multi-project foundation for Agent Manager behind the Multi-Project Agent Manager toggle in Kilo Settings > Experimental. A persistent project registry catalogs additional Git repositories across restarts while the current workspace repository stays the pinned default project, and all repository-bound services (state, worktrees, setup scripts, stale tracking, polling) are now owned by immutable per-project contexts that can be activated, expanded, and switched through new project protocol messages. Every expanded project runs its own git stats and PR pollers, so all visible project accordions show live data at the same time; switching projects keeps previously visited projects expanded and restores the exact worktree or session that was open there. Sessions created anywhere (sidebar, another window, the CLI) appear in the owning project's list immediately, and open tabs stay private to the project they were opened in. Single-project behavior is unchanged when the experiment is disabled.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Make the Agent Manager diff review follow the sidebar selection instead of a single session. Switching session tabs inside a worktree no longer refetches the Branch, Staged, and Unstaged scopes, the Session scope now swaps to the active session's changes on tab switch, and the Local tab gains the Session scope so sessions running in the workspace can be reviewed on their own. The Session scope shows a notice when snapshots are disabled instead of a blank list, worktrees without an open session now still show their branch diff, and the Apply dialog lists the worktree's changes again.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Make Cmd/Ctrl+/ toggle the Agent Manager terminal even when the webview keybinding forwarding drops the key while the prompt input is focused, and stop it from triggering the Agent Manager terminal while the Kilo sidebar is focused.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Match the Agent Manager terminal shortcut fallback to the platform modifier (Cmd on macOS, Ctrl elsewhere) and consume the extension echo once per keypress so unrelated invocations are no longer swallowed.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Show aggregate added and removed line counts for multi-file patch tool calls.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Restore Agent Manager sections and worktree drag-and-drop when multiple projects are shown, with ordering and section moves scoped to the owning project.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Keep the Agent Manager terminal cursor visible on the bottom row.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Run Agent Manager project scripts in the terminal selected by the existing toolbar dropdown. Agent Manager panel uses the named side terminal, while VS Code terminal retains the integrated task flow.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Remove the duplicate border along the Kilo Code sidebar edge.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
"@kilocode/kilo-telemetry": patch
|
||||
---
|
||||
|
||||
Reduce CLI startup time by deferring Kilo-specific module loading until commands actually run, caching the telemetry profile lookup across invocations, and uploading telemetry in the background so process exit is not delayed by a network round trip
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Prevent configured compaction thresholds from interrupting active tool sequences.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Fix settings snapping back to their previous value after being cleared to "Not set" when multiple config files exist (e.g. both `kilo.json` and `kilo.jsonc`)
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Allow users to enable web search for models from all providers through Kilo configuration, VS Code settings, and Kilo Console settings.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": minor
|
||||
---
|
||||
|
||||
Show why a tool call was auto-approved or denied in the TUI, and record the denial reason on the tool call metadata (visible in `kilo export`) alongside the existing auto-approval reason.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Keep config-defined subagents routable when an installed primary agent uses the same name.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Speed up local session recall searches across large conversation histories.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Start voice input faster on macOS with native AVFoundation capture.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Restore keyboard focus to the prompt or pending question when switching Agent Manager worktrees and sessions.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Keep RC installations up to date when a newer stable CLI release is published.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-memory": patch
|
||||
---
|
||||
|
||||
Accept memory digest responses that include additional model-generated fields.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Report Bash commands terminated by a signal with the conventional 128 + signum exit code (e.g. 139 for SIGSEGV) instead of hanging until the command timeout.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Persist MCP server enable and disable changes from VS Code settings across window reloads.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Add bounded, context-aware signal-to-noise controls to grep searches.
|
||||
@@ -1,13 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Non-interactive `kilo run` no longer reports success for runs that did not complete. A plain
|
||||
headless run (neither `--auto` nor `--dangerously-skip-permissions`) in which the CLI
|
||||
auto-rejected at least one permission ask now exits 1 with a stderr diagnostic naming the cause,
|
||||
and a run whose session errors mid-stream now prints that diagnostic to stderr under
|
||||
`--format json` as well (previously the JSON branch swallowed it). Runs that complete their turn
|
||||
with no auto-rejected permission still exit 0. Under `--format json` the auto-reject path adds a
|
||||
new `error` event to the stream; existing event shapes are unchanged. The same exit-1 rule applies
|
||||
to a plain non-interactive `--attach` run that auto-rejects an ask (that run was equally crippled);
|
||||
interactive mode is untouched.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Show and hide Agent Manager worktree hover cards instantly.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Keep the JetBrains prompt send/stop button in sync when attachments are added or removed while a session is busy.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Fix JetBrains diff views to show compact workspace-relative file paths and keep added-file content visible in large branch diffs.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Fix JetBrains chat transcripts rendering cropped when opening existing sessions.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Navigate long conversations from a compact prompt rail that loads earlier history as you scroll.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Fix the `@` "Past chats" picker in Agent Manager showing only the current session's directory. It now lists previous sessions across the whole worktree family — the local workspace and every Agent Manager worktree — each labeled with its worktree name, matching the Agent Manager session search. Any listed session can be attached as context, including chats from other worktrees of the same repository.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Add Persian (Farsi) as a UI language, including right-to-left layout. Contributed by Babak Safabahar.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Keep the prompt controls at a consistent height when the model selector shows the prompt-training indicator.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Update model search results instantly and keep the active match visible while typing.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Allow Agent Manager sessions to move their worktree between sections or ungroup it through the `agent_manager` tool.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Keep Kilo's persona out of generated conversation titles and Agent Manager branch names.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Bind voice input to Cmd/Ctrl+K in Kilo prompt and review comment fields, with hold-to-talk and release-to-send support.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Preserve prompt or Agent Manager terminal focus independently for each session when switching sessions.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Start a fresh shell in the same Agent Manager terminal tab when the user types after the terminal ends.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Open an embedded terminal automatically when switching to a worktree without one.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Show the provider's actual error message when an OpenAI or Azure Responses API stream fails (for example an upstream rate limit) instead of a generic retry notice.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Include the underlying reason in search execution failures instead of showing a bare "ripgrep execution failed" message.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Make Agent Manager panel terminals behave like session tabs: right-click Close and Close Others, arrow-key tab navigation, overflow scrolling with edge fades, and stable tab widths while closing. The new-terminal button now sits directly next to the last terminal tab instead of the far edge of the panel.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/cli": minor
|
||||
---
|
||||
|
||||
Support executing shell commands embedded in skill files. Commands written as `` !`command` `` in a SKILL.md run and their output is inlined into the skill. Only trusted skills can run commands and `KILO_DISABLE_SKILL_SHELL` disables the behavior; when the model loads a skill, the commands are shown in a single up-front approval before running.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Stop treating `` !`cmd` `` shown as an inline code example in skill documentation as a live command, so it no longer triggers a shell permission prompt.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Run Agent Manager worktree setup scripts in the terminal selected by the toolbar dropdown. Agent Manager panel shows live setup output in a named `Setup` side tab that reveals itself while provisioning and hides again on success unless you engaged with the panel, while VS Code terminal retains the integrated task flow.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@kilocode/kilo-telemetry": patch
|
||||
---
|
||||
|
||||
Include the host operating system name, version, and architecture in telemetry events.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": minor
|
||||
---
|
||||
|
||||
A second identical large paste expands its collapsed prompt placeholder.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Fix skill folder path and URL rows clipping and pushing the remove (×) button off-screen in narrow Skills settings panels. Long paths and URLs now truncate within their row, and hovering a truncated value shows the full path or URL in a tooltip.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@kilocode/cli": minor
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Configure a model and reasoning variant for each workflow from Agent Behaviour settings.
|
||||
@@ -21,6 +21,7 @@ Hard rules:
|
||||
- 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.
|
||||
- Never create, modify, or delete packages/kilo-docs/LEARNINGS.md. Automation owns that file.
|
||||
|
||||
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:
|
||||
|
||||
|
||||
+19
-13
@@ -19,6 +19,7 @@ import fs from "node:fs"
|
||||
import path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { backoffMsForAttempt, deadline, remainingMs, runKilo, sleepSync } from "./lib.mjs"
|
||||
import { readLearningsBlock } from "./learn.mjs"
|
||||
|
||||
const BATCH_SIZE = 5
|
||||
const ATTEMPTS = 3
|
||||
@@ -26,7 +27,7 @@ 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 basePrompt = fs.readFileSync(path.join(HERE, "edit-prompt.md"), "utf8") + readLearningsBlock("edit")
|
||||
const model = process.env.EDIT_MODEL
|
||||
if (!model) throw new Error("EDIT_MODEL is required")
|
||||
|
||||
@@ -58,14 +59,7 @@ function editBatch(batch, index, budgetDeadline) {
|
||||
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,
|
||||
),
|
||||
)
|
||||
fs.writeFileSync(triageFile, JSON.stringify(batch.map((d) => priority.get(d.url)).filter(Boolean), null, 2))
|
||||
|
||||
const prompt = `${basePrompt}
|
||||
|
||||
@@ -88,7 +82,21 @@ Batch specifics for this run: the PRs to handle are in the attached ${batchFile}
|
||||
// permission.bash map via KILO_CONFIG_CONTENT should replace --auto once the
|
||||
// required shell patterns are stable (see PR #12605 review thread).
|
||||
const result = runKilo({
|
||||
args: ["run", "--auto", prompt, "-m", model, "--variant", "high", "--dir", process.cwd(), "-f", batchFile, "-f", triageFile],
|
||||
args: [
|
||||
"run",
|
||||
"--auto",
|
||||
prompt,
|
||||
"-m",
|
||||
model,
|
||||
"--variant",
|
||||
"high",
|
||||
"--dir",
|
||||
process.cwd(),
|
||||
"-f",
|
||||
batchFile,
|
||||
"-f",
|
||||
triageFile,
|
||||
],
|
||||
timeoutMs: Math.min(BATCH_TIMEOUT_MS, left),
|
||||
streamStdout: true,
|
||||
label: `edit batch ${index} attempt ${attempt}`,
|
||||
@@ -120,9 +128,7 @@ Batch specifics for this run: the PRs to handle are in the attached ${batchFile}
|
||||
console.warn(`batch ${index}: backing off ${wait / 1000}s before attempt ${attempt + 1}`)
|
||||
sleepSync(wait)
|
||||
} else if (wait > 0) {
|
||||
console.warn(
|
||||
`batch ${index}: skipping backoff — remaining budget cannot fit attempt ${attempt + 1} after wait`,
|
||||
)
|
||||
console.warn(`batch ${index}: skipping backoff — remaining budget cannot fit attempt ${attempt + 1} after wait`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,937 @@
|
||||
// kilocode_change - new file
|
||||
|
||||
/**
|
||||
* Learns general rules of thumb from maintainer corrections to the docs-sync
|
||||
* bot's rolling pull request, and writes them into packages/kilo-docs/LEARNINGS.md
|
||||
* so the triage and edit passes follow them on every subsequent run.
|
||||
*
|
||||
* Two modes:
|
||||
* node learn.mjs — extraction: fetch corrections, call the model, validate
|
||||
* node learn.mjs --apply — apply: write learnings.json into LEARNINGS.md
|
||||
*
|
||||
* Env: TRIAGE_MODEL (provider/model, reused), GH_TOKEN (or GITHUB_TOKEN).
|
||||
* Budget: LEARNINGS_BUDGET_MINUTES (default 10).
|
||||
* Test hook: DOCS_SYNC_FIXTURE. When set to a fixture JSON path, skips every
|
||||
* GitHub API call and writes any marker PATCH to <fixture>.patched instead of
|
||||
* the network. The workflow never sets it — only selftests do.
|
||||
*
|
||||
* Test hook: DOCS_SYNC_BACKOFF_MS replaces wait between extraction retries, same as
|
||||
* lib.mjs:138 documents for triage.mjs and edit.mjs.
|
||||
*
|
||||
* Patch suppression: DRY_RUN=true or LEARNINGS_NO_PATCH=1 suppress the marker PATCH.
|
||||
*/
|
||||
|
||||
import { execFileSync } from "node:child_process"
|
||||
import fs from "node:fs"
|
||||
import path from "node:path"
|
||||
import { fileURLToPath, pathToFileURL } from "node:url"
|
||||
|
||||
const LEARNINGS_FILE = "packages/kilo-docs/LEARNINGS.md"
|
||||
const OUT_DIR = "docs-sync-out"
|
||||
const ATTEMPTS = 2
|
||||
const LEARNINGS_BUDGET_MINUTES = Number(process.env.LEARNINGS_BUDGET_MINUTES) || 10
|
||||
const EXTRACTION_TIMEOUT_MS = LEARNINGS_BUDGET_MINUTES * 60 * 1000
|
||||
const COMMENT_BODY_CAP = 5000
|
||||
|
||||
const HERE = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
const LINE_RE =
|
||||
/^- (?<rule>.+?) <!-- id=(?<id>[a-z0-9][a-z0-9-]{2,48}) scope=(?<scope>triage|edit|both) source=(?<source>commit:[0-9a-f]{7,40}|comment:\d+) date=(?<date>\d{4}-\d{2}-\d{2}) -->$/
|
||||
|
||||
const LEARNED_THROUGH_RE = /<!--\s*docs-sync:\s*learned-through\s+commit=(\S+)\s+comment=(\S+)\s*-->/
|
||||
|
||||
// Agent-generated strings land in the PR body next to machine-read markers.
|
||||
// Identical to clean() at upsert-pr.mjs:37.
|
||||
function clean(value) {
|
||||
return String(value ?? "")
|
||||
.replaceAll("<!--", "")
|
||||
.replaceAll("-->", "")
|
||||
}
|
||||
|
||||
function warn(msg) {
|
||||
console.warn(`::warning::${msg}`)
|
||||
}
|
||||
|
||||
function log(msg) {
|
||||
console.log(msg)
|
||||
}
|
||||
|
||||
// --- pure exports ---
|
||||
|
||||
/**
|
||||
* Parse the LEARNINGS.md file text into an entry array.
|
||||
* Drops lines inside the markers that do not match the format.
|
||||
*/
|
||||
export function parseLearnings(text) {
|
||||
const m = String(text ?? "").match(
|
||||
/<!--\s*docs-sync:learnings:start\s*-->([\s\S]*?)<!--\s*docs-sync:learnings:end\s*-->/,
|
||||
)
|
||||
if (!m) return []
|
||||
const entries = []
|
||||
for (const line of m[1].split("\n")) {
|
||||
const trimmed = line.trim()
|
||||
if (!trimmed) continue
|
||||
const parsed = trimmed.match(LINE_RE)
|
||||
if (!parsed) {
|
||||
warn(`LEARNINGS.md: dropping unparseable line: ${trimmed.slice(0, 80)}`)
|
||||
continue
|
||||
}
|
||||
entries.push({
|
||||
id: parsed.groups.id,
|
||||
rule: clean(parsed.groups.rule).replaceAll("\n", " "),
|
||||
scope: parsed.groups.scope,
|
||||
source: parsed.groups.source,
|
||||
date: parsed.groups.date,
|
||||
})
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
/** Render the full LEARNINGS.md file text from an entry array. Deterministic order. */
|
||||
export function renderLearnings(entries) {
|
||||
const list = [...entries].sort((a, b) => {
|
||||
if (a.date !== b.date) return a.date < b.date ? -1 : 1
|
||||
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0
|
||||
})
|
||||
const lines = list.map(
|
||||
(e) =>
|
||||
`- ${clean(e.rule).replaceAll("\n", " ")} <!-- id=${e.id} scope=${e.scope} source=${e.source} date=${e.date} -->`,
|
||||
)
|
||||
return [
|
||||
"# docs-sync learnings",
|
||||
"",
|
||||
"Rules the docs-sync bot learned from maintainer corrections to its rolling pull request.",
|
||||
"The bot reads this file at the start of every run and follows every rule below.",
|
||||
"",
|
||||
"To unlearn a rule, delete its line and commit. The next run reads this file from the",
|
||||
"branch, so the rule is gone from its input, and the deletion itself is a correction the",
|
||||
"extraction step is instructed not to undo.",
|
||||
"",
|
||||
"<!-- docs-sync:learnings:start -->",
|
||||
...lines,
|
||||
"<!-- docs-sync:learnings:end -->",
|
||||
"",
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
/** Parse the learned-through watermark from a PR body. Returns { commit, comment } with nulls for absent/none. */
|
||||
export function parseLearnedThrough(body) {
|
||||
const m = String(body ?? "").match(LEARNED_THROUGH_RE)
|
||||
if (!m) return { commit: null, comment: null }
|
||||
const commit = m[1] === "none" ? null : m[1]
|
||||
const comment = m[2] === "none" ? null : m[2]
|
||||
return { commit, comment }
|
||||
}
|
||||
|
||||
/** Render a single learned-through marker line. */
|
||||
export function renderLearnedThrough({ commit, comment }) {
|
||||
const c = commit ?? "none"
|
||||
const m = comment ?? "none"
|
||||
return `<!-- docs-sync: learned-through commit=${c} comment=${m} -->`
|
||||
}
|
||||
|
||||
/** Replace or append the learned-through marker in a PR body. Pure — no API call. */
|
||||
export function patchMarkerIntoBody(body, marker) {
|
||||
const b = String(body ?? "")
|
||||
if (LEARNED_THROUGH_RE.test(b)) {
|
||||
return b.replace(LEARNED_THROUGH_RE, marker)
|
||||
}
|
||||
return b + "\n" + marker + "\n"
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract { add, remove } from raw model stdout.
|
||||
* Mirrors parseTriageEntries at extract-json.mjs:14-38, adapted for an object.
|
||||
* `kilo run` prints the assistant message twice; the last copy wins.
|
||||
* Walk "{" positions from right to left; return the first that parses to an object
|
||||
* holding an array `add` or an array `remove`.
|
||||
*/
|
||||
export function parseDelta(raw) {
|
||||
const r = String(raw ?? "")
|
||||
const end = r.lastIndexOf("}")
|
||||
if (end < 0) return null
|
||||
|
||||
const starts = []
|
||||
for (let i = 0; i <= end; i++) {
|
||||
if (r[i] === "{") starts.push(i)
|
||||
}
|
||||
|
||||
for (let s = starts.length - 1; s >= 0; s--) {
|
||||
let parsed
|
||||
try {
|
||||
parsed = JSON.parse(r.slice(starts[s], end + 1))
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) continue
|
||||
if (Array.isArray(parsed.add) || Array.isArray(parsed.remove)) {
|
||||
return {
|
||||
add: Array.isArray(parsed.add) ? parsed.add : [],
|
||||
remove: Array.isArray(parsed.remove) ? parsed.remove : [],
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/** Cap a review comment body so a single long comment cannot dominate extraction input. */
|
||||
function capBody(body) {
|
||||
const b = String(body ?? "")
|
||||
if (b.length <= COMMENT_BODY_CAP) return b
|
||||
return b.slice(0, COMMENT_BODY_CAP) + " [truncated]"
|
||||
}
|
||||
|
||||
/** Normalize rule text for duplicate comparison: lowercase, strip punctuation and whitespace runs. */
|
||||
function norm(text) {
|
||||
return String(text ?? "")
|
||||
.toLowerCase()
|
||||
.replace(/[^\w\s]/g, "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a delta against the existing entries and constraints.
|
||||
* Returns { add, remove, rejected }. Never throws.
|
||||
*/
|
||||
export function validateDelta(delta, { existing, candidateSources, deletedInWindow }) {
|
||||
const add = Array.isArray(delta.add) ? delta.add : []
|
||||
const remove = Array.isArray(delta.remove) ? delta.remove : []
|
||||
const ex = Array.isArray(existing) ? existing : []
|
||||
const candidates = Array.isArray(candidateSources) ? candidateSources : []
|
||||
const deleted = Array.isArray(deletedInWindow) ? deletedInWindow : []
|
||||
|
||||
const rejected = []
|
||||
const valid = []
|
||||
const toRemove = []
|
||||
const existingIds = new Set(ex.map((e) => e.id))
|
||||
// One model response can repeat an id or a rule. Both would render two lines for
|
||||
// one id, so an accepted addition also blocks the next one.
|
||||
const acceptedIds = new Set()
|
||||
const acceptedRules = new Set()
|
||||
|
||||
// Process remove first so toRemove is populated before the add loop checks
|
||||
// for id collisions with entries listed in remove (criterion 8).
|
||||
for (const id of remove) {
|
||||
if (!existingIds.has(id)) {
|
||||
rejected.push({ entry: { id, remove: id }, reason: `remove target ${id} not in existing entries` })
|
||||
} else {
|
||||
toRemove.push(id)
|
||||
}
|
||||
}
|
||||
|
||||
for (const a of add) {
|
||||
let reason = null
|
||||
|
||||
// Reject null, undefined, and non-object entries before any property access.
|
||||
if (a === null || a === undefined || typeof a !== "object" || Array.isArray(a)) {
|
||||
rejected.push({ entry: a, reason: "add entry is null, undefined, or not a plain object" })
|
||||
continue
|
||||
}
|
||||
|
||||
if (!a.rule || String(a.rule).length < 10 || String(a.rule).length > 300) {
|
||||
reason = "rule text absent, shorter than 10 characters, or longer than 300"
|
||||
} else if (!["triage", "edit", "both"].includes(a.scope)) {
|
||||
reason = `invalid scope: ${a.scope}`
|
||||
} else if (!/^commit:[0-9a-f]{7,40}$/.test(a.source) && !/^comment:\d+$/.test(a.source)) {
|
||||
reason = `invalid source format: ${a.source}`
|
||||
} else if (!candidates.includes(a.source)) {
|
||||
reason = `source ${a.source} not in candidate sources`
|
||||
} else if (!/^[a-z0-9][a-z0-9-]{2,48}$/.test(a.id)) {
|
||||
reason = `invalid id format: ${a.id}`
|
||||
} else if (existingIds.has(a.id) && !toRemove.includes(a.id)) {
|
||||
reason = `id ${a.id} collides with an existing entry not listed in remove`
|
||||
} else if (acceptedIds.has(a.id)) {
|
||||
reason = `id ${a.id} collides with an earlier addition in this delta`
|
||||
} else if (!/^\d{4}-\d{2}-\d{2}$/.test(a.date)) {
|
||||
reason = `invalid date format: ${a.date}`
|
||||
} else {
|
||||
// Check that date is a real calendar date.
|
||||
const d = new Date(a.date + "T00:00:00Z")
|
||||
if (Number.isNaN(d.getTime()) || d.toISOString().slice(0, 10) !== a.date) {
|
||||
reason = `invalid calendar date: ${a.date}`
|
||||
}
|
||||
}
|
||||
|
||||
if (reason) {
|
||||
rejected.push({ entry: a, reason })
|
||||
continue
|
||||
}
|
||||
|
||||
const n = norm(a.rule)
|
||||
|
||||
// Duplicate of an existing entry not being removed.
|
||||
if (ex.some((e) => norm(e.rule) === n && !remove.includes(e.id))) {
|
||||
reason = `rule text is a duplicate of an existing entry not listed in remove`
|
||||
rejected.push({ entry: a, reason })
|
||||
continue
|
||||
}
|
||||
|
||||
// Duplicate of an earlier addition in the same response.
|
||||
if (acceptedRules.has(n)) {
|
||||
reason = `rule text is a duplicate of an earlier addition in this delta`
|
||||
rejected.push({ entry: a, reason })
|
||||
continue
|
||||
}
|
||||
|
||||
// Names a PR, URL, person, or docs page. The URL clause keeps docs-check-links.yml green.
|
||||
if (String(a.rule).match(/#\d{2,}|https?:\/\/|@[A-Za-z0-9-]|packages\/kilo-docs|\.md\b/)) {
|
||||
reason = "rule names a PR, URL, person, or docs page"
|
||||
rejected.push({ entry: a, reason })
|
||||
continue
|
||||
}
|
||||
|
||||
// Duplicate of a rule deleted in this window.
|
||||
if (deleted.some((d) => norm(d) === n)) {
|
||||
reason = "rule text matches a line a maintainer deleted in this window"
|
||||
rejected.push({ entry: a, reason })
|
||||
continue
|
||||
}
|
||||
|
||||
acceptedIds.add(a.id)
|
||||
acceptedRules.add(n)
|
||||
valid.push({
|
||||
id: a.id,
|
||||
rule: clean(String(a.rule)).replaceAll("\n", " "),
|
||||
scope: a.scope,
|
||||
source: a.source,
|
||||
date: a.date,
|
||||
})
|
||||
}
|
||||
|
||||
return { add: valid, remove: toRemove, rejected }
|
||||
}
|
||||
|
||||
/** Apply a validated delta to an existing entry array. Drops removed ids, appends adds. */
|
||||
export function applyDelta(existing, delta) {
|
||||
const ex = Array.isArray(existing) ? existing : []
|
||||
const remove = new Set(Array.isArray(delta.remove) ? delta.remove : [])
|
||||
const add = Array.isArray(delta.add) ? delta.add : []
|
||||
return [...ex.filter((e) => !remove.has(e.id)), ...add]
|
||||
}
|
||||
|
||||
/** Trust a review comment whose author_association is OWNER, MEMBER, or COLLABORATOR and is not a bot. */
|
||||
export function isTrustedComment(comment) {
|
||||
if (!comment) return false
|
||||
const login = String(comment.user?.login ?? "")
|
||||
if (login.endsWith("[bot]")) return false
|
||||
return ["OWNER", "MEMBER", "COLLABORATOR"].includes(comment.author_association)
|
||||
}
|
||||
|
||||
/** Render the prompt block for a given scope. Returns "" when no entry matches. */
|
||||
export function promptBlock(entries, scope) {
|
||||
const matches = (Array.isArray(entries) ? entries : []).filter((e) => e.scope === scope || e.scope === "both")
|
||||
if (matches.length === 0) return ""
|
||||
return [
|
||||
"## Learnings from maintainer corrections",
|
||||
"",
|
||||
"Follow every rule below. Each was extracted from a correction a maintainer made to an",
|
||||
"earlier run of this bot. A rule here outranks a general instruction above when they conflict.",
|
||||
"",
|
||||
...matches.map((e) => `- ${e.rule}`),
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
/** Read a prompt block artifact from docs-sync-out. Returns the content or "" when absent. */
|
||||
export function readLearningsBlock(scope) {
|
||||
const file = `${OUT_DIR}/learnings-${scope}.md`
|
||||
try {
|
||||
return fs.readFileSync(file, "utf8")
|
||||
} catch {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// --- helpers for main ---
|
||||
|
||||
function git(args) {
|
||||
return execFileSync("git", args, { stdio: ["ignore", "pipe", "inherit"] })
|
||||
.toString()
|
||||
.trim()
|
||||
}
|
||||
|
||||
// --- main ---
|
||||
|
||||
async function main() {
|
||||
// Step 0: ensure docs-sync-out exists. collect.mjs:139 is the only other unconditional
|
||||
// mkdirSync of this directory, and it runs after the learn step. Without this line the
|
||||
// empty-candidate path throws ENOENT on its first write, continue-on-error swallows it,
|
||||
// and the feature silently never works.
|
||||
fs.mkdirSync(OUT_DIR, { recursive: true })
|
||||
|
||||
if (process.argv.includes("--apply")) {
|
||||
await apply()
|
||||
return
|
||||
}
|
||||
|
||||
await extract()
|
||||
}
|
||||
|
||||
// --- apply mode ---
|
||||
|
||||
async function apply() {
|
||||
const learningsPath = `${OUT_DIR}/learnings.json`
|
||||
if (!fs.existsSync(learningsPath)) {
|
||||
log("learnings.json absent — extraction was skipped or failed; nothing to apply")
|
||||
return
|
||||
}
|
||||
const entries = JSON.parse(fs.readFileSync(learningsPath, "utf8"))
|
||||
const file = renderLearnings(entries)
|
||||
fs.writeFileSync(LEARNINGS_FILE, file)
|
||||
log(`wrote ${LEARNINGS_FILE} with ${entries.length} entries`)
|
||||
}
|
||||
|
||||
// --- extraction mode ---
|
||||
|
||||
async function extract() {
|
||||
// Step 0: seed the prompt artifacts from the checked-out file before any fallible
|
||||
// work. Every later step can throw, the workflow step is continue-on-error, and
|
||||
// triage and edit read only these two files. Without the seed one failed API call
|
||||
// silently drops every learned rule for the whole run. Later steps replace them
|
||||
// with the rolling-branch copy and then with the validated delta.
|
||||
writePromptArtifacts(parseLearnings(readFileOrEmpty(LEARNINGS_FILE)))
|
||||
|
||||
// Load fixture when DOCS_SYNC_FIXTURE is set.
|
||||
const fixturePath = process.env.DOCS_SYNC_FIXTURE
|
||||
let fixture = null
|
||||
let patchFile = null
|
||||
if (fixturePath) {
|
||||
fixture = JSON.parse(fs.readFileSync(fixturePath, "utf8"))
|
||||
patchFile = fixturePath + ".patched"
|
||||
}
|
||||
|
||||
const { api, repo, searchIssues, appendOutput, appendSummary, backoffMsForAttempt, runKilo, sleepSync } =
|
||||
await import("./lib.mjs")
|
||||
|
||||
let prData
|
||||
let prBody = ""
|
||||
let prNumber = ""
|
||||
let branch = ""
|
||||
|
||||
if (fixture) {
|
||||
// Fixture mode: skip all API calls.
|
||||
prData = fixture.pr
|
||||
prBody = prData.body ?? ""
|
||||
prNumber = String(prData.number ?? 1)
|
||||
branch = prData.head?.ref ?? "docs/auto-sync"
|
||||
} else {
|
||||
// Step 1: resolve the rolling PR. Use prepare-branch.mjs's selection rule so both
|
||||
// target the same branch. searchIssues takes prs[0] with no author filter (like
|
||||
// prepare-branch.mjs:69). But trust the body marker only when authored by
|
||||
// github-actions[bot] (like watermark.mjs:35). The two rules differ on purpose:
|
||||
// the branch must match what prepare-branch.mjs will check out, but a body is
|
||||
// editable so its marker needs the author filter.
|
||||
const r = repo()
|
||||
const prs = await searchIssues(`repo:${r} is:pr is:open label:auto-docs sort:created-desc`, { maxPages: 1 })
|
||||
if (prs.length === 0) {
|
||||
log("no open rolling pull request — nothing to learn from")
|
||||
|
||||
// Read existing learnings from main for empty-state artifacts.
|
||||
let existing = []
|
||||
try {
|
||||
const existingText = git(["show", `origin/main:${LEARNINGS_FILE}`])
|
||||
existing = parseLearnings(existingText)
|
||||
} catch {
|
||||
existing = []
|
||||
}
|
||||
log(`no-PR existing entries from main: ${existing.length}`)
|
||||
writeEmptyStateArtifacts(existing)
|
||||
appendOutput("count", String(existing.length))
|
||||
appendSummary("### docs-sync learnings\n\nNo open auto-docs pull request; extraction skipped.")
|
||||
return
|
||||
}
|
||||
prData = await api(`/repos/${r}/pulls/${prs[0].number}`)
|
||||
prBody = prData.body ?? ""
|
||||
prNumber = String(prData.number)
|
||||
branch = prData.head?.ref ?? "docs/auto-sync"
|
||||
}
|
||||
|
||||
// Step 2: read existing entries.
|
||||
let existing = []
|
||||
let existingText = ""
|
||||
if (fixture) {
|
||||
existingText = readFileOrEmpty(LEARNINGS_FILE)
|
||||
existing = parseLearnings(existingText)
|
||||
} else {
|
||||
try {
|
||||
existingText = git(["show", `origin/${branch}:${LEARNINGS_FILE}`])
|
||||
} catch {
|
||||
// branch copy absent — fall back to main, then empty.
|
||||
// Required for the first live run: the rolling branch predates the seeded file.
|
||||
try {
|
||||
existingText = git(["show", `origin/main:${LEARNINGS_FILE}`])
|
||||
} catch {
|
||||
existingText = ""
|
||||
}
|
||||
}
|
||||
existing = parseLearnings(existingText)
|
||||
}
|
||||
log(`existing entries: ${existing.length}`)
|
||||
|
||||
// Replace the seed with the rolling-branch copy. Every step below can throw, and
|
||||
// these two files are all triage and edit read.
|
||||
writePromptArtifacts(existing)
|
||||
|
||||
// Step 3: parse marker. Trust only when authored by github-actions[bot] (like watermark.mjs:35).
|
||||
let commitWm = null
|
||||
let commentWm = null
|
||||
const trusted = prData.user?.login === "github-actions[bot]"
|
||||
if (trusted) {
|
||||
;({ commit: commitWm, comment: commentWm } = parseLearnedThrough(prBody))
|
||||
} else {
|
||||
log("PR author is not github-actions[bot]; ignoring body marker")
|
||||
}
|
||||
log(`watermark: commit=${commitWm ?? "none"} comment=${commentWm ?? "none"}`)
|
||||
|
||||
// Step 4: fetch and tip SHA.
|
||||
let tipSha
|
||||
if (fixture) {
|
||||
tipSha = git(["rev-parse", "HEAD"])
|
||||
} else {
|
||||
git(["fetch", "origin", "main", branch])
|
||||
tipSha = git(["rev-parse", `origin/${branch}`])
|
||||
}
|
||||
|
||||
// Step 5: candidate commits.
|
||||
let rangeArgs = [`origin/main..origin/${branch}`]
|
||||
if (fixture) {
|
||||
// In fixture mode, work from the local repo state.
|
||||
try {
|
||||
git(["rev-parse", "--verify", branch])
|
||||
rangeArgs = [`origin/main..${branch}`]
|
||||
} catch {
|
||||
rangeArgs = [`origin/main..HEAD`]
|
||||
}
|
||||
}
|
||||
|
||||
if (commitWm) {
|
||||
let wmExists = false
|
||||
try {
|
||||
git(["cat-file", "-e", `${commitWm}^{commit}`])
|
||||
wmExists = true
|
||||
} catch {
|
||||
wmExists = false
|
||||
}
|
||||
if (wmExists) {
|
||||
rangeArgs.push(`^${commitWm}`)
|
||||
}
|
||||
// A missing watermark commit (force-push, rebase) drops the exclusion.
|
||||
// The duplicate-rule-text rejection in validateDelta blocks the re-added duplicate.
|
||||
}
|
||||
|
||||
const logOut = git(["log", "--no-merges", "--format=%H|%ae|%cI|%s", ...rangeArgs])
|
||||
const rawCommits = logOut ? logOut.split("\n").filter(Boolean) : []
|
||||
|
||||
const botEmail = "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
const candidates = []
|
||||
const candidateSources = []
|
||||
const deletedInWindow = []
|
||||
|
||||
for (const line of rawCommits) {
|
||||
const [sha, email, dateIso] = line.split("|")
|
||||
// Drop commits authored by the sync job itself (criterion 5).
|
||||
if (email === botEmail) continue
|
||||
// Everything reachable from main is already excluded by the range (criterion 6).
|
||||
|
||||
// Get the full file list.
|
||||
let files = []
|
||||
try {
|
||||
const out = git(["show", "--name-only", "--format=", sha])
|
||||
files = out
|
||||
? out
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.filter((f) => f)
|
||||
: []
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get the docs-scoped diff and message.
|
||||
let message = ""
|
||||
let docDiff = ""
|
||||
try {
|
||||
message = git(["show", "--format=%B", "--no-patch", sha]).trim()
|
||||
docDiff = git(["show", "--format=", sha, "--", "packages/kilo-docs"])
|
||||
// Cap diff sizes.
|
||||
if (docDiff.length > 20000) docDiff = docDiff.slice(0, 20000) + "\n[truncated]"
|
||||
} catch {
|
||||
// skip on error
|
||||
}
|
||||
|
||||
// Drop commits whose docs-scoped diff is empty.
|
||||
if (!docDiff.trim()) continue
|
||||
|
||||
// Collect deleted rule lines from LEARNINGS.md.
|
||||
for (const dl of docDiff.split("\n")) {
|
||||
if (!dl.startsWith("-")) continue
|
||||
const stripped = dl.slice(1).trim()
|
||||
const parsed = stripped.match(LINE_RE)
|
||||
if (parsed) {
|
||||
deletedInWindow.push(clean(parsed.groups.rule).replaceAll("\n", " "))
|
||||
}
|
||||
}
|
||||
|
||||
// Cap total diff data.
|
||||
const totalDiff = candidates.reduce((n, c) => n + (c.diff ? c.diff.length : 0), 0)
|
||||
if (totalDiff > 120000) {
|
||||
log(`diff cap reached at commit ${sha.slice(0, 7)}; truncating`)
|
||||
candidates.push({
|
||||
source: `commit:${sha.slice(0, 7)}`,
|
||||
iso: dateIso,
|
||||
date: dateIso.slice(0, 10),
|
||||
message,
|
||||
files,
|
||||
diff: "[truncated]",
|
||||
})
|
||||
candidateSources.push(`commit:${sha.slice(0, 7)}`)
|
||||
break
|
||||
}
|
||||
|
||||
candidates.push({
|
||||
source: `commit:${sha.slice(0, 7)}`,
|
||||
iso: dateIso,
|
||||
date: dateIso.slice(0, 10),
|
||||
message,
|
||||
files,
|
||||
diff: docDiff,
|
||||
})
|
||||
candidateSources.push(`commit:${sha.slice(0, 7)}`)
|
||||
}
|
||||
|
||||
// Step 6: candidate comments.
|
||||
let allComments = []
|
||||
let maxCommentAt = "none"
|
||||
|
||||
if (fixture && fixture.comments) {
|
||||
allComments = fixture.comments
|
||||
} else if (prNumber) {
|
||||
const pages = []
|
||||
for (let page = 1; page <= 5; page++) {
|
||||
const batch = await api(`/repos/${repo()}/pulls/${prNumber}/comments?per_page=100&page=${page}`)
|
||||
pages.push(...batch)
|
||||
if (batch.length < 100) break
|
||||
}
|
||||
allComments = pages
|
||||
}
|
||||
|
||||
if (allComments.length > 0) {
|
||||
let max = ""
|
||||
for (const c of allComments) {
|
||||
if (c.created_at && c.created_at > max) max = c.created_at
|
||||
}
|
||||
maxCommentAt = max || "none"
|
||||
}
|
||||
|
||||
// Filter trusted comments.
|
||||
const trustedComments = allComments.filter((c) => {
|
||||
if (!isTrustedComment(c)) return false
|
||||
if (commentWm && c.created_at <= commentWm) return false
|
||||
return true
|
||||
})
|
||||
|
||||
// Step 7: correlate comments to commits.
|
||||
// A comment is a commit's trigger when c.path is in that commit's full file list
|
||||
// and c.created_at < commit date. The earliest such commit claims it.
|
||||
// Compare parsed timestamps so different timezone offsets do not skew the ordering.
|
||||
for (const c of trustedComments) {
|
||||
let best = null
|
||||
const cTime = Date.parse(c.created_at)
|
||||
for (const cc of candidates) {
|
||||
if (!Array.isArray(cc.files) || !cc.files.includes(c.path)) continue
|
||||
const ccTime = Date.parse(cc.iso)
|
||||
if (cTime < ccTime) {
|
||||
if (!best || ccTime < Date.parse(best.iso)) {
|
||||
best = cc
|
||||
}
|
||||
}
|
||||
}
|
||||
if (best) {
|
||||
best.comment = {
|
||||
author_association: c.author_association,
|
||||
path: c.path,
|
||||
body: capBody(c.body),
|
||||
}
|
||||
} else {
|
||||
candidates.push({
|
||||
source: `comment:${c.id}`,
|
||||
date: (c.created_at ?? "").slice(0, 10),
|
||||
path: c.path,
|
||||
body: capBody(c.body),
|
||||
author_association: c.author_association,
|
||||
})
|
||||
candidateSources.push(`comment:${c.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Step 8: no candidates → empty delta route.
|
||||
const hasCandidates = candidates.length > 0
|
||||
|
||||
if (!hasCandidates) {
|
||||
log("no candidate corrections; advancing marker with no model call")
|
||||
writeEmptyStateArtifacts(existing)
|
||||
appendOutput("count", String(existing.length))
|
||||
appendSummary(
|
||||
`### docs-sync learnings\n\nNo new candidate corrections. Entries: ${existing.length}. Marker route: empty (no candidates).`,
|
||||
)
|
||||
|
||||
const marker = renderLearnedThrough({ commit: tipSha, comment: maxCommentAt })
|
||||
await patchOrLogMarker({ prBody, prNumber, marker, fixture, patchFile })
|
||||
return
|
||||
}
|
||||
|
||||
// Step 9: write learnings input.
|
||||
const input = {
|
||||
existing: existing.map((e) => ({ id: e.id, rule: e.rule, scope: e.scope, source: e.source, date: e.date })),
|
||||
deleted_in_window: deletedInWindow,
|
||||
corrections: candidates,
|
||||
}
|
||||
const inputFile = `${OUT_DIR}/learnings-input.json`
|
||||
fs.writeFileSync(inputFile, JSON.stringify(input, null, 2))
|
||||
log(`wrote ${inputFile} with ${candidates.length} candidates`)
|
||||
|
||||
// Step 10: call the model.
|
||||
// Deliberately no --auto. Every input is in the attached file and the output goes to
|
||||
// stdout, so the agent needs no tool. Omitting --auto makes "the extraction step never
|
||||
// writes outside LEARNINGS.md" structurally true instead of prompt-deep. triage.mjs:76
|
||||
// and edit.mjs:86 carry the opposite comment; do not copy them without updating the reason.
|
||||
const prompt = fs.readFileSync(path.join(HERE, "learnings-prompt.md"), "utf8")
|
||||
const model = process.env.TRIAGE_MODEL
|
||||
if (!model) throw new Error("TRIAGE_MODEL is required")
|
||||
|
||||
const budgetDeadline = Date.now() + EXTRACTION_TIMEOUT_MS
|
||||
|
||||
let raw = null
|
||||
let lastCause = "extraction failed"
|
||||
|
||||
for (let attempt = 1; attempt <= ATTEMPTS; attempt++) {
|
||||
const left = Math.max(0, budgetDeadline - Date.now())
|
||||
if (left <= 0) {
|
||||
log("budget exhausted before extraction attempt")
|
||||
break
|
||||
}
|
||||
|
||||
const result = runKilo({
|
||||
args: ["run", prompt, "-m", model, "--dir", process.cwd(), "-f", inputFile],
|
||||
timeoutMs: Math.min(EXTRACTION_TIMEOUT_MS, left),
|
||||
streamStdout: false,
|
||||
label: "learnings extraction",
|
||||
})
|
||||
|
||||
if (result.stdout) {
|
||||
fs.writeFileSync(`${OUT_DIR}/learnings-raw.txt`, result.stdout)
|
||||
raw = result.stdout
|
||||
const delta = parseDelta(raw)
|
||||
if (delta) break
|
||||
lastCause = `parseDelta returned null (attempt ${attempt})`
|
||||
} else {
|
||||
lastCause = result.timedOut ? "timed out" : `exit ${result.exitCode}`
|
||||
}
|
||||
|
||||
if (attempt < ATTEMPTS) {
|
||||
const wait = backoffMsForAttempt(1) // 60s, same as the sibling convention
|
||||
if (wait > 0) {
|
||||
log(`backing off ${wait / 1000}s before attempt ${attempt + 1}`)
|
||||
sleepSync(wait)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 11: parse and validate.
|
||||
const delta = raw ? parseDelta(raw) : null
|
||||
|
||||
if (!delta) {
|
||||
// parseDelta null after every try — retryable unhappy.
|
||||
warn(`extraction failed: ${lastCause}. Leaving learnings untouched.`)
|
||||
writeEmptyStateArtifacts(existing)
|
||||
appendOutput("count", String(existing.length))
|
||||
appendSummary(
|
||||
`### docs-sync learnings\n\nExtraction failed: ${lastCause}. Entries unchanged: ${existing.length}. No marker advance.`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const validated = validateDelta(delta, { existing, candidateSources, deletedInWindow })
|
||||
|
||||
if (validated.rejected.length > 0) {
|
||||
for (const r of validated.rejected) {
|
||||
warn(`rejected: ${r.reason}` + (r.entry?.id ? ` (id=${r.entry.id})` : ""))
|
||||
}
|
||||
}
|
||||
|
||||
const nonEmpty = validated.add.length > 0 || validated.remove.length > 0
|
||||
|
||||
// Step 12: route by outcome (G5 table, exact).
|
||||
if (nonEmpty) {
|
||||
// Non-empty validated delta.
|
||||
const newEntries = applyDelta(existing, { add: validated.add, remove: validated.remove })
|
||||
fs.writeFileSync(`${OUT_DIR}/learnings.json`, JSON.stringify(newEntries, null, 2))
|
||||
const marker = renderLearnedThrough({ commit: tipSha, comment: maxCommentAt })
|
||||
const suppressed = process.env.DRY_RUN === "true" || process.env.LEARNINGS_NO_PATCH === "1"
|
||||
if (!suppressed) appendOutput("learned_through", marker)
|
||||
if (suppressed) log(`learned-through output suppressed: ${marker}`)
|
||||
|
||||
const added = validated.add.length
|
||||
const removed = validated.remove.length
|
||||
const rejected = validated.rejected.length
|
||||
log(`delta: +${added} -${removed} (${rejected} rejected)`)
|
||||
appendSummary(
|
||||
`### docs-sync learnings\n\n- added: ${added}\n- removed: ${removed}\n- rejected: ${rejected}\n- candidates: ${candidates.length}\n- marker route: upsert (non-empty delta)\n`,
|
||||
)
|
||||
|
||||
writePromptArtifacts(newEntries)
|
||||
appendOutput("count", String(newEntries.length))
|
||||
|
||||
// Marker rides through LEARNED_THROUGH into upsert-pr.mjs. No direct PATCH.
|
||||
} else {
|
||||
// Empty validated delta (nothing added, nothing removed, including every-add-rejected).
|
||||
log("empty validated delta; advancing marker directly")
|
||||
fs.writeFileSync(`${OUT_DIR}/learnings.json`, JSON.stringify(existing, null, 2))
|
||||
writePromptArtifacts(existing)
|
||||
appendOutput("count", String(existing.length))
|
||||
|
||||
const marker = renderLearnedThrough({ commit: tipSha, comment: maxCommentAt })
|
||||
await patchOrLogMarker({ prBody, prNumber, marker, fixture, patchFile })
|
||||
|
||||
const rejected = validated.rejected.length
|
||||
appendSummary(
|
||||
`### docs-sync learnings\n\n- added: 0\n- removed: 0\n- rejected: ${rejected}\n- candidates: ${candidates.length}\n- marker route: direct PATCH (empty delta)\n`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// --- shared helpers ---
|
||||
|
||||
function writeEmptyStateArtifacts(entries) {
|
||||
fs.writeFileSync(`${OUT_DIR}/learnings.json`, JSON.stringify(entries, null, 2))
|
||||
writePromptArtifacts(entries)
|
||||
}
|
||||
|
||||
// A later call must be able to shrink a seeded block back to nothing, so an empty
|
||||
// block removes the file instead of leaving the earlier content in place.
|
||||
function writePromptArtifacts(entries) {
|
||||
writeOrRemove(`${OUT_DIR}/learnings-triage.md`, promptBlock(entries, "triage"))
|
||||
writeOrRemove(`${OUT_DIR}/learnings-edit.md`, promptBlock(entries, "edit"))
|
||||
}
|
||||
|
||||
function writeOrRemove(file, text) {
|
||||
if (text) fs.writeFileSync(file, text)
|
||||
else fs.rmSync(file, { force: true })
|
||||
}
|
||||
|
||||
function readFileOrEmpty(file) {
|
||||
try {
|
||||
return fs.readFileSync(file, "utf8")
|
||||
} catch {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
async function patchOrLogMarker({ prBody, prNumber, marker, fixture, patchFile }) {
|
||||
const suppressed = process.env.DRY_RUN === "true" || process.env.LEARNINGS_NO_PATCH === "1"
|
||||
|
||||
if (suppressed) {
|
||||
log(
|
||||
`marker PATCH suppressed (DRY_RUN=${process.env.DRY_RUN}, LEARNINGS_NO_PATCH=${process.env.LEARNINGS_NO_PATCH})`,
|
||||
)
|
||||
log(`would have written marker: ${marker}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (fixture) {
|
||||
// Write to the fixture patch file instead of the network.
|
||||
fs.writeFileSync(patchFile, marker)
|
||||
log(`wrote marker to ${patchFile}`)
|
||||
return
|
||||
}
|
||||
|
||||
// Live PATCH: body-only, one line changed. The job already holds pull-requests: write.
|
||||
// Re-read the body first. The body in hand was fetched before the extraction call, so
|
||||
// patching that copy would drop any edit made in the minutes since. GitHub has no
|
||||
// conditional update for a pull request body, so a short fetch-to-PATCH race remains.
|
||||
const { api, repo } = await import("./lib.mjs")
|
||||
let latestBody = prBody
|
||||
try {
|
||||
const fresh = await api(`/repos/${repo()}/pulls/${prNumber}`)
|
||||
latestBody = fresh.body ?? ""
|
||||
} catch (err) {
|
||||
warn(`could not re-read PR #${prNumber} before the marker PATCH: ${err.message}. Using the earlier body.`)
|
||||
}
|
||||
const newBody = patchMarkerIntoBody(latestBody, marker)
|
||||
await api(`/repos/${repo()}/pulls/${prNumber}`, {
|
||||
method: "PATCH",
|
||||
body: { body: newBody },
|
||||
})
|
||||
log(`PATCHed learned-through marker on PR #${prNumber}`)
|
||||
}
|
||||
|
||||
// --- entry point ---
|
||||
|
||||
const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href
|
||||
|
||||
// --- self-test harness (run: node .github/docs-sync/learn.mjs --self-test) ---
|
||||
if (isMain && process.argv.includes("--self-test")) {
|
||||
const failures = []
|
||||
const check = (label, fn) => {
|
||||
try {
|
||||
const ok = fn()
|
||||
if (!ok) failures.push(label)
|
||||
} catch (e) {
|
||||
failures.push(label + " THREW: " + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
check("null in add does not throw", () => {
|
||||
const r = validateDelta({ add: [null], remove: [] }, { existing: [], candidateSources: [], deletedInWindow: [] })
|
||||
return r.add.length === 0 && r.rejected.length === 1 && r.rejected[0].reason.includes("not a plain object")
|
||||
})
|
||||
|
||||
check("undefined in add does not throw", () => {
|
||||
const r = validateDelta(
|
||||
{ add: [undefined], remove: [] },
|
||||
{ existing: [], candidateSources: [], deletedInWindow: [] },
|
||||
)
|
||||
return r.add.length === 0 && r.rejected.length === 1 && r.rejected[0].reason.includes("not a plain object")
|
||||
})
|
||||
|
||||
check("mixed valid and null retains valid", () => {
|
||||
const r = validateDelta(
|
||||
{
|
||||
add: [
|
||||
{
|
||||
id: "valid-a",
|
||||
rule: "Do not document experimental features",
|
||||
scope: "both",
|
||||
source: "commit:bbbbbbb",
|
||||
date: "2026-08-03",
|
||||
},
|
||||
null,
|
||||
{
|
||||
id: "valid-b",
|
||||
rule: "Keep release notes concise",
|
||||
scope: "edit",
|
||||
source: "commit:bbbbbbb",
|
||||
date: "2026-08-03",
|
||||
},
|
||||
],
|
||||
remove: [],
|
||||
},
|
||||
{ existing: [], candidateSources: ["commit:bbbbbbb"], deletedInWindow: [] },
|
||||
)
|
||||
return r.add.length === 2 && r.rejected.length === 1
|
||||
})
|
||||
|
||||
if (failures.length) {
|
||||
console.error("SELF-TEST FAILURES:", failures)
|
||||
process.exit(1)
|
||||
}
|
||||
console.log("SELF-TEST PASSED (" + 3 + " checks)")
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
if (isMain) {
|
||||
main().catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
You are the extraction pass of an automated documentation pipeline for Kilo Code. Your only job: extract general rules of thumb from maintainer corrections to the docs-sync bot's rolling pull request. A correction is a commit or review comment a maintainer made to fix something the bot got wrong, and a learning is the general principle behind it that the bot should follow from now on.
|
||||
|
||||
The attached `learnings-input.json` file contains:
|
||||
|
||||
- `existing`: rules the bot already knows, each with `id`, `rule`, `scope`, `source`, and `date`.
|
||||
- `deleted_in_window`: rule texts (not ids) a maintainer deleted from the learnings file in this extraction window. A maintainer deleted these on purpose — do not re-add them.
|
||||
- `corrections`: the maintainer corrections to learn from. Each entry has a `source` (commit or comment id), `date`, and the relevant context. Commit entries have `message`, `files`, and `diff`. Comment entries have `path` and `body`. Some commits also carry an attached inline review `comment` that triggered them.
|
||||
|
||||
Before writing anything:
|
||||
|
||||
1. Read every correction in `corrections` and every rule in `existing`.
|
||||
2. For each correction, decide whether it implies a general rule of thumb the bot should follow. Not every correction does — returning no new rules is a valid and expected answer.
|
||||
3. When a correction implies a rule, write it as one imperative sentence stating the general principle, not what the specific correction did.
|
||||
|
||||
Response format: a strict JSON object with no prose, no markdown fences, no comments:
|
||||
|
||||
```json
|
||||
{
|
||||
"add": [
|
||||
{
|
||||
"id": "kebab-case-slug",
|
||||
"rule": "One imperative sentence.",
|
||||
"scope": "triage|edit|both",
|
||||
"source": "commit:<sha>|comment:<id>",
|
||||
"date": "<yyyy-mm-dd>"
|
||||
}
|
||||
],
|
||||
"remove": []
|
||||
}
|
||||
```
|
||||
|
||||
- `id`: a short kebab-case slug unique across this response.
|
||||
- `rule`: one general imperative sentence. Never name a pull request, a number, a URL, a docs page, a file path, or a person. State the rule the correction implies, not what the correction changed.
|
||||
- `scope`: `triage` when the rule changes which pull requests deserve documentation; `edit` when it changes how a page is written; `both` when it changes both.
|
||||
- `source`: copied verbatim from the correction's `source` field. Never invent one.
|
||||
- `date`: the correction's date, copied verbatim.
|
||||
|
||||
The `remove` array lists `id` values of existing entries to drop. Remove an id only when a new rule contradicts or supersedes it.
|
||||
|
||||
Hard rules:
|
||||
|
||||
- The list of `add` entries may be empty. Returning `{"add": [], "remove": []}` is a valid and expected answer when no correction implies a general rule.
|
||||
- Never re-add a rule listed in `deleted_in_window`, and never add a reworded near-duplicate of one. A maintainer deleted it.
|
||||
- When a new rule is a near-duplicate of an existing one, merge them into one `add` and list the old id in `remove`.
|
||||
- When a new rule contradicts an existing rule, `add` the new one and `remove` the contradicted id.
|
||||
- Every `add` entry must have a `source` that appears in the input's `corrections` list. Never invent a source.
|
||||
- Do not read files and do not run commands. Every input is already attached.
|
||||
|
||||
Example. Input:
|
||||
|
||||
```json
|
||||
{
|
||||
"existing": [],
|
||||
"deleted_in_window": [],
|
||||
"corrections": [
|
||||
{
|
||||
"source": "commit:9dd2c07",
|
||||
"date": "2026-08-03",
|
||||
"message": "docs: remove experimental features page",
|
||||
"files": ["packages/kilo-docs/pages/code-with-ai/experimental-features.md"],
|
||||
"diff": "- removed the entire experimental features page\n- the page documented features behind unreleased flags"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Expected output:
|
||||
|
||||
```json
|
||||
{
|
||||
"add": [
|
||||
{
|
||||
"id": "no-experimental-features",
|
||||
"rule": "Do not document features that are behind unreleased flags.",
|
||||
"scope": "both",
|
||||
"source": "commit:9dd2c07",
|
||||
"date": "2026-08-03"
|
||||
}
|
||||
],
|
||||
"remove": []
|
||||
}
|
||||
```
|
||||
|
||||
The rule is `both` because documenting unreleased features is wrong at triage time (the feature is not docs-worthy yet) and at edit time (the page should not exist).
|
||||
@@ -9,7 +9,9 @@
|
||||
import { spawnSync } from "node:child_process"
|
||||
import fs from "node:fs"
|
||||
|
||||
const API = "https://api.github.com"
|
||||
// Test hook: DOCS_SYNC_API_BASE points the API at a local stub server. The workflow
|
||||
// never sets it — only selftests do.
|
||||
const API = process.env.DOCS_SYNC_API_BASE || "https://api.github.com"
|
||||
const MAX_RETRIES = 3
|
||||
|
||||
export function token() {
|
||||
|
||||
+1633
-4
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,7 @@ import path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { parseTriageEntries } from "./extract-json.mjs"
|
||||
import { appendSummary, backoffMsForAttempt, deadline, remainingMs, runKilo, sleepSync } from "./lib.mjs"
|
||||
import { readLearningsBlock } from "./learn.mjs"
|
||||
|
||||
const CHUNK_SIZE = 25
|
||||
const ATTEMPTS = 3
|
||||
@@ -28,7 +29,7 @@ const OUT_DIR = "docs-sync-out"
|
||||
const CHUNK_TIMEOUT_MS = 10 * 60 * 1000
|
||||
|
||||
const HERE = path.dirname(fileURLToPath(import.meta.url))
|
||||
const prompt = fs.readFileSync(path.join(HERE, "triage-prompt.md"), "utf8")
|
||||
const prompt = fs.readFileSync(path.join(HERE, "triage-prompt.md"), "utf8") + readLearningsBlock("triage")
|
||||
const model = process.env.TRIAGE_MODEL
|
||||
if (!model) throw new Error("TRIAGE_MODEL is required")
|
||||
|
||||
@@ -115,9 +116,7 @@ function triageChunk(chunk, index, budgetDeadline) {
|
||||
console.warn(`chunk ${index}: backing off ${wait / 1000}s before attempt ${attempt + 1}`)
|
||||
sleepSync(wait)
|
||||
} else if (wait > 0) {
|
||||
console.warn(
|
||||
`chunk ${index}: skipping backoff — remaining budget cannot fit attempt ${attempt + 1} after wait`,
|
||||
)
|
||||
console.warn(`chunk ${index}: skipping backoff — remaining budget cannot fit attempt ${attempt + 1} after wait`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,7 +124,12 @@ function triageChunk(chunk, index, budgetDeadline) {
|
||||
console.warn(
|
||||
`::warning::chunk ${index} failed triage after up to ${ATTEMPTS} attempts; marking ${chunk.length} PRs pending`,
|
||||
)
|
||||
return chunk.map((d) => pendingEntry(d, lastCause.includes("triage failed") ? lastCause : `triage failed to classify this PR (${lastCause})`))
|
||||
return chunk.map((d) =>
|
||||
pendingEntry(
|
||||
d,
|
||||
lastCause.includes("triage failed") ? lastCause : `triage failed to classify this PR (${lastCause})`,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const chunks = []
|
||||
|
||||
@@ -27,15 +27,23 @@ const FILE_CAP = 15
|
||||
const ROW_CAP = 150
|
||||
const PENDING_DISPLAY_CAP = 60
|
||||
const SUMMARY_FILE = ".docs-sync-summary.json"
|
||||
// Owner of the rolling docs PR: assigned and asked for review on creation.
|
||||
const DOCS_OWNER = "emilieschario"
|
||||
const DOCS_PATH = "packages/kilo-docs"
|
||||
export const LEARNINGS_FILE = "packages/kilo-docs/LEARNINGS.md"
|
||||
|
||||
const git = (args) => execFileSync("git", args, { stdio: ["ignore", "pipe", "inherit"] }).toString().trim()
|
||||
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("-->", "")
|
||||
return String(value ?? "")
|
||||
.replaceAll("<!--", "")
|
||||
.replaceAll("-->", "")
|
||||
}
|
||||
|
||||
function shortRef(url) {
|
||||
@@ -52,7 +60,9 @@ function skippedRow(e) {
|
||||
}
|
||||
|
||||
function pendingRow(e) {
|
||||
const reason = clean(e.reason ?? e.cause ?? "").replaceAll("|", "\\|").replaceAll("\n", " ")
|
||||
const reason = clean(e.reason ?? e.cause ?? "")
|
||||
.replaceAll("|", "\\|")
|
||||
.replaceAll("\n", " ")
|
||||
return `| [${shortRef(e.url)}](${clean(e.url)}) | ${reason} |`
|
||||
}
|
||||
|
||||
@@ -79,7 +89,18 @@ function section(name, header, rows) {
|
||||
return `<!-- docs-sync:${name}:start -->\n${body}\n<!-- docs-sync:${name}:end -->`
|
||||
}
|
||||
|
||||
export function renderBody({ date, since, through, changesRows, pendingRows, skippedRows, verified, draftReasons, note }) {
|
||||
export function renderBody({
|
||||
date,
|
||||
since,
|
||||
through,
|
||||
learnedThrough = "",
|
||||
changesRows,
|
||||
pendingRows,
|
||||
skippedRows,
|
||||
verified,
|
||||
draftReasons,
|
||||
note,
|
||||
}) {
|
||||
const pendingDisplay =
|
||||
pendingRows.length > PENDING_DISPLAY_CAP
|
||||
? [...pendingRows.slice(0, PENDING_DISPLAY_CAP), `| +${pendingRows.length - PENDING_DISPLAY_CAP} more | |`]
|
||||
@@ -108,7 +129,7 @@ ${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} -->
|
||||
`
|
||||
${learnedThrough ? learnedThrough + "\n" : ""}`
|
||||
}
|
||||
|
||||
function mergeRows(oldRows, newRows) {
|
||||
@@ -242,6 +263,23 @@ export function computeProcessedThrough({ uncovered, digest, now, fallback }) {
|
||||
return new Date(earliest - 1).toISOString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Content gate: legitimate bot edits are docs pages and nav files. Only
|
||||
* those are built and tested during verify (content-integrity.test.ts
|
||||
* walks pages/ only), so anything else in the docs package forces human
|
||||
* review. LEARNINGS.md is a root-level .md file, like the three sibling
|
||||
* .md files already at that level, so it is not built or tested and is
|
||||
* safe to exclude from the gate.
|
||||
*/
|
||||
export function nonContentFiles(changedFiles) {
|
||||
return (Array.isArray(changedFiles) ? changedFiles : []).filter(
|
||||
(f) =>
|
||||
f !== LEARNINGS_FILE &&
|
||||
!f.startsWith("packages/kilo-docs/pages/") &&
|
||||
!f.startsWith("packages/kilo-docs/lib/nav/"),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Route summary + triage into the three body sections.
|
||||
* changesRows = action neither skipped nor pending
|
||||
@@ -280,6 +318,22 @@ export function dropLegacySkipped(rows) {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the learned-through marker for renderBody.
|
||||
*
|
||||
* Order: env LEARNED_THROUGH when set and non-empty; else the marker
|
||||
* parsed out of the existing PR body; else "".
|
||||
* The fallback is load-bearing: a run where extraction was skipped,
|
||||
* failed, or already PATCHed the marker itself must not clobber a good
|
||||
* marker.
|
||||
*/
|
||||
export function resolveLearnedThrough({ envValue, prBody }) {
|
||||
const fromEnv = String(envValue ?? "").trim()
|
||||
if (fromEnv) return fromEnv
|
||||
const m = String(prBody ?? "").match(/<!--\s*docs-sync:\s*learned-through\s+commit=\S+\s+comment=\S+\s*-->/)
|
||||
return m ? m[0] : ""
|
||||
}
|
||||
|
||||
/**
|
||||
* No-diff early-return report. Returns summary markdown and an optional
|
||||
* replay warning. Warns IFF sinceOverride && uncovered non-empty (no commit
|
||||
@@ -347,18 +401,14 @@ async function main() {
|
||||
const through = computeProcessedThrough({ uncovered, digest, now, fallback: since })
|
||||
|
||||
// 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 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/"),
|
||||
)
|
||||
const nonContent = nonContentFiles(changedFiles)
|
||||
if (nonContent.length > 0) {
|
||||
// File paths are agent-chosen; sanitize before they land in the PR body.
|
||||
const listed = nonContent
|
||||
@@ -369,9 +419,17 @@ async function main() {
|
||||
}
|
||||
const draft = draftReasons.length > 0
|
||||
|
||||
git(mode === "update" ? ["push", "origin", `HEAD:${BRANCH}`] : ["push", "--force-with-lease", "origin", `HEAD:${BRANCH}`])
|
||||
git(
|
||||
mode === "update"
|
||||
? ["push", "origin", `HEAD:${BRANCH}`]
|
||||
: ["push", "--force-with-lease", "origin", `HEAD:${BRANCH}`],
|
||||
)
|
||||
|
||||
const { changesRows: changesNew, pendingRows: pendingNew, skippedRows: skippedNew } = routeRows({
|
||||
const {
|
||||
changesRows: changesNew,
|
||||
pendingRows: pendingNew,
|
||||
skippedRows: skippedNew,
|
||||
} = routeRows({
|
||||
summary: agentSummary,
|
||||
triage,
|
||||
uncovered,
|
||||
@@ -380,8 +438,10 @@ async function main() {
|
||||
let oldChanges = []
|
||||
let oldSkipped = []
|
||||
let oldPending = []
|
||||
let prBody = ""
|
||||
if (mode === "update" && existingPr) {
|
||||
const pr = await api(`/repos/${repo()}/pulls/${existingPr}`)
|
||||
prBody = pr.body ?? ""
|
||||
oldChanges = extractSectionRows(pr.body, "changes")
|
||||
oldSkipped = dropLegacySkipped(extractSectionRows(pr.body, "skipped"))
|
||||
oldPending = extractSectionRows(pr.body, "pending")
|
||||
@@ -392,10 +452,13 @@ async function main() {
|
||||
// extractSectionRows stays exercised; discarded deliberately.
|
||||
void oldPending
|
||||
|
||||
const learnedThrough = resolveLearnedThrough({ envValue: process.env.LEARNED_THROUGH, prBody })
|
||||
|
||||
const body = renderBody({
|
||||
date,
|
||||
since,
|
||||
through,
|
||||
learnedThrough,
|
||||
changesRows: mergeRows(oldChanges, changesNew),
|
||||
pendingRows: pendingNew,
|
||||
skippedRows: mergeRows(oldSkipped, skippedNew),
|
||||
@@ -445,6 +508,20 @@ async function main() {
|
||||
prNumber = pr.number
|
||||
prUrl = pr.html_url
|
||||
await api(`/repos/${repo()}/issues/${prNumber}/labels`, { method: "POST", body: { labels: ["auto-docs"] } })
|
||||
// Best effort: the PR already exists here, so a non-collaborator or a
|
||||
// revoked account must not fail the run.
|
||||
try {
|
||||
await api(`/repos/${repo()}/issues/${prNumber}/assignees`, {
|
||||
method: "POST",
|
||||
body: { assignees: [DOCS_OWNER] },
|
||||
})
|
||||
await api(`/repos/${repo()}/pulls/${prNumber}/requested_reviewers`, {
|
||||
method: "POST",
|
||||
body: { reviewers: [DOCS_OWNER] },
|
||||
})
|
||||
} catch (err) {
|
||||
console.warn(`::warning::docs-sync: could not assign or request review from ${DOCS_OWNER}: ${err.message}`)
|
||||
}
|
||||
if (mode === "conflict" && existingPr) {
|
||||
await api(`/repos/${repo()}/issues/${existingPr}/comments`, {
|
||||
method: "POST",
|
||||
@@ -459,7 +536,9 @@ async function main() {
|
||||
appendSummary(
|
||||
`### docs-sync PR\n\n- ${prUrl}\n- changed files: ${changedFiles.length}\n- draft: ${draft}\n- uncovered: ${uncovered.length}\n- processed-through: ${through}\n`,
|
||||
)
|
||||
console.log(`PR ${prNumber}: ${prUrl} (draft=${draft}, files=${changedFiles.length}, uncovered=${uncovered.length}, through=${through})`)
|
||||
console.log(
|
||||
`PR ${prNumber}: ${prUrl} (draft=${draft}, files=${changedFiles.length}, uncovered=${uncovered.length}, through=${through})`,
|
||||
)
|
||||
}
|
||||
|
||||
const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href
|
||||
|
||||
@@ -66,13 +66,13 @@ jobs:
|
||||
sync:
|
||||
if: github.repository == 'Kilo-Org/kilocode' && github.event_name != 'pull_request'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
# Budget: 4 setup/collect + 90 triage + 120 edit + 2 verify + 10 fix + 2 upsert = 228 min, 12-minute reserve.
|
||||
# Budget: 4 setup/collect + 10 learn + 90 triage + 120 edit + 2 verify + 10 fix + 2 upsert = 238 min, 12-minute reserve.
|
||||
# These are ceilings, not costs: a caught-up run triages ~2 chunks and edits
|
||||
# ~1 batch and finishes in ~25 min. The old 35/50 pair was the binding
|
||||
# constraint on backlog drain — run 30306629290 deferred 54 PRs untriaged and
|
||||
# 31 unedited purely on budget, with no attempt made. See the throughput note
|
||||
# in the PR description for the arithmetic.
|
||||
timeout-minutes: 240
|
||||
timeout-minutes: 250
|
||||
env:
|
||||
# Both are required: without KILO_ORG_ID the gateway bills the key
|
||||
# owner's personal balance (402 "Add credits") instead of the org.
|
||||
@@ -110,6 +110,15 @@ jobs:
|
||||
INPUT_SINCE: ${{ inputs.since }}
|
||||
run: node .github/docs-sync/watermark.mjs
|
||||
|
||||
- name: Learn from maintainer corrections
|
||||
id: learn
|
||||
continue-on-error: true
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
LEARNINGS_BUDGET_MINUTES: "10"
|
||||
DRY_RUN: ${{ inputs.dry_run }}
|
||||
run: node .github/docs-sync/learn.mjs
|
||||
|
||||
- name: Collect merged PRs
|
||||
id: collect
|
||||
env:
|
||||
@@ -215,10 +224,15 @@ jobs:
|
||||
echo "ok=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Write the learnings file
|
||||
if: (steps.worthy.outputs.count || '0') != '0' && inputs.dry_run != true
|
||||
run: node .github/docs-sync/learn.mjs --apply
|
||||
|
||||
- name: Upsert rolling PR
|
||||
if: (steps.worthy.outputs.count || '0') != '0' && inputs.dry_run != true
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
LEARNED_THROUGH: ${{ steps.learn.outputs.learned_through }}
|
||||
PROCESSED_THROUGH: ${{ steps.wm.outputs.now }}
|
||||
SINCE: ${{ steps.wm.outputs.since }}
|
||||
SINCE_OVERRIDE: ${{ steps.wm.outputs.since_override }}
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
---
|
||||
name: chart
|
||||
description: Use when the user asks to visualize data with charts, graphs, or plots using the `chart` tool (bar, line, scatter, pie, time series, etc.).
|
||||
---
|
||||
|
||||
# Data Visualization
|
||||
|
||||
The `chart` tool is ALWAYS available in this environment. When the user asks to visualize data (charts, graphs, plots), you MUST call the `chart` tool. Never output the config as text, never say the tool is unavailable, never suggest external renderers. Always use the tool call — it is the only correct response for data visualization requests. Do NOT repeat or echo the config JSON in your text response.
|
||||
|
||||
Use the `chart` tool only when the user explicitly asks for a chart, graph, or plot. Only use these supported Chart.js v4 types: `bar`, `bubble`, `pie`, `doughnut`, `line`, `mixed`, `polarArea`, `radar`, `scatter`. For area charts, use `line` with `fill: true` on the dataset — do NOT use `area` as a type.
|
||||
|
||||
Use mermaid fenced code blocks (` ```mermaid `) when:
|
||||
- The user asks for a diagram, flowchart, sequence diagram, ER diagram, or architecture diagram
|
||||
- Visualizing relationships, processes, or structure — not data values
|
||||
|
||||
Mermaid is NOT a tool and is NOT Chart.js — never call the `chart` tool for mermaid diagrams. Just write the mermaid syntax directly in your text response inside a fenced code block. No tool call needed.
|
||||
|
||||
Do not use either for: code, text, or data that is already clear in prose or table form.
|
||||
|
||||
The `chart` tool input accepts:
|
||||
- `title` (string) — short label shown in the tool header
|
||||
- `description` (string, optional) — subtitle shown below the title
|
||||
- `spec` (string) — a Chart.js config object as a JSON string
|
||||
|
||||
The `spec` field must be a Chart.js config JSON string with `type`, `data`, and optionally `options`. Examples:
|
||||
|
||||
Bar chart:
|
||||
```json
|
||||
{
|
||||
"type": "bar",
|
||||
"data": {
|
||||
"labels": ["A", "B", "C"],
|
||||
"datasets": [{ "label": "Value", "data": [10, 20, 15] }]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Area chart (line with fill):
|
||||
```json
|
||||
{
|
||||
"type": "line",
|
||||
"data": {
|
||||
"labels": ["Jan", "Feb", "Mar", "Apr"],
|
||||
"datasets": [{ "label": "Value", "data": [10, 28, 19, 45], "fill": true }]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Line chart:
|
||||
```json
|
||||
{
|
||||
"type": "line",
|
||||
"data": {
|
||||
"labels": ["Jan", "Feb", "Mar", "Apr"],
|
||||
"datasets": [{ "label": "Value", "data": [10, 28, 19, 45], "fill": false }]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Scatter plot:
|
||||
```json
|
||||
{
|
||||
"type": "scatter",
|
||||
"data": {
|
||||
"datasets": [{
|
||||
"label": "Points",
|
||||
"data": [{ "x": 1, "y": 5 }, { "x": 2, "y": 8 }, { "x": 3, "y": 3 }]
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Time series:
|
||||
```json
|
||||
{
|
||||
"type": "line",
|
||||
"data": {
|
||||
"labels": ["2024-01", "2024-02", "2024-03", "2024-04"],
|
||||
"datasets": [{ "label": "Value", "data": [120, 145, 132, 178], "fill": true }]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Pie chart:
|
||||
```json
|
||||
{
|
||||
"type": "pie",
|
||||
"data": {
|
||||
"labels": ["A", "B", "C"],
|
||||
"datasets": [{ "data": [30, 50, 20] }]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Doughnut chart:
|
||||
```json
|
||||
{
|
||||
"type": "doughnut",
|
||||
"data": {
|
||||
"labels": ["A", "B", "C"],
|
||||
"datasets": [{ "data": [30, 50, 20] }]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Radar chart:
|
||||
```json
|
||||
{
|
||||
"type": "radar",
|
||||
"data": {
|
||||
"labels": ["Speed", "Power", "Agility", "Stamina"],
|
||||
"datasets": [{ "label": "Player", "data": [80, 60, 90, 70] }]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Bubble chart:
|
||||
```json
|
||||
{
|
||||
"type": "bubble",
|
||||
"data": {
|
||||
"datasets": [{
|
||||
"label": "Group A",
|
||||
"data": [{ "x": 10, "y": 20, "r": 8 }, { "x": 15, "y": 10, "r": 5 }]
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Polar area chart:
|
||||
```json
|
||||
{
|
||||
"type": "polarArea",
|
||||
"data": {
|
||||
"labels": ["A", "B", "C", "D"],
|
||||
"datasets": [{ "data": [11, 16, 7, 14] }]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Mixed chart (bar + line):
|
||||
```json
|
||||
{
|
||||
"type": "bar",
|
||||
"data": {
|
||||
"labels": ["Jan", "Feb", "Mar"],
|
||||
"datasets": [
|
||||
{ "type": "bar", "label": "Revenue", "data": [100, 120, 90] },
|
||||
{ "type": "line", "label": "Trend", "data": [95, 115, 100] }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You may customize colors by setting `backgroundColor` and `borderColor` arrays on datasets. The renderer handles sizing — do not set width or height.
|
||||
|
||||
Only include `scales` in `options` for cartesian chart types: `bar`, `line`, `scatter`, `bubble`. Do NOT include `scales` for `pie`, `doughnut`, `polarArea`, `radar`, or `mixed` — it will cause them to fail.
|
||||
@@ -0,0 +1,137 @@
|
||||
---
|
||||
name: icon-jetbrains
|
||||
description: Create or review IntelliJ New UI SVG icons, theme variants, sizes, and palette.
|
||||
---
|
||||
|
||||
# IntelliJ Platform New UI Icons
|
||||
|
||||
Guidance for authoring SVG icons for the JetBrains plugin. This skill is the single source of truth for icon sizing, palette, dark variants, composition rules, and placement. Other docs (including `packages/kilo-jetbrains/AGENTS.md`) defer here for SVG authoring details.
|
||||
|
||||
Icons follow IntelliJ New UI conventions: a fixed canvas per role, a strict light/dark palette, and explicit per-shape colors (the IntelliJ SVG loader recolors by matching literal hex values, so `currentColor` and CSS do not work). The plugin loads icons directly from its resource folder — see [Where the SVGs live](#where-the-svgs-live).
|
||||
|
||||
## Golden rules
|
||||
|
||||
1. **Always ship two SVGs** — one for the light theme (e.g. `add-file.svg`) and one for the dark theme with the `_dark` suffix (`add-file_dark.svg`). Geometry must be identical between them; only the palette swaps.
|
||||
- **Tool-window icons ship as a quartet, not a pair.** When a tool window has both a 16×16 base (`name.svg` / `name_dark.svg`) and a 20×20 stripe variant (`name@20x20.svg` / `name@20x20_dark.svg`), they must share the same metaphor. The stripe is only one surface — the 16×16 base also appears in **Search Everywhere**, **Find Action**, context menus, the Services tool window, recent locations, and the View ▸ Tool Windows menu. Changing only the @20x20 leaves users seeing two different icons for the same tool window depending on where they encounter it. Always update all four files together (this repo's tool-window quartet is `kilo.svg` / `kilo_dark.svg` + `kilo@20x20.svg` / `kilo@20x20_dark.svg`).
|
||||
2. **Only use colors from the canonical palette.** See [palette.md](./palette.md). Picking a one-off color breaks theming and contrast.
|
||||
3. **One canvas size per icon role.** See [Icon roles](#icon-roles). Do not invent new sizes or pad with empty space — IntelliJ scales the canvas as a single unit.
|
||||
4. **No raster, no gradients, no filters, no embedded fonts.** Path geometry only (`<path>`, `<rect>`, `<circle>`, `<line>`, `<polyline>`, `<polygon>`). Text must be converted to outlines.
|
||||
5. **Use `fill="none"` on the root `<svg>`** and set `fill` / `stroke` explicitly per shape — never rely on CSS or `currentColor`.
|
||||
6. **Strokes use `stroke-width="1"`, `stroke-linecap="round"`, `stroke-linejoin="round"`** (or `stroke-miterlimit="10"` for hard joins). Heavier strokes are reserved for hero glyphs inside a circle badge (e.g. status checkmarks) and use `stroke-width="1.5"` or `"2"`. The `1` applies to the **primary glyph stroke** — do not force *every* stroke to 1. Hairline/decorative strokes (e.g. a thin stroke used to fatten a filled dot) and strokes whose width is coupled to geometry (e.g. a badge ring meant to sit flush with a fill edge) must keep their intended weight (scale with the artwork), or they fatten and misalign.
|
||||
7. **Pixel-grid align**: keep stroke axes on half-pixel centers (`x.5`) and fills on whole pixels so the icon stays crisp at 1× rendering. Getting the base grid right also keeps it crisp on HiDPI/Retina; fine sub-pixel detail blurs at fractional scales (125%/150%), so keep geometry simple rather than chasing detail that won't survive scaling. SVGs are resolution-independent — ship one vector per theme, never `@2x` raster variants.
|
||||
8. **File names use kebab-case and stay ASCII-only** (e.g. `arrow-down-to-line.svg`, `book-open-check.svg`, `kilo@20x20.svg`), matching every existing icon in `packages/kilo-jetbrains/frontend/src/main/resources/icons/`.
|
||||
|
||||
## Icon roles
|
||||
|
||||
Pick the canvas size from the role, not the other way around. All icons land in this repo's `packages/kilo-jetbrains/frontend/src/main/resources/icons/` (with a `views/` subfolder for in-view icons — see [Where the SVGs live](#where-the-svgs-live)).
|
||||
|
||||
| Role | Canvas | Filename pattern |
|
||||
|---|---|---|
|
||||
| Action icons (menus, popups, toolbars) | **16×16** | `name.svg` + `name_dark.svg` |
|
||||
| Tree node icons (PSI, structure view) | **16×16** | `name.svg` + `name_dark.svg` |
|
||||
| Tool-window stripe icons (compact/16) | **16×16** | `name.svg` + `name_dark.svg` |
|
||||
| Tool-window stripe icons (New UI) | **20×20** | `name@20x20.svg` + `name@20x20_dark.svg` |
|
||||
| Main toolbar (New UI) | **20×20** | `name@20x20.svg` + `name@20x20_dark.svg` |
|
||||
| Editor gutter icons | **14×14** (small marks **12×12**, **9×9**) | `name.svg` + `name_dark.svg` |
|
||||
| Status bar / inline status | **16×16** | `name.svg` + `name_dark.svg` |
|
||||
| Breakpoint marks | **14×14** (12×12 for sub-marks) | `name.svg` + `name_dark.svg` |
|
||||
| Run-config tags & disclosure chevrons | **16×16** (`chevron*.svg` may be 9×9 to 16×16) | `name.svg` + `name_dark.svg` |
|
||||
| Welcome/onboarding & logos | **16, 20, 28, 48** (per surface) | `name.svg` + `name_dark.svg` |
|
||||
|
||||
When in doubt, find a sibling icon of the same role already in the icons folder and copy its `width` / `height` / `viewBox`.
|
||||
|
||||
## Where the SVGs live
|
||||
|
||||
Plugin icons go into `packages/kilo-jetbrains/frontend/src/main/resources/icons/` (action icons, tool-window icons) or the `icons/views/` subfolder (in-view icons used by the chat/session UI). Icons are loaded directly via `IconLoader` — there is no role-based subfolder structure and no mapping file. Place both the light SVG and its `_dark` sibling in the folder and reference them from the plugin's icon-holder class.
|
||||
|
||||
## SVG skeleton
|
||||
|
||||
```svg
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- shapes, ordered back-to-front -->
|
||||
</svg>
|
||||
```
|
||||
|
||||
The dark variant is the same file with light-palette colors swapped for their dark-theme partner — see [palette.md](./palette.md). Never change geometry between the two.
|
||||
|
||||
## Composition rules
|
||||
|
||||
- **One semantic meaning per icon.** A status badge, an accent dot, or a "+" overlay is fine; two unrelated glyphs in one icon is not.
|
||||
- **Optical centering, not geometric.** Plus/arrow/refresh glyphs sit slightly above center; round badges (class, method, status) are centered on `(cx=8, cy=8)` for 16×16 and `(cx=10, cy=10)` for 20×20.
|
||||
- **Outer keep-out**: leave at least **1 px** of empty padding on each side of a 16×16 icon (so meaningful geometry lives within `1..15`). For 20×20 use **2 px** of padding. Stripe icons must stay visually balanced inside their 20×20 cell.
|
||||
- **Round caps overshoot the endpoint.** A round `stroke-linecap`/`stroke-linejoin` extends **half the stroke width past the endpoint**, so a 1px round-capped stroke ending at `0` or `16` is clipped by the canvas. Keep stroke endpoints within `0.5..15.5` (this is stricter than the fill keep-out).
|
||||
- **Stroke + fill pairing for node-style icons** (class/method nodes): a light-tinted fill at radius 6.5 with a stroke in the accent color, plus a glyph filled with the same accent.
|
||||
- Light: `<circle cx="8" cy="8" r="6.5" fill="<accent-bg-light>" stroke="<accent-light>"/>` then glyph `fill="<accent-light>"`.
|
||||
- Dark: same circle with `fill="<accent-bg-dark>" stroke="<accent-dark>"` and glyph filled with `<accent-dark>`.
|
||||
- **Stroke-only icons** (chevrons, refresh, edit pencil): a single-color path using the neutral stroke (`#6C707E` light / `#CED0D6` dark) at `stroke-width="1"`. Use `#818594` (light) / `#6F737A` (dark) for "secondary" stroke glyphs like dropdown chevrons.
|
||||
- **Status badges** (error/warning/success/info) follow this template:
|
||||
- Light: filled circle/triangle in the *accent* color, glyph painted in `white`.
|
||||
- Dark: filled circle/triangle in the *dark accent* color, glyph painted in the matching *muted dark fill* (e.g. `#5E4D33` inside `#F2C55C` warning) — never plain white.
|
||||
- **Two-tone action icons** (a base glyph plus a small modifier): the base glyph uses the neutral gray, and the small modifier (`+`, ✕, ↻, gear) uses the primary blue accent. Light gray + blue accent → dark gray + blue accent in the dark variant.
|
||||
- **Disabled / stroke-only variants** (e.g. `*-stroke.svg`): outline-only, same neutral stroke color, no fills.
|
||||
|
||||
## Palette quick reference
|
||||
|
||||
The full lookup is in [palette.md](./palette.md). Most icons only need:
|
||||
|
||||
| Role | Light | Dark |
|
||||
|---|---|---|
|
||||
| Primary stroke / fill | `#6C707E` | `#CED0D6` |
|
||||
| Secondary stroke | `#818594` | `#6F737A` |
|
||||
| Disabled / faint fill | `#EBECF0` | `#43454A` |
|
||||
| Accent — Blue | `#3574F0` | `#548AF7` |
|
||||
| Accent — Blue (bg) | `#EDF3FF` / `#E7EFFD` | `#25324D` |
|
||||
| Accent — Red | `#DB3B4B` | `#DB5C5C` |
|
||||
| Accent — Red (bg) | `#FFF7F7` | `#402929` |
|
||||
| Status — Error fill | `#E55765` | `#DB5C5C` |
|
||||
| Status — Warning fill | `#FFAF0F` | `#F2C55C` |
|
||||
| Status — Success fill | `#55A76A` | `#57965C` |
|
||||
| Accent — Green | `#208A3C` | `#57965C` |
|
||||
| Accent — Green (bg) | `#F2FCF3` | `#253627` |
|
||||
| Accent — Orange | `#E66D17` | `#C77D55` |
|
||||
| Accent — Orange (bg) | `#FFF4EB` | `#45322B` |
|
||||
| Accent — Yellow/Gold | `#FFAF0F` / `#C27D04` | `#F2C55C` / `#D6AE58` |
|
||||
| Accent — Purple | `#834DF0` | `#B589EC` |
|
||||
| Accent — Purple (bg) | `#FAF5FF` | `#2F2936` |
|
||||
| White on dark badge | `white` | matching muted-dark fill (e.g. `#5E4D33`) |
|
||||
|
||||
Do not use plain `#000000` or off-the-palette grays.
|
||||
|
||||
## Generation workflow
|
||||
|
||||
1. **Pick the role and canvas size** from the table above. Find at least two visually similar sibling icons already in the icons folder and mirror their stroke/fill mix.
|
||||
2. **Lay out geometry on the pixel grid** (whole-pixel fills, half-pixel stroke centers). Optical-center the glyph.
|
||||
3. **Apply the canonical light palette** from [palette.md](./palette.md). Never invent colors.
|
||||
4. **Save the light SVG** with `width`/`height`/`viewBox` matching the role and `fill="none"` on `<svg>`.
|
||||
5. **Duplicate to the `_dark` filename** and swap each color for its dark-theme partner from the palette mapping. Keep paths byte-identical otherwise.
|
||||
6. **Place and wire the file** → drop both files into `packages/kilo-jetbrains/frontend/src/main/resources/icons/` (action icons, tool-window icons) or `icons/views/` (in-view chat/session icons), then reference the icon from the plugin's icon-holder class.
|
||||
7. **Verify visually** in both themes via the image preview in the IDE, or run the IDE and toggle *View ▸ Appearance ▸ New UI* to compare. Check selection states for stripe icons.
|
||||
|
||||
## Adapting or rescaling an existing icon
|
||||
|
||||
Importing a Lucide/Codicon-style icon (often drawn on a 20- or 24-unit grid) or moving one onto the 16×16 grid is not a blanket "set everything to the 16 defaults" operation:
|
||||
|
||||
1. **Scale geometry *and* every `stroke-width`** by the same factor (`16 ÷ source size`). This keeps the render faithful before you change anything intentionally.
|
||||
2. **Only then re-weight the primary glyph stroke** to `1` if it wasn't already ~1px effective. Leave hairlines and geometry-coupled strokes at their scaled value (see rule 6).
|
||||
3. **Re-check what scaling breaks:**
|
||||
- Round caps clipping at the edge → nudge stroke endpoints into `0.5..15.5`.
|
||||
- Strokes that were flush with a fill edge (e.g. a badge ring around a status dot) → the stroke must still straddle the fill boundary after scaling; scale its width too.
|
||||
4. **Crispness is a separate pass.** Scaling rarely lands coordinates on the pixel grid, so the result is faithful but not automatically crisp; the pixel-grid snap (rule 7) is manual and per-icon.
|
||||
|
||||
## Common mistakes
|
||||
|
||||
- Off-palette colors (e.g. importing from Figma without remapping). They will not theme correctly and reviewers will reject the PR.
|
||||
- Different geometry between light and dark variants — selection animations and HiDPI overlays will glitch.
|
||||
- A 16-px glyph saved into a 20×20 canvas without re-balancing. Tool-window/main-toolbar icons need geometry tuned for 20×20, not a 16×16 reused with extra whitespace.
|
||||
- Using `currentColor`, CSS, or `<style>` blocks. The IntelliJ icon loader requires explicit colors on every shape so it can do palette-based recoloring.
|
||||
- Forgetting the `_dark` variant. The icon will look fine in Light theme then turn invisible in Dark.
|
||||
- Pure-black (`#000`) or pure-white (`#FFF`) fills outside the status-badge glyph pattern. They break under accent recoloring.
|
||||
- Blanket-setting every `stroke-width` to `1` when rescaling. Only the primary glyph stroke is 1px; hairlines and fill-coupled/ring strokes must scale with the geometry or they fatten and misalign.
|
||||
- Round caps at the canvas edge. A round cap overshoots its endpoint by half the stroke width, so an endpoint at `0` or `16` clips — keep stroke endpoints within `0.5..15.5`.
|
||||
|
||||
## References
|
||||
|
||||
- [palette.md](./palette.md) — full color palette with light↔dark mapping.
|
||||
- [examples.md](./examples.md) — annotated SVG snippets for each icon role.
|
||||
- `packages/kilo-jetbrains/AGENTS.md` — repository-specific JetBrains plugin constraints; use it together with this skill for icon work.
|
||||
- `packages/kilo-jetbrains/frontend/src/main/resources/icons/` (with its `views/` subfolder) — where plugin icons live; this is the ground truth for placement.
|
||||
@@ -0,0 +1,176 @@
|
||||
# Annotated New UI Icon Examples
|
||||
|
||||
Each example below is paired with a dark variant and a short note on the pattern it represents. Use these as templates when generating new icons. Place generated icons in `packages/kilo-jetbrains/frontend/src/main/resources/icons/` (or the `icons/views/` subfolder for in-view chat/session icons).
|
||||
|
||||
---
|
||||
|
||||
## 1. Single-color action (16×16) — primary gray stroke + filled glyph
|
||||
|
||||
Plus / add action — the most common action icon shape.
|
||||
|
||||
**Light:**
|
||||
```svg
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.5 1C7.77614 1 8 1.22386 8 1.5V7H13.5C13.7761 7 14 7.22386 14 7.5C14 7.77614 13.7761 8 13.5 8H8V13.5C8 13.7761 7.77614 14 7.5 14C7.22386 14 7 13.7761 7 13.5V8H1.5C1.22386 8 1 7.77614 1 7.5C1 7.22386 1.22386 7 1.5 7H7V1.5C7 1.22386 7.22386 1 7.5 1Z" fill="#6C707E"/>
|
||||
</svg>
|
||||
```
|
||||
|
||||
**Dark:** swap `#6C707E` → `#CED0D6`.
|
||||
|
||||
Why it works: one filled path, primary gray. Drop in any glyph and you have a complete action icon.
|
||||
|
||||
---
|
||||
|
||||
## 2. Two-tone action (16×16) — neutral base + blue accent modifier
|
||||
|
||||
Add-file action — base glyph in neutral gray, "+" sticker in primary blue.
|
||||
|
||||
**Light:**
|
||||
```svg
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.5 9C12.7761 9 13 9.22386 13 9.5V12H15.5C15.7761 12 16 12.2239 16 12.5C16 12.7761 15.7761 13 15.5 13H13V15.5C13 15.7761 12.7761 16 12.5 16C12.2239 16 12 15.7761 12 15.5V13H9.5C9.22386 13 9 12.7761 9 12.5C9 12.2239 9.22386 12 9.5 12H12V9.5C12 9.22386 12.2239 9 12.5 9Z" fill="#3574F0"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 13V5.82843C3 5.29799 3.21071 4.78929 3.58579 4.41421L6.41421 1.58579C6.78929 1.21071 7.29799 1 7.82843 1H11C12.1046 1 13 1.89543 13 3V8H12V3C12 2.44772 11.5523 2 11 2H8V4C8 5.10457 7.10457 6 6 6H4V13C4 13.5523 4.44772 14 5 14H8V15H5C3.89543 15 3 14.1046 3 13ZM4.41421 5L7 2.41421V4C7 4.55228 6.55228 5 6 5H4.41421Z" fill="#6C707E"/>
|
||||
</svg>
|
||||
```
|
||||
|
||||
**Dark:** `#3574F0` → `#548AF7`, `#6C707E` → `#CED0D6`.
|
||||
|
||||
Pattern: stick the small modifier glyph in the bottom-right of the canvas (`(9..15, 9..15)` for 16×16) and let the base glyph occupy the top-left two-thirds.
|
||||
|
||||
---
|
||||
|
||||
## 3. Stroke-only action (16×16) — chevrons, refresh, edit
|
||||
|
||||
Refresh action — multiple stroked sub-paths sharing one color.
|
||||
|
||||
**Light:**
|
||||
```svg
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.5 9V8C2.5 4.96243 4.96243 2.5 8 2.5C9.10679 2.5 10.1372 2.82692 11 3.38947" stroke="#6C707E" stroke-linecap="round"/>
|
||||
<path d="M5 12.6105C5.86278 13.1731 6.89321 13.5 8 13.5C11.0376 13.5 13.5 11.0376 13.5 8V7" stroke="#6C707E" stroke-linecap="round"/>
|
||||
<path d="M0.49997 7.50027L2.5 9.5L4.49998 7.50023" stroke="#6C707E" stroke-linecap="round"/>
|
||||
<path d="M11.5 8.49982L13.5 6.5L15.5 8.49982" stroke="#6C707E" stroke-linecap="round"/>
|
||||
</svg>
|
||||
```
|
||||
|
||||
`stroke-width` defaults to `1`. `stroke-linecap="round"` softens path ends; pair it with `stroke-linejoin="round"` whenever paths bend.
|
||||
|
||||
For secondary glyphs (chevrons in popups, dropdown arrows) use `#818594` light / `#6F737A` dark instead of the primary gray.
|
||||
|
||||
---
|
||||
|
||||
## 4. Node icon (16×16) — tinted circle with stroke + glyph
|
||||
|
||||
Class / method nodes — the canonical PSI-node shape.
|
||||
|
||||
**Light (class — blue):**
|
||||
```svg
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="8" cy="8" r="6.5" fill="#E7EFFD" stroke="#3574F0"/>
|
||||
<path d="M8.13295 11.5C9.61223 11.5 10.8836 10.6105 11.2075 9.33909H10.2213C9.90229 10.0739 9.11914 10.6057 8.13295 10.6057C6.77936 10.6057 5.80284 9.51796 5.80284 8C5.80284 6.48204 6.77936 5.39434 8.13295 5.39434C9.11914 5.39434 9.90229 5.92611 10.2213 6.66091H11.2075C10.8836 5.3895 9.61223 4.5 8.13295 4.5C6.21859 4.5 4.79248 5.99378 4.79248 8C4.79248 10.0062 6.21859 11.5 8.13295 11.5Z" fill="#3574F0"/>
|
||||
</svg>
|
||||
```
|
||||
|
||||
**Dark (class):**
|
||||
```svg
|
||||
<circle cx="8" cy="8" r="6.5" fill="#25324D" stroke="#548AF7"/>
|
||||
<path d="M8.13295 11.5C9.61223 11.5 10.8836 10.6105 11.2075 9.33909H10.2213C9.90229 10.0739 9.11914 10.6057 8.13295 10.6057C6.77936 10.6057 5.80284 9.51796 5.80284 8C5.80284 6.48204 6.77936 5.39434 8.13295 5.39434C9.11914 5.39434 9.90229 5.92611 10.2213 6.66091H11.2075C10.8836 5.3895 9.61223 4.5 8.13295 4.5C6.21859 4.5 4.79248 5.99378 4.79248 8C4.79248 10.0062 6.21859 11.5 8.13295 11.5Z" fill="#548AF7"/>
|
||||
```
|
||||
|
||||
**Method (red) follows the same template** with `#FFF7F7`/`#DB3B4B` (light) → `#402929`/`#DB5C5C` (dark).
|
||||
|
||||
Rules:
|
||||
- Circle is always `cx="8" cy="8" r="6.5"` (so the 1-px stroke is centered between pixels 1 and 15).
|
||||
- Fill = soft background pair, stroke = accent.
|
||||
- Glyph inside uses the same accent color filled — never stroked.
|
||||
|
||||
---
|
||||
|
||||
## 5. Status badge (16×16) — circle/triangle with white glyph
|
||||
|
||||
Error badge:
|
||||
```svg
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="8" cy="8" r="7" fill="#E55765"/>
|
||||
<path d="M9 5C9 4.44772 8.55228 4 8 4C7.44772 4 7 4.44772 7 5V7.5C7 8.05229 7.44772 8.5 8 8.5C8.55229 8.5 9 8.05228 9 7.5L9 5Z" fill="white"/>
|
||||
<path d="M8 12C8.55228 12 9 11.5523 9 11C9 10.4477 8.55228 10 8 10C7.44772 10 7 10.4477 7 11C7 11.5523 7.44772 12 8 12Z" fill="white"/>
|
||||
</svg>
|
||||
```
|
||||
|
||||
The warning badge is the same idea but with a rounded triangle filled `#FFAF0F` (light) / `#F2C55C` (dark). In the dark theme, the glyph inside the warning triangle uses `#5E4D33` instead of `white` (white on warm yellow is unreadable).
|
||||
|
||||
The success badge uses a 2-px stroked checkmark inside a `#55A76A` (light) / `#57965C` (dark) circle — `stroke="white"` and `stroke-width="2"`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Tool window stripe (20×20) — single-tone glyph on a larger canvas
|
||||
|
||||
Build tool-window stripe:
|
||||
```svg
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.25 1C4.44891 1 4.63962 1.07907 4.78027 1.21973L5.56055 2H6.43945L7.21973 1.21973L7.33398 1.12598C7.45628 1.04445 7.60085 1 7.75 1H13.125C14.9473 1 16.4277 1.87993 17.4346 3.08105C18.4322 4.27143 19 5.81492 19 7.25C19 7.51735 18.8572 7.76425 18.626 7.89844C18.3948 8.03247 18.11 8.0339 17.8779 7.90137L15.4258 6.5H14.0605L13 7.56055V18.25C13 18.6642 12.6642 19 12.25 19H7.75C7.33579 19 7 18.6642 7 18.25V7.56055L6.43945 7H5.56055L4.78027 7.78027C4.63962 7.92093 4.44891 8 4.25 8H1.75C1.33579 8 1 7.66421 1 7.25V1.75C1 1.33579 1.33579 1 1.75 1H4.25ZM8.5 17.5H11.5V8H8.5V17.5ZM2.5 6.5H3.93945L4.71973 5.71973L4.83398 5.62598C4.95628 5.54445 5.10085 5.5 5.25 5.5H6.75C6.94891 5.5 7.13962 5.57907 7.28027 5.71973L8.06055 6.5H11.9395L13.2197 5.21973L13.334 5.12598C13.4563 5.04445 13.6008 5 13.75 5H15.625C15.7555 5 15.8838 5.03392 15.9971 5.09863L17.2637 5.82227C17.0602 5.19249 16.7287 4.57423 16.2842 4.04395C15.5098 3.12029 14.4276 2.5 13.125 2.5H8.06055L7.28027 3.28027C7.13962 3.42093 6.94891 3.5 6.75 3.5H5.25C5.05109 3.5 4.86038 3.42093 4.71973 3.28027L3.93945 2.5H2.5V6.5Z" fill="#6C707E"/>
|
||||
</svg>
|
||||
```
|
||||
|
||||
Notes:
|
||||
- 20×20 canvas with **at least 2 px** breathing room on each edge (geometry lives in `2..18`).
|
||||
- Re-balance vs. a 16×16 sibling — do not just paste a 16×16 path into a 20×20 viewBox.
|
||||
- Keep a 16×16 sibling (`name.svg`) for compact mode; the `@20x20` variant is used by default.
|
||||
|
||||
---
|
||||
|
||||
## 7. Main toolbar action (20×20) — same template as tool window
|
||||
|
||||
Add action (20×20):
|
||||
```svg
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.75586 17.25C9.34165 17.25 9.00586 16.9142 9.00586 16.5V10.5059H3C2.58594 10.5059 2.25025 10.1699 2.25 9.75586C2.25 9.34165 2.58579 9.00586 3 9.00586H9.00586V3C9.00607 2.58597 9.34178 2.25 9.75586 2.25C10.1699 2.25 10.5056 2.58597 10.5059 3V9.00586H16.5C16.9142 9.00586 17.25 9.34165 17.25 9.75586C17.2498 10.1699 16.9141 10.5059 16.5 10.5059H10.5059V16.5C10.5059 16.9142 10.1701 17.25 9.75586 17.25Z" fill="#6C707E"/>
|
||||
</svg>
|
||||
```
|
||||
|
||||
Main-toolbar icons reuse the 20×20 conventions of tool window stripes — the canvas size and stroke weights are identical, only the visual semantics differ.
|
||||
|
||||
---
|
||||
|
||||
## 8. Gutter icon (14×14) — thin stroke, no padding
|
||||
|
||||
Edit-doc gutter mark:
|
||||
```svg
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.5973 6.65471L12.6882 4.56049C13.1053 4.15406 13.1003 3.49602 12.6948 3.08627L11.0267 1.3136L11.0224 1.30932C10.6123 0.900035 9.94199 0.893268 9.53311 1.31079L7.3867 3.44406M10.5973 6.65471L7.3867 3.44406M10.5973 6.65471L4.74041 12.5H1.50036L1.5 9.32001L7.3867 3.44406" stroke="#6C707E" stroke-miterlimit="10"/>
|
||||
</svg>
|
||||
```
|
||||
|
||||
Gutter icons:
|
||||
- Canvas is 14×14 (not 16×16). Glyph extends to the edge — there is *no* outer padding margin in the gutter.
|
||||
- Prefer `stroke-miterlimit="10"` for hard-pointed glyphs (pencil tip), `stroke-linecap="round"` for chevrons / arrows.
|
||||
- Compound gutter glyphs (e.g. implemented + override marker) layer two badges; keep each badge ≤ 9 px so the composition stays legible.
|
||||
|
||||
---
|
||||
|
||||
## 9. Breakpoint mark (14×14) — solid filled circle
|
||||
|
||||
Breakpoint mark:
|
||||
```svg
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7 13C10.3137 13 13 10.3137 13 7C13 3.68629 10.3137 1 7 1C3.68629 1 1 3.68629 1 7C1 10.3137 3.68629 13 7 13Z" fill="#E55765"/>
|
||||
</svg>
|
||||
```
|
||||
|
||||
Breakpoint marks read at a glance — a single solid fill in the status-error red. Variants (disabled, conditional, log) layer a small overlay in the bottom-right corner.
|
||||
|
||||
---
|
||||
|
||||
## 10. Dark-variant gotcha — white-on-warm vs. muted-dark
|
||||
|
||||
The warning badge dark variant shows the only common case where a literal find-and-replace from the light SVG would produce a broken icon:
|
||||
|
||||
```svg
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.27603 10.8634L6.3028 1.98903C7.04977 0.670323 8.94893 0.670326 9.69589 1.98903L14.7227 10.8634C15.516 12.2639 14.5047 14 12.8956 14H3.10308C1.494 14 0.482737 12.2639 1.27603 10.8634Z" fill="#F2C55C"/>
|
||||
<path d="M9 5C9 4.44772 8.55228 4 8 4C7.44772 4 7 4.44772 7 5V7.5C7 8.05229 7.44772 8.5 8 8.5C8.55229 8.5 9 8.05228 9 7.5L9 5Z" fill="#5E4D33"/>
|
||||
<path d="M8 12C8.55228 12 9 11.5523 9 11C9 10.4477 8.55228 10 8 10C7.44772 10 7 10.4477 7 11C7 11.5523 7.44772 12 8 12Z" fill="#5E4D33"/>
|
||||
</svg>
|
||||
```
|
||||
|
||||
`white` → `#5E4D33` inside the warning triangle. Apply the same swap whenever a light icon paints a glyph as `white` over a warm fill (`#FFAF0F`, `#E66D17`, etc.). For cool fills (`#3574F0`, `#208A3C`, `#DB3B4B`), the dark-theme glyph stays `white` because contrast is still sufficient.
|
||||
@@ -0,0 +1,118 @@
|
||||
# New UI Icon Palette
|
||||
|
||||
Canonical color palette used by New UI icons. When picking colors, always pull from this table; the IntelliJ icon engine recolors by exact-string match, so a one-off hue will fail to theme.
|
||||
|
||||
## How to read the mapping
|
||||
|
||||
- **Light** column is the color used in the default (light) SVG file: `foo.svg`.
|
||||
- **Dark** column is its replacement in the `_dark` partner: `foo_dark.svg`.
|
||||
- Always swap **all** light colors at once when generating the dark variant. Do not mix-and-match — the palette pairs are tuned for contrast.
|
||||
|
||||
## Neutrals (most icons need only these)
|
||||
|
||||
| Role | Light | Dark |
|
||||
|---|---|---|
|
||||
| **Primary stroke / fill** (main glyph) | `#6C707E` | `#CED0D6` |
|
||||
| Secondary stroke (chevrons, faint glyph) | `#818594` | `#6F737A` |
|
||||
| Tertiary stroke (very faint) | `#A8ADBD` | `#9DA0A8` |
|
||||
| Disabled / inert fill background | `#EBECF0` | `#43454A` |
|
||||
| Background plate (rare) | `#F0F1F2` | `#5A5D63` / `#1E1F22` |
|
||||
| Plain white (status glyph only) | `white` | per-status muted (see below) |
|
||||
|
||||
`#6C707E` ↔ `#CED0D6` is the single most-used pair across the entire icon set. If you only remember one pair, remember this one.
|
||||
|
||||
## Accent — Blue (primary action, info, default node)
|
||||
|
||||
| Role | Light | Dark |
|
||||
|---|---|---|
|
||||
| Stroke / fill | `#3574F0` | `#548AF7` |
|
||||
| Soft background fill (node circle) | `#E7EFFD` | `#25324D` |
|
||||
| Tinted-panel fill | `#EDF3FF` | `#25324D` |
|
||||
| Alt blue (rare) | `#4682FA` | `#5F93FF` |
|
||||
|
||||
## Accent — Red (delete, error nodes, breakpoints)
|
||||
|
||||
| Role | Light | Dark |
|
||||
|---|---|---|
|
||||
| Stroke / fill | `#DB3B4B` | `#DB5C5C` |
|
||||
| Soft background fill | `#FFF7F7` | `#402929` |
|
||||
| Status badge fill (error circle) | `#E55765` | `#DB5C5C` |
|
||||
|
||||
The slightly different `#DB3B4B` (line / accent) vs `#E55765` (badge fill) split is intentional — the filled status circle is a touch more saturated than the line accent.
|
||||
|
||||
## Accent — Green (run, success, version-control add)
|
||||
|
||||
| Role | Light | Dark |
|
||||
|---|---|---|
|
||||
| Stroke / fill | `#208A3C` | `#57965C` |
|
||||
| Soft background fill | `#F2FCF3` | `#253627` |
|
||||
| Status badge fill (success circle) | `#55A76A` | `#57965C` |
|
||||
| Alt green (rare) | `#369650` | `#5FAD65` |
|
||||
|
||||
## Accent — Orange / Yellow (warning, modified, bookmark)
|
||||
|
||||
| Role | Light | Dark |
|
||||
|---|---|---|
|
||||
| Orange stroke / fill | `#E66D17` | `#C77D55` |
|
||||
| Orange soft fill | `#FFF4EB` | `#45322B` |
|
||||
| Warning yellow (status triangle) | `#FFAF0F` | `#F2C55C` |
|
||||
| Warning soft fill | `#FFFAEB` / `#F7E4CD` | `#3D3223` |
|
||||
| Brown / gold deep | `#C27D04` | `#D6AE58` |
|
||||
| Muted dark glyph inside warning fill | n/a (uses `white`) | `#5E4D33` |
|
||||
|
||||
The "white on yellow" combination is illegible in Dark theme; that's why the dark warning glyph uses `#5E4D33` (a deep brown). Apply the same idea — muted-dark glyph inside the warm fill — for any other warm-color badge in the dark theme.
|
||||
|
||||
## Accent — Purple (annotations, special / preview)
|
||||
|
||||
| Role | Light | Dark |
|
||||
|---|---|---|
|
||||
| Stroke / fill | `#834DF0` | `#B589EC` |
|
||||
| Soft background fill | `#FAF5FF` | `#2F2936` |
|
||||
| Alt purple (rare) | n/a | `#A571E6` |
|
||||
|
||||
## Reserved / utility
|
||||
|
||||
| Role | Light/Dark |
|
||||
|---|---|
|
||||
| Inside-circle glyph (light status) | `white` |
|
||||
| Empty / placeholder | `#231F20` (avoid) |
|
||||
| Tool window panel base (very rare) | `#1E1F22` (dark) |
|
||||
| Other repository-specific tints | only when a sibling icon already uses them |
|
||||
|
||||
If your icon needs a color that is not in this table, find a sibling icon that uses something similar and reuse its exact hex string. If no sibling uses it, change your design — do not introduce a new color.
|
||||
|
||||
## Algorithmic light→dark swap (for scripted generation)
|
||||
|
||||
```text
|
||||
#6C707E -> #CED0D6 primary stroke/fill
|
||||
#818594 -> #6F737A secondary stroke
|
||||
#A8ADBD -> #9DA0A8 tertiary stroke
|
||||
#EBECF0 -> #43454A disabled fill
|
||||
|
||||
#3574F0 -> #548AF7 blue
|
||||
#EDF3FF -> #25324D blue soft
|
||||
#E7EFFD -> #25324D blue node fill
|
||||
|
||||
#DB3B4B -> #DB5C5C red
|
||||
#FFF7F7 -> #402929 red soft
|
||||
#E55765 -> #DB5C5C error badge fill
|
||||
|
||||
#208A3C -> #57965C green
|
||||
#55A76A -> #57965C success badge fill
|
||||
#F2FCF3 -> #253627 green soft
|
||||
#369650 -> #5FAD65 alt green
|
||||
|
||||
#E66D17 -> #C77D55 orange
|
||||
#FFF4EB -> #45322B orange soft
|
||||
#FFAF0F -> #F2C55C warning yellow
|
||||
#FFFAEB -> #3D3223 warning soft
|
||||
#F7E4CD -> #3D3223 warning soft alt
|
||||
#C27D04 -> #D6AE58 gold
|
||||
|
||||
#834DF0 -> #B589EC purple
|
||||
#FAF5FF -> #2F2936 purple soft
|
||||
|
||||
white -> (status-glyph specific muted dark — see Orange/Yellow section)
|
||||
```
|
||||
|
||||
A script can do a literal find-and-replace using this map to produce a `_dark.svg` from a light SVG; review the result manually for any white-on-warm cases that need the muted-dark glyph treatment.
|
||||
Vendored
+14
@@ -23,6 +23,20 @@
|
||||
"env": {
|
||||
"KILO_API_URL": "http://localhost:3000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "VSCode - Run Extension (Isolated)",
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"command": "bun script/launch.ts --isolated --workspace \"${workspaceFolder}\"",
|
||||
"cwd": "${workspaceFolder}/packages/kilo-vscode"
|
||||
},
|
||||
{
|
||||
"name": "VSCode - Run Extension (Isolated Clean)",
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"command": "bun script/launch.ts --isolated --clean --workspace \"${workspaceFolder}\"",
|
||||
"cwd": "${workspaceFolder}/packages/kilo-vscode"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+23
@@ -94,6 +94,29 @@
|
||||
"dependsOn": ["VSCode - Install"],
|
||||
"problemMatcher": ["$tsc", "$eslint-stylish"]
|
||||
},
|
||||
{
|
||||
"label": "VSCode - Clear Isolated State",
|
||||
"type": "process",
|
||||
"command": "bun",
|
||||
"args": [
|
||||
"-e",
|
||||
"const fs = require(\"node:fs\"); const dir = process.argv[1]; if (!dir) { console.error(\"Isolated state path argument is required\"); process.exit(1); } fs.rmSync(dir, { recursive: true, force: true }); console.log(\"Cleared \" + dir)",
|
||||
"${workspaceFolder}/.kilo-dev"
|
||||
],
|
||||
"presentation": {
|
||||
"reveal": "always"
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "VSCode - Compile (Clean Isolated)",
|
||||
"dependsOn": ["VSCode - Clear Isolated State", "VSCode - Compile"],
|
||||
"dependsOrder": "sequence",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "VSCode - Tests",
|
||||
"type": "shell",
|
||||
|
||||
@@ -11,7 +11,7 @@ Kilo CLI is an open source AI coding agent that generates code from natural lang
|
||||
|
||||
- **Dev**: `bun run dev` (runs from root) or `bun run --cwd packages/opencode --conditions=browser src/index.ts`
|
||||
- **Dev with params**: `bun dev -- help`
|
||||
- **Extension**: `bun run extension` (build + launch VS Code with the extension in dev mode). Pass `--no-build` to skip the build.
|
||||
- **Extension**: `bun run extension` (build + launch VS Code with the extension in dev mode). Pass `--no-build` to skip the build. When asked to run an isolated VS Code/Kilo environment, use the CLI scripts instead of interactive launch configs: `bun run extension:isolated` reuses `.kilo-dev/`, and `bun run extension:isolated:clean` clears `.kilo-dev/` first. Pass an optional workspace path after `--`, for example `bun run extension:isolated -- ../sample-project`.
|
||||
- **Typecheck**: `bun turbo typecheck` (uses `tsgo`, not `tsc`). Includes the JetBrains plugin and requires Java 21; do not run `java -version` as a routine preflight. Only check Java when a Gradle/Java command fails with a Java-version or missing-Java error. If missing, install via SDKMAN: `sdk install java 21-tem && sdk use java 21-tem`. If SDKMAN is not installed, see https://sdkman.io/install.
|
||||
- **Test**: `bun test` from `packages/opencode/` (NOT from root -- root blocks tests)
|
||||
- **Single test**: `bun test ./test/tool/tool-define.test.ts` from `packages/opencode/`
|
||||
|
||||
+11
-2
@@ -114,10 +114,19 @@ For manual docs validation, run the docs site locally, preview the affected page
|
||||
Build and launch the extension in an isolated VS Code instance:
|
||||
|
||||
```bash
|
||||
bun run extension # Build + launch in dev mode
|
||||
bun run extension # Build + launch in dev mode
|
||||
bun run extension:isolated # Build + launch with persistent isolated IDE + Kilo state
|
||||
bun run extension:isolated:clean # Clear isolated state, then build + launch
|
||||
```
|
||||
|
||||
This auto-detects VS Code on macOS, Linux, and Windows. Override with `--app-path PATH` or `VSCODE_EXEC_PATH`. Use `--insiders` to prefer Insiders, `--workspace PATH` to open a specific folder, or `--clean` to reset cached state.
|
||||
This auto-detects VS Code on macOS, Linux, and Windows. Override with `--app-path PATH` or `VSCODE_EXEC_PATH`. Use `--insiders` to prefer Insiders, pass a directory argument to open a specific folder, or use `--workspace PATH` for the same behavior.
|
||||
|
||||
The isolated modes are for testing the extension without touching your primary VS Code profile or real Kilo config. `extension:isolated` reuses `.kilo-dev/` on each run, so installed extensions, VS Code settings, Kilo auth, sessions, config, state, and cache persist across launches. `extension:isolated:clean` deletes `.kilo-dev/` before launching, which simulates a fresh install while still keeping all state inside this repo checkout.
|
||||
|
||||
```bash
|
||||
bun run extension:isolated -- ../sample-project
|
||||
bun run extension:isolated:clean -- ../sample-project
|
||||
```
|
||||
|
||||
### Developing the JetBrains Plugin
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opencode-ai/core",
|
||||
"version": "7.4.17",
|
||||
"version": "7.4.18",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -127,7 +127,7 @@
|
||||
},
|
||||
"packages/effect-drizzle-sqlite": {
|
||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||
"version": "7.4.17",
|
||||
"version": "7.4.18",
|
||||
"dependencies": {
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -141,7 +141,7 @@
|
||||
},
|
||||
"packages/effect-sqlite-node": {
|
||||
"name": "@opencode-ai/effect-sqlite-node",
|
||||
"version": "7.4.17",
|
||||
"version": "7.4.18",
|
||||
"dependencies": {
|
||||
"effect": "catalog:",
|
||||
},
|
||||
@@ -153,7 +153,7 @@
|
||||
},
|
||||
"packages/http-recorder": {
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"version": "7.4.17",
|
||||
"version": "7.4.18",
|
||||
"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.17",
|
||||
"version": "7.4.18",
|
||||
"dependencies": {
|
||||
"@kilocode/kilo-indexing": "workspace:*",
|
||||
"@kilocode/kilo-web-ui": "workspace:*",
|
||||
@@ -197,7 +197,7 @@
|
||||
},
|
||||
"packages/kilo-docs": {
|
||||
"name": "@kilocode/kilo-docs",
|
||||
"version": "7.4.17",
|
||||
"version": "7.4.18",
|
||||
"dependencies": {
|
||||
"@docsearch/css": "^4",
|
||||
"@docsearch/js": "^4",
|
||||
@@ -227,7 +227,7 @@
|
||||
},
|
||||
"packages/kilo-gateway": {
|
||||
"name": "@kilocode/kilo-gateway",
|
||||
"version": "7.4.17",
|
||||
"version": "7.4.18",
|
||||
"dependencies": {
|
||||
"@ai-sdk/alibaba": "1.0.17",
|
||||
"@ai-sdk/anthropic": "3.0.82",
|
||||
@@ -263,7 +263,7 @@
|
||||
},
|
||||
"packages/kilo-i18n": {
|
||||
"name": "@kilocode/kilo-i18n",
|
||||
"version": "7.4.17",
|
||||
"version": "7.4.18",
|
||||
"devDependencies": {
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
@@ -273,7 +273,7 @@
|
||||
},
|
||||
"packages/kilo-indexing": {
|
||||
"name": "@kilocode/kilo-indexing",
|
||||
"version": "7.4.17",
|
||||
"version": "7.4.18",
|
||||
"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.16",
|
||||
"version": "7.4.17",
|
||||
},
|
||||
"packages/kilo-memory": {
|
||||
"name": "@kilocode/kilo-memory",
|
||||
"version": "7.4.17",
|
||||
"version": "7.4.18",
|
||||
"dependencies": {
|
||||
"effect": "catalog:",
|
||||
"zod": "catalog:",
|
||||
@@ -323,7 +323,7 @@
|
||||
},
|
||||
"packages/kilo-sandbox": {
|
||||
"name": "@kilocode/sandbox",
|
||||
"version": "7.4.17",
|
||||
"version": "7.4.18",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sandbox-runtime": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -338,7 +338,7 @@
|
||||
},
|
||||
"packages/kilo-telemetry": {
|
||||
"name": "@kilocode/kilo-telemetry",
|
||||
"version": "7.4.17",
|
||||
"version": "7.4.18",
|
||||
"dependencies": {
|
||||
"@kilocode/kilo-gateway": "workspace:*",
|
||||
"posthog-node": "4.4.0",
|
||||
@@ -352,7 +352,7 @@
|
||||
},
|
||||
"packages/kilo-ui": {
|
||||
"name": "@kilocode/kilo-ui",
|
||||
"version": "7.4.17",
|
||||
"version": "7.4.18",
|
||||
"dependencies": {
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"@kobalte/core": "0.13.11",
|
||||
@@ -362,6 +362,7 @@
|
||||
"@solid-primitives/media": "2.3.3",
|
||||
"@solid-primitives/resize-observer": "2.1.5",
|
||||
"@solid-primitives/rootless": "1.5.2",
|
||||
"chart.js": "4.5.1",
|
||||
"diff": "catalog:",
|
||||
"lucide-solid": "0.576.0",
|
||||
"motion": "12.34.5",
|
||||
@@ -389,7 +390,7 @@
|
||||
},
|
||||
"packages/kilo-vscode": {
|
||||
"name": "kilo-code",
|
||||
"version": "7.4.17",
|
||||
"version": "7.4.18",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.39.0",
|
||||
"@kilocode/kilo-gateway": "workspace:*",
|
||||
@@ -458,7 +459,7 @@
|
||||
},
|
||||
"packages/kilo-web-ui": {
|
||||
"name": "@kilocode/kilo-web-ui",
|
||||
"version": "7.4.17",
|
||||
"version": "7.4.18",
|
||||
"dependencies": {
|
||||
"@kilocode/kilo-ui": "workspace:*",
|
||||
"@kobalte/core": "catalog:",
|
||||
@@ -475,7 +476,7 @@
|
||||
},
|
||||
"packages/llm": {
|
||||
"name": "@opencode-ai/llm",
|
||||
"version": "7.4.17",
|
||||
"version": "7.4.18",
|
||||
"dependencies": {
|
||||
"@smithy/eventstream-codec": "4.2.14",
|
||||
"@smithy/util-utf8": "4.2.2",
|
||||
@@ -493,7 +494,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "@kilocode/cli",
|
||||
"version": "7.4.17",
|
||||
"version": "7.4.18",
|
||||
"bin": {
|
||||
"kilo": "./bin/kilo",
|
||||
"kilocode": "./bin/kilo",
|
||||
@@ -660,7 +661,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@kilocode/plugin",
|
||||
"version": "7.4.17",
|
||||
"version": "7.4.18",
|
||||
"dependencies": {
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
@@ -688,7 +689,7 @@
|
||||
},
|
||||
"packages/plugin-atomic-chat": {
|
||||
"name": "@kilocode/plugin-atomic-chat",
|
||||
"version": "7.4.17",
|
||||
"version": "7.4.18",
|
||||
"dependencies": {
|
||||
"@kilocode/plugin": "workspace:*",
|
||||
},
|
||||
@@ -702,7 +703,7 @@
|
||||
},
|
||||
"packages/script": {
|
||||
"name": "@opencode-ai/script",
|
||||
"version": "7.4.17",
|
||||
"version": "7.4.18",
|
||||
"dependencies": {
|
||||
"semver": "^7.6.3",
|
||||
},
|
||||
@@ -713,7 +714,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@kilocode/sdk",
|
||||
"version": "7.4.17",
|
||||
"version": "7.4.18",
|
||||
"dependencies": {
|
||||
"cross-spawn": "catalog:",
|
||||
},
|
||||
@@ -728,7 +729,7 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@opencode-ai/server",
|
||||
"version": "7.4.17",
|
||||
"version": "7.4.18",
|
||||
"dependencies": {
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"drizzle-orm": "catalog:",
|
||||
@@ -742,7 +743,7 @@
|
||||
},
|
||||
"packages/storybook": {
|
||||
"name": "@opencode-ai/storybook",
|
||||
"version": "7.4.17",
|
||||
"version": "7.4.18",
|
||||
"devDependencies": {
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@solidjs/meta": "catalog:",
|
||||
@@ -765,7 +766,7 @@
|
||||
},
|
||||
"packages/tui": {
|
||||
"name": "@opencode-ai/tui",
|
||||
"version": "7.4.17",
|
||||
"version": "7.4.18",
|
||||
"dependencies": {
|
||||
"@kilocode/plugin": "workspace:*",
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
@@ -792,7 +793,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "7.4.17",
|
||||
"version": "7.4.18",
|
||||
"dependencies": {
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"@kobalte/core": "catalog:",
|
||||
@@ -1557,6 +1558,8 @@
|
||||
|
||||
"@kobalte/utils": ["@kobalte/utils@0.9.1", "", { "dependencies": { "@solid-primitives/event-listener": "^2.2.14", "@solid-primitives/keyed": "^1.2.0", "@solid-primitives/map": "^0.4.7", "@solid-primitives/media": "^2.2.4", "@solid-primitives/props": "^3.1.8", "@solid-primitives/refs": "^1.0.5", "@solid-primitives/utils": "^6.2.1" }, "peerDependencies": { "solid-js": "^1.8.8" } }, "sha512-eeU60A3kprIiBDAfv9gUJX1tXGLuZiKMajUfSQURAF2pk4ZoMYiqIzmrMBvzcxP39xnYttgTyQEVLwiTZnrV4w=="],
|
||||
|
||||
"@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="],
|
||||
|
||||
"@kwsites/file-exists": ["@kwsites/file-exists@1.1.1", "", { "dependencies": { "debug": "^4.1.1" } }, "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw=="],
|
||||
|
||||
"@kwsites/promise-deferred": ["@kwsites/promise-deferred@1.1.1", "", {}, "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw=="],
|
||||
@@ -2865,6 +2868,8 @@
|
||||
|
||||
"chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="],
|
||||
|
||||
"chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="],
|
||||
|
||||
"check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="],
|
||||
|
||||
"cheerio": ["cheerio@1.2.0", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "encoding-sniffer": "^0.2.1", "htmlparser2": "^10.1.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", "undici": "^7.19.0", "whatwg-mimetype": "^4.0.0" } }, "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg=="],
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-sid/FCql4CmiAb1MNGMH33fwNs6esv9UNC819Yb03bQ=",
|
||||
"aarch64-linux": "sha256-1H1873nLqmcKrumAjnjPPts5+EyJLBm1Z5VKsOYANeg=",
|
||||
"aarch64-darwin": "sha256-i7D5RSPIVxXNY1gUnsYdFhVU2FxYEcjqYcQx6zZrJjg=",
|
||||
"x86_64-darwin": "sha256-5lSTqolFVcaKqN6ndKGJLcCUP8JQ8i60np+3eAulnZg="
|
||||
"x86_64-linux": "sha256-gQFDvDbTsTNge6ji5hToanPCbXHUqbx4ccAN/isBl3c=",
|
||||
"aarch64-linux": "sha256-iLfVhjLJoCyudlcdw1tPW0MYt4fLdgxooC9lGb0wTy0=",
|
||||
"aarch64-darwin": "sha256-ohr/Nu0GbHAECsqpB93AaNBbKHWJTjbqQgEwFoim5kw=",
|
||||
"x86_64-darwin": "sha256-vWaL8d4k01J4R9ROd44BJYTsPLGt7yiismBNThlwlOY="
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -19,6 +19,8 @@
|
||||
"sso": "aws sso login --sso-session=opencode --no-browser",
|
||||
"test": "echo 'do not run tests from root' && exit 1",
|
||||
"extension": "bun --cwd packages/kilo-vscode script/launch.ts",
|
||||
"extension:isolated": "bun --cwd packages/kilo-vscode script/launch.ts --isolated",
|
||||
"extension:isolated:clean": "bun --cwd packages/kilo-vscode script/launch.ts --isolated --clean",
|
||||
"dev-setup": "bun run --cwd packages/opencode --conditions=browser src/index.ts dev-setup"
|
||||
},
|
||||
"workspaces": {
|
||||
@@ -171,6 +173,6 @@
|
||||
"pacote@21.5.1": "patches/pacote@21.5.1.patch",
|
||||
"mammoth@1.12.0": "patches/mammoth@1.12.0.patch"
|
||||
},
|
||||
"version": "7.4.17",
|
||||
"version": "7.4.18",
|
||||
"peerDependencies": {}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "7.4.17",
|
||||
"version": "7.4.18",
|
||||
"name": "@opencode-ai/core",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
+173
-48
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"id": "169a0f0f-d58f-479f-b024-fa1c7b9a09db",
|
||||
"prevIds": ["abd2f920-b822-49af-b8a7-2e48367d424f"],
|
||||
"id": "7a2d751a-b26c-4a89-9905-800e3f6a8b8a",
|
||||
"prevIds": [
|
||||
"169a0f0f-d58f-479f-b024-fa1c7b9a09db"
|
||||
],
|
||||
"ddl": [
|
||||
{
|
||||
"name": "workspace",
|
||||
@@ -1511,9 +1513,13 @@
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"columns": ["project_id"],
|
||||
"columns": [
|
||||
"project_id"
|
||||
],
|
||||
"tableTo": "project",
|
||||
"columnsTo": ["id"],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
@@ -1522,9 +1528,13 @@
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"columns": ["active_account_id"],
|
||||
"columns": [
|
||||
"active_account_id"
|
||||
],
|
||||
"tableTo": "account",
|
||||
"columnsTo": ["id"],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "SET NULL",
|
||||
"nameExplicit": false,
|
||||
@@ -1533,9 +1543,13 @@
|
||||
"table": "account_state"
|
||||
},
|
||||
{
|
||||
"columns": ["aggregate_id"],
|
||||
"columns": [
|
||||
"aggregate_id"
|
||||
],
|
||||
"tableTo": "event_sequence",
|
||||
"columnsTo": ["aggregate_id"],
|
||||
"columnsTo": [
|
||||
"aggregate_id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
@@ -1544,9 +1558,13 @@
|
||||
"table": "event"
|
||||
},
|
||||
{
|
||||
"columns": ["project_id"],
|
||||
"columns": [
|
||||
"project_id"
|
||||
],
|
||||
"tableTo": "project",
|
||||
"columnsTo": ["id"],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
@@ -1555,9 +1573,13 @@
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"columns": ["project_id"],
|
||||
"columns": [
|
||||
"project_id"
|
||||
],
|
||||
"tableTo": "project",
|
||||
"columnsTo": ["id"],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
@@ -1566,9 +1588,13 @@
|
||||
"table": "project_directory"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"tableTo": "session",
|
||||
"columnsTo": ["id"],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
@@ -1577,9 +1603,13 @@
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"columns": ["message_id"],
|
||||
"columns": [
|
||||
"message_id"
|
||||
],
|
||||
"tableTo": "message",
|
||||
"columnsTo": ["id"],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
@@ -1588,9 +1618,13 @@
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"tableTo": "session",
|
||||
"columnsTo": ["id"],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
@@ -1599,9 +1633,13 @@
|
||||
"table": "session_context_epoch"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"tableTo": "session",
|
||||
"columnsTo": ["id"],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
@@ -1610,9 +1648,13 @@
|
||||
"table": "session_input"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"tableTo": "session",
|
||||
"columnsTo": ["id"],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
@@ -1621,9 +1663,13 @@
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"columns": ["project_id"],
|
||||
"columns": [
|
||||
"project_id"
|
||||
],
|
||||
"tableTo": "project",
|
||||
"columnsTo": ["id"],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
@@ -1632,9 +1678,13 @@
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"tableTo": "session",
|
||||
"columnsTo": ["id"],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
@@ -1643,9 +1693,13 @@
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"tableTo": "session",
|
||||
"columnsTo": ["id"],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
@@ -1654,133 +1708,174 @@
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"columns": ["email", "url"],
|
||||
"columns": [
|
||||
"email",
|
||||
"url"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "control_account_pk",
|
||||
"entityType": "pks",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"columns": ["project_id", "directory"],
|
||||
"columns": [
|
||||
"project_id",
|
||||
"directory"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "project_directory_pk",
|
||||
"entityType": "pks",
|
||||
"table": "project_directory"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id", "position"],
|
||||
"columns": [
|
||||
"session_id",
|
||||
"position"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "todo_pk",
|
||||
"entityType": "pks",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "workspace_pk",
|
||||
"table": "workspace",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["name"],
|
||||
"columns": [
|
||||
"name"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "data_migration_pk",
|
||||
"table": "data_migration",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "account_state_pk",
|
||||
"table": "account_state",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "account_pk",
|
||||
"table": "account",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "credential_pk",
|
||||
"table": "credential",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["aggregate_id"],
|
||||
"columns": [
|
||||
"aggregate_id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "event_sequence_pk",
|
||||
"table": "event_sequence",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "event_pk",
|
||||
"table": "event",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "permission_pk",
|
||||
"table": "permission",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "project_pk",
|
||||
"table": "project",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "message_pk",
|
||||
"table": "message",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "part_pk",
|
||||
"table": "part",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "session_context_epoch_pk",
|
||||
"table": "session_context_epoch",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "session_input_pk",
|
||||
"table": "session_input",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "session_message_pk",
|
||||
"table": "session_message",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "session_pk",
|
||||
"table": "session",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "session_share_pk",
|
||||
"table": "session_share",
|
||||
@@ -1902,6 +1997,36 @@
|
||||
"entityType": "indexes",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "session_id",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "id",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "message_id",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "json_extract(\"data\", '$.type')",
|
||||
"isExpression": true
|
||||
},
|
||||
{
|
||||
"value": "CASE WHEN json_extract(\"data\", '$.type') = 'text' THEN coalesce(json_extract(\"data\", '$.text'), '') WHEN json_extract(\"data\", '$.type') = 'file' THEN trim(coalesce(json_extract(\"data\", '$.filename'), '') || ' ' || CASE WHEN coalesce(json_extract(\"data\", '$.url'), '') NOT LIKE 'data:%' THEN coalesce(json_extract(\"data\", '$.url'), '') ELSE '' END || ' ' || coalesce(json_extract(\"data\", '$.source.path'), '') || ' ' || coalesce(json_extract(\"data\", '$.source.name'), '') || ' ' || CASE WHEN coalesce(json_extract(\"data\", '$.source.uri'), '') NOT LIKE 'data:%' THEN coalesce(json_extract(\"data\", '$.source.uri'), '') ELSE '' END || ' ' || coalesce(json_extract(\"data\", '$.source.clientName'), '')) ELSE coalesce(json_extract(\"data\", '$.state.error'), '') END",
|
||||
"isExpression": true
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": "(json_extract(\"part\".\"data\", '$.type') = 'text' AND coalesce(json_extract(\"part\".\"data\", '$.synthetic'), 0) = 0 AND coalesce(json_extract(\"part\".\"data\", '$.ignored'), 0) = 0) OR json_extract(\"part\".\"data\", '$.type') = 'file' OR (json_extract(\"part\".\"data\", '$.type') = 'tool' AND json_extract(\"part\".\"data\", '$.state.status') = 'error')",
|
||||
"origin": "manual",
|
||||
"name": "recall_part_search_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
@@ -2098,4 +2223,4 @@
|
||||
}
|
||||
],
|
||||
"renames": []
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ export class Info extends Schema.Class<Info>("CommandV2.Info")({
|
||||
description: Schema.String.pipe(Schema.optional),
|
||||
agent: Schema.String.pipe(Schema.optional),
|
||||
model: ModelV2.Ref.pipe(Schema.optional),
|
||||
variant: ModelV2.VariantID.pipe(Schema.optional), // kilocode_change - support variant-only command overrides
|
||||
subtask: Schema.Boolean.pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ export * as ConfigCommand from "./command"
|
||||
import { Schema } from "effect"
|
||||
|
||||
export class Info extends Schema.Class<Info>("ConfigV2.Command")({
|
||||
template: Schema.String,
|
||||
template: Schema.String.pipe(Schema.optional), // kilocode_change - allow partial command overrides
|
||||
description: Schema.String.pipe(Schema.optional),
|
||||
agent: Schema.String.pipe(Schema.optional),
|
||||
model: Schema.String.pipe(Schema.optional),
|
||||
|
||||
@@ -29,22 +29,33 @@ export const Plugin = PluginV2.define({
|
||||
}).pipe(Effect.map((documents) => documents.flat()))
|
||||
|
||||
yield* transform((editor) => {
|
||||
for (const document of documents) {
|
||||
for (const [name, command] of Object.entries(document.commands ?? {})) {
|
||||
editor.update(name, (item) => {
|
||||
item.template = command.template
|
||||
if (command.description !== undefined) item.description = command.description
|
||||
if (command.agent !== undefined) item.agent = command.agent
|
||||
if (command.model !== undefined) {
|
||||
const model = ModelV2.parse(command.model)
|
||||
item.model = { id: model.modelID, providerID: model.providerID, variant: item.model?.variant }
|
||||
}
|
||||
if (command.variant !== undefined && item.model !== undefined) {
|
||||
item.model.variant = ModelV2.VariantID.make(command.variant)
|
||||
}
|
||||
if (command.subtask !== undefined) item.subtask = command.subtask
|
||||
})
|
||||
const items = documents.flatMap((document) => Object.entries(document.commands ?? {}))
|
||||
// Register every template first, preserving the normal source priority for
|
||||
// metadata in the second pass. // kilocode_change
|
||||
for (const [name, command] of items) {
|
||||
if (command.template === undefined) {
|
||||
continue
|
||||
}
|
||||
const template = command.template
|
||||
editor.update(name, (item) => {
|
||||
item.template = template
|
||||
})
|
||||
}
|
||||
for (const [name, command] of items) {
|
||||
if (command.template === undefined && !editor.get(name)) continue // kilocode_change
|
||||
editor.update(name, (item) => {
|
||||
if (command.description !== undefined) item.description = command.description
|
||||
if (command.agent !== undefined) item.agent = command.agent
|
||||
if (command.model !== undefined) {
|
||||
const model = ModelV2.parse(command.model)
|
||||
item.model = { id: model.modelID, providerID: model.providerID, variant: item.model?.variant }
|
||||
}
|
||||
if (command.variant !== undefined) item.variant = ModelV2.VariantID.make(command.variant) // kilocode_change
|
||||
if (command.variant !== undefined && item.model !== undefined) {
|
||||
item.model.variant = ModelV2.VariantID.make(command.variant)
|
||||
}
|
||||
if (command.subtask !== undefined) item.subtask = command.subtask
|
||||
})
|
||||
}
|
||||
})
|
||||
}),
|
||||
|
||||
@@ -245,6 +245,7 @@ export default {
|
||||
yield* tx.run(`CREATE INDEX \`message_session_time_created_id_idx\` ON \`message\` (\`session_id\`,\`time_created\`,\`id\`);`)
|
||||
yield* tx.run(`CREATE INDEX \`part_message_id_id_idx\` ON \`part\` (\`message_id\`,\`id\`);`)
|
||||
yield* tx.run(`CREATE INDEX \`part_session_idx\` ON \`part\` (\`session_id\`);`)
|
||||
yield* tx.run(`CREATE INDEX \`recall_part_search_idx\` ON \`part\` (\`session_id\`,\`id\`,\`message_id\`,json_extract("data", '$.type'),CASE WHEN json_extract("data", '$.type') = 'text' THEN coalesce(json_extract("data", '$.text'), '') WHEN json_extract("data", '$.type') = 'file' THEN trim(coalesce(json_extract("data", '$.filename'), '') || ' ' || CASE WHEN coalesce(json_extract("data", '$.url'), '') NOT LIKE 'data:%' THEN coalesce(json_extract("data", '$.url'), '') ELSE '' END || ' ' || coalesce(json_extract("data", '$.source.path'), '') || ' ' || coalesce(json_extract("data", '$.source.name'), '') || ' ' || CASE WHEN coalesce(json_extract("data", '$.source.uri'), '') NOT LIKE 'data:%' THEN coalesce(json_extract("data", '$.source.uri'), '') ELSE '' END || ' ' || coalesce(json_extract("data", '$.source.clientName'), '')) ELSE coalesce(json_extract("data", '$.state.error'), '') END) WHERE json_valid("part"."data") AND ((json_extract("part"."data", '$.type') = 'text' AND coalesce(json_extract("part"."data", '$.synthetic'), 0) = 0 AND coalesce(json_extract("part"."data", '$.ignored'), 0) = 0) OR json_extract("part"."data", '$.type') = 'file' OR (json_extract("part"."data", '$.type') = 'tool' AND json_extract("part"."data", '$.state.status') = 'error'));`)
|
||||
yield* tx.run(`CREATE INDEX \`session_input_session_pending_delivery_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`,\`delivery\`,\`admitted_seq\`);`)
|
||||
yield* tx.run(`CREATE UNIQUE INDEX \`session_input_session_admitted_seq_idx\` ON \`session_input\` (\`session_id\`,\`admitted_seq\`);`)
|
||||
yield* tx.run(`CREATE UNIQUE INDEX \`session_input_session_promoted_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`);`)
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { Match } from "../filesystem/schema"
|
||||
|
||||
export interface Options {
|
||||
readonly context?: number
|
||||
readonly literal?: boolean
|
||||
readonly ignoreCase?: boolean
|
||||
}
|
||||
|
||||
export type GrepMatch = Match & {
|
||||
readonly context: boolean
|
||||
readonly textTruncated: boolean
|
||||
}
|
||||
|
||||
export const flags = (input: Options) => [
|
||||
...(input.literal ? ["--fixed-strings"] : []),
|
||||
...(input.ignoreCase ? ["--ignore-case"] : []),
|
||||
...(input.context ? [`--context=${input.context}`] : []),
|
||||
]
|
||||
|
||||
export const stop = (limit: number) => {
|
||||
let matches = 0
|
||||
return (row: { readonly context: boolean }) => !row.context && ++matches > limit
|
||||
}
|
||||
|
||||
export const select = <
|
||||
A extends {
|
||||
readonly context: boolean
|
||||
readonly path: { readonly text: string }
|
||||
readonly line_number: number
|
||||
},
|
||||
>(
|
||||
input: { readonly limit: number; readonly context?: number },
|
||||
items: readonly A[],
|
||||
) => {
|
||||
let count = 0
|
||||
const overflow = items.findIndex((row) => !row.context && ++count > input.limit)
|
||||
const selected = items.slice(0, overflow === -1 ? items.length : overflow)
|
||||
const matches = selected.filter((row) => !row.context)
|
||||
return selected.filter(
|
||||
(row) =>
|
||||
!row.context ||
|
||||
matches.some(
|
||||
(match) =>
|
||||
match.path.text === row.path.text && Math.abs(match.line_number - row.line_number) <= (input.context ?? 0),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export const decorate = (match: Match, context: boolean, textTruncated: boolean): GrepMatch => ({
|
||||
...match,
|
||||
context,
|
||||
textTruncated,
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
import { sql } from "drizzle-orm"
|
||||
import { index, type AnySQLiteColumn } from "drizzle-orm/sqlite-core"
|
||||
|
||||
export namespace RecallPartIndex {
|
||||
export const createSql = `CREATE INDEX IF NOT EXISTS \`recall_part_search_idx\` ON \`part\` (\`session_id\`,\`id\`,\`message_id\`,json_extract("data", '$.type'),CASE WHEN json_extract("data", '$.type') = 'text' THEN coalesce(json_extract("data", '$.text'), '') WHEN json_extract("data", '$.type') = 'file' THEN trim(coalesce(json_extract("data", '$.filename'), '') || ' ' || CASE WHEN coalesce(json_extract("data", '$.url'), '') NOT LIKE 'data:%' THEN coalesce(json_extract("data", '$.url'), '') ELSE '' END || ' ' || coalesce(json_extract("data", '$.source.path'), '') || ' ' || coalesce(json_extract("data", '$.source.name'), '') || ' ' || CASE WHEN coalesce(json_extract("data", '$.source.uri'), '') NOT LIKE 'data:%' THEN coalesce(json_extract("data", '$.source.uri'), '') ELSE '' END || ' ' || coalesce(json_extract("data", '$.source.clientName'), '')) ELSE coalesce(json_extract("data", '$.state.error'), '') END) WHERE json_valid("part"."data") AND ((json_extract("part"."data", '$.type') = 'text' AND coalesce(json_extract("part"."data", '$.synthetic'), 0) = 0 AND coalesce(json_extract("part"."data", '$.ignored'), 0) = 0) OR json_extract("part"."data", '$.type') = 'file' OR (json_extract("part"."data", '$.type') = 'tool' AND json_extract("part"."data", '$.state.status') = 'error'));`
|
||||
|
||||
export function make(table: {
|
||||
session_id: AnySQLiteColumn
|
||||
id: AnySQLiteColumn
|
||||
message_id: AnySQLiteColumn
|
||||
data: AnySQLiteColumn
|
||||
}) {
|
||||
return index("recall_part_search_idx")
|
||||
.on(
|
||||
table.session_id,
|
||||
table.id,
|
||||
table.message_id,
|
||||
sql`json_extract(${table.data}, '$.type')`,
|
||||
sql`CASE WHEN json_extract(${table.data}, '$.type') = 'text' THEN coalesce(json_extract(${table.data}, '$.text'), '') WHEN json_extract(${table.data}, '$.type') = 'file' THEN trim(coalesce(json_extract(${table.data}, '$.filename'), '') || ' ' || CASE WHEN coalesce(json_extract(${table.data}, '$.url'), '') NOT LIKE 'data:%' THEN coalesce(json_extract(${table.data}, '$.url'), '') ELSE '' END || ' ' || coalesce(json_extract(${table.data}, '$.source.path'), '') || ' ' || coalesce(json_extract(${table.data}, '$.source.name'), '') || ' ' || CASE WHEN coalesce(json_extract(${table.data}, '$.source.uri'), '') NOT LIKE 'data:%' THEN coalesce(json_extract(${table.data}, '$.source.uri'), '') ELSE '' END || ' ' || coalesce(json_extract(${table.data}, '$.source.clientName'), '')) ELSE coalesce(json_extract(${table.data}, '$.state.error'), '') END`,
|
||||
)
|
||||
.where(
|
||||
sql`json_valid(${table.data}) AND ((json_extract(${table.data}, '$.type') = 'text' AND coalesce(json_extract(${table.data}, '$.synthetic'), 0) = 0 AND coalesce(json_extract(${table.data}, '$.ignored'), 0) = 0) OR json_extract(${table.data}, '$.type') = 'file' OR (json_extract(${table.data}, '$.type') = 'tool' AND json_extract(${table.data}, '$.state.status') = 'error'))`,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,24 @@ const Cost = Schema.Struct({
|
||||
),
|
||||
})
|
||||
|
||||
// kilocode_change start - models.dev reasoning_options (snatched from upstream
|
||||
// v1.18.11, #36624): effort tiers, thinking toggles, and token budgets.
|
||||
const ReasoningOption = Schema.Union([
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("effort"),
|
||||
values: Schema.Array(Schema.NullOr(Schema.String)),
|
||||
}),
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("toggle"),
|
||||
}),
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("budget_tokens"),
|
||||
min: Schema.optional(Schema.Finite),
|
||||
max: Schema.optional(Schema.Finite),
|
||||
}),
|
||||
])
|
||||
// kilocode_change end
|
||||
|
||||
export const Model = Schema.Struct({
|
||||
id: Schema.String,
|
||||
name: Schema.String,
|
||||
@@ -51,6 +69,7 @@ export const Model = Schema.Struct({
|
||||
release_date: Schema.String,
|
||||
attachment: Schema.Boolean,
|
||||
reasoning: Schema.Boolean,
|
||||
reasoning_options: Schema.optional(Schema.Array(ReasoningOption)), // kilocode_change
|
||||
temperature: Schema.Boolean,
|
||||
tool_call: Schema.Boolean,
|
||||
interleaved: Schema.optional(
|
||||
|
||||
@@ -6,6 +6,7 @@ import path from "path"
|
||||
import { LayerNode } from "./effect/layer-node"
|
||||
import { Entry, Match } from "./filesystem/schema"
|
||||
import { FSUtil } from "./fs-util"
|
||||
import * as KiloGrep from "./kilocode/ripgrep-grep" // kilocode_change
|
||||
import * as SpawnValidation from "./kilocode/spawn-validation" // kilocode_change
|
||||
import { AppProcess, collectStream, waitForAbort } from "./process"
|
||||
import { NonNegativeInt, PositiveInt, RelativePath } from "./schema"
|
||||
@@ -23,7 +24,7 @@ const MAX_RECORD_BYTES = 64 * 1024
|
||||
const MAX_SUBMATCHES = 100
|
||||
|
||||
const RawMatch = Schema.Struct({
|
||||
type: Schema.Literal("match"),
|
||||
type: Schema.Literals(["match", "context"]), // kilocode_change - retain requested context records
|
||||
data: Schema.Struct({
|
||||
path: Schema.Struct({ text: Schema.String }),
|
||||
lines: Schema.Struct({ text: Schema.String }),
|
||||
@@ -39,7 +40,7 @@ const RawMatch = Schema.Struct({
|
||||
}),
|
||||
})
|
||||
|
||||
type RawMatchData = (typeof RawMatch.Type)["data"]
|
||||
type RawMatchData = (typeof RawMatch.Type)["data"] & { readonly context: boolean } // kilocode_change
|
||||
|
||||
export class Error extends Schema.TaggedErrorClass<Error>()("Ripgrep.Error", {
|
||||
message: Schema.String,
|
||||
@@ -71,7 +72,8 @@ export interface GlobInput {
|
||||
readonly validate?: Effect.Effect<void, unknown> // kilocode_change - bind approved searches at spawn
|
||||
}
|
||||
|
||||
export interface GrepInput {
|
||||
export interface GrepInput extends KiloGrep.Options {
|
||||
// kilocode_change
|
||||
readonly cwd: string
|
||||
readonly pattern: string
|
||||
readonly file?: string
|
||||
@@ -84,7 +86,7 @@ export interface GrepInput {
|
||||
export interface Interface {
|
||||
readonly find: (input: FindInput) => Effect.Effect<readonly Entry[], Error>
|
||||
readonly glob: (input: GlobInput) => Effect.Effect<SearchResult<Entry>, Error> // kilocode_change
|
||||
readonly grep: (input: GrepInput) => Effect.Effect<SearchResult<Match>, Error | InvalidPatternError> // kilocode_change
|
||||
readonly grep: (input: GrepInput) => Effect.Effect<SearchResult<KiloGrep.GrepMatch>, Error | InvalidPatternError> // kilocode_change
|
||||
}
|
||||
|
||||
// kilocode_change start - retain truncation state through model-facing tools
|
||||
@@ -116,6 +118,7 @@ export const layer = Layer.effect(
|
||||
readonly parse: (line: string) => Effect.Effect<A | undefined, Error>
|
||||
readonly pattern?: string
|
||||
readonly onItem?: (item: A) => Effect.Effect<void>
|
||||
readonly stop?: (item: A) => boolean // kilocode_change - stop bounded searches at the overflow match
|
||||
readonly validate?: Effect.Effect<void, unknown> // kilocode_change - spawn-bound target validation
|
||||
}) => {
|
||||
const program = Effect.scoped(
|
||||
@@ -136,6 +139,13 @@ export const layer = Layer.effect(
|
||||
Effect.forkScoped,
|
||||
)
|
||||
let observed = 0
|
||||
let stopped = false // kilocode_change
|
||||
const take = input.stop // kilocode_change start
|
||||
? Stream.takeUntil<A>((row) => {
|
||||
stopped = input.stop?.(row) ?? false
|
||||
return stopped
|
||||
})
|
||||
: Stream.take(input.limit + 1) // kilocode_change end
|
||||
const rows = yield* Stream.decodeText(handle.stdout).pipe(
|
||||
Stream.splitLines,
|
||||
Stream.filter((line) => line.length > 0),
|
||||
@@ -145,11 +155,12 @@ export const layer = Layer.effect(
|
||||
if (!input.onItem || observed++ >= input.limit) return Effect.void
|
||||
return input.onItem(row)
|
||||
}),
|
||||
Stream.take(input.limit + 1),
|
||||
take, // kilocode_change
|
||||
Stream.runCollect,
|
||||
Effect.map((chunk) => [...chunk]),
|
||||
)
|
||||
const truncated = rows.length > input.limit
|
||||
if (stopped) return { items: rows, truncated: true, partial: false } // kilocode_change
|
||||
const truncated = input.stop ? false : rows.length > input.limit // kilocode_change - custom stop predicates own truncation
|
||||
if (truncated) return { items: rows.slice(0, input.limit), truncated, partial: false }
|
||||
|
||||
const code = yield* handle.exitCode
|
||||
@@ -249,11 +260,13 @@ export const layer = Layer.effect(
|
||||
grep: (input) =>
|
||||
run<RawMatchData>({
|
||||
...input,
|
||||
stop: KiloGrep.stop(input.limit), // kilocode_change
|
||||
args: [
|
||||
"--no-config",
|
||||
"--json",
|
||||
"--hidden",
|
||||
"--no-messages",
|
||||
...KiloGrep.flags(input), // kilocode_change
|
||||
...(input.include ? [`--glob=${input.include}`] : []),
|
||||
"--glob=!**/.git/**",
|
||||
"--",
|
||||
@@ -269,13 +282,19 @@ export const layer = Layer.effect(
|
||||
})
|
||||
).pipe(
|
||||
Effect.flatMap((json) => {
|
||||
if (!json || typeof json !== "object" || !("type" in json) || json.type !== "match")
|
||||
if (
|
||||
!json ||
|
||||
typeof json !== "object" ||
|
||||
!("type" in json) ||
|
||||
(json.type !== "match" && json.type !== "context") // kilocode_change
|
||||
)
|
||||
return Effect.succeed(undefined)
|
||||
return Schema.decodeUnknownEffect(RawMatch)(json).pipe(
|
||||
Effect.map((match) => ({
|
||||
...match.data,
|
||||
path: { text: match.data.path.text.replace(/^\.[\\/]/, "") },
|
||||
submatches: match.data.submatches.slice(0, MAX_SUBMATCHES),
|
||||
context: match.type === "context", // kilocode_change
|
||||
})),
|
||||
Effect.mapError((cause) => failure("Invalid ripgrep match output", cause)),
|
||||
)
|
||||
@@ -285,13 +304,13 @@ export const layer = Layer.effect(
|
||||
// kilocode_change start - retain spawn metadata after mapping matches
|
||||
Effect.map((result) => ({
|
||||
...result,
|
||||
items: result.items.map((match) => {
|
||||
items: KiloGrep.select(input, result.items).map((match) => {
|
||||
const relative = match.path.text
|
||||
.replace(/^(?:\.[\\/])+/u, "")
|
||||
.replace(/^[\\/]+/u, "")
|
||||
.replaceAll("\\", "/")
|
||||
const absolute = path.resolve(input.cwd, relative)
|
||||
return new Match({
|
||||
const item = new Match({
|
||||
entry: new Entry({
|
||||
path: RelativePath.make(relative),
|
||||
type: "file",
|
||||
@@ -306,6 +325,7 @@ export const layer = Layer.effect(
|
||||
end: submatch.end,
|
||||
})),
|
||||
})
|
||||
return KiloGrep.decorate(item, match.context, match.lines.text.length > 2_000)
|
||||
}),
|
||||
})),
|
||||
// kilocode_change end
|
||||
|
||||
@@ -13,6 +13,7 @@ import { WorkspaceV2 } from "../workspace"
|
||||
import { Timestamps } from "../database/schema.sql"
|
||||
import type { SystemContext } from "../system-context/index"
|
||||
import { AgentV2 } from "../agent"
|
||||
import { RecallPartIndex } from "../kilocode/session/recall-part-index" // kilocode_change
|
||||
|
||||
type SessionMessageData = Omit<(typeof SessionMessage.Message)["Encoded"], "type" | "id">
|
||||
type V1MessageData = Omit<SessionV1.Info, "id" | "sessionID">
|
||||
@@ -93,6 +94,7 @@ export const PartTable = sqliteTable(
|
||||
(table) => [
|
||||
index("part_message_id_id_idx").on(table.message_id, table.id),
|
||||
index("part_session_idx").on(table.session_id),
|
||||
RecallPartIndex.make(table), // kilocode_change
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ export * as ConfigCommandV1 from "./command"
|
||||
import { Schema } from "effect"
|
||||
|
||||
export const Info = Schema.Struct({
|
||||
template: Schema.String,
|
||||
template: Schema.optional(Schema.String), // kilocode_change - allow global workflow model/variant overrides
|
||||
description: Schema.optional(Schema.String),
|
||||
agent: Schema.optional(Schema.String),
|
||||
model: Schema.optional(Schema.String),
|
||||
|
||||
@@ -207,6 +207,7 @@ export const SubtaskPart = Schema.Struct({
|
||||
modelID: ModelV2.ID,
|
||||
}),
|
||||
),
|
||||
variant: Schema.optional(Schema.String), // kilocode_change - preserve workflow subtask variant
|
||||
command: Schema.optional(Schema.String),
|
||||
}).annotate({ identifier: "SubtaskPart" })
|
||||
export type SubtaskPart = Types.DeepMutable<Schema.Schema.Type<typeof SubtaskPart>>
|
||||
@@ -500,6 +501,7 @@ export const SubtaskPartInput = Schema.Struct({
|
||||
modelID: ModelV2.ID,
|
||||
}),
|
||||
),
|
||||
variant: Schema.optional(Schema.String), // kilocode_change - preserve workflow subtask variant
|
||||
command: Schema.optional(Schema.String),
|
||||
}).annotate({ identifier: "SubtaskPartInput" })
|
||||
export type SubtaskPartInput = Types.DeepMutable<Schema.Schema.Type<typeof SubtaskPartInput>>
|
||||
|
||||
@@ -69,6 +69,7 @@ Review files`,
|
||||
id: ModelV2.ID.make("claude"),
|
||||
variant: ModelV2.VariantID.make("high"),
|
||||
},
|
||||
variant: ModelV2.VariantID.make("high"),
|
||||
subtask: true,
|
||||
}),
|
||||
new CommandV2.Info({ name: "empty", template: "" }),
|
||||
@@ -78,4 +79,38 @@ Review files`,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
it.effect("applies a global partial override after project command definitions", () =>
|
||||
Effect.gen(function* () {
|
||||
const command = yield* CommandV2.Service
|
||||
yield* ConfigCommandPlugin.Plugin.effect.pipe(
|
||||
Effect.provideService(CommandV2.Service, command),
|
||||
Effect.provideService(
|
||||
Config.Service,
|
||||
Config.Service.of({
|
||||
entries: () =>
|
||||
Effect.succeed([
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode({ commands: { review: { model: "anthropic/claude", variant: "high" } } }),
|
||||
}),
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode({ commands: { review: { template: "Review files" } } }),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
expect(yield* command.get("review")).toMatchObject({
|
||||
template: "Review files",
|
||||
model: {
|
||||
providerID: ProviderV2.ID.make("anthropic"),
|
||||
id: ModelV2.ID.make("claude"),
|
||||
variant: ModelV2.VariantID.make("high"),
|
||||
},
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -11,6 +11,8 @@ import { testEffect } from "../lib/effect"
|
||||
|
||||
type PtyEvent = { type: "created" | "exited" | "deleted"; id: PtyID }
|
||||
|
||||
const PTY_TEST_TIMEOUT = "15 seconds" // kilocode_change - PTY startup can exceed the default test timeout on macOS CI
|
||||
|
||||
const locationLayer = Layer.succeed(
|
||||
Location.Service,
|
||||
Location.Service.of(location({ directory: AbsolutePath.make("/tmp") })),
|
||||
@@ -59,7 +61,7 @@ const waitForEvents = (events: Queue.Queue<PtyEvent>, id: PtyID, count: number)
|
||||
return picked
|
||||
}).pipe(
|
||||
Effect.timeoutOrElse({
|
||||
duration: "5 seconds",
|
||||
duration: PTY_TEST_TIMEOUT, // kilocode_change
|
||||
orElse: () => Effect.fail(new Error("timeout waiting for pty events")),
|
||||
}),
|
||||
)
|
||||
@@ -73,6 +75,7 @@ const attachCollecting = Effect.fn("PtySessionTest.attachCollecting")(function*
|
||||
onData: (chunk) => Queue.offerUnsafe(output, chunk),
|
||||
onEnd: (event) => Deferred.doneUnsafe(ended, Effect.succeed(event)),
|
||||
})
|
||||
if (attachment.replay) Queue.offerUnsafe(output, attachment.replay)
|
||||
attachment.activate()
|
||||
return { attachment, output, ended }
|
||||
})
|
||||
@@ -84,7 +87,7 @@ const waitForOutput = (output: Queue.Queue<string>, text: string) =>
|
||||
return received
|
||||
}).pipe(
|
||||
Effect.timeoutOrElse({
|
||||
duration: "5 seconds",
|
||||
duration: PTY_TEST_TIMEOUT, // kilocode_change
|
||||
orElse: () => Effect.fail(new Error(`timeout waiting for output containing ${JSON.stringify(text)}`)),
|
||||
}),
|
||||
)
|
||||
@@ -144,7 +147,7 @@ describe("pty", () => {
|
||||
Effect.gen(function* () {
|
||||
const pty = yield* Pty.Service
|
||||
const marker = "café-über-北京-🚀"
|
||||
const info = yield* createPty("sh", ["-c", `printf '${marker}\\n'`])
|
||||
const info = yield* createPty("sh", ["-c", "printf 'caf\\303\\251-\\303\\274ber-\\345\\214\\227\\344\\272\\254-\\360\\237\\232\\200\\n'"])
|
||||
const attached = yield* attachCollecting(info.id)
|
||||
expect(yield* waitForOutput(attached.output, marker)).toContain(marker)
|
||||
}),
|
||||
@@ -268,7 +271,7 @@ describe("pty", () => {
|
||||
attachment.write("ignored")
|
||||
yield* pty.remove(info.id)
|
||||
attachment.activate()
|
||||
expect(yield* Deferred.await(ended).pipe(Effect.timeout("5 seconds"))).toEqual({ exitCode: 7 })
|
||||
expect(yield* Deferred.await(ended).pipe(Effect.timeout(PTY_TEST_TIMEOUT))).toEqual({ exitCode: 7 })
|
||||
attachment.detach()
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "7.4.17",
|
||||
"version": "7.4.18",
|
||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "7.4.17",
|
||||
"version": "7.4.18",
|
||||
"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.17"
|
||||
version = "7.4.18"
|
||||
schema_version = 1
|
||||
authors = ["Anomaly"]
|
||||
repository = "https://github.com/Kilo-Org/kilocode"
|
||||
@@ -11,26 +11,26 @@ name = "Kilo"
|
||||
icon = "./icons/opencode.svg"
|
||||
|
||||
[agent_servers.opencode.targets.darwin-aarch64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.17/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.18/opencode-darwin-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.darwin-x86_64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.17/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.18/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.17/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.18/opencode-linux-arm64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-x86_64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.17/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.18/opencode-linux-x64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.windows-x86_64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.17/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.18/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "7.4.17",
|
||||
"version": "7.4.18",
|
||||
"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.17",
|
||||
"version": "7.4.18",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# docs-sync learnings
|
||||
|
||||
Rules the docs-sync bot learned from maintainer corrections to its rolling pull request.
|
||||
The bot reads this file at the start of every run and follows every rule below.
|
||||
|
||||
To unlearn a rule, delete its line and commit. The next run reads this file from the
|
||||
branch, so the rule is gone from its input, and the deletion itself is a correction the
|
||||
extraction step is instructed not to undo.
|
||||
|
||||
<!-- docs-sync:learnings:start -->
|
||||
<!-- docs-sync:learnings:end -->
|
||||
@@ -28,6 +28,7 @@ export const AiProvidersNav: NavSection[] = [
|
||||
links: [
|
||||
{ href: "/ai-providers/openrouter", children: "OpenRouter" },
|
||||
{ href: "/ai-providers/requesty", children: "Requesty" },
|
||||
{ href: "/ai-providers/daoxe", children: "DaoXE" },
|
||||
{ href: "/ai-providers/unbound", children: "Unbound" },
|
||||
{ href: "/ai-providers/zenmux", children: "ZenMux" },
|
||||
{
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@
|
||||
| `kilo session` | manage sessions |
|
||||
| `kilo remote` | enable remote connection for real-time session relay |
|
||||
| `kilo daemon` | manage the local kilo daemon |
|
||||
| `kilo console` | open or stop the local Kilo Console |
|
||||
| `kilo console` | open or stop the local Kilo Console (deprecated) |
|
||||
| `kilo cloud` | run Cloud Agent tasks |
|
||||
| `kilo db` | database tools |
|
||||
| `kilo config` | configuration tools |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kilocode/kilo-docs",
|
||||
"version": "7.4.17",
|
||||
"version": "7.4.18",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --webpack --port 3002",
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
---
|
||||
title: "Using DaoXE with Kilo Code"
|
||||
description: "Connect Kilo Code to DaoXE's multi-model API gateway with an API key and an account-available model ID."
|
||||
sidebar_label: DaoXE
|
||||
---
|
||||
|
||||
# Using DaoXE With Kilo Code
|
||||
|
||||
[DaoXE](https://daoxe.com) is an AI gateway with native Claude protocol, verifiable routing, and one-key access to Claude, GPT, Gemini, and more. Kilo Code uses the `daoxe` provider ID and reads your API key from `DAOXE_API_KEY`.
|
||||
|
||||
{% callout type="warning" %}
|
||||
DaoXE is not available in mainland China. Requests from mainland China may be blocked or rejected.
|
||||
{% /callout %}
|
||||
|
||||
## Before you begin
|
||||
|
||||
1. Create an account at [daoxe.com](https://daoxe.com).
|
||||
2. Create an API key in your DaoXE dashboard.
|
||||
3. Choose an exact model ID available to your account. Model availability and pricing can change, so check the live catalog instead of copying an old model list.
|
||||
|
||||
## Configure Kilo Code
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="VSCode" %}
|
||||
|
||||
1. Open **Settings** in the Kilo Code extension.
|
||||
2. Go to the **Providers** tab and add **DaoXE**. If it is not visible, click **Show more providers**.
|
||||
3. Enter your DaoXE API key.
|
||||
4. Select a model that is available to your DaoXE account.
|
||||
|
||||
The provider credentials are stored in Kilo's `auth.json` store.
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="CLI" %}
|
||||
|
||||
**Recommended:** connect interactively so the API key is stored in Kilo's `auth.json` store (same credential store as VS Code).
|
||||
|
||||
1. In the TUI, run `/connect` and choose **DaoXE**, then paste your API key.
|
||||
2. Or from the shell:
|
||||
|
||||
```bash
|
||||
kilo auth login --provider daoxe
|
||||
```
|
||||
|
||||
Then pick a model from the model picker, or set a default model using the `provider-id/model-id` format:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"model": "daoxe/your-account-model-id",
|
||||
}
|
||||
```
|
||||
|
||||
Replace `your-account-model-id` with an exact model ID available to your account.
|
||||
|
||||
**Manual configuration (optional):** set the key in the environment and declare the provider in `~/.config/kilo/kilo.json` or `./kilo.json` without writing the secret into the project file:
|
||||
|
||||
```bash
|
||||
export DAOXE_API_KEY="your-api-key"
|
||||
```
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"provider": {
|
||||
"daoxe": {
|
||||
"env": ["DAOXE_API_KEY"],
|
||||
},
|
||||
},
|
||||
"model": "daoxe/your-account-model-id",
|
||||
}
|
||||
```
|
||||
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
## API compatibility
|
||||
|
||||
Kilo Code connects to DaoXE through the OpenAI-compatible API at `https://daoxe.com/v1`. DaoXE also exposes OpenAI Responses, Anthropic Messages, and image-generation-compatible endpoints, but the provider configuration on this page uses Kilo Code's OpenAI-compatible chat path.
|
||||
|
||||
For standalone cURL, Node.js, Python, Postman, and Claude Code examples, see the [DaoXE-AI examples repository](https://github.com/seven7763/DaoXE-AI).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Invalid API key:** Create a new key in your DaoXE dashboard, then reconnect with `/connect` / `kilo auth login --provider daoxe`, or update `DAOXE_API_KEY` if you use manual configuration.
|
||||
- **Model not found:** Copy an exact model ID available to your account. Do not rely on a static model list.
|
||||
- **Provider not visible:** Refresh Kilo Code's provider catalog, then check **Show more providers**.
|
||||
- **Connection rejected:** Confirm that you are using the service from an available region.
|
||||
|
||||
{% callout type="note" %}
|
||||
This documentation was contributed by a DaoXE affiliate.
|
||||
{% /callout %}
|
||||
@@ -45,6 +45,7 @@ Route requests through unified APIs with additional features:
|
||||
|
||||
- **[OpenRouter](/docs/ai-providers/openrouter)** - Access multiple providers through one API
|
||||
- **[Requesty](/docs/ai-providers/requesty)** - Smart routing and fallbacks
|
||||
- **[DaoXE](/docs/ai-providers/daoxe)** - Connect multiple model families through one API
|
||||
- **[Cloudflare AI Gateway](/docs/ai-providers/cloudflare)** - Route providers through your Cloudflare account
|
||||
|
||||
## Choosing a Provider
|
||||
|
||||
@@ -159,9 +159,9 @@ Each request can include 1-20 tasks. Each task must include at least one of `pro
|
||||
|
||||
The companion `agent_manager_models` tool searches models and their supported reasoning variants on demand. Results are grouped by model name (with the offering providers listed for reference) and limited to 20 per call, so the full catalog is never added to the conversation context.
|
||||
|
||||
The same tool also manages existing sessions. It can return a compact overview of sections, worktrees, and local sessions, send a prompt to one managed session, or stop a managed session. Stopping aborts the session's active work and removes it from the panel, just like closing the session tab.
|
||||
The same tool also manages existing sessions. It can return an overview of sections, worktrees, and local sessions, send a prompt to one managed session, stop a managed session, or move a session's worktree into a section. The overview includes section IDs, each section's assigned worktrees, worktree IDs, and session IDs. Use those exact IDs for a subsequent move. Moving accepts a section ID from the overview, or `null` to ungroup the worktree. Moving a session moves its whole worktree, including multi-version siblings. Local sessions cannot be assigned to a section. Stopping aborts the session's active work and removes it from the panel, just like closing the session tab.
|
||||
|
||||
The tool uses the `agent_manager` permission. Approval prompts are scoped to the requested capability, so approving `worktree` does not automatically approve `local`, an overview, or a targeted prompt. Prompting an existing managed session requires an explicit `prompt` approval the first time, even if Agent Manager session creation was previously approved broadly. Stopping a session likewise requires an explicit `stop` approval.
|
||||
The tool uses the `agent_manager` permission. Approval prompts are scoped to the requested capability, so approving `worktree` does not automatically approve `local`, an overview, or a targeted prompt. Prompting an existing managed session requires an explicit `prompt` approval the first time, even if Agent Manager session creation was previously approved broadly. Stopping a session likewise requires an explicit `stop` approval, and moving a worktree requires an explicit `move` approval.
|
||||
|
||||
## Sections
|
||||
|
||||
@@ -217,11 +217,39 @@ Press `Cmd+D` (macOS) / `Ctrl+D` (Windows/Linux) to toggle the diff panel. It sh
|
||||
- Markdown files include an eye/code toggle in the file header to switch between rendered Markdown and the raw diff
|
||||
- **Drag file headers into chat** — drag a file header from the diff panel into the chat input to insert an `@file` mention, giving the agent context about specific changed files
|
||||
|
||||
### Diff Scope
|
||||
|
||||
A scope selector in the diff toolbar (both in the side panel and the full-screen review) chooses which changes the diff shows:
|
||||
|
||||
- **Branch** (default) — the full worktree diff against its parent branch, matching the review behavior above
|
||||
- **Staged** — staged changes in the selected worktree
|
||||
- **Unstaged** — unstaged changes in the selected worktree
|
||||
- **Session** — changes from the selected session
|
||||
|
||||
The Branch scope also has a base-branch picker next to it for overriding the comparison branch. **Apply to local** works only on the Branch scope — switch back to Branch to apply.
|
||||
|
||||
See [Agent Manager Workflows](/docs/automate/agent-manager-workflows#merging-worktree-and-parent-branch) for the full integration story, including when to apply locally vs. merge directly vs. open a pull request.
|
||||
|
||||
## Terminals
|
||||
|
||||
Each session has a dedicated integrated terminal rooted in the session's worktree directory. Press `Cmd+/` (macOS) / `Ctrl+/` (Windows/Linux) to focus the terminal for the active session.
|
||||
Each session has a dedicated terminal rooted in the session's worktree directory. Press `Cmd+/` (macOS) / `Ctrl+/` (Windows/Linux) to focus the terminal for the active session.
|
||||
|
||||
### Choosing the Terminal Destination
|
||||
|
||||
The toolbar's terminal button is a split button: click it to open a terminal, or use its dropdown to choose where terminals open:
|
||||
|
||||
- **VS Code terminal** (default) — opens or focuses the VS Code integrated terminal at the bottom of the window
|
||||
- **Agent Manager panel** — opens an embedded terminal in the side panel that also hosts the diff view, so the shell stays inside the Agent Manager layout
|
||||
|
||||
The dropdown choice is remembered per panel and becomes the default for new panels. You can also set the default directly with the `kilo-code.new.agentManager.terminalButtonDestination` setting (`vscode` or `agentManager`). The `Cmd+/` (macOS) / `Ctrl+/` (Windows/Linux) shortcut follows the same destination.
|
||||
|
||||
With the **Agent Manager panel** destination, the terminal works like the diff panel: press `Cmd+/` to reveal it and press again to hide it. Hiding never stops the terminal — scrollback and running processes continue in the background, and focus returns to the chat input. A terminal stops only when you close its tab in the panel.
|
||||
|
||||
### Multiple Terminals
|
||||
|
||||
The side panel hosts multiple terminals per context (the local workspace or a worktree). The panel header is a tab strip: click a tab to switch, click **+** to open another terminal, and click **X** (or middle-click) to close a single terminal. Drag tabs to reorder them. Closing a terminal no longer hides the panel — closing the last one lands on the empty state. Pressing `Cmd+W` (macOS) / `Ctrl+W` (Windows/Linux) with a focused side terminal closes exactly that terminal.
|
||||
|
||||
New terminals are named "Terminal N" using the lowest free number, and tabs pick up the live title from the shell or running program, so a dev server or editor names its own tab.
|
||||
|
||||
### Switching Between Terminal and Agent Manager
|
||||
|
||||
|
||||
@@ -456,7 +456,7 @@ For tools that don't need the full plugin context, drop them in a `tool/` or `to
|
||||
|
||||
The CLI has built-in attention alerts for session completion, errors, and prompts that need input. You do not need a plugin or platform-specific notification command.
|
||||
|
||||
Enable notifications and sounds in `kilo console` under **Settings > CLI > Notifications**, or configure the `attention` section of `tui.json`. See [CLI Notifications and Sounds](/docs/code-with-ai/platforms/cli#cli-notifications-and-sounds) for configuration and custom sound overrides.
|
||||
Configure the `attention` section of `tui.json` or `tui.jsonc` to enable notifications and sounds. See [CLI Notifications and Sounds](/docs/code-with-ai/platforms/cli#cli-notifications-and-sounds) for configuration and custom sound overrides.
|
||||
|
||||
### Block reads of `.env` files
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user